1. 程式人生 > >結巴分詞原始碼解析(二)

結巴分詞原始碼解析(二)

本篇分兩部分,一、補充說明動態規劃求最大概率路徑的過程;二、使用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,所以只需要比較這兩個狀態的區域性概率即可。較大者即為全域性的最佳概率和最佳路徑。