本篇博文将详细总结隐马模型相关知识,理解该模型有一定的难度,在此浅薄的谈下自己的理解。参考的实现代码:HMM_EM(无监督式),HMM有监督式
文章目录
概率计算问题
是关于时序的概率模型,描述由一个隐藏的马尔科夫链生成不可观测的状态随机序列,再由各个状态生成观测随机序列的过程。
隐马尔科夫模型随机生成的状态随机序列,称为状态序列;每个状态生成一个观测,由此产生的观测随机序列,称为观测序列。序列的每个位置可看做是一个时刻。
上图中的 表示状态序列, 表示观测序列。假设每个时刻的状态可能有 种可能,每个时刻的观测可能有 种可能。
由初始概率分布、状态转移概率分布 以及观测概率分布 确定。
这个初始概率分布 是一个大小为 的向量, 为大小为 的矩阵, 为大小为 的矩阵。
- 是长度为 的状态序列, 是对应的观测序列,则有:
- 是状态转移概率矩阵,则有:
其中 表示在时刻 处于状态 的条件下时刻 转移到状态 的概率。
HMM的两个基本性质
齐次假设:
也即是当前时刻的隐状态只与前一时刻的隐状态有关。
观测独立性假设:
也即是当前时刻的观测状态只与当前时刻的隐状态有关。
HMM的三个问题
问题一:给定模型 和观测序列,计算模型 下观测序列O出现的概率
我们先看看代码中,是如何做的,参考的代码中,数据的格式为 ,假设了这是由三个高斯模型混合而成的样本集,每个样本有两个特征,共900个样本。注意:样本个数也即是上面所说的步长。形如:
5.8045294e-01 9.6570931e-01
1.0101383e+00 3.9152260e-01
-4.1251308e-01 9.6435345e-01
-2.0477262e+00 1.5029133e+00
-1.3130763e+00 1.6049016e-01
-6.2352642e-01 3.7779862e-01
1.8841870e+00 1.1210070e+00
2.5608726e+00 -1.4935448e+00
3.2966895e-01 -1.1184212e+00
-1.1982074e+00 7.4402510e-01
7.1916729e-01 1.2625977e+00
-3.6946331e-01 1.4573214e+00
-7.3522039e-01 7.0942551e-02
-4.8153116e-01 1.3661593e+00
然后需要初始化出上面所讲的模型的三个要素。
def initForwardBackward(X,K,d,N):##X为数据集,K为隐状态数,在这里为3,d为观测状态数,N为时刻总数,共N步。
# Initialize the state transition matrix, A. A is a KxK matrix where
# element A_{jk} = p(Z_n = k | Z_{n-1} = j)
# Therefore, the matrix will be row-wise normalized. IOW, Sum(Row) = 1
# State transition probability is time independent.
A = np.ones((K,K))##隐状态转移矩阵初始为1
A = A/np.sum(A,1)[None].T ##需要保证每列之和为1
# Initialize the marginal probability for the first hidden variable
# It is a Kx1 vector
PI = np.ones((K,1))/K## 初始pi,即为第0时刻转移到某个隐状态的概率
## 这里我们假设发射矩阵服从高斯分布,所以我们只需要定义均值和方差即可。
## 显然对于不同的隐状态,会得到不同的观测序列。即对于不同的隐状态有不同的高斯分布
## 并且这里数据集每个样本有d(d=2)个feature,相当于有多维度随机变量,每个随机变量的分布都有不同的均值
## 所以MU的shape为[d,k],即每个隐状态对应d个均值
## 每个隐状态对应有不同的协方差矩阵
MU = np.random.rand(d,K)
SIGMA = [np.eye(d) for i in xrange(K)]
return A, PI, MU, SIGMA
这样我们就得到初始的 ,状态转移矩阵,发射矩阵。
前向后向算法—动态规划
给定模型 和观测序列 ,计算模型 下观测序列 出现的概率。
我们首先尝试直接用暴力求解:
- 状态序列 的概率是:
- 对固定的状态序列 ,测序列 的概率是:
- 和 同时出现的联合概率是:
- 对所有可能的状态序列 求和,得到观测序列 的概率:
我们可以试想,在每一个时刻,隐状态都有 个选择,一共有 个时刻,故 ,而求和里面共有 个因子,故时间复杂度为。
显然直接暴力计算 时间复杂度过高。
前向概率
定义:给定,定义到时刻 部分观测序列为 且状态为 的概率称为前向概率,记做:
-
初值:
-
递推:对于(注意这是一个从前向后的递推过程)
需要注意上一步,在第 步时,位于隐状态 的概率转移到第 步的隐状态 ,这里面的 有 种情况, 也有 种情况,在第 步的隐状态 对应有前一步的 种情况求和(这也是为什么前向计算能得出),然后再乘以发射概率。作为第 步处于该种隐状态的概率值。注意 的意义,为当前隐状态 生成特定 的概率。 ** -
最终可得:
对 积分,其结果即为生成指定的 的观测序列的概率。
前向算法的时间复杂度是。
那么在代码中是如何实现前向计算的呢?
def buildAlpha(X,PI,A,MU,SIGMA):## X.shape[feature,N]
# We build up Alpha here using dynamic programming. It is a KxN matrix
# where the element ALPHA_{ij} represents the forward probability
# for jth timestep (j = 1...N) and ith state. The columns of ALPHA are
# normalized for preventing underflow problem as discussed in secion
# 13.2.4 in Bishop's PRML book. So,sum(column) = 1
# c_t is the normalizing costant
N = np.size(X,1)
K = np.size(PI,0)
## 这里需要注意Alpha的shape为[K,N],表示在某个时刻为某个特定隐藏状态,其生成从开始时刻到当前时刻观测序列的概率
Alpha = np.zeros((K,N))
c = np.zeros(N)
# Base case: build the first column of ALPHA
for i in xrange(K):
## PI[i]表示选择该隐状态的概率值。
## normPDF(X[:,0],MU[:,i],SIGMA[i])表示该样本在该隐状态下的高斯分布下对应的概率值
┆ Alpha[i,0] = PI[i]*normPDF(X[:,0],MU[:,i],SIGMA[i])##也就是当前时刻的发射概率
c[0] = np.sum(Alpha[:,0])## 每列求和
Alpha[:,0] = Alpha[:,0]/c[0]## 归一
# 以下就是上面所讲的从前往后的递推过程
for t in xrange(1,N):
┆ for i in xrange(K):
┆ ┆ for j in xrange(K):
┆ ┆ ┆ Alpha[i,t] += Alpha[j,t-1]*A[j,i] # sum part of recursion
┆ ┆ Alpha[i,t] *= normPDF(X[:,t],MU[:,i],SIGMA[i]) # product with emission prob
┆ c[t] = np.sum(Alpha[:,t])
┆ Alpha[:,t] = Alpha[:,t]/c[t] # for scaling factors
return Alpha, c ##注意函数返回的Alpha的shape为[K,N]
特别需要注意上面所求矩阵 的意义:其 为,表示在某个时刻为某个特定隐藏状态,其生成从开始时刻到当前时刻观测序列的概率。
后向计算(同前向计算同理,只不过是从后向前计算)
定义:给定,定义到时刻 状态为 的前提下,从 到 的部分观测序列为 的概率为后向概率,记做:
- 初值:
此时, 观测值为。故为 - 递推:对于(注意:这是一个从后向前的递推过程)
这里需要注意 是可以得到 而不知道,所以对于不同的 ,都要乘以 。 - 最终:
看看代码中是如何实现后向计算的:
def buildBeta(X,c,PI,A,MU,SIGMA):## X.shape[features, N]
# Beta is KxN matrix where Beta_{ij} represents the backward probability
# for jth timestamp and ith state. Columns of Beta are normalized using
# the element of vector c.
N = np.size(X,1)
K = np.size(PI,0)
Beta = np.zeros((K,N))## 同上,shape也是[K,N],表示某一时刻的隐状态为某一隐状态从最后生成到当前时刻序列的概率。
# Base case: build the last column of Beta
for i in xrange(K):
┆ Beta[i,N-1]=1.## 此时,观测值为$O_{N}$。故为1
┆
# 按照上面所说的从后向前进行递推。直到t==0时。
for t in xrange(N-2,-1,-1):
┆ for i in xrange(K):
┆ ┆ for j in xrange(K):
┆ ┆ ┆ Beta[i,t] += Beta[j,t+1]*A[i,j]*normPDF(X[:,t+1],MU[:,j],SIGMA[j])
┆ Beta[:,t] /= c[t+1]
return Beta
一定要注意上面代码中,矩阵 的意义:shape也是[K,N],表示某一时刻的隐状态为某一隐状态生成从最后时刻到当前时刻指定序列的概率。
前向后向概率的关系
这种前向后向计算,每次都是在上一层的基础上进行递推,相对于暴力计算方法,避免了大量的重复计算,降低了复杂度,计算只存在于相邻的时间点内。
单个状态的概率
求给定模型 和观测,在时刻t处于状态 的概率。记:
可计算得:
Gamma = Alpha*Beta ## 矩阵点乘,shape为[k,N],这里没有除以分母,分母即所求gamma矩阵所在列之和。
这个Gamma主要是在M步更新PI的,故在M步再除分母一样
这就意味着只要我们知道 个观测序列,和模型(初始状态,状态矩阵,状态转移矩阵),就可以计算每个时刻的隐状态。即:在每个时刻 选择在该时刻最有可能出现的状态 ,从而得到一个状态序列,将它作为预测的结果。
两个状态的联合概率
求给定模型 和观测,在时刻 处于状态 并且时刻 处于状态 的概率。
那么在代码中如何实现 呢?首先我们可以试想, 共有三种状态, 也有三种状态,故每相邻的状态转移共有 种转移方式。而代码中共有 个时刻。故 的。
i = np.zeros((K,K,N))
for t in xrange(1,N):
## 每个时刻都是3*3 的矩阵,\alpha_{T}(i)*a_{ij}*\Beta_{t+1}(j),i,j都有k种可能,故a_{ij}就是转移矩阵A。
Xi[:,:,t] = (1/c[t])*Alpha[:,t-1][None].T.dot(Beta[:,t][None])*A
# Now columnwise multiply the emission prob
for col in xrange(K):
## 因为还需要乘以b_{jO_{t+1}},而3*3矩阵第二维即表示转移的j
┆ Xi[:,col,t] *= normPDF(trainSet[:,t],MU[:,col],SIGMA[col])
第二个问题:学习问题,给出观测序列,估计模型参数,使得 最大,显然用MLE的方式来估计。
监督学习
若训练数据包括观测序列和状态序列,则 的学习非常简单,是监督学习。利用大数定理的结论 “频率的极限是概率”,给出 的参数估计。
- 初始概率
- 转移概率
- 观测概率
有监督式的学习比较简单,就是统计每个句子里的每个词的状态而已,大概的讲下思路:
- 获取已经分词好的语料库,类似这样
1986年 , 十亿 中华 儿女 踏上 新 的 征 程 。 过去 的 一 年 , 是 全国 各族 人民 在 中国 共产党 领导 下 , 在 建设 有 中国 特色 的 社会主义 道路 上 , 坚持 改革 、 开放 , 团结 奋斗 、 胜利 前进 的 一 年 。
- 每个词即为一个观测状态,再定义词的隐状态,例如参考代码中的 B(开头),M(中间), E(结尾), S(独立成词)作为四种隐状态,则可得到语料库中每句话的隐状态序列。由隐状态序列求得 转移概率。
- 为初始状态,可从语料库中每句开头第一个词对应的隐状态得出
- 由语料库中每个隐状态对应的词得出 观测概率
- 由此可以从语料库中学习到参数矩阵 ,然后可以利用学习到的参数矩阵,对要预测的句子进行分词(Viterbi算法),可根据预测得到的每个词隐状态,决定是否进行分词。
循环遍历语料库中每个句子,统计句子中每个词的隐状态(已经定义好每个词对应的隐状态)。得到该句的 隐状态列表。
for i in range(len(line_state)):## 不同的句子,其line_state不同,可以理解为不同的时刻
if i == 0:
┆ Pi_dic[line_state[0]] += 1## 该句第一个词的隐状态
┆ Count_dic[line_state[0]] += 1## 后面做归一化用的
else:
┆ A_dic[line_state[i-1]][line_state[i]] += 1## 统计转移概率
┆ Count_dic[line_state[i]] += 1
## 统计发射概率,第i个隐状态对应第i个观测状态
┆ if not B_dic[line_state[i]].has_key(word_list[i]):
┆ ┆ B_dic[line_state[i]][word_list[i]] = 0.0
┆ else:
┆ ┆ B_dic[line_state[i]][word_list[i]] += 1
这样统计完语料库中的每个句子后,做完归一化后得到最终的 。
无监督学习(Baum-Welch算法)
若训练数据只有观测序列,则 的学习,需要使用 算法,是非监督学习。
算法整体框架:
所有观测数据写成,所有隐数据写成,完全数据是,完全数据的对数似然函数是 。
在 中,上面公式中的 就是观测,隐随机变量就是隐状态 。则其 公式中的 在 中为 ,这其实就是 步。
假设 是 参数的当前估计值(也就是上一轮中得出的最优的参数)
这个 步在代码中如何实现呢?实际上由后面的 步中为了得到 需要先知道 的值,故在 步时先更新得到:
def Estep(trainSet, PI,A,MU,SIGMA):## PI,A,MU,SIGMA 为上一轮M步迭代更新出的\lambda
## 即在E步利用上一轮更新后的PI,A,MU,SIGMA来计算gamma等
# The goal of E step is to evaluate Gamma(Z_{n}) and Xi(Z_{n-1},Z_{n})
# First, create the forward and backward probability matrices
Alpha, c = buildAlpha(trainSet, PI,A,MU,SIGMA)
Beta = buildBeta(trainSet,c,PI,A,MU,SIGMA)
# Dimension of Gamma is equal to Alpha and Beta where nth column represents
# posterior density of nth latent variable. Each row represents a state
# value of all the latent variables. IOW, (i,j)th element represents
# p(Z_j = i | X,MU,SIGMA)
Gamma = Alpha*Beta
#pdb.set_trace()
# Xi is a KxKx(N-1) matrix (N is the length of data seq)
# Xi(:,:,t) = Xi(Z_{t-1},Z_{t})
N = np.size(trainSet,1)
K = np.size(PI,0)
Xi = np.zeros((K,K,N))
for t in xrange(1,N):
┆ Xi[:,:,t] = (1/c[t])*Alpha[:,t-1][None].T.dot(Beta[:,t][None])*A
┆ # Now columnwise multiply the emission prob
┆ for col in xrange(K):
┆ ┆ Xi[:,col,t] *= normPDF(trainSet[:,t],MU[:,col],SIGMA[col])
return Gamma, Xi, c
为待求 的参数。则有:
我们就是要最求上面 取极值时对应的 ,其实就是 ,这就是 步。
根据上面的暴力计算的结论:
函数可写成:
- 极大化,获得 参数
注意到 加和为1,利用拉格朗日乘子法得:
对上式中的 求导,可得:
对 求和,得到:
从而得到:
PI = (Gamma[:,0]/np.sum(Gamma[:,0]))[None].T
同理可以用拉格朗日乘子法求得:
代码中是如何实现 步呢?
def Mstep(X, Gamma, Xi):
# Goal of M step is to calculate PI, A, MU, and SIGMA while treating
# Gamma and Xi as constant
K = np.size(Gamma,0)
d = np.size(X,0)
PI = (Gamma[:,0]/np.sum(Gamma[:,0]))[None].T
tempSum = np.sum(Xi[:,:,1:],axis=2)
A = tempSum/np.sum(tempSum,axis=1)[None].T ## 转移矩阵A
MU = np.zeros((d,K))
GamSUM = np.sum(Gamma,axis=1)[None].T
SIGMA = []
for k in xrange(K):
┆ MU[:,k] = np.sum(Gamma[k,:]*X,axis=1)/GamSUM[k]
┆ X_MU = X - MU[:,k][None].T
┆ SIGMA.append(X_MU.dot(((X_MU*(Gamma[k,:][None])).T))/GamSUM[k])
return PI,A,MU,SIGMA
问题三:预测算法
在每个时刻 选择在该时刻最有可能出现的状态 ,从而得到一个状态序列,将它作为预测的结果。
Viterbi算法
算法实际是用 动态规划 解 预测问题,用 求概率最大的路径(最优路径),这是一条路径对应一个状态序列。其实就是我们知道了模型参数 后,从时刻 递推到时刻 的最大概率路径。
定义变量:在时刻 状态为 的所有路径中,概率的最大值。
- 递推
- 终止
那么在代码中时如何实现 算法呢?
def viterbi(obs, states, start_p, trans_p, emit_p):
"""
obs: 需要切分的sentence
states: 状态种类序列,例如每个词可能有四个状态[B, M, E, S]
start_p: 就是上面所讲的\PI
trans_p: 状态转移矩阵
emit_p: 发射矩阵
"""
V = [{}] #tabular V[t][state]:t表示时刻,state表示该时刻的隐状态
path = {}
for y in states: #init
## emit_p[y].get(obs[0],0)表示在y隐状态下,观测状态为obs[0] 的发射概率。
## 在t=0 时刻时,观测状态即为obs[0]
┆ V[0][y] = start_p[y] * emit_p[y].get(obs[0],0)
┆ path[y] = [y] ## 记录当前的状态路径
for t in range(1,len(obs)):
┆ V.append({})
┆ newpath = {}
┆ for y in states:
## 在t时刻时,遍历t-1时刻所有可能的隐状态state与当前y隐状态的连接概率,获取最大时对于的state和对应的概率prob
┆ ┆ (prob,state ) = max([(V[t-1][y0] * trans_p[y0].get(y,0) * emit_p[y].get(obs[t],0) ,y0) for y0 in states if V[t-1][y0]>0])
┆ ┆ V[t][y] =prob## 将最大概率prob作为V[t][y]
┆ ┆ newpath[y] = path[state] + [y]## path[state] 表示t-1时刻最大概率对应的隐状态序列,再加上当前时刻的y
┆ path = newpath
## 取最后概率最大的对应的序列作为最后结果。
(prob, state) = max([(V[len(obs) - 1][y], y) for y in states])
return (prob, path[state])
def cut(sentence):
#pdb.set_trace()
prob, pos_list = viterbi(sentence,('B','M','E','S'), prob_start, prob_trans, prob_emit)
return (prob,pos_list)
算法其实就是多步骤、每步多选择模型的最优选择问题,其在每一步的所有选择都保存了从第一步到当前步的的最小或最大代价,以及当前情况下前进步骤的选择。并且记下在每一步每一个隐状态与上一步对应代价最小的隐状态节点,如果有 个隐状态,就形成 个不同的链路,并且保证了每个节点对应的链路都是该节点的最小代价。
个人总结
我个人觉得前向、后向计算、 算法、 这几个算法有几分相似,又有几分区别之处,值得思考一下:
- 前向、后向计算是利用动态规划的思想,每一步的计算都是在前一步计算结果的基础上,大大降低了计算量。
- 算法 同样也是利用了动态规划的思想,能保证找到最优的路径。
- 利用的是贪心的思想,每一步只能找到当前时刻最优的 个不同的,而只是在当前时刻最优,却不能保证整体最优,故最后结果可能不是最优结果。那么有人可能会问,为啥 不用 的那种动态规划的思想,而用贪心?这个问题其实很简单,试想一下,在 中,最后一个时刻每个状态都有对应的最优链路,因此我们可以找到最优的,而在 中呢,你不可能在最后一步遍历所有的隐状态(词表一般很大),你只能采取贪心的方式每一步选择当前最优的 个状态。