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

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

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

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

例以下面一句:git

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

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

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

4.1 序列標註問題

序列標註指的是給定一個序列 \(x=x_1x_2...x_n\),找出序列中每一個元素對應標籤 \(y=y_1y_2...y_n\) 的問題。其中,y 全部可能的取值集合稱爲標註集。好比,輸入一個天然數序列,輸出它們的奇偶性。app

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

  1. 序列標註與中文分詞ide

    考慮一個字符序列(字符串) 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 | π) 描述。其中
    \[ \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(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=\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

    \(\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(y_1=s_i)=\pi_i\]

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

      \[P(y_t=s_j|y_{t-1}=s_i)=A_{i,j}\]

      因此狀態序列的機率爲上面式子的乘積:

      \[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(x_t=O_j|y_t=s_i)=B_{i,j}\]

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

      \[p(x | y)=\prod_{t=1}^{T} p\left(x_{t} | y_{t}\right)\]

    • 最後帶入聯合機率公式得:
      \[ \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 個狀態組成,它們的前驅爲空。
      \[ \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 時每條備選路徑像貪吃蛇同樣吃入一個新狀態,長度增長一個單位,根據轉移機率和發射機率計算花費。找出新的局部最優路徑,更新 δ、ψ 兩個數組。
      \[ \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。
      \[ \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} \]

    • 回溯,根據前驅數組 ψ 回溯前驅狀態,取得最優路徑狀態下標。
      \[ 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 章:深度學習與天然語言處理
相關文章
相關標籤/搜索