您的位置:首页 > 其它

【机器学习】隐马尔科夫模型(下)——学习算法和预测算法

2016-08-17 09:26 323 查看
紧接上篇博客,上文已经介绍了前向-后向算法。前向-后向算法是通过递推地计算前向-后向概率来高效地进行隐马尔可夫模型的概率计算。该算法的时间复杂度是O(N2T)。

五、学习算法

在很多实际的情况下,HMM 不能被直接地判断,这就变成了一个学习问题。因为对于给定的可观察状态序列O来说,没有任何一种方法可以精确地找到一组最优的 HMM 参数λ使P(O|λ) 最大,于是人们寻求使其局部最优的解决办法,而Baum-Welch算法就成了HMM学习问题的一个近似的解决方法。

Baum-Welch算法首先对于HMM的参数进行一个初始的估计,但这个很可能是一个错误的猜测,然后通过对于给定的数据评估这些参数的的有效性(比如交叉验证)并减少它们所引起的错误来更新HMM参数,使得和给定的训练数据的误差变小。

实际上,Baum-Welch算法是EM算法的一个特例。EM算法是解决无监督学习问题的一把“利器”。下面我们就使用这把“利器”解决HMM的参数学习问题。

1.首先我们需要定义两个辅助变量,这两个变量可以用前文介绍过的前向概率和后向概率进行定义。

(1)给定模型λ和观测序列O,在时刻t处于qi的概率,记为γt(i)=P(it=qi|O,λ)

可以通过前向-后向概率计算,事实上γt(i)=P(it=qi|O,λ)=P(it=qi,O|λ)P(O|λ)

根据前向概率和后向概率的定义可知:αt(i)βt(i)=P(it=qi,O|λ),于是得到:γt(i)=αt(i)βt(i)P(O|λ)=αt(i)βt(i)∑Nj=1αt(j)βt(j)

(2)给定模型λ和观测序列O,在时刻t处于状态qi且在时刻t+1处于状态qj的概率,记为ξt(i,j)=P(it=qi,it+1=qj|O,λ)

通过前向-后向概率计算得:ξt(i,j)=αt(i)aijbj(ot+1)βt+1(j)∑Ni=1∑Nj=1αt(i)aijbj(ot+1)βt+1(j)

该变量在网格中所代表的关系如下:



(3)将γt(i)和ξt(i,j)对各个时刻t求和,可以得到Baum-Welch算法中需要的期望值:

a.在观测O下状态i出现的期望值:∑Tt=1γt(i)

b.在观测O下由状态i转移的期望值:∑T−1t=1γt(i)

c.在观测O下由状态i转移到状态j的期望值:∑T−1t=1ξt(i,j)

2.算法

Baum-Welch算法的推导过程实质上就是EM算法的过程,其中还会利用拉格朗日函数法求解极大化问题。具体的证明过程可查阅相关论文。下面直接给出算法:

输入:观测序列O=(o1,o2,⋯,oT)

输出:隐马尔科夫模型参数。

(1)初始化

对n=0,选取a(0)ij,bj(k)(0),π(0)i,得到初始模型λ(0)=(A(0),B(0),π(0))

(2)递推。对n=1,2,⋯

a(n+1)ij=∑T−1t=1ξt(i,j)∑T−1t=1γt(i)

bj(k)(n+1)=∑Tt=1,ot=vkγt(j)∑Tt=1γt(j)

π(n+1)i=γ1(i)

这里的递推公式用到了1.(3)部分提及的期望值。

(3)结束。得到模型参数λ(n+1)=(A(n+1),B(n+1),π(n+1))

3.代码实现

(1)依旧是相同的结构体(这里使用类并不是真的面向对象编程,当然封装完整的算法可以使用OOP范式)定义。

class HMM:
def __init__(self, pi, A, B):
self.pi = pi
self.A = A
self.B = B
self.M = B.shape[1]
self.N = A.shape[0]


(2)主算法函数

def baum_welch(hmm, obs):
T = len(obs)
M = hmm.M
N = hmm.N
alpha = zeros((N,T))
beta = zeros((N,T))
scale = zeros(T)
gamma = zeros((N,T))
xi = zeros((N,N,T-1))

# 计算初始化参数
logprobprev, alpha, scale = forward_with_scale(hmm, obs)    # 计算前向概率
beta = backward_with_scale(hmm, obs, scale)                 # 计算后向概率
gamma = compute_gamma(hmm, alpha, beta)                     # 计算单个状态概率
xi = compute_xi(hmm, obs, alpha, beta)                      # 计算两个状态概率
logprobinit = logprobprev

