淺談分詞算法(3)基於字的分詞方法(HMM)

前言

淺談分詞算法(1)分詞中的基本問題咱們討論過基於詞典的分詞和基於字的分詞兩大類,在淺談分詞算法(2)基於詞典的分詞方法文中咱們利用n-gram實現了基於詞典的分詞方法。在(1)中,咱們也討論了這種方法有的缺陷,就是OOV的問題,即對於未登陸詞會失效在,並簡單介紹瞭如何基於字進行分詞,本文着重闡述下如何利用HMM實現基於字的分詞方法。python

目錄

淺談分詞算法(1)分詞中的基本問題
淺談分詞算法(2)基於詞典的分詞方法
淺談分詞算法(3)基於字的分詞方法(HMM)
淺談分詞算法(4)基於字的分詞方法(CRF)
淺談分詞算法(5)基於字的分詞方法(LSTM)git

隱馬爾可夫模型(Hidden Markov Model,HMM)

首先,咱們將簡要地介紹HMM。HMM包含以下的五元組:github

  • 狀態值集合Q={q1,q2,...,qN},其中N爲可能的狀態數;
  • 觀測值集合V={v1,v2,...,vM},其中M爲可能的觀測數;
  • 轉移機率矩陣A=[aij],其中aij表示從狀態i轉移到狀態j的機率;
  • 發射機率矩陣(也稱之爲觀測機率矩陣)B=[bj(k)],其中bj(k)表示在狀態j的條件下生成觀測vk的機率;
  • 初始狀態分佈π.

通常地,將HMM表示爲模型λ=(A,B,π),狀態序列爲I,對應測觀測序列爲O。對於這三個基本參數,HMM有三個基本問題:算法

  • 機率計算問題,在模型λ下觀測序列O出現的機率;
  • 學習問題,已知觀測序列O,估計模型λ的參數,使得在該模型下觀測序列P(O|λ)最大;
  • 解碼(decoding)問題,已知模型λ與觀測序列O,求解條件機率P(I|O)最大的狀態序列I。

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的狀況,咱們須要做出兩個假設來減小稀疏問題:函數

  • 有限歷史性假設: ti 只由 ti-1 決定
  • 獨立輸出假設:第 i 時刻的接收信號 ci 只由發送信號 ti 決定

即以下:


這樣咱們就能夠將上面的式子轉化爲:

而在咱們的分詞問題中狀態T只有四種即{B,E,M,S},其中P(T)能夠做爲先驗機率經過統計獲得,而條件機率P(C|T)即漢語中的某個字在某一狀態的條件下出現的機率,能夠經過統計訓練語料庫中的頻率得出。學習

Viterbi算法

有了以上東東,咱們應如何求解最優狀態序列呢?解決的辦法即是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算法作了以下的修改:

  • 狀態轉移時應知足PrevStatus條件,即狀態B的前一狀態只能是E或者S,...
  • 最後一個狀態只能是E或者S,表示詞的結尾。

與此同時,這裏在實現地推公式時,對其求對數,將相乘轉化成了相加:
ln\delta_{t+1}(i)=max{ln\delta_t(j)+lna_{ji}+lnb_i(c_{t+1})}
這也就是機率矩陣中出現了負數,是由於對其求了對數。

實現效果

咱們寫一個簡單的自測函數:

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

參考文獻

  1. 《統計學習方法》 李航
  2. 【中文分詞】隱馬爾可夫模型HMM
  3. github jieba
  4. 結巴模型的數據是如何生成的
  5. 一個隱馬爾科夫模型的應用實例:中文分詞
相關文章
相關標籤/搜索