詳解隱馬爾可夫模型(HMM)中的維特比算法

筆記轉載於GitHub項目https://github.com/NLP-LOVE/Introduction-NLP

4. 隱馬爾可夫模型與序列標註

第3章的n元語法模型從詞語接續的流暢度出發,爲全切分詞網中的二元接續打分,進而利用維特比算法求解似然概率最大的路徑。這種詞語級別的模型無法應對 OOV(Out of Vocabulary,即未登錄詞) 問題: 00V在最初的全切分階段就已經不可能進人詞網了,更何談召回。

例如下面一句:

頭上戴着束髮嵌寶紫金冠,齊眉勒着二龍搶珠金抹額

加粗的就是相對陌生的新詞,之前的分詞算法識別不出,但人類確可以,是因爲讀者能夠識別“戴着”,這些構詞法能讓人類擁有動態組詞的能力。我們需要更細粒度的模型,比詞語更細粒度的就是字符。

具體說來,只要將每個漢字組詞時所處的位置(首尾等)作爲標籤,則中文分詞就轉化爲給定漢字序列找出標籤序列的問題。一般而言,由字構詞是序列標註模型的一種應用。 在所有“序列標註”模型中,隱馬爾可夫模型是最基礎的一種。

4.1 序列標註問題

序列標註指的是給定一個序列 x=x1x2...xnx=x_1x_2...x_n,找出序列中每個元素對應標籤 y=y1y2...yny=y_1y_2...y_n 的問題。其中,y 所有可能的取值集合稱爲標註集。比如,輸入一個自然數序列,輸出它們的奇偶性。

求解序列標註問題的模型一般稱爲序列標註器,通常由模型從一個標註數據集 {X,Y}={(x(i),y(i))},i=1,...,K\{X,Y\}=\{(x^{(i)},y^{(i)})\},i=1,...,K 中學習相關知識後再進行預測。再NLP問題中,x 通常是字符或詞語,而 y 則是待預測的組詞角色或詞性等標籤。中文分詞、詞性標註以及命名實體識別,都可以轉化爲序列標註問題。

  1. 序列標註與中文分詞

    考慮一個字符序列(字符串) x,想象切詞器真的是在拿刀切割字符串,如此,中文分詞轉化爲標註集{切,過}的序列標註問題。

    分詞標註集並非只有一種,爲了捕捉漢字分別作爲詞語收尾(Begin、End)、詞中(Middle)以及單字成詞(Single)時不同的成詞概率,人們提出了{B,M,E,S}這種最流行的標註集。

  2. 序列標註與詞性標註

    詞性標註任務是一個天然的序列標註問題:x 是單詞序列,y 是相應的詞性序列。需要綜合考慮前後的單詞與詞性才能決定當前單詞的詞性。

  3. 序列標註與命名實體識別

    所謂命名實體,指的是現實存在的實體,比如人名、地名和機構名,命名實體是 OOV 的主要組成部分。

    考慮到字符級別中文分詞和詞語級別命名實體識別有着類似的特點,都是組合短單位形成長單位的問題。所以命名實體識別可以複用BMES標註集,並沿用中文分詞的邏輯,只不過標註的對象由字符變爲單詞而已。唯一不同的是,命名實體識別還需要確定實體所屬的類別。這個額外的要求依然是個標註問題,可以通過將命名實體類別附着到BMES標籤來達到目的。比如,構成地名的單詞標註爲“B/M/E/S-地名”,以此類推。對於那些不構成命名實體的單詞,則統-標註爲O ( Outside), 即複合詞之外。

總之,序列標註問題是NLP中最常見的問題之一。許多應用任務都可以變換思路,轉化爲序列標註來解決。所以一個準確的序列標註模型非常重要,直接關係到NLP系統的準確率。機器學習領域爲NLP提供了許多標註模型,本着循序漸進的原則,本章介紹其中最基礎的一個隱馬爾可夫模型。

4.2 隱馬爾可夫模型

