Nianwen Xue在《Chinese Word Segmentation as Character Tagging》中將中文分詞視作為序列標注問題(sequence tagging problem),由此引入監督學習算法來解決分詞問題。
1. HMM
首先,我們將簡要地介紹HMM(主要參考了李航老師的《統計學習方法》)。HMM包含如下的五元組:
- 狀態值集合\(Q=\{q_1, q_2, \cdots, q_N\}\),其中\(N\)為可能的狀態數;
- 觀測值集合\(V=\{v_1, v_2, \cdots, v_M\}\),其中\(M\)為可能的觀測數;
- 轉移概率矩陣\(A=\left[ a_{ij} \right]\),其中\(a_{ij}\)表示從狀態\(i\)轉移到狀態\(j\)的概率;
- 發射概率矩陣(在[2]中稱之為觀測概率矩陣)\(B=\left[ b_{j}(k) \right]\),其中\(b_{j}(k)\)表示在狀態\(j\)的條件下生成觀測\(v_k\)的概率;
- 初始狀態分布\(\pi\).
一般地,將HMM表示為模型\(\lambda = (A, B, \pi)\),狀態序列為\(I\),對應測觀測序列為\(O\)。對於這三個基本參數,HMM有三個基本問題:
- 概率計算問題,在模型\(\lambda\)下觀測序列\(O\)出現的概率;
- 學習問題,已知觀測序列\(O\),估計模型\(\lambda\)的參數,使得在該模型下觀測序列\(P(O|\lambda)\)最大;
- 解碼(decoding)問題,已知模型\(\lambda\)與觀測序列\(O\),求解條件概率\(P(I|O)\)最大的狀態序列\(I\)。
2. 中文分詞
將狀態值集合\(Q\)置為\(\{ B, E, M, S\}\),分別表示詞的開始、結束、中間(begin、end、middle)及字符獨立成詞(single);觀測序列即為中文句子。比如,“今天天氣不錯”通過HMM求解得到狀態序列“B E B E B E”,則分詞結果為“今天/天氣/不錯”。
通過上面例子,我們發現中文分詞的任務對應於解碼問題:對於字符串\(C=\{ c_1, \cdots, c_n \}\),求解最大條件概率
其中,\(t_i\)表示字符\(c_i\)對應的狀態。應如何求解狀態序列呢?解決的辦法便是Viterbi算法;其實,Viterbi算法本質上是一個動態規划算法,利用到了狀態序列的最優路徑滿足這樣一個特性:最優路徑的子路徑也一定是最優的。定義在時刻\(t\)狀態為\(i\)的概率最大值為\(\delta_t(i)\),則有遞推公式:
\begin{equation}
\delta_{t+1}(i) = \max { [\delta_t(j) a_{ji}] b_i(o_{t+1}) }
\label{eq:hmm}
\end{equation}
其中,\(o_{t+1}\)即為字符\(c_{t+1}\)。
3. 開源實現
以下的源碼分析基於Jieba 0.36版本。
Jieba的jieba.finalseg實現HMM中文分詞。prob_start.py
定義初始狀態分布\(\pi\):
P={'B': -0.26268660809250016,
'E': -3.14e+100,
'M': -3.14e+100,
'S': -1.4652633398537678}
prob_trans.py
轉移概率矩陣\(A\):
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}}
prob_emit.py
定義了發射概率矩陣\(B\),比如,P("和"|M)
表示狀態為M的情況下出現“和”這個字的概率;
P={'B': {'一': -3.6544978750449433,
'丁': -8.125041941842026,
'七': -7.817392401429855,
...}
'S': {':': -15.828865681131282,
'一': -4.92368982120877,
...}
...}
關於訓練模型的生成,作者在這里有解釋,來源主要有兩個:標准的切分語料 + ICTCLAS切分的txt小說。還有一個大家可能會疑惑的問題,為什么Jieba中的概率矩陣中出現了負數?不急,我們先來看看Viterbi算法的實現——jieba.finalseg.viterbi
函數:
PrevStatus = {
'B': 'ES',
'M': 'MB',
'S': 'SE',
'E': 'BM'
}
def viterbi(obs, states, start_p, trans_p, emit_p):
V = [{}] # tabular
path = {}
for y in states: # init
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)
(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])
為了適配中文分詞任務,Jieba對Viterbi算法做了如下的修改:
- 狀態轉移時應滿足
PrevStatus
條件,即狀態\(B\)的前一狀態只能是\(E\)或者\(S\),... - 最后一個狀態只能是\(E\)或者\(S\),表示詞的結尾。
與此同時,Jieba在實現公式\eqref{eq:hmm}時,對其求對數,將相乘轉化成了相加:
這就回答了上面的問題——為什么概率矩陣中出現了負數,是因為對其求了對數。
Jieba的HMM分詞:
from jieba.finalseg import cut
sentence = "小明碩士畢業於中國科學院計算所,后在日本京都大學深造"
print('/'.join(cut(sentence)))
分詞結果為“小明/碩士/畢業於/中國/科學院/計算/所/,/后/在/日/本京/都/大學/深造”,我們發現:關於“日本京都”出現分詞錯誤的情況。這是因為最大條件概率\(P(I|O)\)對應的狀態序列不一定是分詞正確的標注序列。此外,HMM做了兩個基本假設:
- 齊次Markov性假設,即任意時刻t的狀態僅與前一時刻狀態相關,與其他時刻的狀態、時刻t均無關;
- 觀測獨立性假設,任意時刻t的觀測僅依賴於該時刻HMM的狀態,與其他的觀測及狀態均無關。
HMM受限於這兩個假設(字符\(c_t\)僅與前一字符\(c_{t-1}\)相關),而不能學習到更多的特征,泛化能力有限。
4. 參考資料
[1] Xue, Nianwen. "Chinese word segmentation as character tagging." Computational Linguistics and Chinese Language Processing 8.1 (2003): 29-48.
[2] 李航. "統計學習方法." 清華大學出版社, 北京 (2012).
[3] Itenyh, Itenyh版-用HMM做中文分詞二:模型准備.
[4] Django夢之隊, 對Python中文分詞模塊結巴分詞算法過程的理解和分析.(源鏈接掛了,為轉載鏈接)