2.2.2 隐马尔可夫模型

隐马尔可夫模型(Hidden Markov Models,HMM)是序列数据处理和统计学习的一种重要概率模型,具有建模简单、数据计算量小、运行速度快、识别率高等特点,近几年来已经被成功应用到许多工程任务中。

1.隐马尔可夫概念

隐马尔可夫模型(HMM)由两个序列组成,一个是不可观察的(隐藏的)状态变化序列,一个是由该隐藏的状态所产生的可观察的符号序列。隐马尔可夫模型由其初始分布、转移概率矩阵和发射概率矩阵完全确定。一个建好的隐马尔可夫模型的运作过程可以这样描述:由初始分布随机地产生一个状态,然后按转移概率矩阵随机地从一个状态转移到另一个状态,在每一个状态按发射概率矩阵随机地生成一个字符,结果是产生一个状态序列和一个相应的字符序列。

一般情况下,不仅模型能够产生的状态序列不是唯一的,每一个状态序列能够产生的字符序列也不是唯一的。模型以一定的概率产生特定的状态序列,该状态序列又以一定的概率产生特定的字符序列。在实际问题中,通常字符序列是可观测的,状态序列是不可观测的,是“隐藏的”。

HMM可以表示为λ=(A,S,π),描述如下:

(a)隐状态集SS={S1,S2,S3,…,SN},N表示状态数。

(b)观察符号集VV ={V1,V2,V3,…,VM},M表示符号数。

(c)状态转换概率矩阵AA={ai}j

式中

式中,Q=(q1,q2,…,qL)表示状态序列,其中qiS,1≤iLL表示序列的长度。t为当前时刻。aij为从状态si转换到s j的概率。

(d)符号释放概率矩阵EE={e jk},其中

式中,O=(o1,o2,…,oL)表示符号序列,其中o jV,1≤ jLe jk表示在状态S j下产生符号Vk的概率。

(e)初始状态分布ππ={πi},其中

表示模型起始于状态Si(即时间为0时的状态)的概率。

基于HMM的模式识别方法的基本思想:对于一个可用HMM描述的模式识别问题,其可能出现的所有不同的隐马尔可夫模型λ1,λ2,λ3,…,λn构成一个集合Λ,其中任意一个元素λi为特定的隐马尔可夫模型,其中包括此特定隐马尔可夫模型的转移矩阵、初分布及观测概率。观测概率的定义如下:

给定一组样本观测值(o1,o2,…,on),选择λΛ,使得

λ是观测(o1,o2,…,on)的最可能模式。

2.隐马尔可夫模型基本算法

HMM需解决的基本问题:

·概率问题。给定一个HMM(λ=(A,S,π))和一个观测符号序列O,求由该模型得到序列O的概率P(O|λ)。

·解码问题:给定HMM(λ=(A,S,π))和一个观测序列O,解码问题就是寻找最可能的隐状态序列,即如何选择产生该观察序列O的最佳状态序列Q*,也就是如何最好地解释符号序列O

·学习问题。给定一组观测序列样本集,如何确定其对应的HMM(λ=(A,S,π)),使该模型能够最好地描述观测样本集,也就是该模型的参数能够使P(O|λ)最大。

(1)前向后向算法

对于可能性问题,引入前向变量和后向变量进行动态规划求解。

1)前向算法

前向算法的简单图解如图2-8所示。

图2-8 前向算法示意图

前向变量αj(t)表示在t时刻状态为s j,且序列的前t个符号为o1,o2,…,ot的概率,即

根据前向变量的定义,可以得到:

前向变量的迭代公式:

前向算法的流程:

(a)初始化:

ai(0)=πiei(o 1),1≤iN

(b)递归:

          t=1,2,3,…,L
              j=1,2,3,…,N
                  a j(t)=ej(ot)∑k(ak(t-1)akj)
              next
          next

(c)终止:

P (o|λ)=∑kαk(L)

(d)算法结束。

2)后向算法

后向算法的简单图解如图2-9所示。

图2-9 后向算法示意图

后向变量βj(t)表示在t时刻状态为s j,且序列从t+1到L个符号为ot+1,ot+2,…,ot+L的概率,即

根据后向变量的定义,可以得到:

后向变量的迭代公式:

后向算法的流程如下:

(a)初始化:

βi (L)=1 1≤iN

(b)递归:

          t=L-1,…,1
              j= 1,2,…,N
                βj(t)=∑kαjk ek(ot+ 1)βk(t+1)
              next
        next

(c)终止:

Po λ( | )=∑k k k(1) k(1)