# 开始迭代
while True:
# E-step
hmm.pi = 0.001 + 0.999*gamma[:,0]                       # 这里使用平滑化技巧,以免参数过于接近0
for i in range(N):
denominator = sum(gamma[i,0:T-1])
for j in range(N):
numerator = sum(xi[i,j,0:T-1])
hmm.A[i,j] = numerator / denominator

hmm.A = 0.001 + 0.999*hmm.A
for j in range(0,N):
denominator = sum(gamma[j,:])
for k in range(0,M):
numerator = 0.0
for t in range(0,T):
if observation.index(observed[t]) == k:
numerator += gamma[j,t]
hmm.B[j,k] = numerator / denominator
hmm.B = 0.001 + 0.999*hmm.B

# M-step
logprobcur, alpha, scale = forward_with_scale(hmm, obs)
beta = backward_with_scale(hmm, obs, scale)
gamma = compute_gamma(hmm, alpha, beta)
xi = compute_xi(hmm, obs, alpha, beta)

delta = logprobcur - logprobprev
logprobprev = logprobcur

if delta <= DELTA:   # 迭代停止条件
break

logprobfinal = logprobcur

return hmm.A,hmm.B,hmm.pi


(3)相关支持函数

主函数调用了均值平滑化的前向-后向概率计算算法:

def forward_with_scale(hmm, obs):
"""see scaling chapter in "A tutorial on hidden Markov models and
selected applications in speech recognition."
"""
T = len(obs)
N = hmm.N
alpha = zeros((N,T))
scale = zeros(T)

alpha[:,0] = hmm.pi[:] * hmm.B[:,observation.index(obs[0])]
scale[0] = sum(alpha[:,0])
alpha[:,0] /= scale[0]

for t in range(1,T):
for n in range(0,N):
alpha[n,t] = sum(alpha[:,t-1] * hmm.A[:,n]) * hmm.B[n,observation.index(obs[t])]
scale[t] = sum(alpha[:,t])
alpha[:,t] /= scale[t]

logprob = sum(log(scale[:]))
return logprob, alpha, scale

def backward_with_scale(hmm, obs, scale):
T = len(obs)
N = hmm.N
beta = zeros((N,T))
beta[:,T-1] = 1 / scale[T-1]
for t in reversed(range(0,T-1)):
for n in range(0,N):
beta[n,t] = sum(hmm.B[:,observation.index(obs[t+1])] * hmm.A[n,:] * beta[:,t+1])
beta[n,t] /= scale[t]
return beta


计算γt(i)和ξt(i,j)的函数如下:

def compute_gamma(hmm, alpha, beta):
gamma = zeros(alpha.shape)
gamma = alpha[:,:] * beta[:,:]
gamma = gamma / sum(gamma,0)
return gamma

def compute_xi(hmm, obs, alpha, beta):
T = len(obs)
N = hmm.N
xi = zeros((N, N, T-1))

for t in range(0,T-1):
for i in range(0,N):
for j in range(0,N):
xi[i,j,t] = alpha[i,t] * hmm.A[i,j] * hmm.B[j,observation.index(obs[t+1])] * beta[j,t+1]
xi[:,:,t] /= sum(sum(xi[:,:,t],1),0)
return xi


(4)测试程序

这里随机初始化参数,我们依然使用上篇博文的测试用例:

if __name__ == "__main__":
A = random.random([3,3])
B = random.random([3,2])
pi = random.random(3)

hmm = HMM(pi, A, B)

observed = ['red', 'write', 'red']
A,B,pi = baum_welch(hmm, observed)

print('state transition probability matrix A:\n', A)
print('observation probability matrix B:\n', B)
print('initial state probability vector pi:\n', pi)


测试结果如下:



由于此算法是无监督学习算法,在实际项目使用中应该不断使用交叉验证方法使得模型验证误差最小。由于这里缺乏充足数据,所以就不演示此过程了。

六、预测算法

这个问题比较有趣。在很多情况下,我们对隐藏状态更有兴趣,因为其中包含了一些不能被直接观察到的有价值的信息。比如说在海藻和天气的例子中,一个隐居的人只能看到海藻的状态,但是他想知道天气的状态。这时候我们就可以使用Viterbi算法来根据可观察序列预测最优可能的隐藏状态的序列,当然前提是已知一个HMM模型。

Viterbi算法实际上是用动态规划求解预测问题,即求解概率最大路径(最优路径),实际上任何一个HMM模型都可以用图(graph)表示。

