1. 程式人生 > >jieba原始碼分析(二)

jieba原始碼分析(二)

0、寫在前面

jieba原始碼分析(一)裡面已經jieba分詞的一部分進行了分析,本文主要解決分詞的另一塊:未登陸詞,也就是我們常說的新詞。對於這些新詞,我們前面所說的字首詞典中是不存在的,那麼之前的分詞方法自然就不能適用了。為了解決這一問題,jieba使用了隱馬爾科夫(HMM)模型。關於HMM模型的具體細節,這裡不會過多介紹,網上也已經有很多資源可以參考

 1、演算法簡介及例項

利用HMM模型進行分詞,主要就是將分詞問題看作是一個序列標註(sequence labeling)問題。其中,句子為觀測序列,分詞結果為狀態序列。現在我們的問題就轉變成了:已知句子和HMM模型,求解最有可能的對於的狀態序列(即分詞結果)。熟悉HMM的同學應該知道,對應於這個問題,使用的演算法就是Viterbi。

舉個栗子

今天我們不“去北京大學玩”,我們“去上海交通大學玩”。我們可以很明顯地看出分詞結果是“去/上海交通大學/玩”。

對於分詞狀態,由於jieba分詞中使用的是4-tag,因此我們以4-tag進行計算。4-tag,也就是每個字處在詞語中的4種可能狀態,B、M、E、S,分別表示Begin(這個字處於詞的開始位置)、Middle(這個字處於詞的中間位置)、End(這個字處於詞的結束位置)、Single(這個字是單字成詞)。其中,B後面只能接M或者E;而M後面只能接M或E;E後面只能接S或B;S後面只能接B或S。具體可以看下圖。

注意,這裡“去上海交通大學玩”是觀測狀態序列,而對應的“SBMMMMES”則是隱藏狀態序列。

HMM模型有幾個重要的引數:

  • 初始狀態概率π
  • 狀態轉移概率A:比如上圖中的S→B的概率
  • 狀態發射概率B:比如上圖中的S→去的概率

初始狀態概率

狀態初始概率表示,每個詞初始狀態的概率;jieba分詞訓練出的狀態初始概率模型如下所示。其中的概率值都是取對數之後的結果(可以讓概率相乘轉變為概率相加),其中-3.14e+100代表負無窮,對應的概率值就是0。這個概率表說明一個詞中的第一個字屬於{B、M、E、S}這四種狀態的概率,如下可以看出,E和M的概率都是0,這也和實際相符合:開頭的第一個字只可能是每個詞的首字(B),或者單字成詞(S)。這部分對應jieba/finaseg/prob_start.py,具體可以進入原始碼檢視。

P={'B': -0.26268660809250016,
 'E': -3.14e+100,
 'M': -3.14e+100,
 'S': -1.4652633398537678}

狀態轉移概率

再看jieba中的狀態轉移概率,其實就是一個巢狀的詞典,數值是概率值求對數後的值,如下所示。這部分在jieba/finaseg/prob_trans.py,具體可以檢視原始碼。

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}}

狀態發射概率

根據HMM模型中觀測獨立性假設,發射概率,即觀測值只取決於當前狀態值,這部分對應jieba/finaseg/prob_emit.py,具體可以檢視原始碼。

2、原始碼分析

ieba分詞中HMM模型識別未登入詞的原始碼目錄在jieba/finalseg/下,

__init__.py 實現了HMM模型識別未登入詞;

prob_start.py 儲存了已經訓練好的HMM模型的狀態初始概率表;

prob_trans.py 儲存了已經訓練好的HMM模型的狀態轉移概率表;

prob_emit.py 儲存了已經訓練好的HMM模型的狀態發射概率表;

基於HMM的分詞流程

jieba分詞會首先呼叫函式cut(sentence),cut函式會先將輸入句子進行解碼,然後呼叫__cut函式進行處理。__cut函式就是jieba分詞中實現HMM模型分詞的主函式。__cut函式會首先呼叫viterbi演算法,求出輸入句子的隱藏狀態,然後基於隱藏狀態進行分詞。

def __cut(sentence):
    """
    jieba首先會呼叫函式cut(sentence),cut函式會先將輸入的句子進行解碼,
    然後呼叫__cut()函式進行處理。該函式是實現HMM模型分詞的主函式
    __cut()函式首先呼叫viterbi演算法, 求出輸入句子的隱藏狀態,然後基於隱藏狀態分詞
    """
    global emit_P
    # 通過viterbi演算法求出隱藏狀態序列
    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]
        # 如果字所處的位置是開始位置 Begin
        if pos == 'B':
            begin = i
        # 如果字所處的位置是結束位置 END
        elif pos == 'E':
            # 這個子序列就一個分詞
            yield sentence[begin:i + 1]
            nexti = i + 1
        # 如果單獨成字 Single
        elif pos == 'S':
            yield char
            nexti = i + 1
    # 剩餘的直接作為一個分詞,返回
    if nexti < len(sentence):
        yield sentence[nexti:]

Viterbi演算法

關於Viterbi演算法的具體細節參考李航老師的小藍書,後面還有一個具體的例子。

def viterbi(obs, states, start_p, trans_p, emit_p):
    """
    viterbi函式會先計算各個初始狀態的對數概率值,
    然後遞推計算,每時刻某狀態的對數概率值取決於
    上一時刻的對數概率值、
    上一時刻的狀態到這一時刻的狀態的轉移概率、
    這一時刻狀態轉移到當前的字的發射概率三部分組成。
    """
    V = [{}]  # tabular表示Viterbi變數,下標表示時間
    path = {}  # 記錄從當前狀態回退路徑
    # 時刻t=0,初始狀態
    for y in states:  # init
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT) # 這裡加號應該是乘號吧?
        path[y] = [y]
    # 時刻t = 1,...,len(obs) - 1
    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關於分詞部分的大致內容,還有一些比如全模式分詞cut_all()、搜尋模式cut_for_search()等會補充到程式碼裡上傳至Github。

enjoy yourself~