(d)算法结束。前向算法和后向算法的时间复杂度为o(N 2L),空间复杂度为o(NL)。

(2)解码问题

根据给定的HMM(λ=(A,E,π))和符号序列O=(o1,o2,…,oL),确定一条具有最大概率的状态序列,即最优路径Q*

应用Viterbi算法求解最优路径。定义变量δj(t),它表示由状态序列q1,q 2,…,qtqt =s j产生出符号序列o1,o2,…,ot的最大概率,即

δj (t)可以通过迭代计算得到:

从初始状态开始迭代计算δj(t),一直到序列末端,然后再回溯得到最优路径。

Viterbi算法流程如下:

(a)初始化:

δi(1)=πiei(o 1) 1≤iN

(b)递归:

(c)终止:

(d)回溯:

qt *=φt+1(qt+1),t=L-1,L-2,…,1

(e)算法结束。

Viterbi算法提供了有效的计算方法来分析HMM中的观察序列,以此得到最可能的隐状态序列,并利用递归来减小计算负担。

(3)训练问题当只有符号序列o1,o2,…,on是给定的时,才要通过训练学习估计模型的各项参数,使观测的符号序列能够“最好”地被描述,即λ能够使P(o1,o2,…,on|λ)最大。

如果符号序列是相互独立的,则它们的联合分布概率即为它们单个概率的乘积:

如果路径已知,则要统计各个特定的初始状态,转移概率以及产生概率。记某种初始状态、转移和出现的次数为πiAijEi(Vk),则初始状态概率、转移概率和产生概率的极大似然概率估计值为

如果路径未知,常采用Baum-Welch算法来实现对HMM的参数估计。

根据前向变量和后向变量的定义,可以得到:

通过对所有位置以及所有序列的求和,可以得到:

再根据式(2-116)~式(2-118)即可以得到模型的各项参数估计。

Baum-Welch算法流程如下:

定义变量γt(i)=P(qt =si|O,λ),ξt(i,j)=P(qt =si,qt+1=s j|O,λ)

=时刻t=1在状态Si的期望值=γ1(i)。

此时有

,1≤iN,1≤ jN

可以通过证明得出结论:P(o|λ)迭代收敛到局部最佳。

3.隐马尔可夫模型的C语言实现

下面介绍隐马尔可夫模型的C语言实现代码。

动态数组申请的代码如代码2-2所示。

