基础介绍
首先看下模型结构,对模型有一个直观的概念:
描述下这个图:
分成两排,第一排是$y$序列,第二排是$x$序列。每个$x$都只有一个$y$指向它,每个$y$也都有另一个$y$指向它。
OK,直觉上的东西说完了,下面给出定义(参考《统计学习方法》):
- 状态序列(上图中的$y$,下面的$I$): 隐藏的马尔科夫链随机生成的状态序列,称为状态序列(state sequence)
- 观测序列(上图中的$x$,下面的$O$): 每个状态生成一个观测,而由此产生的观测的随机序列,称为观测序列(obeservation sequence)
- 马尔科夫模型: 马尔科夫模型是关于时序的概率模型,描述由一个隐藏的马尔科夫链随机生成不可观测的状态随机序列,再由各个状态生成一个观测而产生观测随机序列的过程。
形式定义
设$Q$是所有可能的状态的集合,$V$是所有可能的观测的集合。
$Q={q_1,q_2,…,q_N},V={v_1,v_2,…,v_M}$
其中,$N$是可能的状态数,$M$是可能的观测数。
$I$是长度为$T$的状态序列,$O$是对应的观测序列。
$I=(i_1,i_2,…,i_T),O=(o_1,o_2,…,o_T)$
A是状态转移矩阵:$A=[a_{ij}]_{N×N}$ $i=1,2,…,N; j=1,2,…,N$
其中,在时刻$t$,处于$q_i$ 状态的条件下在时刻$t+1$转移到状态$q_j$ 的概率:
$a_{ij}=P(i_{t+1}=q_j|i_t=q_i)$
B是观测概率矩阵:$B=[b_j(k)]_{N×M}$ $k=1,2,…,M; j=1,2,…,N$
其中,在时刻$t$处于状态$q_j$ 的条件下生成观测$v_k$ 的概率:
$b_j(k)=P(o_t=v_k|i_t=q_j)$
π是初始状态概率向量:$π=(π_i)$
其中,$π_i=P(i_1=q_i)$
隐马尔科夫模型由初始状态概率向量$π$、状态转移概率矩阵A和观测概率矩阵$B$决定。$π$和$A$决定状态序列,$B$决定观测序列。因此,隐马尔科夫模型$λ$可以由三元符号表示,即:$λ=(A,B,π)$。$A,B,π$称为隐马尔科夫模型的三要素。
隐马尔科夫模型的两个基本假设
- 设隐马尔科夫链在任意时刻$t$的状态只依赖于其前一时刻的状态,与其他时刻的状态及观测无关,也与时刻$t$无关。(齐次马尔科夫性假设)
- 假设任意时刻的观测只依赖于该时刻的马尔科夫链的状态,与其他观测和状态无关。(观测独立性假设)
一个关于感冒的实例
定义讲完了,举个实例,参考hankcs和知乎上的感冒预测的例子(实际上都是来自wikipidia: https://en.wikipedia.org/wiki/Viterbi_algorithm#Example ),这里我用最简单的语言去描述。
假设你是一个医生,眼前有个病人,你的任务是确定他是否得了感冒。
- 首先,病人的状态($Q$)只有两种:{感冒,没有感冒}。
- 然后,病人的感觉(观测$V$)有三种:{正常,冷,头晕}。
- 手头有病人的病例,你可以从病例的第一天确定$π$(初始状态概率向量);
- 然后根据其他病例信息,确定$A$(状态转移矩阵)也就是病人某天是否感冒和他第二天是否感冒的关系;
- 还可以确定$B$(观测概率矩阵)也就是病人某天是什么感觉和他那天是否感冒的关系。
1 | import numpy as np |
1 | {0: 'Healthy', 1: 'Fever'} {'Healthy': 0, 'Fever': 1} |
1 | def convert_map_to_vector(map_, label2id): |
1 | A = convert_map_to_matrix(transition_probability, states_label2id, states_label2id) |
1 | [[ 0.7 0.3] |
1 | # 生成模拟数据 |
1 | [0 0 1 1 2 1 2 2 2 0] |
2.HMM的三个问题
HMM在实际应用中,一般会遇上三种问题:
- 1.概率计算问题:给定模型$λ=(A,B,π)$
和观测序列$O={o_1,o_2,…,o_T}$,计算在模型$λ$下观测序列$O$出现的概率$P(O|λ)$。 - 2.学习问题:已知观测序列$O={o_1,o_2,…,o_T}$,估计模型$λ=(A,B,π)$,使$P(O|λ)$最大。即用极大似然法的方法估计参数。
- 3.预测问题(也称为解码(decoding)问题):已知观测序列$O={o_1,o_2,…,o_T}$ 和模型$λ=(A,B,π)$,求给定观测序列条件概率$P(I|O)$最大的状态序列$I=(i_1,i_2,…,i_T)$,即给定观测序列,求最有可能的对应的状态序列。
回到刚才的例子,这三个问题就是:
- 1.概率计算问题:如果给定模型参数,病人某一系列观测的症状出现的概率。
- 2.学习问题:根据病人某一些列观测的症状,学习模型参数。
- 3.预测问题:根据学到的模型,预测病人这几天是不是有感冒。
2.1 概率计算问题
概率计算问题计算的是:在模型$λ$下观测序列$O$出现的概率$P(O|λ)$。
直接计算:
对于状态序列$I=(i_1,i_2, …,i_T)$的概率是:$P(I|\lambda)=\pi_{i_1}a_{i_1i_2}a_{i_2i_3}…a_{i_{T-1}i_T}$。
对上面这种状态序列,产生观测序列$O=(o_1, o_2, …,o_T)$的概率是$P(O|I,\lambda)=b_{i_1}(o_1)b_{i_2}(o_2)…b_{i_T}(o_{T})$。
$I$和$O$的联合概率为:
$$P(O,I|\lambda)=P(O|I,\lambda)P(I|\lambda)=\pi_{i_1}b_{i_1}(o_1)a_{i_1i_2}b_{i_2}(o_2)…a_{i_{T-1}i_T}b_{i_T}(o_{T})$$
对所有可能的$I$求和,得到:
$$P(O|λ)=\sum_IP(O,I|\lambda)=\sum_{i_1,…,i_T}\pi_{i_1}b_{i_1}(o_1)a_{i_1i_2}b_{i_2}(o_2)…a_{i_{T-1}i_T}b_{i_T}(o_{T})$$
如果直接计算,时间复杂度太高,是$O(TN^T)$。
前向算法(或者后向算法):
首先引入前向概率:
给定模型$λ$,定义到时刻$t$部分观测序列为$o_1,o_2,…,o_t$ 且状态为$q_i$ 的概率为前向概率。记作:
$$α_t(i)=P(o_1,o_2,…,o_t,i_t=q_i|λ)$$
用感冒例子描述就是:某一天是否感冒以及这天和这天之前所有的观测症状的联合概率。
后向概率定义类似。
前向算法
输入:隐马模型$λ$,观测序列$O$;
输出:观测序列概率$P(O|λ)$.
- 初值$(t=1)$,$α_1(i)=P(o_1,i_1=q_1|λ)=π_ib_i(o_1)$,$i=1,2,…,N $
- 递推:对$t=1,2,…,N$,$α_{t+1}(i)=[\sum^N_{j=1}α_t(j)a_{ji}]b_i(o_{t+1})$
- 终结:$P(O|λ)=\sum^N_{i=1}α_T(i)$
前向算法理解:
前向算法使用前向概率的概念,记录每个时间下的前向概率,使得在递推计算下一个前向概率时,只需要上一个时间点的所有前向概率即可。原理上也是用空间换时间。这样的时间复杂度是$O(N^2T)$。
前向算法/后向算法python实现:
1 | def forward(obs_seq): |
2.2学习问题
学习问题我们这里只关注非监督的学习算法,有监督的学习算法在有标注数据的前提下,使用极大似然估计法可以很方便地估计模型参数。
非监督的情况,也就是我们只有一堆观测数据,对应到感冒预测的例子,即,我们只知道病人之前的几天是什么感受,但是不知道他之前是否被确认为感冒。
在这种情况下,我们可以使用EM算法,将状态变量视作隐变量。使用EM算法学习HMM参数的算法称为Baum-Weich算法。
模型表达式:
$$P(O|λ)=\sum_IP(O|I,λ)P(I|λ)$$
Baum-Weich算法:
(1). 确定完全数据的对数似然函数
完全数据是$(O,I)=(o_1,o_2,…,o_T,i_1,…,i_T)$
完全数据的对数似然函数是:$logP(O,I|λ)$。
(2). EM算法的E步:
$$Q(λ,\hatλ)=\sum_IlogP(O,I|λ)P(O,I|\hatλ)$$
注意,这里忽略了对于$λ$而言是常数因子的$\frac{1}{P(O|\hatλ)}$
其中,$\hatλ$是隐马尔科夫模型参数的当前估计值,λ是要极大化的因马尔科夫模型参数。
又有:
$$P(O,I|λ)=π_{i_1}b_{i_1}(o_1)a_{i_1,i_2}b_{i_2}(o_2)…a_{i_T-1,i_T}b_{i_T}(o_T)$$
于是$Q(λ,\hatλ)$可以写成:
$$Q(λ,\hatλ)=\sum_Ilogπ_{i_1}P(O,I|\hatλ)+\sum_I(\sum^{T-1}_{t=1}loga_{i_t-1,i_t})P(O,I|\hatλ)+\sum_I(\sum^{T-1}_{t=1}logb_{i_t}(o_t))P(O,I|\hatλ)$$
(3). EM算法的M步:
极大化Q函数$Q(λ,\hatλ)$ 求模型参数$A,B,π$。
应用拉格朗日乘子法对各参数求偏导,解得Baum-Weich模型参数估计公式:
$$a_{ij}=\frac{\sum_{t=1}^{T-1}ξ_t(i,j)}{\sum_{t=1}^{T-1}γ_t(i)}$$
$$b_j(k)=\frac{\sum^T_{t=1,o_t=v_k}γ_t(j)}{\sum_{t=1}^{T}γ_t(j)}$$
$$π_i=γ_1(i)$$
其中$γ_t(i)$和$ξ_t(i,j)$是:
$$γ_t(i)=P(i_t=q_i|O,λ)$$
$$=\frac{P(i_t=q_i,O|λ)}{P(O|λ)}$$
$$=\frac{α_t(i)β_t(i)}{\sum_{j=1}^Nα_t(j)β_t(j)}$$
读作gamma,即,给定模型参数和所有观测,时刻$t$处于状态$q_i$的概率。
$$ξ_t(i,j)$$
$$=P(i_t=q_i,i_{i+1}=q_j|O,λ)$$
$$=\frac{P(i_t=q_i,i_{i+1}=q_j,O|λ)}{P(O|λ)}$$
$$=\frac{P(i_t=q_i,i_{i+1}=q_j,O|λ)}{\sum_{i=1}^N\sum_{j=1}^NP(i_t=q_i,i_{i+1}=q_j,O|λ)}$$
读作xi,即,给定模型参数和所有观测,时刻$t$处于状态$q_i$且时刻$t+1$处于状态$q_j$的概率。
带入$P(i_t=q_i,i_{i+1}=q_j,O|λ)=α_t(i)a_{ij}b_j(o_{t+1})β_{t+1}(j)$
得到:$ξ_t(i,j)=\frac{α_t(i)a_{ij}b_j(o_{t+1})β_{t+1}(j)}{\sum_{i=1}^N\sum_{j=1}^Nα_t(i)a_{ij}b_j(o_{t+1})β_{t+1}(j)}$
Baum-Weich算法的python实现:
1 | def baum_welch_train(observations, A, B, pi, criterion=0.05): |
回到预测感冒的问题,下面我们先自己建立一个HMM模型,再模拟出一个观测序列和一个状态序列。
然后,只用观测序列去学习模型,获得模型参数。
1 | A = np.array([[0.5, 0.5],[0.5, 0.5]]) |
1 | newA: [[ 0.5 0.5] |
2.3预测问题
考虑到预测问题是求给定观测序列条件概率$P(I|O)$最大的状态序列$I=(i_1,i_2,…,i_T)$,类比这个问题和最短路问题:
我们可以把求$P(I|O)$的最大值类比成求节点间距离的最小值,于是考虑类似于动态规划的viterbi算法。
首先导入两个变量$δ$和$ψ$:
定义在时刻$t$状态为$i$的所有单个路径$(i_1,i_2,i_3,…,i_t)$中概率最大值为(这里考虑$P(I,O)$便于计算,因为给定的$P(O)$,$P(I|O)$正比于$P(I,O)$):
$$δ_t(i)=max_{i_1,i_2,…,i_t-1}P(i_t=i,i_{t-1},…,i_1,o_t,o_{t-1},…,o_1|λ)$$
读作delta,其中,$i=1,2,…,N$
得到其递推公式:
$$δ_t(i)=max_{1≤j≤N}[δ_{t-1}(j)a_{ji}]b_i(o_1)$$
定义在时刻$t$状态为$i$的所有单个路径$(i_1,i_2,i_3,…,i_{t-1},i)$中概率最大的路径的第$t-1$个结点为
$$ψ_t(i)=argmax_{1≤j≤N}[δ_{t-1}(j)a_{ji}]$$
读作psi,其中,$i=1,2,…,N$
下面介绍维特比算法。
维特比(viterbi)算法(动态规划):
输入:模型$λ=(A,B,π)$和观测$O=(o_1,o_2,…,o_T)$
输出:最优路径$I^*=(i^*_1,i^*_2,…,i^*_T)$
(1).初始化:
$$δ_1(i)=π_ib_i(o_1)$$
$$ψ_1(i)=0$$
(2).递推。对$t=2,3,…,T$
$$δ_t(i)=max_{1≤j≤N}[δ_{t-1}(j)a_{ji}]b_i(o_t)$$
$$ψ_t(i)=argmax_{1≤j≤N}[δ_{t-1}(j)a_{ji}]$$
(3).终止:
$$P^*=max_{1≤i≤N}δ_T(i)$$
$$i^*_T=argmax_{1≤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^*)$
注:上面的$b_i(o_t)$和$ψ_{t+1}(i^*_{t+1})$的括号,并不是函数,而是类似于数组取下标的操作。
viterbi算法python实现(V对应δ,prev对应ψ):
1 | def viterbi(obs_seq, A, B, pi): |
继续感冒预测的例子,根据刚才学得的模型参数,再去预测状态序列,观测准确率。
1 | states_out = state_path(observations_data, newA, newB, newpi)[1] |
1 | 0.54 |
因为是随机生成的样本,因此准确率较低也可以理解。
使用Viterbi算法计算病人的病情以及相应的概率:
1 | A = convert_map_to_matrix(transition_probability, states_label2id, states_label2id) |
1 | normal cold dizzy |
3.完整代码
代码主要参考Hankcs的博客,hankcs参考的是colostate大学的教学代码。
完整的隐马尔科夫用类包装的代码:
1 | class HMM: |