做者:zhbzz2007 出处:http://www.cnblogs.com/zhbzz2007 欢迎转载,也请保留这段声明。谢谢!html
在 结巴分词2--基于前缀词典及动态规划实现分词 博文中,博主已经介绍了基于前缀词典和动态规划方法实现分词,可是若是没有前缀词典或者有些词不在前缀词典中,jieba分词同样能够分词,那么jieba分词是如何对未登陆词进行分词呢?这就是本文将要讲解的,基于汉字成词能力的HMM模型识别未登陆词。python
利用HMM模型进行分词,主要是将分词问题视为一个序列标注(sequence labeling)问题,其中,句子为观测序列,分词结果为状态序列。首先经过语料训练出HMM相关的模型,而后利用Viterbi算法进行求解,最终获得最优的状态序列,而后再根据状态序列,输出分词结果。git
序列标注,就是将输入句子和分词结果看成两个序列,句子为观测序列,分词结果为状态序列,当完成状态序列的标注,也就获得了分词结果。github
以“去北京大学玩”为例,咱们知道“去北京大学玩”的分词结果是“去 / 北京大学 / 玩”。对于分词状态,因为jieba分词中使用的是4-tag,所以咱们以4-tag进行计算。4-tag,也就是每一个字处在词语中的4种可能状态,B、M、E、S,分别表示Begin(这个字处于词的开始位置)、Middle(这个字处于词的中间位置)、End(这个字处于词的结束位置)、Single(这个字是单字成词)。具体以下图所示,“去”和“玩”都是单字成词,所以状态就是S,“北京大学”是多字组合成的词,所以“北”、“京”、“大”、“学”分别位于“北京大学”中的B、M、M、E。算法
关于HMM模型的介绍,网络上有不少的资源,好比 52nlp整理的 HMM相关文章索引 。博主在此就再也不具体介绍HMM模型的原理,可是会对分词涉及的基础知识进行讲解。网络
HMM模型做的两个基本假设:app
1.齐次马尔科夫性假设,即假设隐藏的马尔科夫链在任意时刻t的状态只依赖于其前一时刻的状态,与其它时刻的状态及观测无关,也与时刻t无关;函数
P(states[t] | states[t-1],observed[t-1],...,states[1],observed[1]) = P(states[t] | states[t-1]) t = 1,2,...,T源码分析
2.观测独立性假设,即假设任意时刻的观测只依赖于该时刻的马尔科夫链的状态,与其它观测和状态无关,学习
P(observed[t] | states[T],observed[T],...,states[1],observed[1]) = P(observed[t] | states[t]) t = 1,2,...,T
HMM模型有三个基本问题:
其中,jieba分词主要中主要涉及第三个问题,也即预测问题。
HMM模型中的五元组表示:
这里仍然以“去北京大学玩”为例,那么“去北京大学玩”就是观测序列。
而“去北京大学玩”对应的“SBMMES”则是隐藏状态序列,咱们将会注意到B后面只能接(M或者E),不可能接(B或者S);而M后面也只能接(M或者E),不可能接(B或者S)。
状态初始几率表示,每一个词初始状态的几率;jieba分词训练出的状态初始几率模型以下所示。其中的几率值都是取对数以后的结果(可让几率相乘转变为几率相加),其中-3.14e+100表明负无穷,对应的几率值就是0。这个几率表说明一个词中的第一个字属于{B、M、E、S}这四种状态的几率,以下能够看出,E和M的几率都是0,这也和实际相符合:开头的第一个字只多是每一个词的首字(B),或者单字成词(S)。这部分对应jieba/finaseg/prob_start.py,具体能够进入源码查看。
P={'B': -0.26268660809250016, 'E': -3.14e+100, 'M': -3.14e+100, 'S': -1.4652633398537678}
状态转移几率是马尔科夫链中很重要的一个知识点,一阶的马尔科夫链最大的特色就是当前时刻T = i的状态states(i),只和T = i时刻以前的n个状态有关,即{states(i-1),states(i-2),...,states(i-n)}。
再看jieba中的状态转移几率,其实就是一个嵌套的词典,数值是几率值求对数后的值,以下所示,
P={'B': {'E': -0.510825623765990, 'M': -0.916290731874155}, 'E': {'B': -0.5897149736854513, 'S': -0.8085250474669937}, 'M': {'E': -0.33344856811948514, 'M': -1.2603623820268226}, 'S': {'B': -0.7211965654669841, 'S': -0.6658631448798212}}
P['B']['E']表明的含义就是从状态B转移到状态E的几率,由P['B']['E'] = -0.5897149736854513,表示当前状态是B,下一个状态是E的几率对数是-0.5897149736854513,对应的几率值是0.6,相应的,当前状态是B,下一个状态是M的几率是0.4,说明当咱们处于一个词的开头时,下一个字是结尾的几率要远高于下一个字是中间字的几率,符合咱们的直觉,由于二个字的词比多个字的词更常见。这部分对应jieba/finaseg/prob_trans.py,具体能够查看源码。
状态发射几率,根据HMM模型中观测独立性假设,发射几率,即观测值只取决于当前状态值,也就以下所示,
P(observed[i],states[j]) = P(states[j]) * P(observed[i] | states[j])
其中,P(observed[i] | states[j])就是从状态发射几率中得到的。
P={'B': {'一': -3.6544978750449433, '丁': -8.125041941842026, '七': -7.817392401429855, ... 'S': {':': -15.828865681131282, '一': -4.92368982120877, '丁': -9.024528361347633, ...
P['B']['一']表明的含义就是状态处于'B',而观测的字是‘一’的几率对数值为P['B']['一'] = -3.6544978750449433。这部分对应jieba/finaseg/prob_emit.py,具体能够查看源码。
Viterbi算法其实是用动态规划求解HMM模型预测问题,即用动态规划求几率路径最大(最优路径)。这时候,一条路径对应着一个状态序列。
根据动态规划原理,最优路径具备这样的特性:若是最优路径在时刻t经过结点 \(i_{t}^{*}\) ,那么这一路径从结点 \(i_{t}^{*}\) 到终点 \(i_{T}^{*}\) 的部分路径,对于从 \(i_{t}^{*}\) 到 \(i_{T}^{*}\) 的全部可能的部分路径来讲,必须是最优的。由于假如不是这样,那么从 \(i_{t}^{*}\) 到 \(i_{T}^{*}\) 就有另外一条更好的部分路径存在,若是把它和从 \(i_{t}^{*}\) 到达 \(i_{T}^{*}\) 的部分路径链接起来,就会造成一条比原来的路径更优的路径,这是矛盾的。依据这个原理,咱们只须要从时刻t=1开始,递推地计算在时刻t状态i的各条部分路径的最大几率,直至获得时刻t=T状态为i的各条路径的最大几率。时刻t=T的最大几率就是最优路径的几率 \(P^{*}\) ,最优路径的终结点 \(i_{T}^{*}\) 也同时获得。以后,为了找出最优路径的各个结点,从终结点 \(i_{T}^{*}\) 开始,由后向前逐步求得结点 \(i_{T-1}^{*},...,i_{1}^{*}\) ,最终获得最优路径 \(I^{*}=(i_{1}^{*},i_{2}^{*},...,i_{T}^{*})\) 。
由Viterbi算法获得状态序列,也就能够根据状态序列获得分词结果。其中状态以B开头,离它最近的以E结尾的一个子状态序列或者单独为S的子状态序列,就是一个分词。以”去北京大学玩“的隐藏状态序列”SBMMES“为例,则分词为”S / BMME / S“,对应观测序列,也就是”去 / 北京大学 / 玩”。
jieba分词中HMM模型识别未登陆词的源码目录在jieba/finalseg/下,
__init__.py 实现了HMM模型识别未登陆词;
prob_start.py 存储了已经训练好的HMM模型的状态初始几率表;
prob_trans.py 存储了已经训练好的HMM模型的状态转移几率表;
prob_emit.py 存储了已经训练好的HMM模型的状态发射几率表;
HMM模型的参数是如何训练出来,此处能够参考jieba中Issue 模型的数据是如何生成的? #7,以下是jieba的开发者的解释:
来源主要有两个,一个是网上能下载到的1998人民日报的切分语料还有一个msr的切分语料。另外一个是我本身收集的一些txt小说,用ictclas把他们切分(可能有必定偏差),而后用python脚本统计词频。
要统计的主要有三个几率表:1)位置转换几率,即B(开头),M(中间),E(结尾),S(独立成词)四种状态的转移几率;2)位置到单字的发射几率,好比P("和"|M)表示一个词的中间出现”和"这个字的几率;3) 词语以某种状态开头的几率,其实只有两种,要么是B,要么是S。
jieba分词会首先调用函数cut(sentence),cut函数会先将输入句子进行解码,而后调用__cut函数进行处理。__cut函数就是jieba分词中实现HMM模型分词的主函数。__cut函数会首先调用viterbi算法,求出输入句子的隐藏状态,而后基于隐藏状态进行分词。
def __cut(sentence): global emit_P # 经过viterbi算法求出隐藏状态序列 prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P) begin, nexti = 0, 0 # print pos_list, sentence # 基于隐藏状态序列进行分词 for i, char in enumerate(sentence): pos = pos_list[i] # 字所处的位置是开始位置 if pos == 'B': begin = i # 字所处的位置是结束位置 elif pos == 'E': # 这个子序列就是一个分词 yield sentence[begin:i + 1] nexti = i + 1 # 单独成字 elif pos == 'S': yield char nexti = i + 1 # 剩余的直接做为一个分词,返回 if nexti < len(sentence): yield sentence[nexti:]
首先先定义两个变量, \(\delta,\psi\),定义在时刻t状态i的全部单个路径 \((i_{1},i_{2},...,i_{t})\) 中几率最大值为
\(\delta_{t}(i) = max_{i_{1},i_{2},..,i_{n}}P(i_{t} = i,i_{t-1},...,i_{1},o_{t},...,o_{1}|\lambda), i = 1,2,...,N\)
由此可得变量 \(\delta\) 的递推公式为,
\(\delta_{t+1}(i) = max_{i_{1},i_{2},..,i_{n}}P(i_{t+1} = i,i_{t},...,i_{1},o_{t+1},...,o_{1}|\lambda)\)
\(=max_{1\le j \le N}[\delta_{t}(j)*a_{ji}]*b_{i}(o_{t+1}),i = 1,2,...,N,t = 1,2,...,N-1\)
定义在时刻t状态i的全部单个路径 \((i_{1},i_{2},...,i_{t-1},i)\) 中几率最大的路径的第t-1个结点为,
\(\psi_{t}(i) = argmax_{1 \le j \le N}[\delta_{t-1}(j)*a_{ji}]\)
Viterbi算法的大体流程:
输入:模型 \(\lambda =(A,B,\pi)\) 和观测序列 \(O=(o_{1},o_{2},...,o_{T})\) ;
输出:最优路径 \(I^{*}=(i_{1}^{*},i_{2}^{*},...,i_{T}^{*})\);
(1)初始化
\(\delta_{1}(i) = \pi_{i} * b_{i}(o_{1}),i = 1,2,...,N\)
\(\psi_{1}(i) = 0,i = 1,2,...,N\)
(2)递推
\(\delta_{t}(i) = max_{1\le j \le N}[\delta_{t-1}(j)*a_{ji}]*b_{i}(o_{t}),i = 1,2,...,N\)
\(\psi_{t}(i) = argmax_{1 \le j \le N}[\delta_{t-1}(j)*a_{ji}],,i = 1,2,...,N\)
(3)终止
\(P^{*} = max_{1\le j \le N}\delta_{T}(i)\)
\(i_{T}^{*} = argmax_{1 \le i \le N}[\delta_{T}(i)]\)
(4)最优路径回溯,对于t=T-1,T-2,...,1,
\(i_{t}^{*} = \psi_{t+1}(i)\)
最终求得最优路径 \(I^{*}=(i_{1}^{*},i_{2}^{*},...,i_{T}^{*})\) ;
jieba分词实现Viterbi算法是在viterbi(obs, states, start_p, trans_p, emit_p)函数中实现。viterbi函数会先计算各个初始状态的对数几率值,而后递推计算,每时刻某状态的对数几率值取决于上一时刻的对数几率值、上一时刻的状态到这一时刻的状态的转移几率、这一时刻状态转移到当前的字的发射几率三部分组成。
def viterbi(obs, states, start_p, trans_p, emit_p): V = [{}] # tabular path = {} # 时刻t = 0,初始状态 for y in states: # init V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT) path[y] = [y] # 时刻t = 1,...,len(obs) - 1 for t in xrange(1, len(obs)): V.append({}) newpath = {} # 当前时刻所处的各类可能的状态 for y in states: # 获取发射几率对数 em_p = emit_p[y].get(obs[t], MIN_FLOAT) # 分别获取上一时刻的状态的几率对数,该状态到本时刻的状态的转移几率对数,本时刻的状态的发射几率对数 # 其中,PrevStatus[y]是当前时刻的状态所对应上一时刻可能的状态 (prob, state) = max( [(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]]) V[t][y] = prob # 将上一时刻最优的状态 + 这一时刻的状态 newpath[y] = path[state] + [y] path = newpath # 最后一个时刻 (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES') # 返回最大几率对数和最优路径 return (prob, path[state])
相关优化:
1.将每一时刻最优几率路径记录下来,避免了第4步的最优路径回溯;
2.提早创建一个当前时刻的状态到上一时刻的状态的映射表,记录每一个状态在前一时刻的可能状态,下降计算量;以下所示,当前时刻的状态是B,那么前一时刻的状态只多是(E或者S),而不多是(B或者M);
PrevStatus = {
'B': 'ES',
'M': 'MB',
'S': 'SE',
'E': 'BM'
}