Python中类和实例学习笔记
以创建一个命名实体识别(Named Entity Recognition,NER)模型为例,学习Python中类和实例的相关内容。预处理函数参考了这篇博客。
类(Class)和实例(Instance)
类和实例是面向对象编程最重要的概念之二,类是抽象的模板,实例是类的具体表现。下面用具体的例子分析说明这一点。
建模流程及本文引言
在Python中,以sklearn
为例,一般训练一个模型的步骤可以分为:
- 准备数据
- 搭建模型
- 将数据“喂入”模型(即:
model.fit()
方法) - 模型的应用(预测、分类等)
其中,第2、3、4步只需要利用sklearn
进行傻瓜式操作即可,但第1步准备数据则需要我自己操作。
第1步即所谓的“数据预处理”,在搭建此NER模型时,我也利用了第三方包为我们完成后续步骤;对于数据预处理,我准备编写一个pre_precessing
类来集成所有在第1步用到的处理方法。
数据一览
关于NER的理论及技术细节,可以去这里,本文只给出源数据格式以及模型需要的数据格式。
训练语料库为人民日报199801语料库,具体长这样:
19980101-01-001-001/m 迈向/v 充满/v 希望/n 的/u 新/a 世纪/n ——/w 一九九八年/t 新年/t 讲话/n (/w 附/v 图片/n 1/m 张/q )/w
19980101-01-001-002/m 中共中央/nt 总书记/n 、/w 国家/n 主席/n 江/nr 泽民/nr
而模型需要的数据,是这样:
[[{'bias': 1.0, 'w': '迈', 'w+1': '向', 'w-1': '<BOS>',
'w-1:w': '<BOS>迈', 'w:w+1': '迈向'},
{'bias': 1.0, 'w': '向', 'w+1': '充', 'w-1': '迈',
'w-1:w': '迈向', 'w:w+1': '向充'},
{'bias': 1.0, 'w': '充', 'w+1': '满', 'w-1': '向',
'w-1:w': '向充', 'w:w+1': '充满'},
{'bias': 1.0, 'w': '满', 'w+1': '希', 'w-1': '充',
'w-1:w': '充满', 'w:w+1': '满希'},
{'bias': 1.0, 'w': '希', 'w+1': '望', 'w-1': '满',
'w-1:w': '满希', 'w:w+1': '希望'}
...
]]
当然,还需要对应的实体标签,即Y_train
,但这里只用X_train
就足以说明问题了。
我需要定义一个类,以完成上面的转换过程。
类的声明及实例的创建
定义一个类pre_processing
很简单,代码如下:
class pre_processing(obj):
pass
其中,pre_processing
是类的名字,参数obj
是表明所定义的类是从哪里继承来的,该项参数可以为空(在本例中我就是这么做的)。关于什么是继承先不要管,如果不想为空也可以设为object
,事实上所有的类追根溯源都继承自object
类。
创建一个实例pp
更简单,代码如下:
pp = pre_processing()
至此,我想应该可以对上文说的类是抽象的模板,实例是类的具体表现有了一个更加深入的认识了吧?如果没有,继续往下看。
如果我定义多个实例会出现什么情况呢?输入以下代码:
pp1 = pre_processing()
pp2 = pre_processing()
亦即从一个类定义了两个实例pp1
和pp2
,接着输入:
print(pp1)
print(pp2)
那么会打印出类似如下的内容:
<__main__.pre_processing object at 0x00000162F15CB9B0>
<__main__.pre_processing object at 0x00000162F15CB7B8>
这说明,在同一类pre_processing
下定义的两个不同的实例具有不同的内存地址。
实例的属性(attribute)
对于上文定义的实例pp1
,我可以如下为其添加属性:
pp1.attribute = "This is an attribute"
如此之后,pp1
就多了一个属性attribute
,其值为This is an attribute
。要想查看某个实例所有的属性,可以用dir
函数:
print(dir(pp1))
输出为:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'attribute']
注意,最后一项为我们刚刚定义的属性。而前面那些诸多以__
开头的属性表明该属性为私有属性,只有在类内可以访问,而类外无法访问,要想获取这些属性的值,可以在类内定义相应功能的函数(即方法)。这样看似麻烦的操作,实际上是保证了外部的代码无法随意修改内部的对象,增加代码的健壮性。
实例的方法及__init__
实例的方法指的就是在类的内部定义的函数。上文说可以对已经定义的实例添加各种属性,这属于“后天”的做法,实际上可以强制在创建实例时传递给实例必要的属性,这就要依赖于类中特殊的__init__
方法了。
在本例中,我定义的类的目的是为了完成数据预处理,因此在创建实例时需要让它具有训练集数据(X_train
)这一必备属性,于是__init__
方法按如下定义:
class pre_processing():
def __init__(self, X_train):
self.X_train = X_train
值得注意的是,在类中定义方法与定义函数有一点不同:所有方法的必须含有self
参数,且该参数必须是第一个,表示此类创建的实例本身,于是在__init__
内部就可以把各种属性直接赋给self
,因为它指向该实例。
这样定义类之后,执行:
pp1 = pre_processing()
则会提示错误:
TypeError: _ _ init _ _ () missing 1 required positional argument: 'X_train'
这说明,在利用新定义的类创建实例时,必须给实例以必要的X_train
属性。例如:
X_train = u'19980101-01-001-001/m 迈向/v 充满/v 希望/n 的/u 新/a 世纪/n ——/w 一九九八年/t 新年/t 讲话/n (/w 附/v 图片/n 1/m 张/q )/w ' \
u'19980101-01-001-002/m 中共中央/nt 总书记/n 、/w 国家/n 主席/n 江/nr 泽民/nr '
pp1 = pre_processing(X_train=X_train)
则不会报错,说明实例创建成功。这时候,pp1
就具有了属性X_train
,可以按如下方式访问:
print(pp1.X_train)
19980101-01-001-001/m 迈向/v 充满/v 希望/n 的/u 新/a 世纪/n ——/w 一九九八年/t 新年/t 讲话/n (/w 附/v 图片/n 1/m 张/q )/w 19980101-01-001-002/m 中共中央/nt 总书记/n 、/w 国家/n 主席/n 江/nr 泽民/nr
若将__init__
中的self.X_train
改为self.__X_trian
重复运行上述代码,会出现什么情况呢?实验表明,在创建实例时仍需要传入一个必须的X_train
参数,但此时的属性以__
开头,表明类外不可访问,因此执行print(pp1.__X_train)
时提示:'pre_processing' object has no attribute '__X_train'
。
通过dir(pp1)
查看时会发现该实例具有_pre_processing__X_train
属性,意思就是说,该属性为pre_processing
内部的属性,外部无法直接访问。如果想获取,怎么处理呢?可以新加一个get__X_train
的方法:
def get__X_train(self):
return self.__X_train
那么,要想获取__X_train
属性的值,只需要调用pp1.get__X_train()
即可。这也说明了,类的内部是可以任意调用该属性的。
类内数据预处理方法的定义
明确了上述内容之后,就可以在类内定义一系列方法进行数据预处理了。类的名字仍然延续上文,强制限定的属性为原始数据路径(data_path
)。
首先,给出类中应该包含的方法列表:
- 从本地读取数据(
load_data(self, data_path)
) - 读取的内容以一个字符串存储,因此将其中的非中文字符全部转化为半角类型(
q2b(self, train_data)
) - 合并原始数据中分开标注的姓和名(
process_nr(self, train_data)
) - 合并原始数据中分开标注的时间词(
process_t(self, train_data)
) - 合并原始数据中以
[]
小粒度词(process_k(self, train_data)
)
下面是该类的完整代码:
import re
class pre_processing():
def __init__(self, data_path):
self.data_path = data_path
def load_data(self):
data = open(self.data_path, encoding='utf-8').read()
self.data = re.sub('199801.{13}/m ', '', data)
def q2b(self):
b_str = ""
for uchar in self.data:
inside_code = ord(uchar)
if inside_code == 12288:
inside_code = 32
elif 65374 >= inside_code >= 65281:
inside_code -= 65248
b_str += chr(inside_code)
self.data = b_str
def str2list(self):
self.data = self.data.split()
def process_t(self):
pro_words = []
index = 0
temp = u''
while True:
word = self.data[index] if index < len(self.data) else u''
if u'/t' in word:
temp = temp.replace(u'/t', u'') + word
elif temp:
pro_words.append(temp)
pro_words.append(word)
temp = u''
elif word:
pro_words.append(word)
else:
break
index += 1
self.data = pro_words
def process_nr(self):
pro_words = []
index = 0
while True:
word = self.data[index] if index < len(self.data) else u''
if u'/nr' in word:
next_index = index + 1
if next_index < len(self.data) and u'/nr' in self.data[next_index]:
pro_words.append(word.replace(u'/nr', u'') + self.data[next_index])
index = next_index
else:
pro_words.append(word)
elif word:
pro_words.append(word)
else:
break
index += 1
self.data = pro_words
def process_k(self):
pro_words = []
index = 0
temp = u''
while True:
word = self.data[index] if index < len(self.data) else u''
if u'[' in word:
temp += re.sub(pattern=u'/[a-zA-Z]*', repl=u'', string=word.replace(u'[', u''))
elif u']' in word:
w = word.split(u']')
temp += re.sub(pattern=u'/[a-zA-Z]*', repl=u'', string=w[0])
pro_words.append(temp+u'/'+w[1])
temp = u''
elif temp:
temp += re.sub(pattern=u'/[a-zA-Z]*', repl=u'', string=word)
elif word:
pro_words.append(word)
else:
break
index += 1
self.data = pro_words
定义好类之后,接下来就是要调用类中的方法来对数据进行预处理。假设代码文件与数据文件在同一目录下,则数据路径只需要输入文件名即可:
data = pre_processing("rmrb199801.txt")
data.load_data()
data.q2b()
data.str2list()
data.process_t()
data.process_nr()
data.process_k()