隱馬爾可夫模型( Hidden Markov Model, HMM)是描述兩個時序序列聯合分佈 p(x,y) 的概率模型: x 序列外界可見(外界指的是觀測者),稱爲觀測序列(obsevation sequence); y 序列外界不可見,稱爲狀態序列(state sequence)。比如觀測 x 爲單詞,狀態 y 爲詞性,我們需要根據單詞序列去猜測它們的詞性。隱馬爾可夫模型之所以稱爲“隱”,是因爲從外界來看,狀
態序列(例如詞性)隱藏不可見,是待求的因變量。從這個角度來講,人們也稱狀態爲隱狀態(hidden state),而稱觀測爲顯狀態( visible state)。隱馬爾可夫模型之所以稱爲“馬爾可夫模型”,”是因爲它滿足馬爾可夫假設

  1. 從馬爾可夫假設到隱馬爾可夫模型

    馬爾可夫假設:每個事件的發生概率只取決於前一個事件。

    馬爾可夫鏈:將滿足馬爾可夫假設的連續多個事件串聯起來,就構成了馬爾可夫鏈。

    如果把事件具象爲單詞,那麼馬爾可夫模型就具象爲二元語法模型。

    隱馬爾可夫模型:它的馬爾可夫假設作用於狀態序列,

    假設 ① 當前狀態 Yt 僅僅依賴於前一個狀態 Yt-1, 連續多個狀態構成隱馬爾可夫鏈 y。有了隱馬爾可夫鏈,如何與觀測序列 x 建立聯繫呢?

    隱馬爾可夫模型做了第二個假設: ② 任意時刻的觀測 x 只依賴於該時刻的狀態 Yt,與其他時刻的狀態或觀測獨立無關。如果用箭頭表示事件的依賴關係(箭頭終點是結果,依賴於起點的因緣),則隱馬爾可夫模型可以表示爲下圖所示

    狀態與觀測之間的依賴關係確定之後,隱馬爾可夫模型利用三個要素來模擬時序序列的發生過程----即初始狀態概率向量、狀態轉移概率矩陣和發射概率矩陣

  2. 初始狀態概率向量

    系統啓動時進入的第一個狀態 Y1 稱爲初始狀態,假設 y 有 N 種可能的取值,那麼 Y1 就是一個獨立的離散型隨機變量,由 P(y1 | π) 描述。其中
    π=(π1,,πN)T,0πi1,i=1Nπi=1 \pi=\left(\pi_{1}, \cdots, \pi_{N}\right)^{\mathrm{T}}, 0 \leqslant \pi_{i} \leqslant 1, \sum_{i=1}^{N} \pi_{i}=1
    是概率分佈的參數向量,稱爲初始狀態概率向量

    給定 π ,初始狀態 Y1 的取值分佈就確定了,比如採用{B,M,E,S}標註集時概率如下:
    p(y1=B)=0.7p(y1=M)=0p(y1=E)=0p(y1=S)=0.3 p(y_1=B)=0.7\\ p(y_1=M)=0\\ p(y_1=E)=0\\ p(y_1=S)=0.3
    那麼此時隱馬爾可夫模型的初始狀態概率向量爲 π=[0.7,0,0,0.3],注意,句子第一個詞是單字的可能性要小一些。

  3. 狀態轉移矩陣

    Yt 如何轉移到 Yt+1 呢?根據馬爾可夫假設,t+1 時的狀態僅僅取決於 t 時的狀態,既然一共有 N 種狀態,那麼從狀態 Si 到狀態 Sj 的概率就構成了一個 N*N 的方陣,稱爲狀態轉移矩陣 A
    A=[p(yt+1=sjyt=si)]N×N A=\left[p\left(y_{t+1}=s_{j} | y_{t}=s_{i}\right)\right]_{N \times N}
    其中下標 i、j 分別表示狀態的第 i、j 種取值。狀態轉移概率的存在有其實際意義,在中文分詞中,標籤 B 的後面不可能是 S,於是就有 P(Yt+1 = S | Yt = B) = 0。同樣,詞性標註中的“形容詞->名詞”“副詞->動詞”也可通過狀態轉移概率來模擬,這些概率分佈參數不需要手動設置,而是通過語料庫上的統計自動學習

  4. 發射概率矩陣

    有了狀態 Yt 之後,如何確定觀測 Xt 的概率分佈呢?根據隱馬爾可夫假設②,當前觀測 Xt 僅僅取決於當前狀態 Yt。也就是說,給定每種 y,x 都是一個獨立的離散型隨機變量,其參數對應一個向量。 假設觀測 x 一共有 M 種可能的取值,則 x 的概率分佈參數向量維度爲 M。由於 y 共有 N 種,所以這些參數向量構成了 N*M 的矩陣,稱爲發射概率矩陣B

    B=[p(xt=oiyt=sj)]N×M\boldsymbol{B}=\left[p\left(x_{t}=o_{i} | y_{t}=s_{j}\right)\right]_{N \times M}

    其中,第 i 行 j 列的元素下標 i 和 j 分別代表觀測和狀態的第 i 種和第 j 種取值。

  5. 隱馬爾可夫模型的三個基本用法

    • 樣本生成問題:給定模型,如何有效計算產生觀測序列的概率?換言之,如何評估模型與觀測序列之間的匹配程度?

    • 序列預測問題:給定模型和觀測序列,如何找到與此觀測序列最匹配的狀態序列?換言之,如何根據觀測序列推斷出隱藏的模型狀態?

    • 模型訓練問題:給定觀測序列,如何調整模型參數使得該序列出現的概率最大?換言之,如何訓練模型使其能最好地描述觀測數據?

    前兩個問題是模式識別的問題:1) 根據隱馬爾科夫模型得到一個可觀察狀態序列的概率(評價);2) 找到一個隱藏狀態的序列使得這個序列產生一個可觀察狀態序列的概率最大(解碼)。第三個問題就是根據一個可以觀察到的狀態序列集產生一個隱馬爾科夫模型(學習)。

