維特比算法

維特比算法（Viterbi algorithm）是一種動態規劃演算法。它用於尋找最有可能產生觀測事件序列的維特比路徑——隱含狀態序列，特別是在馬爾可夫資訊源上下文和隱馬爾可夫模型中。

術語「維特比路徑」和「維特比算法」也被用於尋找觀察結果最有可能解釋相關的動態規劃演算法。例如在統計句法分析中動態規劃演算法可以被用於發現最可能的上下文無關的派生（解析）的字串，有時被稱為「維特比分析」。

維特比算法由安德魯·維特比於1967年提出，用於在數字通信鏈路中解卷積以消除噪音。此演算法被廣泛應用於CDMA和GSM數字蜂窩網絡、撥號數據機、衛星、深空通信和802.11無線網絡中解卷積碼。現今也被常常用於語音識別、關鍵字識別、計算語言學和生物資訊學中。例如在語音（語音識別）中，聲音信號做為觀察到的事件序列，而文本字符串，被看作是隱含的產生聲音信號的原因，因此可對聲音信號應用維特比算法尋找最有可能的文本字符串。

## 演算法

假設給定隱式馬爾可夫模型（HMM）狀態空間 $S$，共有k個狀態，初始狀態 $i$ 的機率為 $\pi_i$，從狀態 $i$ 到狀態 $j$ 的轉移機率為 $a_{i,j}$。令觀察到的輸出為 $y_1,\dots, y_T$。產生觀察結果的最有可能的狀態序列 $x_1,\dots,x_T$ 由遞推關係給出：

$$\begin{array}{rcl} V_{1,k} &=& \mathrm{P}\big( y_1 \ | \ k \big) \cdot \pi_k \\ V_{t,k} &=& \max_{x \in S} \left( \mathrm{P}\big( y_t \ | \ k \big) \cdot a_{x,k} \cdot V_{t-1,x}\right) \end{array}$$

此處 $V_{t,k}$ 是前 $t$ 個最終狀態為 $k$ 的觀測結果最有可能對應的狀態序列的機率。通過保存向後指針記住在第二個等式中用到的狀態 $x$ 可以獲得維特比路徑。聲明一個函數 $\mathrm{Ptr}(k,t)$，它返回若 $t > 1$時計算 $V_{t,k}$ 用到的 $x$ 值 或若 $t=1$時的$k$ . 這樣:

$$\begin{array}{rcl} x_T &=& \arg\max_{x \in S} (V_{T,x}) \\ x_{t-1} &=& \mathrm{Ptr}(x_t,t) \end{array}$$

這裡我們使用了arg max的標準定義演算法複雜度為 $O(T\times\left|{S}\right|^2)$

## 例子

想像一個鄉村診所。村民有著非常理想化的特性，要麼健康要麼發燒。他們只有問診所的醫生的才能知道是否發燒。聰明的醫生通過詢問病人的感覺診斷他們是否發燒。村民只回答他們感覺正常、頭暈或冷。

假設一個病人每天來到診所並告訴醫生他的感覺。醫生相信病人的健康狀況如同一個離散馬爾可夫鏈。病人的狀態有兩種「健康」和「發燒」，但醫生不能直接觀察到，這意味著狀態對他是「隱含」的。每天病人會告訴醫生自己有以下幾種由他的健康狀態決定的感覺的一種：正常、冷或頭暈。這些是觀察結果。整個系統為一個隱馬爾可夫模型(HMM)。

醫生知道村民的總體健康狀況，還知道發燒和沒發燒的病人通常會抱怨什麼症狀。換句話說，醫生知道隱馬爾可夫模型的參數。這可以用Python語言表示如下:

```python
states = ('Healthy', 'Fever')
 
observations = ('normal', 'cold', 'dizzy')
 
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},
   }
```

在這段代碼中, 起始機率`start_probability` 表示病人第一次到訪時醫生認為其所處的HMM狀態，他唯一知道的是病人傾向於是健康的。這裡用到的特定機率分布不是均衡的,如轉移機率大約是`{'Healthy': 0.57, 'Fever': 0.43}`。轉移機率`transition_probability`表示潛在的馬爾可夫鏈中健康狀態的變化。在這個例子中，當天健康的病人僅有30%的機會第二天會發燒。放射機率`emission_probability`表示每天病人感覺的可能性。假如他是健康的，50%會感覺正常。如果他發燒了，有60%的可能感覺到頭暈。

<image>

病人連續三天看醫生，醫生發現第一天他感覺正常，第二天感覺冷，第三天感覺頭暈。於是醫生產生了一個問題：怎樣的健康狀態序列最能夠解釋這些觀察結果。維特比算法解答了這個問題。

```python
def viterbi(obs, states, start_p, trans_p, emit_p):
    V = [{}]
    path = {}

    # Initialize
    for st in states:
        V[0][st] = start_p[st] * emit_p[st][obs[0]]
        path[st] = [st]

    # Run Viterbi when t > 0
    for t in range(1,len(obs)):
        V.append({})
        newpath = {}

        for curr_st in states:
            paths_to_curr_st = []
            for prev_st in states:
                paths_to_curr_st.append((V[t-1][prev_st] * trans_p[prev_st][curr_st] * emit_p[curr_st][obs[t]], prev_st))
            curr_prob, prev_state = max(paths_to_curr_st)
            V[t][curr_st] = curr_prob
            newpath[curr_st] = path[prev_state] + [curr_st]

        # No need to keep the old paths
        path = newpath

    for line in dptable(V):
        print(line)
    prob, end_state = max([(V[-1][st], st) for st in states])
    return prob, path[end_state]

def dptable(V):
    # Print a table of steps from dictionary
    yield ' ' * 4 + '    '.join(states)
    for t in range(len(V)):
        yield '{}   '.format(t) + '    '.join(['{:.4f}'.format(V[t][state]) for state in V[0]])
```

