中文分词学过CRF,RNN等,基于压缩感知机的中文分词还没接触过,发现了一篇很好的压缩感知机的中文分词博客,http://www.hankcs.com/nlp/segment/implementation-of-word-segmentation-device-java-based-on-structured-average-perceptron.html,作者用的是java的实现,由于本人对python比较熟悉,在此简单记录一下基于压缩感知机的中文分词及其的python代码的学习笔记,代码参考地址:https://github.com/zhangkaixu/minitools/blob/master/cws.py
原理为对于每个字ci,取其7个特征,分别为ci,ci-1,ci+1,ci-2ci-1,ci-1ci,cici+1,ci+1ci+2,特征生成代码如下:
def update(self, x, y, delta): # 更新权重
for i, features in zip(range(len(x)), self.gen_features(x)):
for feature in features:
self.weights.update_weights(str(y[i]) + feature, delta)#每个特征为tag+word
for i in range(len(x) - 1):
self.weights.update_weights(str(y[i]) + ':' + str(y[i + 1]), delta)#更新相邻两个tag的特征的权重
因此,每个字可以生成7个特征,这7个特征与该字的前后两个字相关,同时在分词的时候,我们需要对每个词分类,例如B、M、E、S,分别对于0,1,2,3数字,我们生成每两个标签的特征,例如01,00,11,13,...。这样总共生成了N组个特征,我们对每个特征分配一个权重,在训练的时候,若分类结果正确,则对于的特征权重+1,分类错误,则特征权重-1。
在训练的时候,对于输入句子x,和分类结果y,我们首先通过genfeature(x)函数生成句子中每个字的特征向量,之后根据每个字的每个特征向量(7个),更新每个字的权重,代码如下:
def update(self, x, y, delta): # 更新权重
for i, features in zip(range(len(x)), self.gen_features(x)):
for feature in features:
self.weights.update_weights(str(y[i]) + feature, delta)#每个特征为tag+word
for i in range(len(x) - 1):
self.weights.update_weights(str(y[i]) + ':' + str(y[i + 1]), delta)#更新相邻两个tag的特征的权重
上面的代码可知,对于每个字,生成了7个特征,每个字可能有4个分类结果,因此将7个特征与4个分类结果组合,需要28个特征权重与其对应,同时对于分类结果tag,每相邻两个字的tag组合为一个特征,有16中组合结果,即转移特征transitions,也需要16个特征权重。根据分类结果我们可以根系每个特征对于的权重。特征权重更新代码如下:
def update_weights(self, key, delta): # 更新权重
if key not in self._values:
self._values[key] = 0
self._acc[key] = 0
self._last_step[key] = self._step
self.updateF[key]=1
else:
self._new_value(key)
self.updateF[key]+=1
self._values[key] += delta#特征更新权重
代码中key为特征,delta为更新不长,若分类正确,delta=1,错误则delta=-1。
训练:训练过程为首先对于输入句子x,采用viterbi 算法进行解码:
def decode(self, x): # 类似隐马模型的动态规划解码算法
# 类似隐马模型中的转移概率
transitions = [[self.weights.get_value(str(i) + ':' + str(j), 0) for j in range(4)]
for i in range(4)]
# 类似隐马模型中的发射概率
emissions = [[sum(self.weights.get_value(str(tag) + feature, 0) for feature in features)
for tag in range(4)] for features in self.gen_features(x)]
# 类似隐马模型中的前向概率
# if len(emissions)<1:
# print("x is ")
# print(x)
alphas = [[[e, None] for e in emissions[0]]]
for i in range(len(x) - 1):
alphas.append([max([alphas[i][j][0] + transitions[j][k] + emissions[i + 1][k], j]
for j in range(4))
for k in range(4)])
# 根据alphas中的“指针”得到最优序列
alpha = max([alphas[-1][j], j] for j in range(4))
i = len(x)
tags = []
while i:
tags.append(alpha[1])#先计算最后一个状态,再往前推
i -= 1
alpha = alphas[i][alpha[1]]
return list(reversed(tags))
首先初始化tag的16中转移特征的权重即transition矩阵为0,生成句子x的特征,我们用self.values保存特征权重,刚开使时self.values={}为空,对于每个特征f,若其不在self.values中,则将其添加进self.values中,且初始化self.values[f]=0。解码阶段,生成x的特征之后,便可以查找self.values中特征的权重,得到转移矩阵trainsiton,发射矩阵emissions,再根据viterbi算法,解码得到预测值z.
得到y_pred后便可以更新self.values,代码如下:
if z != y:
cws.update(x, y, 1)
cws.update(x, z, -1)
代码中,z为预测值,y为输入正确值,若预测值语正确值不想等,则更新两次权重,对y,更新权重delta=1,对于错误分类值z,更新权重delta=-1。
在预测阶段,我们便可以直接对输入句子x,生成特征,得到t转移矩阵trainsiton,发射矩阵emissions,再采用viterbi算法解码得到预测值y。
总结,在训练的时候,由于对于句子中的每个字会产生7个特征,对于同一个字,若其前后2个字不同,则产生的特征又不同,这样会产生很多特征,而很多时候特征分词的时候是不需要用到的,保留这些无用的特征会需要很大的空间,因此通常会想到对特征进行压缩。
邓知龙 《基于感知器算法的高效中文分词与词性标注系统设计与实现》中有提到对模型进行压缩,具体方法是在更新每个特征权重的时候,记录每个特征权重更新的次数,对于分词结果影响重要的特征,我们在训练的时候对其的更新当然比较频繁,其更新次数会较大,而另一些影响较小的特征其更新次数较小,因此我们可以将更新次数较小的特征去除,从而压缩模型。