4.3 隱馬爾可夫模型的訓練

  1. 案例假設和模型構造

    設想如下案例:某醫院招標開發“智能”醫療診斷系統,用來輔助感冒診斷。已知①來診者只有兩種狀態:要麼健康,要麼發燒。②來診者不確定自己到底是哪種狀態,只能回答感覺頭暈、體寒或正常。醫院認爲,③感冒這種病,只跟病人前一天的狀態有關,並且,④當天的病情決定當天的身體感覺。有位來診者的病歷卡上完整地記錄了最近 T 天的身體感受(頭暈、體寒或正常),請預測這 T 天的身體狀態(健康或發燒)。由於醫療數據屬於機密隱私,醫院無法提供訓練數據,但根據醫生經驗,感冒發病的規律如下圖所示(箭頭上的數值表示概率):

    根據已知條件①②,病情狀態(健康、發燒)可作爲隱馬爾可夫模型的隱狀態(上圖藍色狀態),而身體感受(頭暈、體寒或正常)可作爲隱馬爾可夫模型的顯狀態(圖中白色狀態)。條件③符合隱馬爾可夫模型假設一,條件④符 合隱馬爾可夫模型假設二。這個案例其實描述了一個隱馬爾可夫模型, 並且參數已經給定。構造模型代碼見:

    import numpy as np
    from pyhanlp import *
    from jpype import JArray, JFloat, JInt
    
    to_str = JClass('java.util.Arrays').toString
    
    ## 隱馬爾可夫模型描述
    states = ('Healthy', 'Fever')
    start_probability = {'Healthy': 0.6, 'Fever': 0.4}
    transition_probability = {
        'Healthy': {'Healthy': 0.7, 'Fever': 0.3},
        'Fever': {'Healthy': 0.4, 'Fever': 0.6},
    }
    emission_probability = {
        'Healthy': {'normal': 0.5, 'cold': 0.4, 'dizzy': 0.1},
        'Fever': {'normal': 0.1, 'cold': 0.3, 'dizzy': 0.6},
    }
    observations = ('normal', 'cold', 'dizzy')
    
    
    def generate_index_map(lables):
        index_label = {}
        label_index = {}
        i = 0
        for l in lables:
            index_label[i] = l
            label_index[l] = i
            i += 1
        return label_index, index_label
    
    
    states_label_index, states_index_label = generate_index_map(states)
    observations_label_index, observations_index_label = generate_index_map(observations)
    
    
    
    def convert_map_to_matrix(map, label_index1, label_index2):
        m = np.empty((len(label_index1), len(label_index2)), dtype=float)
        for line in map:
            for col in map[line]:
                m[label_index1[line]][label_index2[col]] = map[line][col]
        return JArray(JFloat, m.ndim)(m.tolist())
    
    def convert_observations_to_index(observations, label_index):
        list = []
        for o in observations:
            list.append(label_index[o])
        return list
    
    def convert_map_to_vector(map, label_index):
        v = np.empty(len(map), dtype=float)
        for e in map:
            v[label_index[e]] = map[e]
        return JArray(JFloat, v.ndim)(v.tolist())  # 將numpy數組轉爲Java數組
    
    
    ## pi:初始狀態概率向量
    ## A:狀態轉移概率矩陣
    ## B:發射概率矩陣
    A = convert_map_to_matrix(transition_probability, states_label_index, states_label_index)
    B = convert_map_to_matrix(emission_probability, states_label_index, observations_label_index)
    observations_index = convert_observations_to_index(observations, observations_label_index)
    pi = convert_map_to_vector(start_probability, states_label_index)
    
    FirstOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.FirstOrderHiddenMarkovModel')
    given_model = FirstOrderHiddenMarkovModel(pi, A, B)
    
  2. 樣本生成算法

    它的生成過程就是沿着隱馬爾可夫鏈走 T 步:

    • 根據初始狀態概率向量採樣第一個時刻的狀態 Y1 = Si,即 Y1 ~ π。
    • Yt 採樣結束得到 Si 後,根據狀態轉移概率矩s陣第 i 行的概率向量,採樣下一時刻的狀態 Yt+1。
    • 對每個 Yt = Si,根據發射概率矩陣的第 i 行採樣 Xt。
    • 重複步驟 2 共計 T-1 次,重複步驟 3 共計 T 次,輸出序列 x 與 y。

    代碼如下(接上),直接通過模型進行生成:

    ## 第一個參數:序列最低長度
    ## 第二個參數:序列最高長度
    ## 第三個參數:需要生成的樣本數
    for O, S in given_model.generate(3, 5, 2):
        print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in zip(O, S)))
    
  3. 隱馬爾可夫模型的訓練

    樣本生成後,我們就可以利用生成的數據重新訓練,通過極大似然法來估計隱馬爾可夫模型的參數。參數指的是三元組(π,A,B)。

    利用給定的隱馬爾可夫模型 P生成十萬個樣本,在這十萬個樣本上訓練新模型Q,比較新舊模型參數是否一致。

    trained_model = FirstOrderHiddenMarkovModel()
    
    ## 第一個參數:序列最低長度
    ## 第二個參數:序列最高長度
    ## 第三個參數:需要生成的樣本數
    trained_model.train(given_model.generate(3, 10, 100000))
    print('新模型與舊模型是否相同:', trained_model.similar(given_model))
    

    輸出:

    新模型與舊模型是否相同: True
    

    運行後一般都成立,由於隨機數,僅有小概率發生失敗。

