中文分词
最近工作中碰到很多query分析的技术,包括query的分词,纠错,改写,同义词,权重等等常见的自然语言处理任务,在这里简单的记录一下这些任务的常见算法,持续更新。
1:中文分词
传统中文分词技术根据实现原理和特点,可以分为以下2个类别:
1、基于词典分词算法
也称字符串匹配分词算法。该算法是按照一定的策略将待匹配的字符串和一个已建立好的“充分大的”词典中的词进行匹配,若找到某个词条,则说明匹配成功,识别了该词。常见的基于词典的分词算法分为以下几种:正向最大匹配法、逆向最大匹配法和双向匹配分词法等。
正向最大匹配从最左字符开始,如果匹配就向右增加一个字符,继续匹配,如果未匹配,则切分该词。而逆向最大匹配从最右边开始选取最大长度的子串,如果未匹配,去掉最左边单词,继续匹配。
例子(正序最大匹配):’我一个人吃饭’
round 1
我 =》词典存在,匹配成功,增加一个字符
我一 =》 不存在, 返回我
一 =》存在,增加字符
一个 =》 存在,增加字符
一个人 =》 存在
一个人吃 =》 不存在,返回一个人
吃 =》 存在,增加
吃饭 =》 存在
分词结果:我/一个人/吃饭
例子(逆序最大匹配):’我一个人吃饭’
反向最大匹配方式,最大长度为5
round 1
个人吃饭
人吃饭
吃饭 ====》得到一个词– 吃饭
round 2
我一个人
一个人
个人 ====》得到一个词– 个人
round 3
我一
一 ====》得到一个词– 一
round 4
我 ====》得到一个词– 我
最后反向最大匹配的结果是:
/我/一/个人/吃饭/
https://zhuanlan.zhihu.com/p/103392455对于最大匹配算法有比较好的解释,下面的代码用python实现了前向最大匹配和逆向最大匹配:
def bmmseg(sen,max,strs):
'''
sen: 待切分句子
max: 最大切分长度
strs: 词典列表
'''
maxs=max
returnlist=[]
while len(sen)>0:
while max>0:
if(max==1):
returnlist.append(sen[-1:])
sen=sen[:-1]
max=maxs
break
else:
word=sen[-max:]
if(word in strs):
returnlist.append(word)
sen=sen[:-max]
max=maxs
break
else:
max-=1
return returnlist
def fmmseg(sen,max,strs):
'''
sen: 待切分句子
max: 最大切分长度
strs: 词典列表
'''
maxs=max
returnlist=[]
while len(sen)>0:
while max>0:
if(max==1):
returnlist.append(sen[:1])
sen=sen[1:]
max=maxs
break
else:
word=sen[:max]
if(word in strs):
returnlist.append(word)
sen=sen[max:]
max=maxs
break
else:
max-=1
return returnlist
此外,基于前向和逆向最大匹配,我们可以实现双向最大匹配算法。双向最大匹配算法的难点主要在于如何利用前向结果和逆向结果,有的算法使用了这样的规则:
双向最大匹配算法:
一般是综合考虑了正向和逆向最大匹配的结果,加入了一些启发式的规则来对分词结果进行进一步消歧的。
启发式规则:
1.如果正反向分词结果词数不同,则取分词数量较少的那个。
2.如果分词结果词数相同
a.分词结果相同,就说明没有歧义,可返回任意一个。
b.分词结果不同,返回其中单字较少的那个。
https://blog.csdn.net/u012684062/article/details/78444323这篇博客介绍了一个基于双向最大匹配和N-gram的分词算法。
N-gram的作用主要是根据语料库的统计数据得到分词结果之间的条件概率,然后选择联合概率最大的分词结果,比如说『thenonprofit』可以被切分为the non profit和then on profit(引用自https://medium.com/@phylypo/nlp-text-segmentation-with-ngram-b5506dbb514c):
<s> the 258483382 the non 739031 non profit 218392 <s> then 11926082 then on 1263045 on profit 105801 P(the non profit) = P(the|<s>) * P(non|the) * P(profit|non) = P(<s> the) * P(the non) * P(non profit) = 258483382/t* 739031/t * 218392/t = 3.88E-17 P(then on profit) = P(then|<s>) * P(on|then) * P(profit|on) = P(<s> then) * P(then on) * P(on profit) = 11926082/t* 1263045/t * 105801/t = 1.48E-18
基于词典的分词算法是应用最广泛、分词速度最快的。很长一段时间内研究者都在对基于字符串匹配方法进行优化,比如最大长度设定、字符串存储和查找方式以及对于词表的组织结构,比如采用TRIE索引树、哈希索引等。
jieba分词里面的全模式就和最大前缀匹配方法类似,从此表里面选出最长的前缀:
def __cut_all(self, sentence):
'''
我来到北京清华大学
{0: [0], 1: [1, 2], 2: [2], 3: [3, 4], 4: [4], 5: [5, 6, 8], 6: [6, 7], 7: [7, 8], 8: [8]}
结果:我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学
算法:
完全根据dag产出,感觉和最大前缀匹配很类似,完全依赖于词典
'''
dag = self.get_DAG(sentence)
old_j = -1
eng_scan = 0
eng_buf = u''
for k, L in iteritems(dag):
if eng_scan == 1 and not re_eng.match(sentence[k]):
eng_scan = 0
yield eng_buf
if len(L) == 1 and k > old_j:
word = sentence[k:L[0] + 1]
if re_eng.match(word):
if eng_scan == 0:
eng_scan = 1
eng_buf = word
else:
eng_buf += word
if eng_scan == 0:
yield word
old_j = L[0]
else:
for j in L:
#去除当前词
if j > k:
yield sentence[k:j + 1]
old_j = j
if eng_scan == 1:
yield eng_buf
2、基于统计的机器学习算法
这类目前常用的是算法是HMM、CRF、SVM、深度学习等算法,比如stanford、Hanlp分词工具是基于CRF算法。以CRF为例,基本思路是对汉字进行标注训练,不仅考虑了词语出现的频率,还考虑上下文,具备较好的学习能力,因此其对歧义词和未登录词的识别都具有良好的效果。
参考:
jieba分词:jieba分词(动态规划),jieba分词详解