Viterbi算法是一种动态规划算法,用于寻找最有可能产生观测事件序列的--viterbi路径--隐含状态序列,特别是在马尔可夫信息源上下文和隐马尔可夫模型中。
在语音识别中,声音信号作为观察到的事件序列,而文本字符串被看作是隐含的产生声音信号的原因,因此可对声音信号应用维特比算法寻找最有可能的文本字符串。
Viterbi算法解决的是栅栏(Lattice)图的最短路径问题,图的节点按列组织,每一列的节点只能和相邻的列的节点相连,不能跨列相连,节点之间有着不同的距离。
Viterbi算法可以概括成以下三点:
(1)如果概率最大的路径p(或者说最短路径)经过某个点,比如途中的X22,那么这条路径上的起始点start到X22的这段子路径Q,一定是start到X22之间的最短路径。否则,用start到X22的最短路径R替代Q,便构成一条比P更短的路径,这显然是矛盾的。证明了满足最优性原理。
(2)从start到end的路径必定经过第i个时刻的某个状态,假定第i个时刻有k个状态,那么如果记录了从S到第i个状态的所有k个节点的最短路径,最终的最短路径必经过其中一条,这样在任意时刻,只要考虑非常有限的最短路径即可。
(3)结合以上两点,假定当我们从状态i进入状态i+1时,从start到状态i上各个节的最短路径已经找到,并且记录在这些节点上,那么在计算从起点S到第i+1状态的某个节点X(i+1)的最短路径时,只要考虑从start到前一个状态i所有的k个节点的最短路径,以及从这个节点到X(i+1, j)的距离即可。
举例:
用Viterbi算法针对HMM三大问题中的解码问题(给定模型和观测序列,如何找到与此观测序列最匹配的状态序列的问题)进行求解。
问题描述如下:
首先,已经知道状态序列X会产生观测序列O:
但是观测序列O对应的状态序列X有很多种可能,每种都有着不同概率,如下图所示:
因此问题就转变成了,在图中求从start到end的最短路径的形式,如下图所示:
也可以描述为下图所示:
解法描述如下:
如上图,为了在start和end之间找一条最短的路径,从start开始从左到右一列一列的进行计算。
首先从start到A列的路径,有三种可能:start-A1、start-A2、start-A3,如下图:
不能决定start-A1、start-A2、start-A3中的哪一条路径是最短路径中的一部分。接下来在看B列,对B列的B1、B2、B3一一分析。先看B1:
如上图,经过B1的路径有3条:start-A1-B1、start-A2-B1、start-A3-B1这三条路径中肯定可以确定哪一条是最短的,假设start-A1-B1是最短的,就得出了经过B1的所有路径当中start-A1-B1是最短的,其它两条路径路径start-A2-B1和start-A3-B1可以删掉了。
接下来,继续看B2:
如上图,经过B2的路径有3条:start-A1-B2、start-A2-B2、start-A3-B2这三条路径中肯定可以确定其中哪一条是最短的,假设start-A1-B2是最短的,那么就得出了经过B2的所有路径当中start-A1-B2是最短的,其它两条路径路径start-A2-B2和start-A3-B1可以删掉了。
接下来继续看B3:
如上图,经过B3的路径有3条:start-A1-B3、start-A2-B3、start-A3-B3这三条路径中肯定可以确定其中哪一条是最短的,假设start-A2-B3是最短的,那么就得出了经过B3的所有路径当中start-A2-B3是最短的,其它两条路径路径start-A1-B3和start-A3-B3可以删掉了。
现在对于B列的所有节点都遍历了一遍,可以看到最终留下了三个可能的最短路径:start-A1-B1、start-A1-B2、start-A2-B3,汇总到下图:
start-A1-B1、start-A1-B2、start-A2-B3都有可能是全局的最短路径,依然不能确定地说哪一条一定是全局最短路径的一部分。
接下来继续往下看C列,依然从C1、C2、C3一个个节点分析。经过C1节点的路径有:start-A1-B1-C1、start-A1-B2-C1、start-A2-B3-C1。
可以从这三条路径中过找到最短的那条,假如最短路径是start-A1-B2-C1,其它两条就可以删掉了。
同理,可以找到经过C2和C3节点的最短路径,则到C列的最短路径start-A1-B2-C1,start-A2-B3-C2,start-A2-B3-C3汇总如下:
在C列也剩下3条备选的最短路径,仍然不能确定哪条路径最短。接下里继续看D列了。到D列最后也只有3种可能性:start-A1-B2-C1-D1,start-A1-B2-C1-D2,start-A2-B3-C3-D3汇总如下:
接下来就到了最后一个节点end了。因为在D列剩下3条最短路径,可以在从D列到end的三条路径中找一条最短的路径(即红色的路径start-A1-B2-C1-D2-end为start到end的最短路径)即可。
在效率方面相对于粗暴的遍历所有路径,维特比算法到达每一列的时候都会删除不符合最短路径要求的路径【剪枝】,大大降低时间复杂度。
import numpy as np
"""
A1(0.7) B1(0.2) C1(0.4) D1(0.3)
A2(0.2) B2(0.2) C2(0.4) D2(0.3)
A3(0.1) B3(0.6) C3(0.2) D3(0.4)
state = [[0.7, 0.2, 0.1], ---> A1 A2 A3
[0.2, 0.2, 0.6], ---> B1 B2 B3
[0.4, 0.4, 0.2], ---> C1 C2 C3
[0.3, 0.3, 0.4]] ---> D1 D2 D3
A1(0.7) --> B1 (0.1) B1 --> C1(0.8) C1 --> D1(0.7)
B2 (0.4) --> C2(0.1) --> D2(0.2)
B3 (0.5) --> C3(0.1) --> D3(0.1)
A2(0.2) --> B1 (0.4) B2 --> C1(0.4) C2 --> D1(0.6)
B2 (0.3) --> C2(0.3) --> D2(0.2)
B3 (0.3) --> C3(0.3) --> D3(0.2)
A3(0.1) --> B1 (0.3) B3 --> C1(0.1) C3 --> D1(0.4)
B2 (0.6) --> C2(0.2) --> D2(0.1)
B3 (0.1) --> C3(0.7) --> D3(0.5)
其他也如上所示,组成weight数组
"""
state = [[0.7, 0.2, 0.1],
[0.2, 0.2, 0.6],
[0.4, 0.4, 0.2],
[0.3, 0.3, 0.4]]
weight = [[[0.1, 0.4, 0.5], [0.4, 0.3, 0.3], [0.3, 0.6, 0.1]],
[[0.8, 0.1, 0.1], [0.4, 0.3, 0.3], [0.1, 0.2, 0.7]],
[[0.7, 0.2, 0.1], [0.6, 0.2, 0.2], [0.4, 0.1, 0.5]]]
def viterbi(state, weight):
"""
:param state: 状态矩阵
:param weight: 权重矩阵
:return:
"""
state = np.array(state)
weight = np.array(weight)
row, col = state.shape
assert weight.shape == (row - 1, col, col), 'state not match path!'
# 路径矩阵,元素值表示当前节点从前一层的那一个节点过来是最优的
# 因为:每层有3个节点,共4层;因此每条路径都有4个几点,即A-B-C-D。
# 每次保留最优的3个路径。
path = np.zeros(shape=(col, row))
for i in range(row):
print(f'进入第 {i} 层')
if i == 0:
# 此时,只遍历到了A列,因此path中保留了A1;A2;A3。
path[:, i] = np.array(range(col)) + 1
continue
# i不等于0时,遍历到了B、C、D列。并更新path中的数据
for j in range(col):
# 循环遍历B列的所有:B1、B2、B3;C列的所有:C1、C2、C3;C列的所有:D1、D2、D3;
# 以A1-->B1, A2--->B1, A3--->B1为例,求出到B1的最优路径(temp最大)。
temp = state[i - 1, :] * weight[i - 1, :, j]
temp_max = max(temp)
temp_index = np.where(temp == temp_max)
path[j, i] = temp_index[0] + 1
# 保存中间过程的值
state[i, j] = max(temp) * state[i, j]
print(state)
print(path)
if __name__ == '__main__':
viterbi(state, weight)
运行结果:
可以用手算结果和代码运行结果,进行比较。