4.4 隱馬爾可夫模型的預測

隱馬爾可夫模型最具實際意義的問題當屬序列標註了:給定觀測序列,求解最可能的狀態序列及其概率。

  1. 概率計算的前向算法

    給定觀測序列 x 和一個狀態序列 y,就可以估計兩者的聯合概率 P(x,y),聯合概率就是一種結果的概率,在這些結果當中找到最大的聯合概率就是找到最有可能的結果預測。聯合概率:P(x,y) = P(y) P(x|y),下面我們來分別求出P(y)和P(x|y)

    • 順着隱馬爾可夫鏈走,首先 t=1 時初始狀態沒有前驅狀態,發生概率由 π 決定:

      P(y1=si)=πiP(y_1=s_i)=\pi_i

    • 接着對 t >= 2,狀態 Yt 由前驅狀態 Yt-1 轉移而來,轉移矩陣由矩陣 A 決定:

      P(yt=sjyt1=si)=Ai,jP(y_t=s_j|y_{t-1}=s_i)=A_{i,j}

      所以狀態序列的概率爲上面式子的乘積:

      p(y)=p(y1,,yr)=p(y1)i=2Tp(yiyi1)p(y)=p\left(y_{1}, \cdots, y_{r}\right)=p\left(y_{1}\right) \prod_{i=2}^{T} p\left(y_{i} | y_{i-1}\right)

    • P(y) 我們已經求出來了,下面要求 P(x|y)

      對於每個 Yt = Si,都會“發射”一個 Xt = Oj,發射概率由矩陣 B 決定:

      P(xt=Ojyt=si)=Bi,jP(x_t=O_j|y_t=s_i)=B_{i,j}

    • 那麼給定一個狀態序列 Y,對應的 X 的概率累積形式:

      p(xy)=t=1Tp(xtyt)p(x | y)=\prod_{t=1}^{T} p\left(x_{t} | y_{t}\right)

    • 最後帶入聯合概率公式得:
      p(x,y)=p(y)p(xy)=p(y1)t=2Tp(ytyt1)t=1Tp(xtyt) \begin{aligned} p(x, y) &=p(y) p(x | y) \\ &=p\left(y_{1}\right) \prod_{t=2}^{T} p\left(y_{t} | y_{t-1}\right) \prod_{t=1}^{T} p\left(x_{t} | y_{t}\right) \end{aligned}

    將其中的每個 Xt、Yt 對應上實際發生序列的 Si、Oj,就能帶入(π,A,B)中的相應元素,從而計算出任意序列的概率,最後找出這些概率的最大值就得到預測結果。找出概率最大值要用到維特比算法

  2. 搜索狀態序列的維特比算法

    理解了前向算法之後,找尋最大概率所對應的狀態序列無非是一個搜索問題。具體說來,將每個狀態作爲有向圖中的一個節點, 節點間的距離由轉移概率決定,節點本身的花費由發射概率決定。那麼所有備選狀態構成一幅有 向無環圖,待求的概率最大的狀態序列就是圖中的最長路徑,此時的搜索算法稱爲維特比算法,如圖下圖所示:

    上圖從左往右時序遞增,虛線由初始狀態概率決定,實線則是轉移概率。由於受到觀測序列的約束,不同狀態發射觀測的概率不同,所以每個節點本身也必須計算自己的花費,由發射概率決定。又由於 Yt+1 僅依賴於 Yt,所以網狀圖可以動態規劃的搜索,也就是維特比算法

    • 初始化,t=1 時初始最優路徑的備選由 N 個狀態組成,它們的前驅爲空。
      δ1,i=πiBi,o1,i=1,,Nψ1,i=0,i=1,,N \begin{aligned} \delta_{1, i}=\pi_{i} \boldsymbol{B}_{i, o_{1}}, & i=1, \cdots, N \\ \psi_{1, i}=0, & i=1, \cdots, N \end{aligned}
      其中,δ 存儲在時刻 t 以 Si 結尾的所有局部路徑的最大概率。ψ 存儲局部最優路徑末狀態 Yt 的前驅狀態。

    • 遞推,t >= 2 時每條備選路徑像貪吃蛇一樣吃入一個新狀態,長度增加一個單位,根據轉移概率和發射概率計算花費。找出新的局部最優路徑,更新 δ、ψ 兩個數組。
      δt,i=max1jN(δt1,jAj,i)Bi,ot,i=1,,Nψt,i=argmax1jN(δt1,jAj,i),i=1,,N \begin{array}{ll} {\delta_{t, i}=\max _{1 \leqslant j \leqslant N}\left(\delta_{t-1, j} A_{j, i}\right) B_{i, o_{t}},} & {i=1, \cdots, N} \\ {\psi_{t, i}=\arg \max _{1 \leqslant j \leqslant N}\left(\delta_{t-1, j} A_{j, i}\right),} & {i=1, \cdots, N} \end{array}

    • 終止,找出最終時刻 δt,i 數組中的最大概率 P*,以及相應的結尾狀態下標 i*t。
      p=max1iNδT,iiT=argmax1iNδT,i \begin{aligned} &p^{*}=\max _{1 \leqslant i \leqslant N} \delta_{T, i}\\ &i_{T}^{*}=\arg \max _{1 \leqslant i \leqslant N} \delta_{T, i} \end{aligned}

    • 回溯,根據前驅數組 ψ 回溯前驅狀態,取得最優路徑狀態下標。
      it=ψt+1,it+1,t=T1,T2,,1 i_{t}^{*}=\psi_{t+1, i_{t+1}}, \quad t=T-1, T-2, \cdots, 1

    預測代碼如下(接上面代碼):

    pred = JArray(JInt, 1)([0, 0, 0])
    prob = given_model.predict(observations_index, pred)
    print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in
                   zip(observations_index, pred)) + " {:.3f}".format(np.math.exp(prob)))
    

    輸出爲:

    normal/Healthy cold/Healthy dizzy/Fever 0.015
    

    觀察該結果,“/”隔開觀測和狀態,最後的 0.015 是序列的聯合概率。