根据动态规划原理,我们需要提取一个最优子结构:如果最优路径在时刻t通过结点i∗t,那么这一路径从结点i∗t到终点i∗T的部分子路径必须是都是最优子路径。可以用“剪切-粘贴法”(实则是反证法)证明:假如其中一条子路径不是最优路径(把它“剪切”掉),那么从i∗t到i∗T必然有另一条更好的子路径存在,如果把它和从i∗1到i∗t的部分路径连接起来(粘贴),就会整体上得到一条比原来的路径更优的路径,与原来的最优假设矛盾。依据动态规划原理,我们只需从时刻t=1开始,递推地计算在时刻t状态为i的各条子路径的最大概率,直至得到时刻t=T状态为i的各条路径的最大概率。时刻为t=T的最大概率即为最优路径的概率P∗,同时也得到最优路径的终结点i∗T。之后只需回溯部分最优结点,便可得到最优路径。

1.首先定义两个变量:δ和Ψ。

(1)定义在时刻t状态为i的所有单条路径(i1,i2,⋯,it)中的概率最大值为δt(i)=maxi1,i2,⋯,it−1P(it+1=i,it,⋯,i1,ot+1,⋯,o1|λ),i=1,2,⋯,N

由定义可得变量δ的递推公式:δt+1(t)=max1≤j≤N[δt(j)aji]bi(ot+1),i=1,2,⋯,N;t=1,2,⋯,T−1

(2)定义在时刻t状态为i的所有单个路径(i1,i2,⋯,it−1,i)中概率最大的路径的第t−1个结点为:Ψt(i)=argmax1≤j≤N[δt−1aji],i=1,2,⋯,N即用后向指针Ψ来记录导致某个状态最大局部概率的前一个状态,在算法中用于回溯最优路径。

下面给出算法步骤:

输入:模型参数λ=(A,B,π)和观测序列O=(o1,o2,⋯,oT);

输出:最优路径I∗=(i∗1,i∗2,⋯,i∗T)

(1)初始化δ1(t)=πibi(o1),i=1,2,⋯,N

Ψ1(i)=0,i=1,2,⋯,N

(2)递推。对t=2,3,⋯,T δt(i)=max1≤j≤N[δt−1(j)aji]bi(ot),i=1,2,⋯,NΨt(i)=argmax1≤j≤N[δt−1aji],i=1,2,⋯,N

(3)终止P∗=max1≤i≤NδT(i)i∗T=argmax1≤i≤N[δT(i)]

(4)最优路径回溯。对t=T−1,T−2,⋯,1

i∗t=Ψt+1(i∗t+1)

最后得到最优路径即最优隐藏状态序列I∗=(i∗1,i∗2,⋯,i∗T)

2.代码实现

from numpy import *
observation = ['red','write']
class HMM: def __init__(self, pi, A, B): self.pi = pi self.A = A self.B = B self.M = B.shape[1] self.N = A.shape[0]

def viterbi(hmm, obs):
T = len(obs)
N = hmm.N
psi = zeros((N,T)) # reverse pointer
delta = zeros((N,T))
q = zeros(T)
temp = zeros(N)

delta[:,0] = hmm.pi[:] * hmm.B[:,observation.index(obs[0])] # 初始化
# 动态规划过程
for t in range(1,T):
for n in range(0,N):
temp = delta[:,t-1] * hmm.A[:,n]
max_ind = argmax(temp)
psi[n,t] = max_ind
delta[n,t] = hmm.B[n,observation.index(obs[t])] * temp[max_ind]
# 最优路径回溯
max_ind = argmax(delta[:,T-1])
q[T-1] = max_ind
prob = delta[:,T-1][max_ind]
for t in reversed(range(0,T-1)):
q[t] = psi[int(q[t+1]),t+1]

return prob, q, delta


测试程序如下:

if __name__ == "__main__":
A = array([[0.5,0.2,0.3],
[0.3,0.5,0.2],
[0.2,0.3,0.5]])
B = array([[0.5,0.5],
[0.4,0.6],
[0.7,0.3]])
pi = array([0.2,0.4,0.4])

hmm = HMM(pi, A, B)

observed = ['red', 'write', 'red']
prob, q, delta = viterbi(hmm, observed)

print('The maximum probability of every single path is:\n', delta)
print('The probability of optimal path is %f.' % prob)
print('The optimal sequence of state is:\n', q)


测试结果如下:



结果可用概率图表示如下:



可见最优隐藏状态序列应为I∗=(i∗1,i∗2,i∗3)=(3,3,3)(程序用索引表示为(2,2,2),索引是从0开始的)。

七、参考论文

1.Lawrence R. Rabiner, A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition. Proceedings of the IEEE, 77 (2), p. 257–286, February 1989(下载链接:http://download.csdn.net/detail/herosofearth/9605209
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: