在淺談分詞算法(1)分詞中的基本問題咱們討論過基於詞典的分詞和基於字的分詞兩大類,在淺談分詞算法(2)基於詞典的分詞方法文中咱們利用n-gram實現了基於詞典的分詞方法。在(1)中,咱們也討論了這種方法有的缺陷,就是OOV的問題,即對於未登陸詞會失效在,並簡單介紹瞭如何基於字進行分詞,本文着重闡述下如何利用HMM實現基於字的分詞方法。python
淺談分詞算法(1)分詞中的基本問題
淺談分詞算法(2)基於詞典的分詞方法
淺談分詞算法(3)基於字的分詞方法(HMM)
淺談分詞算法(4)基於字的分詞方法(CRF)
淺談分詞算法(5)基於字的分詞方法(LSTM)git
首先,咱們將簡要地介紹HMM。HMM包含以下的五元組:github
通常地,將HMM表示爲模型λ=(A,B,π),狀態序列爲I,對應測觀測序列爲O。對於這三個基本參數,HMM有三個基本問題:算法
在(1)中咱們已經討論過基於字分詞,是如何將分詞轉換爲標籤序列問題,這裏咱們簡單闡述下HMM用於分詞的相關概念。將狀態值集合Q置爲{B,E,M,S},分別表示詞的開始、結束、中間(begin、end、middle)及字符獨立成詞(single);觀測序列即爲中文句子。好比,「今每天氣不錯」經過HMM求解獲得狀態序列「B E B E B E」,則分詞結果爲「今天/天氣/不錯」。
經過上面例子,咱們發現中文分詞的任務對應於解碼問題:對於字符串C={c1,...,cn},求解最大條件機率
其中,ti表示字符ci對應的狀態。app
在求條件機率
咱們利用貝葉斯公式可得
相似於n-gram的狀況,咱們須要做出兩個假設來減小稀疏問題:函數
即以下:
這樣咱們就能夠將上面的式子轉化爲:
而在咱們的分詞問題中狀態T只有四種即{B,E,M,S},其中P(T)能夠做爲先驗機率經過統計獲得,而條件機率P(C|T)即漢語中的某個字在某一狀態的條件下出現的機率,能夠經過統計訓練語料庫中的頻率得出。學習
有了以上東東,咱們應如何求解最優狀態序列呢?解決的辦法即是Viterbi算法;其實,Viterbi算法本質上是一個動態規劃算法,利用到了狀態序列的最優路徑知足這樣一個特性:最優路徑的子路徑也必定是最優的。定義在時刻t狀態爲i的機率最大值爲δt(i),則有遞推公式:
其中,ot+1即爲字符ct+1。編碼
咱們基於HMM實現一個簡單的分詞器,這裏我主要從jieba分詞中抽取了HMM的部分[3],具體邏輯以下:
prob_start.py定義初始狀態分佈π:code
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的狀況下出現「和」這個字的機率(注:在實際的代碼中漢字都用unicode編碼表示);
P={'B': {'一': -3.6544978750449433, '丁': -8.125041941842026, '七': -7.817392401429855, ...} 'S': {':': -15.828865681131282, '一': -4.92368982120877, ...} ...}
關於模型的訓練做者給出瞭解解釋:「來源主要有兩個,一個是網上能下載到的1998人民日報的切分語料還有一個msr的切分語料。另外一個是我本身收集的一些txt小說,用ictclas把他們切分(可能有必定偏差)。 而後用python腳本統計詞頻。要統計的主要有三個機率表:1)位置轉換機率,即B(開頭),M(中間),E(結尾),S(獨立成詞)四種狀態的轉移機率;2)位置到單字的發射機率,好比P("和"|M)表示一個詞的中間出現」和"這個字的機率;3) 詞語以某種狀態開頭的機率,其實只有兩種,要麼是B,要麼是S。」
在seg_hmm.py中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 range(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算法作了以下的修改:
與此同時,這裏在實現地推公式時,對其求對數,將相乘轉化成了相加:
這也就是機率矩陣中出現了負數,是由於對其求了對數。
咱們寫一個簡單的自測函數:
if __name__ == "__main__": ifile = '' ofile = '' try: opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["ifile=", "ofile="]) except getopt.GetoptError: print('seg_hmm.py -i <inputfile> -o <outputfile>') sys.exit(2) for opt, arg in opts: if opt == '-h': print('seg_hmm.py -i <inputfile> -o <outputfile>') sys.exit() elif opt in ("-i", "--ifile"): ifile = arg elif opt in ("-o", "--ofile"): ofile = arg with open(ifile, 'rb') as inf: for line in inf: rs = cut(line) print(' '.join(rs)) with open(ofile, 'a') as outf: outf.write(' '.join(rs) + "\n")
運行以下:
我將完整的代碼放到了github上,同上一篇文章相似,這裏代碼基本是從結巴抽取過來,方便你們學習查閱,模型我也直接拿過來,並無從新找語料train,你們能夠瞅瞅:
https://github.com/xlturing/machine-learning-journey/tree/master/seg_hmm