4.5 隱馬爾可夫模型應用於中文分詞

HanLP 已經實現了基於隱馬爾可夫模型的中文分詞器 HMMSegmenter,並且實現了訓練接口。代碼詳見:

hmm_cws.py:https://github.com/NLP-LOVE/Introduction-NLP/tree/master/code/ch04/hmm_cws.py

4.6 性能評測

如果隱馬爾可夫模型中每個狀態僅依賴於前一個狀態, 則稱爲一階;如果依賴於前兩個狀態,則稱爲二階。既然一階隱馬爾可夫模型過於簡單,是否可以切換到二階來提高分數呢?

答案是可以的,跟一階類似,這裏不再詳細介紹二階隱馬爾可夫模型,詳細請看原書。

這裏我們使用 MSR語料庫進行評測,結果如下表所示:

算法 P R F1 R(oov) R(IV)
最長匹配 89.41 94.64 91.95 2.58 97.14
二元語法 92.38 96.70 94.49 2.58 99.26
一階HHM 78.49 80.38 79.42 41.11 81.44
二階HHM 78.34 80.01 79.16 42.06 81.04

可以看到,二階隱馬爾可夫模型的 Roov 有少許提升,但綜合 F1 反而下降了。這說明增加隱馬爾可夫模型的階數並不能提高分詞器的準確率,單靠提高轉移概率矩陣的複雜度並不能提高模型的擬合能力,我們需要從別的方面想辦法。目前市面上一些開源分詞器仍然停留在一階隱馬爾可夫模型的水平,比如著名的結巴分詞,它們的準確率也只能達到80%左右。