函數`viterbi` 具有以下參數: `obs` 為觀察結果序列, 例如 `['normal', 'cold', 'dizzy']`； `states` 為一組隱含狀態； `start_p` 為起始狀態機率; `trans_p` 為轉移機率; 而 `emit_p` 為放射機率。為了簡化代碼，我們假設觀察序列 `obs` 非空且 `trans_p[i][j]` 和 `emit_p[i][j]` 對所有狀態 i,j 有定義。

在運行的例子中正向/維特比算法使用如下:

```python
def example():
    return viterbi(observations,
                   states,
                   start_probability,
                   transition_probability,
                   emission_probability)
print(example())
```

維特比算法揭示了觀察結果 `['normal', 'cold', 'dizzy']` 最有可能由狀態序列 `['Healthy', 'Healthy', 'Fever']`產生。換句話說，對於觀察到的活動, 病人第一天感到正常，第二天感到冷時都是健康的，而第三天發燒了。

維特比算法的計算過程可以直觀地由格圖表示。維特比路徑本質上是穿過格式結構的最長路徑。診所例子的格式結構如下, 黑色加粗的是維特比路徑：

<image>

在實現維特比算法時需注意許多程式語言使用浮點數計算，當 p 很小時可能會導致結果算術下溢。避免這一問題的常用技巧是在整個計算過程中使用對數機率，在對數系統中也使用了同樣的技巧。當演算法結束時，可以通過適當的冪運算獲得精確結果。

## 偽代碼

首先是一些問題必要的設置。設觀察值空間為 $O=\{o_1,o_2,\dots,o_N\}$、狀態空間為 $S=\{s_1,s_2,\dots,s_K\}$、觀察值序列為 $Y=\{y_1,y_2,\ldots, y_T\}$, $A$ 為 $K\times K$轉移矩陣，其中 $A_{ij}$ 為從狀態 $s_i$ 轉移到 $s_j$ 的轉移機率、 $B$ 為 $K\times N$ 放射矩陣(emission matrix)，其中 $B_{ij}$ 為在狀態 $s_i$ 觀察到 $o_j$ 的機率、大小為 $K$ 的初始機率陣列 $\pi$， $\pi_i$ 為 $x_1 == s_i$ 的機率。我們稱路徑 $X=\{x_1,x_2,\ldots,x_T\}$ 為生成觀察值 $Y=\{y_1,y_2,\ldots, y_T\}$ 的狀態序列。

在這個動態規劃問題中, 我們構造了兩個大小為 $K\times T$ 的二維表 $T_1, T_2$。$T_1$ 的每個元素, $T_1[i,j]$，保存生成 $Y=\{y_1,y_2,\ldots, y_j\}$ 時最有可能的路徑 $\hat{X}=\{\hat{x}_1,\hat{x}_2,\ldots,\hat{x}_j\}$， $\hat{x}_j=s_i$ 的機率。$T_2$ 的每個元素, $T_2[i,j]$，保存最有可能路徑 $\hat{X}=\{\hat{x}_1,\hat{x}_2,\ldots,\hat{x}_{j-1},\hat{x}_j\}$ , $\forall j, 2\leq j \leq T$ 的 $\hat{x}_{j-1}$。

我們按 $K\cdot j+i$ 增序填充兩個表 $T_1[i,j],T_2[i,j]$。

$T_1[i,j]=\max_{k}{(T_1[k,j-1]\cdot A_{ki}\cdot B_{iy_j})}$,

$$T_2[i,j]=\arg\max_{k}{(T_1[k,j-1]\cdot A_{ki}\cdot B_{iy_j})}$$

輸入:

- 觀察空間 $O=\{o_1,o_2,\dots,o_N\}$。
- 狀態 $S=\{s_1,s_2,\dots,s_K\}$。
- 觀察序列 $Y=\{y_1,y_2,\ldots, y_T\}$ 若在$t$ 時間觀察值為 $o_i$，則 $y_t==i$,
- 大小為 $K\cdot K$ 的轉移矩陣 $A$，$A_{ij}$ 為從狀態 $s_i$ 到 $s_j$ 的轉移機率。
- 大小為 $K\cdot N$ 的放射矩陣 $B$，$B_{ij}$ 為狀態 $s_i$ 觀察到 $o_j$ 的機率。
- 大小為 $K$ 的初始機率陣列 $\pi$，$\pi_i$ 為 $x_1 == s_i$ 的機率。

輸出:

- 最有可能的隱含狀態序列 $X=\{x_1,x_2,\ldots,x_T\}$

```
 '''function''' ''VITERBI''( ''O'', ''S'', ''π'', ''Y'', ''A'', ''B'' ) : X
     '''for''' each state ''s<sub>i</sub>'' '''do'''

     '''end for'''
     '''for''' ''i'' ← ''2'',''3'',...,''T'' '''do'''
         '''for''' each state ''s<sub>j</sub>'' '''do'''
             <math>T_1[j,i] \gets \max_{k}{(T_1[k,i-1]\cdot A_{kj}\cdot B_{jy_i})} </math>
             <math>T_2[j,i] \gets \arg\max_{k}{(T_1[k,i-1]\cdot A_{kj}\cdot B_{jy_i})} </math>
         '''end for'''
     '''end for'''
     <math>z_T \gets \arg\max_{k}{(T_1[k,T])} </math>

     '''for''' ''i'' ← ''T'',''T-1'',...,''2'' '''do'''

     '''end for'''
     '''return''' X
 '''end function'''
```

## 使用動態規劃的演算法

- 最長公共子序列
- Floyd-Warshall演算法

## 參考資料

- (note: the Viterbi decoding algorithm is described in section IV.) Subscription required.