HMM學習最佳範例: 前向算法(Forward Algorithm)

轉載自:http://www.52nlp.cn/hmm-learn-best-practices-five-forward-algorithm-1

五、前向算法(Forward Algorithm)

計算觀察序列的概率(Finding the probability of an observed sequence)

1.窮舉搜索( Exhaustive search for solution)
  給定隱馬爾科夫模型,也就是在模型參數(pi, A, B)已知的情況下,我們想找到觀察序列的概率。還是考慮天氣這個例子,我們有一個用來描述天氣及與它密切相關的海藻溼度狀態的隱馬爾科夫模型(HMM),另外我們還有一個海藻的溼度狀態觀察序列。假設連續3天海藻溼度的觀察結果是(乾燥、溼潤、溼透)——而這三天每一天都可能是晴天、多雲或下雨,對於觀察序列以及隱藏的狀態,可以將其視爲網格:
網格
  網格中的每一列都顯示了可能的的天氣狀態,並且每一列中的每個狀態都與相鄰列中的每一個狀態相連。而其狀態間的轉移都由狀態轉移矩陣提供一個概率。在每一列下面都是某個時間點上的觀察狀態,給定任一個隱藏狀態所得到的觀察狀態的概率由混淆矩陣提供。
  可以看出,一種計算觀察序列概率的方法是找到每一個可能的隱藏狀態,並且將這些隱藏狀態下的觀察序列概率相加。對於上面那個(天氣)例子,將有3^3 = 27種不同的天氣序列可能性,因此,觀察序列的概率是:
  Pr(dry,damp,soggy | HMM) = Pr(dry,damp,soggy | sunny,sunny,sunny) + Pr(dry,damp,soggy | sunny,sunny ,cloudy) + Pr(dry,damp,soggy | sunny,sunny ,rainy) + . . . . Pr(dry,damp,soggy | rainy,rainy ,rainy)
  用這種方式計算觀察序列概率極爲昂貴,特別對於大的模型或較長的序列,因此我們可以利用這些概率的時間不變性來減少問題的複雜度。