4.7 總結

這一章我們想解決的問題是新詞識別,爲此從詞語級模型切換到字符級模型,將中文分詞任務轉換爲序列標註問題。作爲新手起步,我們嘗試了最簡單的序列標註模型----隱馬爾可夫模型。隱馬爾可夫模型的基本問題有三個:樣本生成、參數估計、序列預測

然而隱馬爾可夫模型用於中文分詞的效果並不理想,雖然召回了一半的 OOV,但綜合 F1 甚至低於詞典分詞。哪怕升級到二階隱馬爾可夫模型, F1 值依然沒有提升。 看來樸素的隱馬爾可夫模型不適合中文分詞,我們需要更高級的模型。

話說回來,隱馬爾可夫模型作爲入門模型,比較容易上手,同時也是許多高級模型的基礎。打好基礎,我們才能挑戰高級模型。

4.8 GitHub項目

HanLP何晗–《自然語言處理入門》筆記:

https://github.com/NLP-LOVE/Introduction-NLP

項目持續更新中…

目錄


章節
第 1 章:新手上路
第 2 章:詞典分詞
第 3 章:二元語法與中文分詞
第 4 章:隱馬爾可夫模型與序列標註
第 5 章:感知機分類與序列標註
第 6 章:條件隨機場與序列標註
第 7 章:詞性標註
第 8 章:命名實體識別
第 9 章:信息抽取
第 10 章:文本聚類
第 11 章:文本分類
第 12 章:依存句法分析
第 13 章:深度學習與自然語言處理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章