做者: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'
}