命名实体识别系列:
一、隐马尔科夫模型(HMM)
二、最大熵模型(ME)
三、条件随机场(CRF)
四、实体级/词级别评估(precision, recall, f1-value)
最大熵模型解决命名实体识别问题
命名实体识别问题简要介绍在一、隐马尔科夫模型(HMM)中包含,在此不过多介绍。
最大熵模型
首先对最大熵模型进行简要的介绍(本文最大熵模型的实现方式为调用sklearn库中的LogisticRegression进行训练,对于最大熵模型的细节不做特殊要求)
(1)最大熵模型是由以下条件概率分布表示的分类模型,可用于二类或多分类问题。
其中, Zw(x)Zw(x) 是规范化因子; wiwi 是特征权值; fi(x,y)fi(x,y) 是特征函数,描述输入 xx 和输出 yy 之间的某一个事实,其定义为:
(2)对于给定的训练集T={(x1,y1),(x2,y2),…,(xN,yN)}T={(x1,y1),(x2,y2),…,(xN,yN)} 和特征函数 fi(x,y),i=1,2,…,nfi(x,y),i=1,2,…,n ,应用最大熵原理于分类模型中,得到以下约束最优化问题:
求解此最优化问题的无约束最优化对偶问题可得到最大熵模型,即最大熵模型的学习可归结为对偶函数Ψ(w)的极大化。对偶函数Ψ(w) 为:
其中, L(P,w)L(P,w) 为引入拉格朗日乘子 ww 后的拉格朗日函数
(3)对偶函数的极大化等价于最大熵模型的极大似然估计。
1.已知训练数据的经验概率分布P~(X,Y) ,条件概率分布P(Y|X) 的对数似然函数为
2.对偶函数Ψ(w)
即最大熵模型的学习问题可转换为求解对数似然函数极大化或对偶函数极大化的问题
实现命名实体识别
在实现最大熵模型(可以看作一个多分类的逻辑回归问题)时,调用了sklearn库中的DictVectorizer和LogisticRegression分别用以特征抽取和最大熵模型训练(多分类的逻辑回归) 如果不了解DictVectorizer的同学可以去看我根据官方文档整理的DictVectorizer
核心函数:
外部函数:
- flatten_lists(lists): 将列表的列表拆分成为一个列表
- word2features(sent, i): 抽取sent句字中索引为i的词的特征
- sent2features(sent): 抽取整句话所有词的特征
EMModel成员函数: - init(self):设置LogisticRegression的各项参数,并初始化DictVectorizer
- train(self, sentences, tag_lists): 根据输入的数据训练模型
- test(self, sentences): 输入测试数据,得到预测结果
特征选取:
在特征抽取时,采用了w为中心词,左右各两个词总共五个词的窗口,特征分别为,w-2, w-1, w, w+1, w+2以及两两组合和三个组合,在实际尝试中发现设置四个或五个连续的词组合而成的特征,不仅不会提升性能,反而会导致一定程度的下降,所以并未采用。
在这部分同学们也可以自己去尝试选择不同的特征来取得最好的效果。
以窗口总长度为5,提取三个连续字为特征举例:
窗口一直向后滑动直到结束
在首位边界处要进行单独讨论,当w索引为0时,w-2设为< ss > w-2设为< s > 当w索引为len() - 1时,w+1设为< /s > w+2设为< /ss >
参数设定(调参侠必看)
写在开头:所有参数均以本例为基础,不具普适性,若性能不佳,勿喷。
在训练中,参数的选取也是有一定技巧的,虽然对不同情景,有着不同的推荐,但是每做一个模型都要自己依据实际情况进行调整,动手去尝试,才会得到最佳的拟合效果(预测结果)。
在本例中:
1.最大迭代次数(max_iter)我选择了100,200和500进行尝试,发现三者都没有达到收敛(达到收敛需要跑很久,硬件原因,未曾实现),但是200稍好,100和500稍差。
2.multi_class我最初设置为 multinomial,因为multinomial对于多分类问题分类相对精确,但是其速度较慢(进行T(T-1)/2次分类,在这里就不进行赘述,想搞明白的可以百度),经过测试发现在本例中multinomial除了拉低程序的性能并无其他贡献,在这里果断放弃,转为默认OvR(相对简单)。
3.slover我选择了saga,因为saga在处理数据量较大的样本时较为有优势,还有其他几种‘newton-cg’, ‘lbfgs’, ‘liblinear’, ‘sag’也都可以进行尝试(对于小数据集,‘liblinear’ 是一个不错的选择,而对于大数据集,‘sag’ 和 ‘saga’ 更快,对于多类问题,只有 ‘newton-cg’、‘sag’、‘saga’ 和 ‘lbfgs’ 处理多项式损失)
4.C为惩罚项系数,在比对了0.1、1和2后,果断选择1(默认值),注意选择迭代算法对于L1、L2的支持度不同,在这里不做赘述。
5.n_jobs,在这里我选择为-1(cpu并行个数,-1为全部),通常默认值为1,这样运行速度会快很多。
# 导入sklearn中的LogisticRegression (LogisticRegression即为最大熵模型)
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction import DictVectorizer
def flatten_lists(lists):
"""
将列表的列表拼成一个列表
:param lists:
:return:
"""
flatten_list = []
for l in lists:
if type(l) == list:
flatten_list += l
else:
flatten_list.append(l)
return flatten_list
def word2features(sent, i): #sent为一个句子, i为句子中字的索引
"""抽取单个字的特征"""
word = sent[i]
prev_word = "<s>" if i == 0 else sent[i-1] #前词
# 前词的前词
if i == 0:
pre_pre_word = "<ss>"
elif i == 1:
pre_pre_word = "<s>"
else:
pre_pre_word = sent[i-2]
next_word = "</s>" if i == (len(sent)-1) else sent[i+1] #后词
#后词的后词
if i == (len(sent) - 1):
next_next_word = "</ss>"
elif i == (len(sent) - 2):
next_next_word = "</s>"
else:
next_next_word = sent[i+2]
# 使用的特征:
# 前一个词,当前词,后一个词,
# 前一个词+当前词, 当前词+后一个词
# features = { #从句首到句尾以三个字为窗口提取6个特征,
# 'w': word,
# 'w-1': prev_word,
# 'w+1': next_word,
# 'w-1:w': prev_word+word,
# 'w:w+1': word+next_word,
# 'w-1:w:w+1':prev_word+word+next_word,
# 'bias': 1
# }
features = {
'w': word,
'w-1': prev_word,
'w-2': pre_pre_word,
'w+1': next_word,
'w+2': next_next_word,
'w-2:w-1': pre_pre_word + prev_word,
'w-1:w': prev_word + word,
'w:w+1': word + next_word,
'w+1:w+2': next_word + next_next_word,
'w-2:w-1:w': pre_pre_word + prev_word + word,
'w-1:w:w+1': prev_word + word + next_word,
'w:w+1:w+2': word + next_word + next_next_word,
'bias': 1
}
return features
def sent2features(sent): #sent为一句话(由字组成的列表)
"""抽取序列特征"""
features = [word2features(sent, i) for i in range(len(sent))]
return features
class MEModel(object):
def __init__(self):
self.model = LogisticRegression(
# C=1,
solver="saga", #算法选择saga较好,适用于样本较多的时候
max_iter=100,
n_jobs=-1 #-1尽可能利用cpu
)
self.vec = DictVectorizer() #对于键值对非数值的字典进行向量化sparse=False
#sparse: boolearn, 可选参数, 默认为True。transform是否要使用scipy产生一个sparse矩阵。DictVectorizer
# 的内部实现是将数据直接转换成sparse矩阵,如果sparse为False, 再把sparse矩阵转换成numpy.ndarray型数组。
def train(self, sentences, tag_lists):
"""
:param sentences: 由单个字组成的句列表,再由句列表构成全文列表
:param tag_lists: 由标签构成的列表(单一列表,无嵌套)
:return:
"""
features = [sent2features(s) for s in sentences]
# [{}, {}, {}, {}]每个大括号内都是一个字的特征
# {'w': '年', 'w-1': '两', 'w+1': '、', 'w-1:w': '两年', 'w:w+1': '年、', 'w-1:w:w+1': '两年、', 'bias': 1}
features = flatten_lists(features)
# print(features)
features = self.vec.fit_transform(features) # 将特征转化为特征矩阵 输入为字典的列表
print("featrues为:", features)
self.model.fit(features, np.array(tag_lists)) # fit用训练器数据拟合分类器模型 输入特征和标签列表
def test(self, sentences): #输入多个句子的列表
# features = [sent2features(s) for s in sentences] # s为一个字列表
pred_tag_lists = []
for s in sentences:
feature = sent2features(s)
feature = self.vec.transform(feature)
result = list(self.model.predict(feature))
pred_tag_lists.append(result)
# features = flatten_lists(features)
# features = self.vec.transform(features)
# pred_tag_lists = self.model.predict(features)
return pred_tag_lists
me_model = MEModel()
train_tag_lists = flatten_lists(train_tag_lists)
me_model.train(train_word_lists, train_tag_lists)
pred_tag_lists = me_model.test(test_word_lists) #输入一个句子列表(句子也是由字列表构成)
print(pred_tag_lists)