上一期,王同学已经推导出了求两个序列联合概率的方法。他现在要解决的问题是如何更省时地找到联合概率最大的那个心情序列。
先来张示意图,
图-1中
S
1
S_{1}
S1、
S
2
S_{2}
S2、
S
3
S_{3}
S3表示三种不同的心情。
t
t
t从1到7,共7天。从这张图看,求联合概率最大的心情序列就是求一条使联合概率最大的心情路径。
如果用遍历法,3种心情,7天的序列,时间复杂度是
O
(
3
7
)
O(3^{7})
O(37)。
1 维特比算法的基本思想
现在,让我们看着图-1,展开想像的翅膀,假设在
t
=
3
t=3
t=3时刻,各找到一条以
S
1
S_{1}
S1、
S
2
S_{2}
S2、
S
3
S_{3}
S3结点为尾结点的局部最优路径。那么,问题来了,这三条局部路径对后面时刻的路径选取是否也是最优?如果是最优,那么只要保存下以当前时刻各结点为尾结点的局部最优路径,可以省去从头开始重复搜索。按此法递推,到最后一个时刻,分别保存了以各结点为终点的3条最优路径,我们只要从这三条中挑选出最优那条,它就是所有路径中最优的。
此法是否可行,还需经过证明,下面我们来证明这个设想。
证明:
不妨设有一条在
T
T
T时刻终结点为
S
k
S_{k}
Sk(
1
≤
k
≤
N
1 \le k \le N
1≤k≤N)的最优路径,它在
t
t
t时刻(
1
<
t
<
T
1<t<T
1<t<T)经过结点
S
j
S_{j}
Sj(
1
≤
j
≤
N
1 \le j \le N
1≤j≤N)。这个假设没毛病吧,在
t
t
t时刻必然经过所有结点中的一个。在
T
T
T时刻,必能找到以每个结点为终结点的最优路径。
假设这条路径为:
y
S
k
∗
\boldsymbol y^{*}_{S_{k}}
ySk∗={
y
1
∗
,
y
2
∗
,
…
,
S
t
j
,
y
t
+
1
∗
,
…
,
S
T
k
y^*_{1},y^*_{2},\dots,S^{j}_{t},y^*_{t+1},\dots,S^{k}_{T}
y1∗,y2∗,…,Stj,yt+1∗,…,STk},
y
S
k
∗
\boldsymbol y^{*}_{S_{k}}
ySk∗表示以
S
k
S_{k}
Sk为终结点的最优路径。{
y
1
∗
,
y
2
∗
,
…
,
S
t
j
y^*_{1},y^*_{2},\dots,S^{j}_{t}
y1∗,y2∗,…,Stj}是在
t
t
t时刻以
S
j
S_{j}
Sj为尾结点的局部最优路径。
现在假设存在另一条以结点
S
j
S_{j}
Sj结尾的局部路径{
y
1
′
,
y
2
′
,
…
,
S
t
j
y^{\prime }_{1},y^{\prime }_{2},\dots,S^{j}_{t}
y1′,y2′,…,Stj}优于{
y
1
∗
,
y
2
∗
,
…
,
S
t
j
y^*_{1},y^*_{2},\dots,S^{j}_{t}
y1∗,y2∗,…,Stj},那么将它与{
y
t
+
1
∗
,
…
,
S
T
k
y^*_{t+1},\dots,S^{k}_{T}
yt+1∗,…,STk}拼接起来会得到另一条更优的全局最优路径。因为t时刻的结点
S
t
j
S^{j}_{t}
Stj没变,
S
t
j
S^{j}_{t}
Stj到
y
t
+
1
∗
y^*_{t+1}
yt+1∗的转移概率就不会变。后面那部分路径是原样拼接,转移概率都不变,所以这条新路径比原来的全局最优路径更优。这与
y
S
k
∗
\boldsymbol y^{*}_{S_{k}}
ySk∗的定义矛盾,证毕。
这里弱弱的提一句,证明中的前提与有的书上的前提有点不同。不同之处在于,我这里假设在
t
t
t刻存在的另一条局部更优路径还是以
S
j
S_{j}
Sj为结尾。因为,只有当局部路径的尾结点不变时,尾结点与后继结点的转移概率才不会变。于是才能说路径{
y
1
′
,
y
2
′
,
…
,
S
t
j
y^{\prime }_{1},y^{\prime }_{2},\dots,S^{j}_{t}
y1′,y2′,…,Stj}与{
y
t
+
1
∗
,
…
,
S
T
k
y^*_{t+1},\dots,S^{k}_{T}
yt+1∗,…,STk}拼接起来的新路径比原路径更优。所以本人认为有的书上的证明忽略了这一点。不过,写书不容易,书中有点小错误在所难免,说实话,那本书写的还不错。
上面证明了我们求最优路径的想法没毛病。其实这个想法就是维特比算法的基本思想。
现在我们看着图-1来捋一捋怎么求所有路径中的最优路径。
在
t
=
1
t=1
t=1时刻,各状态结点没有前驱结点,局部最优路径就是只包含它自己的单个结点的路径,路径概率值为初始状态的概率乘上发射概率。
在
t
=
2
t=2
t=2时刻,求以各状态结点为尾结点的局部最优路径。也就是遍历前一时刻的所有局部最优路径,将局部最优路径的概率值与转移概率、发射概率三者相乘,选出使乘积最大的那个前驱结点。
以此方法,逐步递推到
T
T
T时刻,得到
N
N
N条以各状态结点为终结点的全局最优路径。这
N
N
N条路径中最优的那条就是我们要找的答案。这就是大白话的维特比算法求解步骤。
现在我们用数学语言更严谨地描述一下维特比算法的求解步骤。
符号定义
δ
t
,
i
\delta _{t,i}
δt,i:二维数组的某个元素,表示在时刻
t
t
t,以
S
i
S_{i}
Si结尾的所有局部路径中概率最大那条的概率值。
ψ
t
,
i
\psi _{t,i}
ψt,i:二维数组的某个元素,表示在时刻
t
t
t,以
S
i
S_{i}
Si为尾结点的局部最优路径中,
S
i
S_{i}
Si的前驱结点。
算法步骤
(1) 初始化,
t
=
1
t=1
t=1时,初始最优路径的备选由
N
N
N个状态组成,它们的前驱为空。
δ
1
,
i
=
π
i
B
i
,
o
1
,
i
=
1
,
…
,
N
ψ
1
,
i
=
0
,
i
=
1
,
…
,
N
\begin{equation} \begin{align} \delta _{1,i}&=\pi_{i}B_{i,o_{1}}, \qquad i=1,\dots,N \qquad \\ \psi _{1,i}&=0, \qquad \qquad i=1,\dots,N \end{align} \notag \end{equation}
δ1,iψ1,i=πiBi,o1,i=1,…,N=0,i=1,…,N
(2) 递推,
t
≥
2
t \ge2
t≥2时,每条备选路径增加一个当前时刻的新状态,根据转移概率和发射概率计算花费,遍历计算每一个新状态结点,找出新的局部最优路径,更新两个数组。
δ
t
,
i
=
max
1
≤
j
≤
N
(
δ
t
−
1
,
j
⋅
A
j
,
i
)
B
i
,
o
t
,
i
=
1
,
…
,
N
ψ
t
,
i
=
arg
max
1
≤
j
≤
N
(
δ
t
−
1
,
j
⋅
A
j
,
i
)
,
i
=
1
,
…
,
N
\begin{equation} \begin{align} \delta _{t,i}&=\max_{1 \le j \le N}(\delta _{t-1,j}\cdot A_{j,i}) B_{i,o_{t}}, \qquad i=1,\dots,N \qquad \\ \psi _{t,i}&= \arg\max_{1 \le j \le N} (\delta _{t-1,j}\cdot A_{j,i}), \qquad i=1,\dots,N \end{align} \notag \end{equation}
δt,iψt,i=1≤j≤Nmax(δt−1,j⋅Aj,i)Bi,ot,i=1,…,N=arg1≤j≤Nmax(δt−1,j⋅Aj,i),i=1,…,N
(3) 终止,找出最终时刻
δ
\delta
δ数组中的最大概率
p
∗
p^{*}
p∗,以及相应的结尾状态下标
i
T
∗
i^{*}_{T}
iT∗。
(4) 回溯,根据前驱数组
ψ
\psi
ψ,回溯前驱状态,取得最优路径状态下标
i
∗
=
i
1
∗
,
…
,
i
T
∗
\boldsymbol i^{*}=i^{*}_{1},\dots,i^{*}_{T}
i∗=i1∗,…,iT∗。
i
t
∗
=
ψ
t
+
1
,
i
t
+
1
,
t
=
T
−
1
,
T
−
2
,
…
,
1
\begin{equation} \begin{align} i^{*}_{t}=\psi _{t+1,i_{t+1}}, \qquad t=T-1, T-2, \dots, 1 \qquad \end{align} \notag \end{equation}
it∗=ψt+1,it+1,t=T−1,T−2,…,1
2 维特比算法的代码实现
下面我们用维特比算法求使联合概率最大的心情序列,用Python语言实现。
(1) 根据上一期表-1、表-3的行列顺序定义状态字典和观测字典
# 隐状态字典
MAP_STATE = {
0: 'neutral',
1: 'happy',
2: 'unhappy',
}
# 观测事件字典
MAP_ACTIVITY = {
'reading': 0,
'shopping': 1,
'running': 2,
'watching': 3,
}
(2) 根据上一期表-1、表-2、表-3的行列顺序定义三个参数矩阵。为防止概率连乘出现下溢出,这里将概率取对数。
# 初始状态概率数组
pi = [math.log(0.7), math.log(0.2), math.log(0.1)]
# 状态转移概率数组
A = [
[math.log(0.5), math.log(0.3), math.log(0.2)],
[math.log(0.4), math.log(0.5), math.log(0.1)],
[math.log(0.6), math.log(0.1), math.log(0.3)],
]
# 发射概率数组
B = [
[math.log(0.4), math.log(0.1), math.log(0.2), math.log(0.3)],
[math.log(0.2), math.log(0.5), math.log(0.1), math.log(0.2)],
[math.log(0.1), math.log(0.1), math.log(0.6), math.log(0.2)],
]
(3) 构建状态序号与状态名称转换函数
def index2state(index):
emo_list = []
for i in index:
emo_list.append(MAP_STATE[i])
return emo_list
(4) 构建活动名称与活动序号转换函数
def activity2index(activity):
idx = []
for act in activity:
i = MAP_ACTIVITY[act]
idx.append(i)
return idx
(5) 构建预测函数
1) 输入参数:事件序列,即观测序列。
2) 返回值:事件序列,心情序列。
def predict(activity):
seq_len = len(activity) # 序列长度
num_state = len(MAP_STATE) # 状态种数
psi = [[0] * len(MAP_STATE) for i in range(seq_len)] # 前驱数组
score = [0] * num_state # 最优局部路径的概率值
act_idx = activity2index(activity)
# 步骤一:t=1时
for cur_s in range(num_state):
score[cur_s] = pi[cur_s] + B[cur_s][act_idx[0]]
# 步骤二:t>=2时,递推
for t in range(1, seq_len):
pre_score = copy.deepcopy(score)
for cur_s in range(num_state):
score[cur_s] = -sys.maxsize - 1
# 搜索概率最大路径,保存前驱结点编号
for pre_s in range(num_state):
p = pre_score[pre_s] + A[pre_s][cur_s] + B[cur_s][act_idx[t]]
if score[cur_s] < p:
score[cur_s] = p
psi[t][cur_s] = pre_s
# 步骤三:回溯,求全局最优路径
path_idx = []
node_idx = score.index(max(score))
for t in range(seq_len-1, -1, -1):
path_idx.append(node_idx)
node_idx = psi[t][node_idx]
path_idx.reverse()
emo_state = index2state(path_idx)
return activity, emo_state
我们从式(3)可以看出,当前时刻最优路径概率值的求解只与前一时刻最优路径概率值有关,因此,为了节省空间,可以不用保存所有时刻的最大概率,只需要保存前一时刻的最大概率即可。所以,上述代码中只采用一个一维数组pre_score。
(6) 构建main函数
if __name__ == '__main__':
activity = ['reading', 'watching', 'running', 'reading', 'watching', 'shopping', 'watching']
act, emo = predict(activity)
print(act)
print(emo)
(7) 程序运行结果