背景
承接上文,继续 HMM 的公式推导过程,本文主要介绍后向概率算法以及前向概率算法的 Java 实现版本。
后向概率公式及推导过程
1.后向概率定义
给定 λ,定义到时刻 t ,状态为 Si 的前提下,从 t+1 至 T 时刻出现的观测序列为 qt +1,qt+2… qT 的概率。
记作 : βt(i) = P(qt+1,qt+2,…qT|st=si)。
2.后向概率的初始条件
根据定义,第 T 个时刻的后向概率是 βT(i)=P(qT+1,sT=si|)=1 。
βT(i) = 1 是指 T+1 时刻观测序列是指定值的概率,既然 T 时刻已经是最后时刻了,那么 T+1 时刻就是必然发生的事件了,必然发生的事件,其概率值是 1。
3.后向概率的递推公式
向后递推,就是从第 T 个时刻 βT(i) 值,向后推导,计算出 β1(i) ,再将第一个时刻的所有状态累加就得到 HMM 计算问题的解。
递推的核心是根据 t 时刻的后向概率的定义,引入 t+1 时刻的状态,再根据联合概率和全概率计算公式,得到 t 时刻和 t-1 时刻的后向概率之间的递推关系。
4.后向概率递推过程
这是我在简书上看到的一个递推计算公式,HMM 计算问题 原文只有黑色的公式,没有每一步的缩减依据。这里补充我的推导过程,有助于深刻理解 算法 本身。
我们看下推导过程:
- 第 1 步,是公式的基本定义;
- 第 2 步,根据全概率公式,引入 t+1 时刻的状态事件,将全概率计算转换为求解独立事件 st+1 的概率和。
3.第 2 步和第 3 步之前的中间步骤,是联合条件的概率公式, P(AB|C)=P(A|BC)*P(B|C) ,经过该公式转换后得到中间步骤为 P(qt+1…qT|it+1=Sj,it=Si)*P(it+1=Sj|it=Si) 。 - 第 3 步,是根据 HMM 的独立观测假设,t+1 时刻的观测值只与 t+1 时刻的状态无关,与其他时刻的状态如 st=Si 无关,所以可以消掉左侧概率中的 it=si 这个条件。即 P(qt+1…qT|it+1=Sj,it=Si) = P(qt+1…qT|it+1=Sj)
- 第 3 步和第 4 步,之间省略了一个推导过程是联合条件概率的转换公式,中间转换的结果为: P(qt+2…qT|qt+1,it+1=Sj)*P(qt+1|it+1=Sj)
- 第 4 步是根据 HMM 的独立观测假设,消掉左侧联合条件概率中的 qt+1 这个条件得到的简化值,即 P(qt+2…qT|qt+1,it+1=Sj) = P(qt+2…qT|it+1=Sj)
- 第 5 步,再次根据后向概率的定义,以及 HMM 的模型参数的关系,最终得到 t 时刻和 t-1 时候的递推关系。
这样的话,后向概率算法就转换为一个初始值和一个递推公式。
前向概率算法
从上一篇介绍的前向概率的四个基本信息已经构成了一个完整的算法,用数组很容易实现。算法的基本信息为:
- 初始条件,四个数组 π、 A、B、Q;
- 初始值, α1(i) = πi * Bi(q1) ;
- 递推公式, αt(i) = Sum(αt-1(j) * A[j][[i])* B[i][qt] ;
- 目标函数 , sum(αT(i)),i=1,2….n ;
这里来实现经典的取球案例的概率,案例描述:
Java 实现
前向概率 αt(i) 是一个元素,存储 t 时刻状态为 i 、目标观测值 q1,q2…qt 出现的概率,所以需要定义一个二维数组,大小为 T * N (N 为状态的总数)。
实现代码为:
import java.util.Arrays;
/**
* @Title:HMMForward
* @Description: 马尔可夫模型的基本实现过程<br/>
* 1、初始模型参数 π,A,B,最终观测序列 O=[q1,q2, q3 ....qT]
* 2、初始值 α[1][i]= π[i] * B[i][q1]
* 3、循环 t=1....T ,计算 α[t][i]= (∑α[t-1][j]*a[j][i])*b[i][qt]
* 4、目标 P(Q|λ)= ∑α[T][i] ,i=1....M
* @author:wang_ll
* @date :2019年7月29日
*/
public class HMMForward {
/**
* 状态个数
*/
private int M = 0;
/**
* 状态初始的概率
*/
private double PI [] = {0.2,0.4,0.4};
/**
* 状态转移矩阵 M X M
*/
private double A [][] = {{0.5,0.2,0.3},{0.3,0.5,0.2},{0.2,0.3,0.5}};
/**
* 观察指转移矩阵
*/
private double B[][] = {{0.5,0.5},{0.4,0.6},{0.7,0.3}};
/**
* 最终求解的观测序列值
*/
private int [] observeSequence = {0,1,0};
/**
* 存储最终计算的前向概率的值的数组 T X M
*/
private double forwardRate [][] = null;
/**
* 最终的计算目标:P(Q|λ)= ∑α[T][i] ,i=1....M
*/
private double target ;
/**
* 初始化模型参数
*/
public void initModel() {
M = 3;
forwardRate = new double[observeSequence.length][M];
}
/**
* HMM 计算问题求解
*/
public void calculate() {
//初始化初值 α[1][i]= π[i] * B[i][q1]
for(int i=0;i<M;i++) {
forwardRate[0][i] = PI[i]*B[i][observeSequence[0]];
}
//向前递推
for(int t=1;t<observeSequence.length;t++) {
for(int i=0;i<M;i++) {
double sumAllState = 0.0;
for(int j=0;j<M;j++) {
//累加各个状态 t-1 时刻的概率和
sumAllState+= forwardRate[t-1][j]*A[j][i];
}
forwardRate[t][i] = sumAllState* B[i][observeSequence[t]];
}
}
//计算最终的结果
for(int i=0;i<M;i++) {
target+=forwardRate[forwardRate.length-1][i];
}
}
/**
* 打印最终的结果
*/
public void printResult() {
System.out.println("初始模型参数 π:"+Arrays.toString(PI));
System.out.println("状态转移矩阵 A 为:");
for(int i=0;i<M;i++) {
System.out.println(Arrays.toString(A[i]));
}
System.out.println("观测值转移矩阵 B 为:");
for(int i=0;i<M;i++) {
System.out.println(Arrays.toString(B[i]));
}
System.out.println("最终观测序列 :"+Arrays.toString(observeSequence));
System.out.println();
System.out.println("HMM 计算得到的概率值为:"+target);
}
public static void main(String[] args) {
HMMForward instance = new HMMForward();
instance.initModel();
instance.calculate();
instance.printResult();
}
}
运行结果:
初始模型参数 π:[0.2, 0.4, 0.4]
状态转移矩阵 A 为:
[0.5, 0.2, 0.3]
[0.3, 0.5, 0.2]
[0.2, 0.3, 0.5]
观测值转移矩阵 B 为:
[0.5, 0.5]
[0.4, 0.6]
[0.7, 0.3]
最终观测序列值:[0, 1, 0]
最终观测序列描述:[红,白,红]
HMM 计算得到的概率值为:0.130218
参考附录
(一)HMM 模型计算问题完整的推导说明
(二)HMM Java 实现的例子
(三)HMM 解码算法实现
这三篇文章,对我理解 HMM 的过程有很大的帮助,《HMM Java 实现的例子》这篇文章中的 HMM 算法实现,如果不了解算法的推导过程直接看代码是不好懂的;理解过程后,自己实现的代码结果跟它是一致的,也是一种印证。
《HMM Java 实现的例子》这篇文章给我的启示是:原来 HMM 用 Java 代码实现是这么简单。最初的教程使用 python 的 hmmlearn 库,但是没有安装成功,现在看来用 Java 和 python 是殊途同归,自己实现一遍后发现 python 的库显得没那么高深了!