首先抄下《统计学习方法》中HMM的定义和相关知识点:隐马尔科夫模型是关于时序的概率模型,描述由一个隐藏的马尔科夫链随机生成不可观测的状态随机序列,再由各个状态生成一个观测而产生观测随机序列的过程。隐马尔科夫模型由初始状态概率向量、状态转移概率矩阵和观测概率矩阵(也可称为发射概率矩阵)组成。隐马尔科夫模型做了两个基本假设——一是任意时刻的隐状态只与前一时刻的隐状态有关,二是任意时刻的观测只与当前时刻的隐状态有关。隐马尔科夫模型如下图所示:
其中,
- 概率计算问题。给定模型参数和观测序列,计算在模型下观测序列出现的概率
- 学习问题。也就是模型的训练,已知观测序列,估计模型参数
- 预测问题。也称为decode,给定观测序列,求最有可能的对应的隐状态
这3个问题都需要计算概率并进行对比得到结果,当数据很庞大的时候,算法的关键就是如何对概率的计算进行优化。首先,我们将模型中的隐状态转移在时间上展开,得到隐状态的晶格图,如下:
对于概率预测问题,我们假设前向概率为
- 初值,
- 递归,对t=1,2,...,T-1有,
- 终止,
完整的后向算法也是分为3步:
- 初值,
- 递归,对t=T-1,T-2,...,1有,
- 终止,
对于学习问题,可以分为两种情况进行计算。一种是已知隐状态,那么直接使用极大似然估计算法就可以得到;一种是不知隐状态,这种是常见情况,此时就没法用极大似然估计算法了,似然函数是,但是是未知的,是没法得到,所以也就没法直接使用极大似然估计算法了。此时就应该用EM算法,依靠EM算法的迭代训练达到一个比较好的效果。EM算法中不是对直接最大似然估计,而是最优化完全数据的似然函数,所以新似然函数可写成(这里的公式推导请看PRML的9.4节),然后就可以利用极大似然估计算法得到新的模型参数,接着将作为新一轮的,并迭代训练模型,这样就可以得到最终的模型参数。这种方法称为Baum-Welch算法。具体每一轮如何得到,《统计学习方法》中181页有着详细的讲解,这里就不抄上了。
对于预测问题,也是分为两种方法。一种是近似算法,计算公式为:但是这种算法预测的状态序列可能有不发生的部分,即对某些,有。分析一下,根据定义来看,,,中考虑了1到t的连续性,中考虑了t+1到T的连续性,但是它们都没有考虑t和t+1之间的连续性。所以最好就是求一条完整的路径,此时就可以采用第二种方法——维特比算法。维特比算法是采用了动态规划来求解最优路径,每个时刻都保存了每个隐状态的最大可能概率和对应的上一时刻的最优隐状态。详细的维特比算法伪代码如下图:
hmmlearn源码中实现3种模型——GaussianHMM、GMMHMM和MultinomialHMM。其中GaussianHMM假设观测状态是连续型且满足高斯分布,GMMHMM假设观测状态是连续型且满足混合高斯分布,MultinomialGMM假设观测状态是离散型。
在代码实现上,hmmlearn在base.py中定义了基本HMM模型baseHMM类,类中定义了2个重要参数——self.startprob(HMM的初始状态,维度是[n_components])和self.transmat_(HMM的状态转移矩阵,维度是[n_components, n_components]),基类中并没有定义发射矩阵,因为只有观测状态是离散型的HMM模型才有发射矩阵(代码在MultinomialGMM类中单独定义了self.emissionprob_)。而HMM中的3个基本问题则对应了类中的3个函数:
- 概率计算问题,对应了_baseHMM类中的score函数。代码中利用前向算法来计算概率,核心部分如下:
for i, j in iter_from_X_lengths(X, lengths):
framelogprob = self._compute_log_likelihood(X[i:j])
logprobij, _fwdlattice = self._do_forward_pass(framelogprob)
logprob += logprobij
其中,_do_forward_pass函数实现了前向算法中的,代码在https://github.com/hmmlearn/hmmlearn/blob/master/lib/hmmlearn/_hmmc.pyx中。具体核心代码如下:
for t in range(1, n_samples):
for j in range(n_components):
for i in range(n_components):
work_buffer[i] = fwdlattice[t - 1, i] + log_transmat[i, j] # 由于取log操作,这里的*运算变成了+运算
fwdlattice[t, j] = _logsumexp(work_buffer) + framelogprob[t, j]
hmmlearn在计算概率时都对结果进行了取log操作,是为了防止序列过长而导致计算过程中出现数值下溢的情况,这在PRML的13.2.4节中有提到。当然,代码中也是实现了后向算法,可见_do_backward_pass函数:
for t in range(n_samples - 2, -1, -1):
for i in range(n_components):
for j in range(n_components):
work_buffer[j] = (log_transmat[i, j] + framelogprob[t + 1, j] + bwdlattice[t + 1, j])
bwdlattice[t, i] = _logsumexp(work_buffer)
- 学习问题,对应了_baseHMM类中的fit函数。核心代码如下:
for i, j in iter_from_X_lengths(X, lengths):
framelogprob = self._compute_log_likelihood(X[i:j])
logprob, fwdlattice = self._do_forward_pass(framelogprob)
curr_logprob += logprob
bwdlattice = self._do_backward_pass(framelogprob)
posteriors = self._compute_posteriors(fwdlattice, bwdlattice)
self._accumulate_sufficient_statistics(stats, X[i:j], framelogprob, posteriors, fwdlattice, bwdlattice)
其中,framelogprob就是发射概率(如果观测状态是离散型就是矩阵B,如果观测状态是连续型就是高斯对数密度),fwdlattice和bwdlattice分别是前向概率和后向概率,posteriors就是。
- 预测问题,对应了_baseHMM类中的predict函数(也可以说是对应decode函数)。核心部分代码如下:
for i, j in iter_from_X_lengths(X, lengths):
logprobij, state_sequenceij = decoder(X[i:j])
logprob += logprobij
state_sequence[i:j] = state_sequenceij
其中最重要的维特比算法实现代码部分如下:
for t in range(1, n_samples):
for i in range(n_components):
for j in range(n_components):
work_buffer[j] = (log_transmat[j, i] + viterbi_lattice[t - 1, j])
viterbi_lattice[t, i] = _max(work_buffer) + framelogprob[t, i]
其中,viterbi_lattice保存的就是每个中间时刻的隐状态的最大累积概率。