2.使用遞歸降低問題複雜度
  給定一個隱馬爾科夫模型(HMM),我們將考慮遞歸地計算一個觀察序列的概率。我們首先定義局部概率(partial probability),它是到達網格中的某個中間狀態時的概率。然後,我們將介紹如何在t=1和t=n(>1)時計算這些局部概率。
  假設一個T-長觀察序列是:
     t-long 序列
  
 2a.局部概率(alpha's)
  考慮下面這個網格,它顯示的是天氣狀態及對於觀察序列乾燥,溼潤及溼透的一階狀態轉移情況:
   trellis.1
  我們可以將計算到達網格中某個中間狀態的概率作爲所有到達這個狀態的可能路徑的概率求和問題。
  例如,t=2時位於“多雲”狀態的局部概率通過如下路徑計算得出:
   paths.to.cloudy
  我們定義t時刻位於狀態j的局部概率爲at(j)——這個局部概率計算如下:
  alphat ( j )= Pr( 觀察狀態 | 隱藏狀態j ) x Pr(t時刻所有指向j狀態的路徑)
  對於最後的觀察狀態,其局部概率包括了通過所有可能的路徑到達這些狀態的概率——例如,對於上述網格,最終的局部概率通過如下路徑計算得出:
   alphas.at.t_eq_3
  由此可見,對於這些最終局部概率求和等價於對於網格中所有可能的路徑概率求和,也就求出了給定隱馬爾科夫模型(HMM)後的觀察序列概率。
  第3節給出了一個計算這些概率的動態示例。

2b.計算t=1時的局部概率alpha's
  我們按如下公式計算局部概率:
  alphat ( j )= Pr( 觀察狀態 | 隱藏狀態j ) x Pr(t時刻所有指向j狀態的路徑)
  特別當t=1時,沒有任何指向當前狀態的路徑。故t=1時位於當前狀態的概率是初始概率,即Pr(state|t=1)=P(state),因此,t=1時的局部概率等於當前狀態的初始概率乘以相關的觀察概率:
         5.2_2
  所以初始時刻狀態j的局部概率依賴於此狀態的初始概率及相應時刻我們所見的觀察概率。

2c.計算t>1時的局部概率alpha's
  我們再次回顧局部概率的計算公式如下:
  alphat ( j )= Pr( 觀察狀態 | 隱藏狀態j ) x Pr(t時刻所有指向j狀態的路徑)
  我們可以假設(遞歸地),乘號左邊項“Pr( 觀察狀態 | 隱藏狀態j )”已經有了,現在考慮其右邊項“Pr(t時刻所有指向j狀態的路徑)”。
  爲了計算到達某個狀態的所有路徑的概率,我們可以計算到達此狀態的每條路徑的概率並對它們求和,例如:
      allpath
  計算alpha所需要的路徑數目隨着觀察序列的增加而指數級遞增,但是t-1時刻alpha's給出了所有到達此狀態的前一路徑概率,因此,我們可以通過t-1時刻的局部概率定義t時刻的alpha's,即:
     5.1.2.3_1
  故我們所計算的這個概率等於相應的觀察概率(亦即,t+1時在狀態j所觀察到的符號的概率)與該時刻到達此狀態的概率總和——這來自於上一步每一個局部概率的計算結果與相應的狀態轉移概率乘積後再相加——的乘積。
  注意我們已經有了一個僅利用t時刻局部概率計算t+1時刻局部概率的表達式。
  現在我們就可以遞歸地計算給定隱馬爾科夫模型(HMM)後一個觀察序列的概率了——即通過t=1時刻的局部概率alpha's計算t=2時刻的alpha's,通過t=2時刻的alpha's計算t=3時刻的alpha's等等直到t=T。給定隱馬爾科夫模型(HMM)的觀察序列的概率就等於t=T時刻的局部概率之和。

2d.降低計算複雜度
  我們可以比較通過窮舉搜索(評估)和通過遞歸前向算法計算觀察序列概率的時間複雜度。
  我們有一個長度爲T的觀察序列O以及一個含有n個隱藏狀態的隱馬爾科夫模型l=(pi,A,B)。
  窮舉搜索將包括計算所有可能的序列:
   5.1.2.4_1
  公式
    5.1.2.4_2
  對我們所觀察到的概率求和——注意其複雜度與T成指數級關係。相反的,使用前向算法我們可以利用上一步計算的信息,相應地,其時間複雜度與T成線性關係。
注:窮舉搜索的時間複雜度是2TN^T,前向算法的時間複雜度是N^2T,其中T指的是觀察序列長度,N指的是隱藏狀態數目。

3.總結
  我們的目標是計算給定隱馬爾科夫模型HMM下的觀察序列的概率——Pr(observations |lamda)。
  我們首先通過計算局部概率(alpha's)降低計算整個概率的複雜度,局部概率表示的是t時刻到達某個狀態s的概率。
  t=1時,可以利用初始概率(來自於P向量)和觀察概率Pr(observation|state)(來自於混淆矩陣)計算局部概率;而t>1時的局部概率可以利用t-時的局部概率計算。
  因此,這個問題是遞歸定義的,觀察序列的概率就是通過依次計算t=1,2,...,T時的局部概率,並且對於t=T時所有局部概率alpha's相加得到的。
  注意,用這種方式計算觀察序列概率的時間複雜度遠遠小於計算所有序列的概率並對其相加(窮舉搜索)的時間複雜度。

前向算法定義(Forward algorithm definition)

  我們使用前向算法計算T長觀察序列的概率:
     5.2_1
  其中y的每一個是觀察集合之一。局部(中間)概率(alpha's)是遞歸計算的,首先通過計算t=1時刻所有狀態的局部概率alpha
     5.2_2
  然後在每個時間點,t=2,... ,T時,對於每個狀態的局部概率,由下式計算局部概率alpha:
     5.2_3
  也就是當前狀態相應的觀察概率與所有到達該狀態的路徑概率之積,其遞歸地利用了上一個時間點已經計算好的一些值。
  最後,給定HMM,lamda,觀察序列的概率等於T時刻所有局部概率之和:
     5.2_4
  再重複說明一下,每一個局部概率(t > 2 時)都由前一時刻的結果計算得出。
  對於“天氣”那個例子,下面的圖表顯示了t = 2爲狀態爲多雲時局部概率alpha的計算過程。這是相應的觀察概率b與前一時刻的局部概率與狀態轉移概率a相乘後的總和再求積的結果:
   example.forward
(注:本圖及維特比算法4中的相似圖存在問題,具體請見文後評論,非常感謝讀者YaseenTA的指正)

總結(Summary)

  我們使用前向算法來計算給定隱馬爾科夫模型(HMM)後的一個觀察序列的概率。它在計算中利用遞歸避免對網格所有路徑進行窮舉計算。
  給定這種算法,可以直接用來確定對於已知的一個觀察序列,在一些隱馬爾科夫模型(HMMs)中哪一個HMM最好的描述了它——先用前向算法評估每一個(HMM),再選取其中概率最高的一個。

首先需要說明的是,本節不是這個系列的翻譯,而是作爲前向算法這一章的補充,希望能從實踐的角度來說明前向算法。除了用程序來解讀hmm的前向算法外,還希望將原文所舉例子的問題拿出來和大家探討。
  文中所舉的程序來自於UMDHMM這個C語言版本的HMM工具包,具體見《幾種不同程序語言的HMM版本》。先說明一下UMDHMM這個包的基本情況,在Linux環境下,進入umdhmm-v1.02目錄,“make all”之後會產生4個可執行文件,分別是:
  genseq: 利用一個給定的隱馬爾科夫模型產生一個符號序列(Generates a symbol sequence using the specified model sequence using the specified model)
  testfor: 利用前向算法計算log Prob(觀察序列| HMM模型)(Computes log Prob(observation|model) using the Forward algorithm.)
  testvit: 對於給定的觀察符號序列及HMM,利用Viterbi 算法生成最可能的隱藏狀態序列(Generates the most like state sequence for a given symbol sequence, given the HMM, using Viterbi)
  esthmm: 對於給定的觀察符號序列,利用BaumWelch算法學習隱馬爾科夫模型HMM(Estimates the HMM from a given symbol sequence using BaumWelch)。
  這些可執行文件需要讀入有固定格式的HMM文件及觀察符號序列文件,格式要求及舉例如下:
  HMM 文件格式:
--------------------------------------------------------------------
    M= number of symbols
    N= number of states
    A:
    a11 a12 ... a1N
    a21 a22 ... a2N
    . . . .
     . . . .
     . . . .
    aN1 aN2 ... aNN
    B:
    b11 b12 ... b1M
    b21 b22 ... b2M
    . . . .
    . . . .
     . . . .
    bN1 bN2 ... bNM
    pi:
    pi1 pi2 ... piN
--------------------------------------------------------------------

  HMM文件舉例:
--------------------------------------------------------------------
    M= 2
    N= 3
    A:
    0.333 0.333 0.333
    0.333 0.333 0.333
    0.333 0.333 0.333
    B:
    0.5 0.5
    0.75 0.25
    0.25 0.75
    pi:
    0.333 0.333 0.333
--------------------------------------------------------------------

  觀察序列文件格式:
--------------------------------------------------------------------
    T=seqence length
    o1 o2 o3 . . . oT
--------------------------------------------------------------------

  觀察序列文件舉例:
--------------------------------------------------------------------
    T= 10
    1 1 1 1 2 1 2 2 2 2
--------------------------------------------------------------------

  對於前向算法的測試程序testfor來說,運行:
   testfor model.hmm(HMM文件) obs.seq(觀察序列文件)
  就可以得到觀察序列的概率結果的對數值,這裏我們在testfor.c的第58行對數結果的輸出下再加一行輸出:
   fprintf(stdout, "prob(O| model) = %f\n", proba);
  就可以輸出運用前向算法計算觀察序列所得到的概率值。至此,所有的準備工作已結束,接下來,我們將進入具體的程序解讀。
  首先,需要定義HMM的數據結構,也就是HMM的五個基本要素,在UMDHMM中是如下定義的(在hmm.h中):

typedef struct
{
int N; /* 隱藏狀態數目;Q={1,2,...,N} */
int M; /* 觀察符號數目; V={1,2,...,M}*/
double **A; /* 狀態轉移矩陣A[1..N][1..N]. a[i][j] 是從t時刻狀態i到t+1時刻狀態j的轉移概率 */
double **B; /* 混淆矩陣B[1..N][1..M]. b[j][k]在狀態j時觀察到符合k的概率。*/
double *pi; /* 初始向量pi[1..N],pi[i] 是初始狀態概率分佈 */
} HMM;

前向算法程序示例如下(在forward.c中):
/*
 函數參數說明:
 *phmm:已知的HMM模型;T:觀察符號序列長度;
 *O:觀察序列;**alpha:局部概率;*pprob:最終的觀察概率
*/
void Forward(HMM *phmm, int T, int *O, double **alpha, double *pprob)
{
  int i, j;   /* 狀態索引 */
  int t;    /* 時間索引 */
  double sum; /*求局部概率時的中間值 */

  /* 1. 初始化:計算t=1時刻所有狀態的局部概率alpha: */
  for (i = 1; i <= phmm->N; i++)
    alpha[1][i] = phmm->pi[i]* phmm->B[i][O[1]];
  
  /* 2. 歸納:遞歸計算每個時間點,t=2,... ,T時的局部概率 */
  for (t = 1; t < T; t++)   {     for (j = 1; j <= phmm->N; j++)
    {
      sum = 0.0;
      for (i = 1; i <= phmm->N; i++)
        sum += alpha[t][i]* (phmm->A[i][j]);
      alpha[t+1][j] = sum*(phmm->B[j][O[t+1]]);
    }
  }

  /* 3. 終止:觀察序列的概率等於T時刻所有局部概率之和*/
  *pprob = 0.0;
  for (i = 1; i <= phmm->N; i++)
    *pprob += alpha[T][i];
}

  下一節我將用這個程序來驗證英文原文中所舉前向算法演示例子的問題。

在HMM這個翻譯系列的原文中,作者舉了一個前向算法的交互例子,這也是這個系列中比較出彩的地方,但是,在具體運行這個例子的時候,卻發現其似乎有點問題。
  先說一下如何使用這個交互例子,運行時需要瀏覽器支持Java,我用的是firefox。首先在Set按鈕前面的對話框裏上觀察序列,如“Dry,Damp, Soggy” 或“Dry Damp Soggy”,觀察符號間用逗號或空格隔開;然後再點擊Set按鈕,這樣就初始化了觀察矩陣;如果想得到一個總的結果,即Pr(觀察序列|隱馬爾科夫模型),就點旁邊的Run按鈕;如果想一步一步觀察計算過程,即每個節點的局部概率,就單擊旁邊的Step按鈕。
  原文交互例子(即天氣這個例子)中所定義的已知隱馬爾科夫模型如下:
  1、隱藏狀態 (天氣):Sunny,Cloudy,Rainy;
  2、觀察狀態(海藻溼度):Dry,Dryish,Damp,Soggy;
  3、初始狀態概率: Sunny(0.63), Cloudy(0.17), Rainy(0.20);
  4、狀態轉移矩陣:

             weather today
             Sunny Cloudy Rainy
     weather Sunny 0.500 0.375 0.125
    yesterday Cloudy 0.250 0.125 0.625
          Rainy  0.250 0.375 0.375

  5、混淆矩陣:

            observed states
           Dry Dryish Damp Soggy
         Sunny 0.60 0.20 0.15 0.05
    hidden  Cloudy 0.25 0.25 0.25 0.25
    states  Rainy 0.05 0.10 0.35 0.50

  爲了UMDHMM也能運行這個例子,我們將上述天氣例子中的隱馬爾科夫模型轉化爲如下的UMDHMM可讀的HMM文件weather.hmm:
--------------------------------------------------------------------
    M= 4
    N= 3 
    A:
    0.500 0.375 0.125
    0.250 0.125 0.625
    0.250 0.375 0.375
    B:
    0.60 0.20 0.15 0.05
    0.25 0.25 0.25 0.25
    0.05 0.10 0.35 0.50
    pi:
    0.63 0.17 0.20
--------------------------------------------------------------------
  在運行例子之前,如果讀者也想觀察每一步的運算結果,可以將umdhmm-v1.02目錄下forward.c中的void Forward(…)函數替換如下:
--------------------------------------------------------------------
void Forward(HMM *phmm, int T, int *O, double **alpha, double *pprob)
{
  int i, j; /* state indices */
  int t; /* time index */
  double sum; /* partial sum */
  
  /* 1. Initialization */
  for (i = 1; i <= phmm->N; i++)
  {
    alpha[1][i] = phmm->pi[i]* phmm->B[i][O[1]];
    printf( "a[1][%d] = pi[%d] * b[%d][%d] = %f * %f = %f\\n",i, i, i, O[i], phmm->pi[i], phmm->B[i][O[1]], alpha[1][i] );
  }
  
  /* 2. Induction */
  for (t = 1; t < T; t++)   {     for (j = 1; j <= phmm->N; j++)
    {
      sum = 0.0;
      for (i = 1; i <= phmm->N; i++)
      {
        sum += alpha[t][i]* (phmm->A[i][j]);
        printf( "a[%d][%d] * A[%d][%d] = %f * %f = %f\\n", t, i, i, j, alpha[t][i], phmm->A[i][j], alpha[t][i]* (phmm->A[i][j]));
        printf( "sum = %f\\n", sum );
      }
      alpha[t+1][j] = sum*(phmm->B[j][O[t+1]]);
      printf( "a[%d][%d] = sum * b[%d][%d]] = %f * %f = %f\\n",t+1, j, j, O[t+1], sum, phmm->B[j][O[t+1]], alpha[t+1][j] );
    }
  }

  /* 3. Termination */
  *pprob = 0.0;
  for (i = 1; i <= phmm->N; i++)
  {
    *pprob += alpha[T][i];
    printf( "alpha[%d][%d] = %f\\n", T, i, alpha[T][i] );
    printf( "pprob = %f\\n", *pprob );
  }
}
--------------------------------------------------------------------
  替換完畢之後,重新“make clean”,“make all”,這樣新的testfor可執行程序就可以輸出前向算法每一步的計算結果。
  現在我們就用testfor來運行原文中默認給出的觀察序列“Dry,Damp,Soggy”,其所對應的UMDHMM可讀的觀察序列文件test1.seq:
--------------------------------------------------------------------
    T=3
    1 3 4
--------------------------------------------------------------------
  好了,一切準備工作就緒,現在就輸入如下命令:
    testfor weather.hmm test1.seq > result1
  result1就包含了所有的結果細節:
--------------------------------------------------------------------
Forward without scaling
a[1][1] = pi[1] * b[1][1] = 0.630000 * 0.600000 = 0.378000
a[1][2] = pi[2] * b[2][3] = 0.170000 * 0.250000 = 0.042500
a[1][3] = pi[3] * b[3][4] = 0.200000 * 0.050000 = 0.010000

pprob = 0.026901
log prob(O| model) = -3.615577E+00
prob(O| model) = 0.026901


--------------------------------------------------------------------
  黑體部分是最終的觀察序列的概率結果,即本例中的Pr(觀察序列|HMM) = 0.026901。
  但是,在原文中點Run按鈕後,結果卻是:Probability of this model = 0.027386915。
  這其中的差別到底在哪裏?我們來仔細觀察一下中間運行過程:
  在初始化亦t=1時刻的局部概率計算兩個是一致的,沒有問題。但是,t=2時,在隱藏狀態“Sunny”的局部概率是不一致的。英文原文給出的例子的運行結果是:
  Alpha = (((0.37800002*0.5) + (0.0425*0.375) + (0.010000001*0.125)) * 0.15) = 0.03092813
  而UMDHMM給出的結果是:
--------------------------------------------------------------------
  a[1][1] * A[1][1] = 0.378000 * 0.500000 = 0.189000
  sum = 0.189000
  a[1][2] * A[2][1] = 0.042500 * 0.250000 = 0.010625
  sum = 0.199625
  a[1][3] * A[3][1] = 0.010000 * 0.250000 = 0.002500
  sum = 0.202125
  a[2][1] = sum * b[1][3]] = 0.202125 * 0.150000 = 0.030319
--------------------------------------------------------------------
  區別就在於狀態轉移概率的選擇上,原文選擇的是狀態轉移矩陣中的第一行,而UMDHMM選擇的則是狀態轉移矩陣中的第一列。如果從原文給出的狀態轉移矩陣來看,第一行代表的是從前一時刻的狀態“Sunny”分別到當前時刻的狀態“Sunny”,“Cloudy”,“Rainy”的概率;而第一列代表的是從前一時刻的狀態“Sunny”,“Cloudy”,“Rainy”分別到當前時刻狀態“Sunny”的概率。這樣看來似乎原文的計算過程有誤,讀者不妨多試幾個例子看看,前向算法這一章就到此爲止了。

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