結巴分詞源代碼解析(二)

本篇分兩部分,一、補充說明動態規劃求最大概率路徑的過程;二、使用viterbi算法處理未登錄詞。


一、動態規劃求最大概率路徑補充
從全模式中看出一句話有多種劃分方式,那麼哪一種是好的劃分方式,最大概率路徑認爲,如果某個路徑下詞的聯合概率最大,那麼這個路徑爲最好的劃分方式。
(個人認爲這種思想是有缺陷的,我們知道每一個詞的出現頻率是一個較小的小數,小數相乘結果會受到小數的個數較大影響,即分詞結果會偏向於劃分爲較長的詞。)
具體處理方法,由於多個小數連乘會導致結果是一個很小的數,這裏對概率做log處理,這樣問題轉換爲求圖的最長路徑問題。在句子最後增加一個結束節點。那麼動態
規劃的初始狀態爲f(N)=0,f(i)表示從節點i到結束的最長路徑。具體到jieba代碼中使用了route[N]=(0,0)。等式右邊爲一個tuple,第一個元素爲最大路徑的值,第二
個元素爲當前這個詞的末尾座標。
轉移方程爲f(i)=max(v(DAG[]i])+f(DAG[i])),jieba代碼中爲:
route[idx] = max((log(FREQ.get(sentence[idx:x + 1]) or 1) - logtotal + route[x + 1][0], x) for x in DAG[idx])
最終得到route[0]爲結束條件,再根據route得到分詞結果。
以“英語單詞很難記憶”爲例,終止爲route爲:
{"0": [-42.21707512440173, 3], "1": [-46.29273582292598, 1], "2": [-36.80691827609816, 3], "3": [-34.66134438350637, 3], "4": [-25.40413428531462, 4], "5": [-18.63593458171283, 5], "6": [-10.55017769877787, 7], "7": [-12.426756194264565, 7], "8": [0, 0]}
那麼劃分的結果是:英語單詞/很/難/記憶
另:按照算法趨向於取長詞,這裏‘很難’沒有被分在一起很奇怪,然後我去查了詞典,詞典裏真沒有“很難”這個詞。當然也可能是P(‘很’)*P(‘難’)>P('很難')

二、viterbi算法處理未登錄詞
1、cut函數
上文中已經談到未登錄詞的處理時調用finalseg.cut(buf)。
代碼如下:
def __cut(sentence):
    global emit_P
    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:]
主要處理內容就一行prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P),後面內容爲結果輸出。所以cut函數的作用是調用viterbi函數然後輸出。

理解viterbi函數需要對viterbi算法進行理解,參考《HMM模型之viterbi算法》

2、viterbi函數

代碼如下:

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])

參數明說:obs是待分詞的序列,states爲隱藏狀態集,start_p爲初始向量,trans_p是狀態轉移矩陣,emit_p是混合矩陣。

變量V是一個list,list每一個元素是一個字典,list的長度爲obs的長度。字典的keys爲隱藏狀態集,values爲局部概率。

變量path是局部最佳路徑,keys爲隱藏狀態集,values爲list即爲局部最佳路徑。

第一個循環即爲求t=1時刻的局部概率和局部路徑,相關概念請參考《HMM模型之viterbi算法》。get()函數爲取值,第二個參數MIN_FLOAT爲全部變量,值爲:-3.14e100,表示一個極小的概率,函數表示如果在能夠取到第一個參數的值,則返回相應的值,否則返回MIN_FLOAT。

第二個循環求t>1時刻,局部概率和局部路徑。

            (prob, state) = max(
                [(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
PrevStatus[y]表示隱藏狀態y的前一個時刻可能的隱藏狀態集。這個計算含義爲通過t-1時刻的局部概率計算t時刻隱藏狀態爲y的局部概率和反向指針。其中返回值是一個元組,prob是局部概率,state是反向指針。

<span style="white-space:pre">	</span>newpath[y] = path[state] + [y]
這是得到t時刻的局部最佳路徑,即反向指針指向的局部最佳路徑+新的狀態y。

<span style="white-space:pre">	</span>(prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
計算到這裏已經得到末尾元素的各個隱藏狀態的局部概率和局部最佳路徑,因爲最後一個元素的隱藏狀態只可能是E或者S,所以只需要比較這兩個狀態的局部概率即可。較大者即爲全局的最佳概率和最佳路徑。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章