jieba中文分词源码分析(四)
一、未登錄詞問題
在jieba中文分詞的第一節曾提到未登錄詞問題
中文分詞的難點
分詞規范,詞的定義還不明確 (《統計自然語言處理》宗成慶)
歧義切分問題,交集型切分問題,多義組合型切分歧義等
結婚的和尚未結婚的 =>
結婚/的/和/尚未/結婚/的
結婚/的/和尚/未/結婚/的
未登錄詞問題
有兩種解釋:一是已有的詞表中沒有收錄的詞,二是已有的訓練語料中未曾出現過的詞,第二種含義中未登錄詞又稱OOV(Out of Vocabulary)。對于大規模真實文本來說,未登錄詞對于分詞的精度的影響遠超歧義切分。一些網絡新詞,自造詞一般都屬于這些詞。
因此可以看到,未登錄詞是分詞中的一個重要問題,jieba分詞中對于OOV的解決方法是:采用了基于漢字成詞能力的 HMM 模型,使用了 Viterbi 算法。
二、HMM
關于HMM的介紹網絡上有很多資源,比如 52nlp HMM系列,在此不再具體介紹了,但一些基礎知識要明確的:
HMM(Hidden Markov Model): 隱式馬爾科夫模型。
HMM模型可以應用在很多領域,所以它的模型參數描述一般都比較抽象,以下篇幅針對HMM的模型參數介紹直接使用它在中文分詞中的實際含義來講:
HMM解決的三類問題:
a. 評估問題(概率計算問題)
即給定觀測序列 O=O1,O2,O3…Ot和模型參數λ=(A,B,π),怎樣有效計算這一觀測序列出現的概率.
(Forward-backward算法)
b. 解碼問題(預測問題)
即給定觀測序列 O=O1,O2,O3…Ot和模型參數λ=(A,B,π),怎樣尋找滿足這種觀察序列意義上最優的隱含狀態序列S。
(viterbi算法,近似算法)
c. 學習問題
即HMM的模型參數λ=(A,B,π)未知,如何求出這3個參數以使觀測序列O=O1,O2,O3…Ot的概率盡可能的大.
(即用極大似然估計的方法估計參數,Baum-Welch,EM算法)
HMM 模型的五元組表示:
{
states,//狀態空間
observations,//觀察空間
start_probability,//狀態的初始分布,即π
transition_probability,//狀態的轉移概率矩陣,即A
emission_probability//狀態產生觀察的概率,發射概率矩陣,即B
}
三、jieba HMM 分析
使用jieba對句子:”到MI京研大廈”進行分詞,若是使用非HMM模式則分詞的結果為:
到/MI/京/研/大廈, 使用HMM分詞則結果為:到/MI/京研/大廈。下面一段是利用上一節的程序的計算結果。
"到MI京研大廈"的前綴字典:
到 205341
到M 0
到MI 0
到MI京 0
到MI京研 0
到MI京研大 0
到MI京研大廈 0
"到MI京研大廈"的DAG:
0 : [0]
1 : [1]
2 : [2]
3 : [3]
4 : [4]
5 : [5, 6]
6 : [6]
route:
{0: (-73.28491710434629, 0), 1: (-67.60579126740393, 1), 2: (-49.69423813964871, 2), 3: (-31.78268501189349, 3), 4: (-22.663377731606147, 4), 5: (-11.256112777387571, 6), 6: (-12.298425021367148, 6), 7: (0, 0)}
到/MI/京/研/大廈
...
Loading model cost 0.696 seconds.
Prefix dict has been built succesfully.
# HMM切分結果:
到/MI/京研/大廈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
從句子”到MI京研大廈”對應的前綴字典可以看出“京研”并沒有在字典中,但是也被Viterbi算法識別出來了,可以看出HMM的強大之處了,也正是 HMM 三大基本問題之一,即根據觀察序列,求隱藏狀態序列。
上一節中我們說明了HMM由五元組表示,那么這樣的五元組參數在中文分詞中的具體含義是:
states(狀態空間) & observations(觀察空間).
漢字按照BEMS四個狀態來標記,分別代表 Begin End Middle 和 Single, {B:begin, M:middle, E:end, S:single}。分別代表每個狀態代表的是該字在詞語中的位置,B代表該字是詞語中的起始字,M代表是詞語中的中間字,E代表是詞語中的結束字,S則代表是單字成詞。
觀察空間為就是所有漢字(我她…),甚至包括標點符號所組成的集合。
狀態值也就是我們要求的值,在HMM模型中文分詞中,我們的輸入是一個句子(也就是觀察值序列),輸出是這個句子中每個字的狀態值,用這四個狀態符號依次標記輸入句子中的字,可方便的得到分詞方案。 如:
觀察序列:我在北京
狀態序列:SSBE
對于上面的狀態序列,根據規則進行劃分得到 S/S/BE/
對應于觀察序列:我/在/北京/
分詞任務就完成了。
同時我們可以注意到:
B后面只可能接(M or E),不可能接(B or E)。而M后面也只可能接(M or E),不可能接(B, S)。
上文只介紹了五元組中的兩元 states & observations,下文介紹剩下的三元(start_probability,transition_probability,emission_probability).
start_probability(狀態的初始分布).
初始狀態概率分布是最好理解的,如下
P={
'B': -0.26268660809250016,
'E': -3.14e+100,
'M': -3.14e+100,
'S': -1.4652633398537678
}
示例數值是對概率值取對數之后的結果(trick, 讓概率相乘變成對數相加),其中-3.14e+100作為負無窮,也就是對應的概率值是0。它表示了一個句子的第一個字屬于{B,E,M,S}這四種狀態的概率,如上可以看出,E和M的概率都是0,這和實際相符合,開頭的第一個字只可能是詞語的首字(B),或者是單字成詞(S),這部分內容對應 jieba/finalseg/prob_start.py文件,具體源碼。
transition_probability(狀態的轉移概率矩陣)
轉移概率是馬爾科夫鏈很重要的一個知識點,馬爾科夫鏈(一階)最大的特點就是當前T=i時刻的狀態state(i),只和T=i時刻之前的n個狀態有關,即:
{state(i-1), state(i-2), … state(i - n)}
HMM模型有三個基本假設:
a. 系統在時刻t的狀態只與時刻t-1處的狀態相關,(也稱為無后效性);
b. 狀態轉移概率與時間無關,(也稱為齊次性或時齊性);
c. 假設任意時刻的觀測只依賴于該時刻的馬爾科夫鏈的狀態,與其它觀測及狀態無關,(也稱觀測獨立性假設)。
其中前兩個假設為馬爾科夫模型的假設。 模型的這幾個假設能大大簡化問題。
再看下transition_probability,其實就是一個嵌套的字典,數值是概率求對數后的值,示例:
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.510825623765990,表示狀態B的下一個狀態是E的概率對數是-0.510825623765990。
這部分內容對應 jieba/finalseg/prob_trans.py文件,具體源碼。
emission_probability(狀態產生觀察的概率,發射概率)
根據HMM觀測獨立性假設發射概率,即觀察值只取決于當前狀態值,也就是:
P(observed[i], states[j]) = P(states[j]) * P(observed[i]|states[j]),其中P(observed[i]|states[j])這個值就是從emission_probability中獲取。
emission_probability示例如下:
P={'B': {'\u4e00': -3.6544978750449433,
'\u4e01': -8.125041941842026,
'\u4e03': -7.817392401429855,
'\u4e07': -6.3096425804013165,
...,
'S':{...},
...
}
比如P[‘B’][‘\u4e00’]代表的含義就是’B’狀態下觀測的字為’\u4e00’(對應的漢字為’一’)的概率對數P[‘B’][‘\u4e00’] = -3.6544978750449433。
這部分內容對應 jieba/finalseg/prob_emit.py文件,具體源碼。
到這里已經結合HMM模型把jieba的五元參數介紹完,這五元的關系是通過一個叫Viterbi的算法串接起來,observations序列值是Viterbi的輸入,而states序列值是Viterbi的輸出,輸入和輸出之間Viterbi算法還需要借助三個模型參數,分別是start_probability,transition_probability,emission_probability。對于未登錄詞(OOV)的問題,即已知觀察序列S,初始狀態概率prob_start,狀態觀察發射概率prob_emit,狀態轉換概率prob_trans。 求狀態序列W,這是個解碼問題,維特比算法可以解決。
Viterbi 維特比算法
HMM第二個問題又稱為解碼問題(預測問題)即給定觀測序列 O=O1,O2,O3…Ot和模型參數λ=(A,B,π),怎樣尋找滿足這種觀察序列意義上最優的隱含狀態序列S。
(viterbi算法,近似算法),同樣的,暴力算法是計算所有可能性的概率,然后找出擁有最大概率值的隱藏狀態序列。與問題一的暴力解決方案類似,復雜度為O(N^T)。
那應該用什么方案呢?還是動態規劃!
假設觀察序列為O1,O2,O3,…,Ot. 在時刻i ∈ (1,t]時,定義D為觀察O1,O2,…,Oi且Si=Sk時產生該觀察序列的最大概率:
其中,S1,S2,….S(i-1),在此時也已經可以得到(子問題)。
它是一個是對子問題求最大值的最優解問題。
對于解碼問題,因為需要求出的是使得觀察序列概率最大的隱藏狀態的序列,而不是最大概率,所以,在算法計算過程中,還需要記錄前一個隱藏狀態的值。
jieba Viterbi 的應用
jieba中對于未登錄詞問題,通過__cut_DAG 函數我們可以看出這個函數前半部分用 calc 函數計算出了初步的分詞,而后半部分就是就是針對上面例子中未出現在語料庫的詞語進行分詞了。
由于基于頻度打分的分詞會傾向于把不能識別的詞組一個字一個字地切割開,所以對這些字的合并就是識別OOV的一個方向,__cut_DAG定義了一個buf 變量收集了這些連續的單個字,最后把它們組合成字符串再交由 finalseg.cut 函數來進行下一步分詞。
# 利用 viterbi算法得到句子分詞的生成器
def __cut(sentence):
global emit_P
# viterbi算法得到sentence 的切分
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:]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
對應的viterbi算法:
#狀態轉移矩陣,比如B狀態前只可能是E或S狀態
PrevStatus = {
'B':('E','S'),
'M':('M','B'),
'S':('S','E'),
'E':('B','M')
}
def viterbi(obs, states, start_p, trans_p, emit_p):
V = [{}] # 狀態概率矩陣
path = {}
for y in states: # 初始化狀態概率
V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
path[y] = [y] # 記錄路徑
for t in xrange(1, len(obs)):
V.append({})
newpath = {}
for y in states:
em_p = emit_p[y].get(obs[t], MIN_FLOAT)
# t時刻狀態為y的最大概率(從t-1時刻中選擇到達時刻t且狀態為y的狀態y0)
(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
# 求出最后一個字哪一種狀態的對應概率最大,最后一個字只可能是兩種情況:E(結尾)和S(獨立詞)
(prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
其實到這里思路很明確了,給定訓練好的模型(如HMM)參數(λ=(A,B,π)), 然后對模型進行載入,再運行一遍Viterbi算法,就可以找出每個字對應的狀態(B, M, E, S),這樣就可以根據狀態也就可以對句子進行分詞。具體源碼注釋見:
github/init.py 。
參考
http://zh.wikipedia.org/wiki/%E7%BB%B4%E7%89%B9%E6%AF%94%E7%AE%97%E6%B3%95
http://www.52nlp.cn/hmm%E7%9B%B8%E5%85%B3%E6%96%87%E7%AB%A0%E7%B4%A2%E5%BC%95
http://blog.csdn.net/stdcoutzyx/article/details/8522078
http://yanyiwu.com/work/2014/04/07/hmm-segment-xiangjie.html
---------------------
作者:DanielWang_
來源:CSDN
原文:https://blog.csdn.net/daniel_ustc/article/details/48248393
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
總結
以上是生活随笔為你收集整理的jieba中文分词源码分析(四)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个隐马尔科夫模型的应用实例:中文分词
- 下一篇: Python 字典(Dictionary