代码2-2动态数组申请代码

        float *vector(long nl,long nh)
        {
            float *v;
            v=(float *)calloc((unsigned) (nh-nl+1),sizeof(float));
            if (!v) nrerror("allocation failure in vector()");
            return v-nl;
        }
        int *ivector(long nl,long nh)
        {
            int *v;
            v=(int *)calloc((unsigned) (nh-nl+1),sizeof(int));
            if (!v) nrerror("allocation failure in ivector()");
            return v-nl;
        }
        unsigned char *cvector(long nl,long nh)
        {
            unsigned char *v;
            v=(unsigned char *)calloc((unsigned) (nh-nl+1),sizeof(unsigned char));
            if (!v) nrerror("allocation failure in cvector()");
            return v-nl;
        }
        unsigned long *lvector(long nl,long nh)
        {
            unsigned long *v;
            v=(unsigned long *)calloc((unsigned) (nh-nl+1),sizeof(unsigned long));
            if (!v) nrerror("allocation failure in lvector()");
            return v-nl;
        }
        double *dvector(long nl,long nh)
        {
            double *v;
            v=(double *)calloc((unsigned) (nh-nl+1),sizeof(double));
            if (!v) nrerror("allocation failure in dvector()");
            return v-nl;
        }
        float **matrix(long nrl,long nrh,long ncl,long nch)
        {
            int i;
            float **m;
            m=(float **) calloc((unsigned) (nrh-nrl+1),sizeof(float*));
            if (!m) nrerror("allocation failure 1 in matrix()");
            m -= nrl;
            for(i=nrl;i<=nrh;i++) {
                m[i]=(float *) calloc((unsigned) (nch-ncl+1),sizeof(float));
                if (!m[i]) nrerror("allocation failure 2 in matrix()");
                m[i] -= ncl;
            }
            return m;
        }
        double **dmatrix(long nrl,long nrh,long ncl,long nch)
        {
            int i;
            double **m;
            m=(double **) calloc((unsigned) (nrh-nrl+1),sizeof(double*));
            if (!m) nrerror("allocation failure 1 in dmatrix()");
            m -= nrl;
            for(i=nrl;i<=nrh;i++) {
                m[i]=(double *) calloc((unsigned) (nch-ncl+1),sizeof(double));
                if (!m[i]) nrerror("allocation failure 2 in dmatrix()");
                m[i] -= ncl;
            }
            return m;
        }
        int **imatrix(long nrl,long nrh,long ncl,long nch)
        {
            int i,**m;
            m=(int **)calloc((unsigned) (nrh-nrl+1),sizeof(int*));
            if (!m) nrerror("allocation failure 1 in imatrix()");
            m -= nrl;
            for(i=nrl;i<=nrh;i++) {
                m[i]=(int *)calloc((unsigned) (nch-ncl+1),sizeof(int));
                if (!m[i]) nrerror("allocation failure 2 in imatrix()");
                m[i] -= ncl;
            }
            return m;
        }
        unsigned char **cmatrix(long nrl,long nrh,long ncl,long nch)
        {
            int i;
            unsigned char **m;
            m=(unsigned char **) calloc((unsigned) (nrh-nrl+1),sizeof(unsigned char*));
            if (!m) nrerror("allocation failure 1 in dmatrix()");
            m -= nrl;
            for(i=nrl;i<=nrh;i++) {
                m[i]=(unsigned char *) calloc((unsigned) (nch-ncl+1),sizeof(unsigned char));
                if (!m[i]) nrerror("allocation failure 2 in dmatrix()");
                m[i] -= ncl;
            }
            return m;
        }
        //求一个二维矩阵的子矩阵
        float **submatrix(float **a,long oldrl,long oldrh,long oldcl,long oldch,long
            newrl,long newcl)
        {
            int i,j;
            float **m;
            m=(float **) calloc((unsigned) (oldrh-oldrl+1),sizeof(float*));
            if (!m) nrerror("allocation failure in submatrix()");
            m -= newrl;
            for(i=oldrl,j=newrl;i<=oldrh;i++,j++) m[j]=a[i]+oldcl-newcl;
            return m;
        }
        //将一个实数一维数组转化为二维数组
        float **convert_matrix(float *a,long nrl,long nrh,long ncl,long nch)
        {
            int i,j,nrow,ncol;
            float **m;
            nrow=nrh-nrl+1;
            ncol=nch-ncl+1;
            m = (float **) calloc((unsigned) (nrow),sizeof(float*));
            if (!m) nrerror("allocation failure in convert_matrix()");
            m -= nrl;
            for(i=0,j=nrl;i<=nrow-1;i++,j++) m[j]=a+ncol*i-ncl;
            return m;
        }

前向算法的代码如代码2-3所示。

代码2-3前向算法代码

        //前向算法估计参数
        void Forward(HMM *phmm,int T,int *O,double **alpha,double *pprob)
        {
            int i,j;
            int t;
            double sum;
            //初始化
            for(i=1;i<=phmm->N;i++)
                alpha[1][i]=phmm->pi[i]*phmm->B[i][O[1]];
            //递归
            for(t=1;t<T;t++){
                for(j=1;j<=phmm->N;j++){
                    sum=0.0;
                for(i=1;j<=phmm->N;i++)
                    sum+=alpha[t][i]*(phmm->A[i][j]);
                alpha[t+1][j]=sum*(phmm->B[j][O[t+1]]);
                }
            }
            //终止
            *pprob=0.0;
            for(i=1;i<=phmm->N;i++)
                *pprob+=alpha[T][i];
        }
        void ForwardWithScale(HMM*phmm,int T,int *O,double **alpha,double *scale,double
            *pprob)
        // pprob是对数概率
        {
            int i,j;
            int t;
            double sum;
            //初始化
            scale[1]=0.0;
            for(i=1;i<=phmm->N;i++){
                alpha[1][i]=phmm->pi[i]*(phmm->B[i][O[1]+1]);
                scale[1]+=alpha[1][i];
            }
            for(i=1;i<=phmm->N;i++)
                alpha[1][i]/=scale[1];
            //递归
            for(t=1;t<T;t++){
                scale[t+1]=0.0;
                for(j=1;j<=phmm->N;j++){
                    sum=0.0;
                    for(i=1;i<=phmm->N;i++)
                          sum+=alpha[t][i]*(phmm->A[i][j]);
                    alpha[t+1][j]=sum*(phmm->B[j][O[t+1]]);
                    scale[t+1]+=alpha[t+1][j];
                }
                for(j=1;j<=phmm->N;j++)
                    alpha[t+1][j]/=scale[t+1];
            }
            //终止
            *pprob=0.0;
            for(t=1;t<=T;t++)
                *pprob+=log(scale[t]);
        }

后向算法的代码如代码2-4所示。

