前言
自然语言中每句话都由若干个词语组成,相较于英文而言,中文的分词显然难上许多。中文的词语之间没有像英文的空格一样的标记符来区分词语,此外,中文词语的开放性也使得中文分词很难有一个统一的标准,如何提高分词的正确率,已成为当下研究的一个焦点。
这篇博客将会介绍迄今为止一些主流的分词算法及相关的代码实现。文章较长,请按需阅读。废话不多说,以下是本篇文章正文内容。
一、中文分词的痛点
1.1 中文的歧义性
举两个例子,例如在这句话中“南京市长江大桥”,我们即可划分为“南京市/长江大桥”,也可划分为“南京/市长/江/大桥”。又或者是这样一句话:“羽毛球拍卖光了”,即可划分为“羽毛球拍/卖/光了”,也可以划分为“羽毛球/拍卖/光了”。很显然,在没有具体语境的情况下,两种划分都是可行的。这就给中文分词的歧义消除带来了很大的麻烦。
1.2 识别未登录词
首先介绍一下什么是未登录词,未登陆词是指没有收录到词典中的词。传统的分词方法都是基于规则的匹配分词方法。首先会有一个收录了大量词语的中文词典,然后利用正向匹配或是逆向匹配算法进行词语划分。但是随着人们语言的需要,新的词组接连不断的出现在语料中。如果未登录词没有出现在词典中,就会导致错分或者漏分,使得最终的分词准确率大大降低。因此,未登录词识别是提高分词效率的重点之一。
二、基于规则的分词算法
在中文分词中,通过将待分词的句子与词典中的词语进行逐一匹配,进而将句子划分为多个词的分词方法,我们称之为基于规则的分词方法(或基于词典的分词方法、机械分词方法)。
2.1 切分方式
按照匹配词语的切分方式,主要可以分为正向匹配法、逆向匹配法以及双向匹配法。
2.1.1 正向匹配法
顾名思义,正向匹配法就是按照句子的正向序列,从句子的第一个字符开始,逐一进行匹配。正向匹配法中,以正向最大匹配法(Maximum Match Method,MM法)最为典型。其基本思想为:假设分词词典中的最长词有i个字符,则利用被处理句子中的前i个字作为匹配字段,查找字典。若字典中存在这样的词,则匹配成功,匹配字段被切分出来;若不匹配,则将匹配字段的最后一个字去掉,重新进行匹配,重复下去,直到匹配成功,即切分出一个词或剩下的长度为0为止,这样就完成了一轮匹配。然后取下一个长度为i的字符串进行匹配处理,直到文档被扫描完为止。
2.1.2 逆向匹配法
同理,逆向匹配法就是从句子的最后一个字符开始,逐一进行匹配。由于汉语中偏正结构较多,若从后向前匹配,可以适当的提高京都。所以逆向最大匹配法(Reverse Maximun Match Method)比正向最大匹配法的误差要小一点。
2.1.3 双向匹配法
双向最大匹配法(Bi-direction Match Method)则是结合了正向和逆向匹配,选取其中匹配结果串数目少的作为最终的匹配结果。当然,值得注意的是, 当匹配结果切分串数目相同时,则选取切分串中,单字最少的为最终结果。
话不多说,下面是简单的demo演示:
# 正向最大匹配算法
class MM(object):
def __init__(self):
self.window_size = 4
def search(self,text,dic):
result = []
index = 0
text_length = len(text)
while text_length > index:
for size in range (self.window_size + index , index , -1) :
piece = text[index:size]
if piece in dic:
index=size-1
break
index = index +1
result.append(piece)
return result
# 逆向最大匹配算法
class RMM(object):
def __init__(self):
self.window_size = 4
def search(self,text,dic):
result=[]
index = len(text)
text_length = len(text)
while index > 0 :
for size in range(index-self.window_size,index,1):
piece = text[size:index]
if piece in dic :
index = size + 1
break
index = index - 1
result.append(piece)
result.reverse()
return result
#双向最大匹配法
class BiMM(object):
def __init__(self):
self.mm = MM()
self.rmm = RMM()
def search(self,text,dic):
result = []
temp1 = self.mm.search(text,dic)
temp2 = self.rmm.search(text,dic)
if len(temp1)>len(temp2):
result = temp2
elif len(temp1)<len(temp2):
result = temp1
else:
#分词数相同的情况下,若分词结果相同,则返回任意一个,否则返回单字数目少的那一个
if temp1 == temp2:
result = temp1
else:
len_single_temp1 = 0;
len_single_temp2 = 0;
for length in range (len(temp1)):
if len(temp1[length])==1:
len_single_temp1 = len_single_temp1+1
if len(temp2[length]) == 1:
len_single_temp2 = len_single_temp2 + 1
if len_single_temp1 > len_single_temp2:
result = temp2
else:
result = temp1
return result
if __name__ == '__main__':
text1 = '南京市长江大桥'
text2 = '研究生命的起源'
dic = ['南京市', '长江大桥', '南京', '市长', '江', '大桥','研究','研究生','生命','命','的','起源']
tokenizerMM = MM()
tokenizerRMM = RMM()
tokenizerBiMM = BiMM()
print('token : 南京市长江大桥')
print('MM : ')
print(tokenizerMM.search(text1,dic))
print('RMM : ')
print(tokenizerRMM.search(text1, dic))
print('BiMM : ')
print(tokenizerBiMM.search(text1,dic))
print('----------------------------')
print('token : 研究生命的起源')
print('MM : ')
print(tokenizerMM.search(text2, dic))
print('RMM : ')
print(tokenizerRMM.search(text2, dic))
print('BiMM : ')
print(tokenizerBiMM.search(text2, dic))
下面是算法的执行结果:
token : 南京市长江大桥
MM :
['南京市', '长江大桥']
RMM :
['南京市', '长江大桥']
BiMM :
['南京市', '长江大桥']
----------------------------
token : 研究生命的起源
MM :
['研究生', '命', '的', '起源']
RMM :
['研究', '生命', '的', '起源']
BiMM :
['研究', '生命', '的', '起源']
研究表明,99%的中文句子在使用匹配算法进行切分时,必定有一个是正确的,只有不到1%的切分结果是错误的。这也正是双向匹配算法在中文信息处理系统中得以广泛应用的原因。
2.2 词典机制
机械的分词算法中第二个重点就是词典机制。词典的结构机制会直接影响到基于词典的分词算法的匹配效率。为了提高分词效率,设计一个‘优秀’的词典机制是十分重要的。当然建立一个‘优秀’的词典,也不是一件容易的事。下面简单介绍一下部分研究成果。
清华大学孙茂松教授等通过实验对比了整词二分,Trie索引术及逐字二分这3种典型的分词词典机制,结果表明,基于逐字二分的分词词典机制是一种简洁、高效的词典阻止模式;李庆虎等在3种典型的词典基础上,提出一种双字哈希的词典机制,进一步提高了自动分词的时间效率。赵欢等提出了一种改进的双数组Trie索引树分词算法。莫建文等对中文分词词典的双字哈希结构及最大匹配算法进行改进,在一定程度上增加了正向最大匹配的正确率。此外,在词典中建立词长度索引表也可以大大减少匹配次数,提高了分词效率。
词典是中文分词不可或缺的一部分。尽管经过学者的不断完善和改进,提高了分词的效率。但是,单独依赖现有的词典,在各个领域上进行分词明显存在缺陷,尤其是歧义消除和未登录词的识别问题难以得到解决。
三、基于统计的分词算法
随着大规模语料库的建立,统计机器学习方法的研究和发展,基于统计的中文分词算法开始逐渐成为主流。
首先介绍一下何为基于统计的分词算法。在中文句子中,虽然没有任何的分词标记符,但是词由汉字组成,相邻的汉字或者相邻的词能否组合成词,可以利用相邻汉字在语料库中出现的频率来计算概率,作为判断的依据。基于统计的分词算法通过训练大量已经过人工分词的语料库获取经验信息,建立一个能反映响铃汉字间互信度的概率模型,从而来识别词并进行词语的划分。
这里介绍两种经典的基于统计的分词模型和其代码实现:
3.1 HMM(Hidden Markov Model,隐马尔可夫模型)
3.1.1 HMM概念及推导
隐马尔可夫模型是一种具有隐含状态的马尔可夫模型,它是相较于马尔可夫模型来说的。我们将句子中的每一个单词作一个隐含状态划分,其隐含状态集合为O,包含4种隐藏状态信息(O:{B,M,E,S},其中B表示词首,M表示词中,E表示词尾,S表示单独成词)。比如有如下句子:“我爱自然语言处理”,我们标记出其隐含状态则是:“我/S 爱/S 自/B 然/M 语/M 言/M 处/M 理/E”。如果在统计学角度来看这个问题,用数学抽象表示如下:用 λ = λ 1 λ 2 . . . λ n \lambda = \lambda_1\lambda_2...\lambda_n λ=λ1λ2...λn来表示输入的句子,n为句子的长度, λ i \lambda_i λi表示第i个字, o = o 1 o 2 . . . o n o=o_1o_2...o_n o=o1o2...on表示输出状态序列,那么理想的输出就是: m a x = m a x P ( o 1 o 2 . . . o n ∣ λ 1 λ 2 . . . λ n ) max=maxP(o_1o_2...o_n|\lambda_1\lambda_2...\lambda_n) max=maxP(o1o2...on∣λ1λ2...λn),其表示在句子序列为 λ \lambda λ时,最有可能的输出序列 o o o。则通过这个语言模型,原问题则转化为求max P ( o ∣ λ ) P(o|\lambda) P(o∣λ)的问题。
需要注意的是, P ( o ∣ λ ) P(o|\lambda) P(o∣λ)是关于2n个变量的条件概率,且n是不固定的,因此,几乎无法进行精确计算。此时我们引入贝叶斯公式:将P进行如下变换: P ( o ∣ λ ) = P ( o , λ ) P ( λ ) = P ( λ ∣ o ) P ( o ) P ( λ ) P(o|\lambda)=\frac {P(o,\lambda)}{P(\lambda)}=\frac {P(\lambda|o)P(o)}{P(\lambda)} P(o∣λ)=P(λ)P(o,λ)=P(λ)P(λ∣o)P(o)其中:
P ( o , λ ) P(o,\lambda) P(o,λ)为联合概率,即状态 o o o和 λ \lambda λ同时出现的概率;
P ( λ ) P(\lambda) P(λ)为句子 λ \lambda λ在语料库中出现的概率,我们可以用频率代替概率,可以看做是常量;
所以:原公式则转换为求: P ( λ ∣ o ) P ( o ) P(\lambda|o)P(o) P(λ∣o)P(o)
这里,我们针对 P ( λ ∣ o ) P(\lambda|o) P(λ∣o)作观测独立性假设得到: P ( λ ∣ o ) = P ( λ 1 ∣ o 1 ) P ( λ 2 ∣ o 2 ) . . . P ( λ n ∣ o n ) P(\lambda|o) = P(\lambda_1|o_1)P(\lambda_2|o_2)...P(\lambda_n|o_n) P(λ∣o)=P(λ1∣o1)P(λ2∣o2)...P(λn∣on)
同时,针对 P ( o ) P(o) P(o)作齐次马尔科夫假设:t时刻的状态o,仅与其前一个状态有关,于是得到: P ( o ) = P ( o 1 ) P ( o 2 ∣ o 1 ) P ( o 3 ∣ o 2 ) . . . P ( o n ∣ o n − 1 ) P(o)=P(o_1)P(o_2|o_1)P(o_3|o_2)...P(o_n|o_{n-1}) P(o)=P(o1)P(o2∣o1)P(o3∣o2)...P(on∣on−1)
代入原式得到HMM模型: P ( λ ∣ o ) P ( o ) = P ( λ 1 ∣ o 1 ) P ( λ 2 ∣ o 2 ) . . . P ( λ n ∣ o n ) P ( o 1 ) P ( o 2 ∣ o 1 ) P ( o 3 ∣ o 2 ) . . . P ( o n ∣ o n − 1 ) P(\lambda|o)P(o) = P(\lambda_1|o_1)P(\lambda_2|o_2)...P(\lambda_n|o_n)P(o_1)P(o_2|o_1)P(o_3|o_2)...P(o_n|o_{n-1}) P(λ∣o)P(o)=P(λ1∣o1)P(λ2∣o2)...P(λn∣on)P(o1)P(o2∣o1)P(o3∣o2)...P(on∣on−1)
在HMM模型中,将 P ( λ k ∣ o k ) P(\lambda_k|o_k) P(λk∣ok)称谓发射概率,由发射概率组成的矩阵称为发射矩阵,我们用B来表示;将 P ( o k ∣ o k − 1 ) P(o_k|o_{k-1}) P(ok∣ok−1)称为状态转移概率,其组成的矩阵为状态转移矩阵,我们用A来表示; P ( o 1 ) P(o_1) P(o1)我们称之为初始状态概率,由 π \pi π来表示;由此,我们的HMM模型则可以表示为:HMM = λ ( π , A , B ) \lambda(\pi,A,B) λ