中文分词任务,采用的是Sighan2004(backoff2005微软数据)数据。给出训练集和测试集,对测试集进行中文分词,要求给出的分词结果F-score尽量大。
首先是两个naive版本,但是对于这个特定的任务,虽然naive版本原理实现简单,但是效果却很好。
以选出匹配的单词尽可能长为目标分词,具体操作是从一个方向不断尝试匹配出最长单词,再进行下一次匹配,直到匹配完成为止。
同样以选出匹配的单词尽可能长为目标分词,具体操作可以给分词结果打分,找一个合适的打分函数下,匹配单词较长且单词出现次数较多则比较合算,同时希望求解打分函数最大值的复杂度比较低。
在上一篇report中详细介绍过我的双向最大匹配实现方法,原理简单但是效果很好,是我目前能实现效果最好的一个版本。
下面有四个基于概率的版本,算法相对高级一点。
*这四个版本都把分词结果看作汉字的4-tag序列(‘S’单字,’B’词首,’E’词尾,’M’词中),那么分词转化为找到一个最合适的序列。
最大熵模型基于4-tag,并且使得分词留下信息量最大的信念来做,用到了信息论里的一些理论基础。
CRF模型基于4-tag的随机向量场方法,用到了一些概率方法。
与CRF相似的,HMM也是基于4-tag的概率模型,HMM模型找出概率最大的一个4-tag序列作为分词结果,其中的转移概率由训练获得。
读到有人写的源代码,但是具体还没有仔细学习过。等学习了自己实现一个专门写一篇report来阐述原理。
设 l 为序列长度, tag[0:l] 为tag序列, char[0:l] 为character序列,其中 [:] 切片用法和python一样为左闭右开区间。
如果画出状态转移图像,发现状态只能在层间点转移,转移路径上的分数为概率,求一条概率最大的路径。这可以用维特比算法计算,本质上就是一个动态规划,属于很简单的算法问题。以下为我的实现。
def mostPossibleTag(char):
char = numberFormatter(char)
l = len(char)
if l == 0:
return []
p0 = {t: getStartPro(char[0], t) for t in TAG}
tag0 = {t: [t] for t in TAG}
for i in range(1, l):
p = {t: 0 for t in TAG}
tag = {t: [] for t in TAG}
for t in TAG:
for prevT in TAG:
tranP = p0[prevT] * getTranPro(char[i-1], prevT, char[i], t)
if tranP > p[t]:
p[t] = tranP
tag[t] = tag0[prevT] + [t]
p0 = p
tag0 = tag
maxP = -1
maxT = ''
for t in TAG:
tranP = p0[t] * getEndPro(char[l-1], t)
if tranP > maxP:
maxP = tranP
maxT = t
return tag0[maxT]
可以给一些字典外的字一个平均水平的估计值。例如吃馕,馕是一个生字,那么馕就作为吃后面接的字的平均水平。那么通过吃饭,吃醋等高概率出现词汇大致可以学习出吃馕是’B‘->’E‘。
但是对一些非法转移序列必须给出0分的概率(例如’S’->’M’之类的)。
还有一种情况,对于字典里常见字但是从没见过用法的组合,应该给一个很低的惩罚估计概率,例如吃饭,这个搭配很常见,但是‘E’->’B’这个组合可能从来没见过,所以再计算这个组合的时候应该给一个惩罚低概率,但是不能为0。
我的实现不是特别仔细,只大致实现了惩罚情况,仔细的实现边界条件不好处理,一直出现错误,我后来索性try except,一出现错误(汉字,用法没见过,用法次数为0之类的未登录问题)就开启惩罚估计。
因为标点符号必然独立成词,所以可以切开标点符号再对于每一小句切词,可以降低句子长度。
如果句子比较长,概率会很小,应该取对数来计算,但是我的代码没有,因为经过了3.的切分,句子不是很长了。
模型 | F-score |
---|---|
HMM模型 | 0.915 |
双向最大匹配 | 0.951 |
最大熵模型 | 0.839 |
CRF模型 | 0.964 |
本来很早就想自己实现一个HMM模型了,但是看了别人的代码和文章一直觉得自己不行。真的动手实现了,原来HMM也没有我想象中的那么难(虽然我的最终版本没有发挥出HMM的完全威力,而且写的时候边界情况也够呛)。所以不要自己给设置障碍,不要一直告诉自己自己不行。与诸君共勉。