代码2-4后向算法代码

        //后向算法估计参数
        void Backward(HMM *phmm, int T, int *O, double **beta, double *pprob)
        {
            int i, j;
            int t;
            double sum;
            //初始化
            for (i = 1; i <= phmm->N; i++)
                beta[T][i] = 1.0;
            //递归
            for (t = T -1; t >= 1; t--)
            {
                for (i = 1; i <= phmm->N; i++)
                {
                    sum = 0.0;
                    for (j = 1; j <= phmm->N; j++)
                          sum += phmm->A[i][j] * (phmm->B[j][O[t+1]])*beta[t+1][j];
                    beta[t][i] = sum;
                }
            }
            //终止
            *pprob = 0.0;
            for (i = 1; i <= phmm->N; i++)
                *pprob += beta[1][i];
        }
        void BackwardWithScale(HMM *phmm, int T, int *O, double **beta, double *scale,double *pprob)
        {
            int i, j;
            int t;
            double sum;
            //初始化
            for (i = 1; i <= phmm->N; i++)
                beta[T][i] = 1.0/scale[T];
            //递归
            for (t = T -1; t >= 1; t--)
            {
                for (i = 1; i <= phmm->N; i++)
                {
                    sum = 0.0;
                    for (j = 1; j <= phmm->N; j++)
                        sum += phmm->A[i][j] * (phmm->B[j][O[t+1]])*beta[t+1][j];
                    beta[t][i] = sum/scale[t];
                }
            }
        }

Viterbi算法的代码如代码2-5所示。

代码2-5 Viterbi算法代码

        #define VITHUGE  100000000000.0
        void Viterbi(HMM *phmm, int T, int *O, double **delta, int **psi, int *q, double *pprob)
        {
            int i, j;
            int t;
            int maxvalind;
            double  maxval, val;
            //初始化
            for (i = 1; i <= phmm->N; i++)
            {
                delta[1][i] = phmm->pi[i] * (phmm->B[i][O[1]]);
                psi[1][i] = 0;
            }
            //递归
            for (t = 2; t <= T; t++)
            {
                for (j = 1; j <= phmm->N; j++)
                {
                    maxval = 0.0;
                    maxvalind = 1;
                    for (i = 1; i <= phmm->N; i++)
                    {
                          val = delta[t-1][i]*(phmm->A[i][j]);
                          if (val > maxval) {
                              maxval = val;
                              maxvalind = i;
                        }
                    }
                    delta[t][j] = maxval*(phmm->B[j][O[t]]);
                    psi[t][j] = maxvalind;
                }
            }
            //终止
            *pprob = 0.0;
            q[T] = 1;
            for (i = 1; i <= phmm->N; i++)
            {
                if (delta[T][i] > *pprob)
                {
                    *pprob = delta[T][i];
                    q[T] = i;
                }
            }
            for (t = T -1; t >= 1; t--)
                q[t] = psi[t+1][q[t+1]];
      }
      void ViterbiLog(HMM *phmm, int T, int *O, double **delta, int **psi, int *q, double
            *pprob)
      {
            int i, j;
            int t;
            int maxvalind;
            double  maxval, val;
            double  **biot;
            //预处理
            for (i = 1; i <= phmm->N; i++)
                phmm->pi[i] = log(phmm->pi[i]);
            for (i = 1; i <= phmm->N; i++)
                for (j = 1; j <= phmm->N; j++)
                {
                    phmm->A[i][j] = log(phmm->A[i][j]);
                }
            biot = dmatrix(1, phmm->N, 1, T);
            for (i = 1; i <= phmm->N; i++)
                for (t = 1; t <= T; t++)
                {
                    biot[i][t] = log(phmm->B[i][O[t]]);
                }
            //初始化
            for (i = 1; i <= phmm->N; i++)
            {
                delta[1][i] = phmm->pi[i] + biot[i][1];
                psi[1][i] = 0;
            }
            //递归
            for (t = 2; t <= T; t++)
            {
                for (j = 1; j <= phmm->N; j++)
                {
                    maxval = -VITHUGE;
                    maxvalind = 1;
                    for (i = 1; i <= phmm->N; i++)
                    {
                        val = delta[t-1][i] + (phmm->A[i][j]);
                        if (val > maxval)
                        {
                              maxval = val;
                              maxvalind = i;
                        }
                    }
                    delta[t][j] = maxval + biot[j][t];
                    psi[t][j] = maxvalind;
                }
            }
            //终止
            *pprob = -VITHUGE;
            q[T] = 1;
            for (i = 1; i <= phmm->N; i++)
            {
                if (delta[T][i] > *pprob)
                {
                    *pprob = delta[T][i];
                    q[T] = i;
                }
            }
            //回朔
            for (t = T -1; t >= 1; t--)
                q[t] = psi[t+1][q[t+1]];
        }

Baum-Welch算法的代码如代码2-6所示。

代码2-6 Baum-Welch算法代码

        #define DELTA 0.001
        void BaumWelch(HMM *phmm, int T, int *O, double **alpha, double **beta, double**gamma, int *pniter, double *plogprobinit, double *plogprobfinal)
        {
                int i, j, k;
                int t, l = 0;
                double  logprobf, logprobb;
                double  numeratorA, denominatorA;
                double  numeratorB, denominatorB;
                double ***xi, *scale;
                double delta, deltaprev, logprobprev;
                deltaprev = 10e-70;
                xi = AllocXi(T, phmm->N);
                scale = dvector(1, T);
                ForwardWithScale(phmm, T, O, alpha, scale, &logprobf);
                *plogprobinit = logprobf;
                BackwardWithScale(phmm, T, O, beta, scale, &logprobb);
                ComputeGamma(phmm, T, alpha, beta, gamma);
                ComputeXi(phmm, T, O, alpha, beta, xi);
                logprobprev = logprobf;
                do{
         //重新估计t=1时,状态为i的频率
         for (i = 1; i <= phmm->N; i++)
phmm->pi[i] = .001 + .999*gamma[1][i];
         //重新估计转移矩阵和观察矩阵
         for (i = 1; i <= phmm->N; i++)
         {
denominatorA = 0.0;
for (t = 1; t <= T -1; t++)
denominatorA += gamma[t][i];
for (j = 1; j <= phmm->N; j++)
{
numeratorA = 0.0;
for (t = 1; t <= T -1; t++)
numeratorA += xi[t][i][j];
phmm->A[i][j] = .001 + .999*numeratorA/denominatorA;
}
denominatorB = denominatorA + gamma[T][i];
for (k = 1; k <= phmm->M; k++)
{
numeratorB = 0.0;
for (t = 1; t <= T; t++)
{
if (O[t] == k)
numeratorB += gamma[t][i];
                        }
                        phmm->B[i][k] = .001 +.999*numeratorB/denominatorB;
                    }
                }
                ForwardWithScale(phmm, T, O, alpha, scale, &logprobf);
                BackwardWithScale(phmm, T, O, beta, scale, &logprobb);
                ComputeGamma(phmm, T, alpha, beta, gamma);
                ComputeXi(phmm, T, O, alpha, beta, xi);
                //计算两次直接的概率差
                delta = logprobf - logprobprev;
                logprobprev = logprobf;
                l++;
            }
            while (delta > DELTA);
            *pniter = l;
            *plogprobfinal = logprobf; /* log P(O|estimated model) */
            FreeXi(xi, T, phmm->N);
            free_dvector(scale, 1, T);
      }
      void ComputeGamma(HMM *phmm, int T, double **alpha, double **beta, double **gamma)
      {
            int i, j;
            int t;
            double  denominator;
            for (t = 1; t <= T; t++)
            {
                denominator = 0.0;
                for (j = 1; j <= phmm->N; j++)
                {
                    gamma[t][j] = alpha[t][j]*beta[t][j];
                    denominator += gamma[t][j];
                }
                for (i = 1; i <= phmm->N; i++)
                    gamma[t][i] = gamma[t][i]/denominator;
            }
      }
      void ComputeXi(HMM* phmm, int T, int *O, double **alpha, double **beta, double ***xi)
      {
            int i, j;
            int t;
            double sum;
            for (t = 1; t <= T -1; t++) {
                sum = 0.0;
                for (i = 1; i <= phmm->N; i++)
                    for (j = 1; j <= phmm->N; j++)
                    {
                        xi[t][i][j] = alpha[t][i]*beta[t+1][j]
                              *(phmm->A[i][j])
                              *(phmm->B[j][O[t+1]]);
                        sum += xi[t][i][j];
                    }
                for (i = 1; i <= phmm->N; i++)
                    for (j = 1; j <= phmm->N; j++)
                        xi[t][i][j]  /= sum;
            }
        }
        double *** AllocXi(int T, int N)
        {
            int t;
            double ***xi;
            xi = (double ***) malloc(T*sizeof(double **));
            xi --;
            for (t = 1; t <= T; t++)
                xi[t] = dmatrix(1, N, 1, N);
            return xi;
        }
        void FreeXi(double *** xi, int T, int N)
        {
            int t;
            for (t = 1; t <= T; t++)
                free_dmatrix(xi[t], 1, N, 1, N);
            xi ++;
            free(xi);
        }