自然语言处理入门笔记-> 隐马尔科夫模型HMM

918 阅读6分钟

此文为学习何晗老师《自然语言处理入门》笔记

由于词语级别的模型天然缺乏OOV召回能力,我们需要更细粒度的模型。

比词语更细的颗粒就是字符,如果字符级模型能够掌握汉字组词的规律,那么它就能够由字构词、动态的识别新词汇,而不局限于词典

序列标注问题

序列标注(tagging)指的是给定一个序列x=x1x2xnx = x_1 x_2 \ldots x_n,找出序列中每个元素对应的标签y=y1y2yny = y_1 y_2 \ldots y_n的问题。

其中yy是所有可能的取值集合,称为标注集*(tagset)。

最简单的奇偶标注

求解序列标注问题的模型一般称为序列标注器(tagger)。

通常模型从一个标注数据集X,Y={(xi,yi)},i=1,K{X, Y} = \{(x^i, y^i)\}, i = 1, \ldots K中学习相关知识后再进行预测。

在NLP问题中,xx通常是字符或词语,而yy则是待预测的组词角色或词性等标签。

序列标注与中文分词

比如下面,{B,M,E,S}为最流行的标注集,其中:

  • B: Begin 词首
  • E: End 词尾
  • M: Middle 词中
  • S: Single 单字成词

标注后,按照最近的两个BE标签对应区间合并为一个词,S标签对应单字词语,按顺序输出即为分词过程。

序列标注与词性标注

序列标注与命名实体识别

所谓命名实体就是现实存在的实体,比如人名、地名和机构名。命名实体是OOV的主要组成部分,往往也是最令人关注的成分

隐马尔科夫模型

隐马尔科夫模型(Hidden Markov Model, HMM)是描述两个时序序列联合分布p(x,y)p(x, y)的概率模型:

  • 观测序列(Observation Sequence):xx序列外界可见
  • 状态序列(State Sequence):yy序列外界不可见

从马尔科夫假设到隐马尔科夫模型

  • 马尔科夫假设:每个事件发生的概率只取决于前一个事件
  • 马尔科夫链:将满足该假设的连续多个事件串联在一起构成

在NLP语境下,可以将事件具象为单词,于是马尔科夫模型就具象为二元语法模型。

根据联合概率分布特性

p(x,y)=p(x)p(yx)=p(y)(xy)=p(y,x)p(x, y) = p(x)p(y|x) = p(y)(x|y) = p(y, x)

假定先有状态,后有观测,取决于两个序列的可见与否。

状态与观测之间的依赖关系确定以后,隐马尔科夫模型利用三个要素来模拟时序序列的发生过程:

  • 初始状态概率向量(下图中π\pi
  • 状态转移概率矩阵(下图中AA
  • 发射概率矩阵(也称观测概率矩阵,下图中BB

初始概率向量、状态转移概率矩阵和发射概率矩阵被称为隐马尔科夫模型的三元组: ?\lambda =(\pi, A, B)?

初始状态概率向量

系统启动时进入的第一状态yiy_i称为初始状态。

假设yyNN种取值,即y{s1,s2,,sN}y \in \{s_1, s_2, \cdots, s_N\},那么y1y_1就是一个独立的离散型随机变量,由p(yiπ)p(y_i | \pi)描述。 其中初始概率向量: π=(π1,,πN)T,0πi1,i=1Nπi=1\pi = (\pi_1,\cdots,\pi_N)^T, 0 \leq \pi_i \leq 1, \sum_{i=1}^N \pi_i = 1

比如中文分词问题采用{B,M,E,S}\{B, M, E, S\}标注集时: p(yi=B)=0.7p(y_i = B) = 0.7 p(yi=M)=0p(y_i = M) = 0 p(yi=E)=0p(y_i = E) = 0 p(yi=S)=0.3p(y_i = S) = 0.3

π=[0.7,0,0,0.3]\pi = [0.7, 0, 0, 0.3]

状态转移概率矩阵

根据马尔科夫假设,t+1t+1时的状态取决于tt时的状态。在NN种状态下,从状态sis_i转移到状态sjs_j的概率就构成了一个N×NN \times N状态转移概率矩阵AA A=[p(yt+1=sjyt=si)]N×NA = [p(y_{t+1} = s_j| y_t = s_i)]_{N \times N} 其中下标i,ji, j分别表示状态ii转移到状态jj概率。

发射概率矩阵

根据马尔科夫假设,当前观测xtx_t仅取决于当前状态yty_t

假设观测xx一共有MM种可能的取值,即x{o1,,oM}x \in \{o_1, \cdots, o_M\}。由于yy共有NN种,所以这些参数向量构成了N×MN \times M发射概率矩阵BB B=[p(xt=oiyt=sj)]N×MB = [p(x_t = o_i|y_t = s_j)]_{N \times M}

示例

下图中,

N=2N = 2

M=3M = 3

λ=[0.6,0.4]\lambda = [0.6, 0.4]

A=[0.70.30.40.6]A = \left[ \begin{matrix} 0.7 & 0.3 \\ 0.4 & 0.6 \end{matrix} \right]

B=[0.50.40.10.10.30.6]B = \left[ \begin{matrix} 0.5 & 0.4 & 0.1 \\ 0.1 & 0.3 & 0.6 \end{matrix} \right]

yy为健康、发烧,xx为正常、体寒、头晕可观测矩阵

隐马尔科夫模型三个基本用法

  1. 样本生成问题:给定模型λ=(π,A,B)\lambda =(\pi, A, B),生成满足模型约束的样本,即一系列观测序列及对应的状态序列{(xi,yi)}\{(x^i, y^i)\}
  2. 模型训练问题:给定训练集{(xi,yi)}\{(x^i, y^i)\},估计模型参数λ=(π,A,B)\lambda =(\pi, A, B)
  3. 序列预测问题:已知模型参数λ=(π,A,B)\lambda =(\pi, A, B),给定观测序列xx,求最可能的状态序列yy

马尔科夫模型样本生成

隐马尔科夫模型训练

在监督学习中,我们利用极大似然法来估计隐马尔科夫模型参数。

归一化函数及转为为log\log函数,方便后期计算时,使用加法代替乘法

/**
 * 频次向量归一化为概率分布
 * 并将概率转换为log,方便后期将乘法改为加法
 *
 * @param freq 输入的一维度矩阵(1 X n)
 */
void normalizeAndToLog(float[] freq){
    // 累计一行的SUM
    float sum = MathUtility.sum(freq);
    for (int i = 0; i < freq.length; i++) {
        // 归一化
        freq[i] /= sum;
        // 使用log函数
        freq[i] = Math.log(freq[i])
    }
}

初始化状态概率向量的估计

初始状态可以看作是状态转移的一种特例,即y1y_1是由BOS转移而来。 统计y1y_1的所有取值的频次记作向量c(1×n)c(1 \times n)

π^i=cij=1Ncj,i=1,2,,N\hat \pi_i = \frac {c_i} {\sum_{j=1}^{N} c_j}, i = 1, 2, \cdots, N

由初始状态BOSBOSss的概率向量:

BOSBOS \ sss1s_1s2s_2\cdotssNs_N
BOSBOSBOSs1BOS \rightarrow s_1BOSs2BOS \rightarrow s_2\cdotsBOSsNBOS \rightarrow s_N
/**
 * 估计初始状态概率向量
 *
 * @param samples 训练样本集
 *                是包含多个2*N的矩阵样本数据,每个矩阵第0行为观测值,第1行为状态值
 * @param max_state 状态的最大下标,等于N-1
 */
void estimateStartProbability(Collection<int[][]> samples, int max_state) {
    // 初始状态概率向量
    float[] start_probability = new float[max_state + 1];
    for (int[][] sample : samples) {
        // 只取第一个状态进行累加
        int s = sample[1][0];
        ++start_probability[s];
    }
    // 归一化 log化
    normalizeAndToLog(start_probability);
}

转移概率矩阵的估计

记样本序列在时刻tt处于状态sis_i,时刻t+1t+1转移到状态sjs_j

统计这样的转移频次计入矩阵元素Ai,jA_{i,j},根据极大似然估计,从sis_i转移到sjs_j的转移概率ai,ja_{i,j}可估计为矩阵第ii行频次的归一化

a^i,j=Ai,jj=1NAi,j,i,j=1,2,,N\hat a_{i,j} = \frac {A_{i,j}} {\sum_{j = 1}^N A_{i,j}}, i,j=1,2, \cdots,N

有前一状态s^\hat s转移到当前状态ss,矩阵如下:

s^\hat s \ sss1s_1s2s_2\cdotssNs_N
s^1\hat s_1s^1s1\hat s_1 \rightarrow s_1s^1s2\hat s_1 \rightarrow s_2\cdotss^1sN\hat s_1 \rightarrow s_N
s^2\hat s_2s^2s1\hat s_2 \rightarrow s_1s^2s2\hat s_2 \rightarrow s_2\cdotss^2sN\hat s_2 \rightarrow s_N
\vdots\vdots\vdots\vdots\vdots
s^N\hat s_Ns^Ns1\hat s_N \rightarrow s_1s^Ns2\hat s_N \rightarrow s_2\cdotss^NsN\hat s_N \rightarrow s_N
/**
 * 估计转移概率矩阵
 *
 * @param samples 训练样本集
 *                是包含多个2*N的矩阵样本数据,每个矩阵第0行为观测值,第1行为状态值
 * @param max_state 状态的最大下标,等于N-1
 */
void estimateTransitionProbability(Collection<int[][] samples, int max_state) {
    // 状态转移概率矩阵
    float[][] transition_probability = new float[max_state + 1][max_state + 1];
    for (int[][] sample: samples) {
        // 处理一个序列
        int prev_s = sample[1][0];
        for (int i = 1; i < sample[0].length; i++) {
            // 获取当前状态
            int s = sample[1][i];
            // 累计状态转移矩阵的对应值
            ++transition_probability[prev_s][s];
            // 转移到下一个状态
            prev_s = s;
        }
    }
    
    for (int i = 0; i < transition_probality.length; i++) {
        // 归一化每一个状态(一行数据)
        normalizeAndToLog(transition_probability[i])
    }
}

发射概率矩阵的估计

统计样本中状态为sis_i且观测为ojo_j的频次,计入矩阵元素Bi,jB_{i,j},则状态sjs_j发射观测ojo_j的概率估计为: b^i,j=Bi,jj=1MBi,j,i=1,2,,N;j=1,2,,M\hat b_{i,j} = \frac {B_{i,j}} {\sum_{j=1}^{M} B_{i,j}}, i=1, 2, \dots, N;j=1, 2, \dots, M

由当前状态ss发射到oo的矩阵:

ss \ ooo1o_1o2o_2\dotsoMo_M
s1s_1s1o1s_1 \rightarrow o_1s1o2s_1 \rightarrow o_2\cdotss1oMs_1 \rightarrow o_M
s2s_2s2o1s_2 \rightarrow o_1s2o2s_2 \rightarrow o_2\cdotss2oMs_2 \rightarrow o_M
\vdots\vdots\vdots\vdots\vdots
sNs_NsNo1s_N \rightarrow o_1sNo2s_N \rightarrow o_2\cdotssNoMs_N \rightarrow o_M
/**
 * 估计状态发射概率
 *
 * @param samples 训练样本集
 *                是包含多个2*N的矩阵样本数据,每个矩阵第0行为观测值,第1行为状态值
 * @param max_state 状态的最大下标,等于N-1
 * @param max_obser 观测的最大下标
 */
 void estimateEmissionProbability(Collection<int[][]> samples, int max_state, int max_obser) {
    float[][] emission_probability = new float[max_state + 1][max_obser + 1];
    
    for (int[][] sample : samples) {
        // 计算每一个序列
        for (int i = 0; i < sample[0].length; i++) {
            int o = sample[0][i];
            int s = sample[1][i];
            ++emission_probability[s][o];
        }
    }
    for (int i = 0; i < emission_probability.length; i++) {
        normalizeAndToLog(emission_probability[i]);
    }
 }

隐马尔科夫模型的预测

预测:给定观测序列,求解最可能的状态序列极其概率。

比如一个一个病人,他最近三天的感受是:正常、体寒、头晕,请预测他这三天最可能的健康状态和相应的概率。

概率计算的前向算法

给定观测序列xx和一个状态序列yy,计算两者的联合概率p(x,y)p(x, y),以及搜索其中的最大概率。

p(y1=s1)=πi,t=1p(y_1 = s_1) = \pi_i, t = 1

p(yt=sjyt1=si)=Ai,j,t2p(y_t = s_j | y_{t-1} = s_i) = A_{i,j}, t \ge 2

p(y)=p(y1,y2,,yT)=p(y1)t=2Tp(ytyt1)p(y) = p(y_1, y_2, \cdots, y_T) = p(y_1) \prod_{t=2}^{T} p(y_t | y_{t-1})

p(xt=ojyt=si)=Bi,jp(x_t = o_j | y_t = s_i) = B_{i,j}

那么给定长为TT的状态序列yy,对应xx的概率就是:

p(xy)=t=1Tp(xtyt)p(x | y) = \prod_{t=1}^{T} p(x_t | y_t)

由以上公式,得到显隐状态序列的联合概率:

p(x,y)=p(y)p(xy)=p(y1)t=2Tp(ytyt1)t=1Tp(xtyt)p(x, y) = p(y) p(x | y) = p(y_1) \prod_{t=2}^{T} p(y_t | y_{t-1}) \prod_{t=1}^{T} p(x_t | y_t)

将其中的每个xt,ytx_t, y_t对应上实际发生的序列si,ojs_i, o_j,就能带入(π,A,B)(\pi, A, B)中相应元素,从而计算出任意序列的概率了。

搜索状态序列的维比特算法

要搜索上式最大概率所对应的状态,可以将每个状态作为有向图中的一个节点:

  • 节点间的距离由转移概率决定
  • 节点本身的花费由发射概率决定

那么所有备选状态构成一幅有向无环图,待求概率最大的状态序列就是求取图中最长路径,此时的搜索算法称为维比特算法(如红色为示意找到的最佳路径):

  • 暴力搜索:枚举每个时刻的NN种备选状态,相邻两个时刻之间的状态由N2N^2组合,则TT个时刻的复杂度是O(TN2)O(TN^2)

维比特算法

可以论证,最优解情况下yt+1y_{t+1}仅依赖于yty_t。定义二维数据σt,i\sigma_{t,i}表示时刻ttsis_i结尾的所有局部路径最大概率,tt11递推到NN,每次递推都是在上一次的N条局部路径中挑选,所以复杂度O(TN)O(TN)。为了追踪最优解,还需要记录每个状态的前驱:定义另外一个二维数组ψ\psi,同σ\sigma下标定义,存储局部最优路径状态yty_t的前驱状态。

计算过程:

  1. 初始化t=1t = 1时初始最优路径的备选由NN个状态组成,它们的前驱为空: σ1,i=πiBi,o1,i=1,2,,N\sigma_{1, i} = \pi_i B_{i, o_1}, i = 1, 2, \cdots, N

ψ1,i=0,i=1,2,,N\psi_{1,i} = 0, i = 1, 2, \cdots, N

  1. 递推:根据转移概率矩阵和发射概率计算花费,找出新的局部最优解,更新两个数组 σt,j=max(σt1,jAj,i)Bi,ot,i=1,2,,N\sigma_{t, j} = \max(\sigma_{t-1, j}A_{j,i})B_{i, o_t}, i = 1, 2, \cdots, N

ψt,j=argmax1jN(σt1,jAj,i),i=1,2,,N\psi_{t, j} = \arg \max_{1 \le j \le N}(\sigma_{t-1, j}A_{j, i}), i = 1, 2, \cdots, N

  1. 终止:找出最终时刻σT,i\sigma_{T, i}数组中的最大概率pp^*,以及相应的结尾状态iTi_T^*

p=max1iNσT,ip^* = \max_{1 \le i \le N} \sigma_{T, i}

iT=argmax1iNσT,ii_T^* = \arg \max_{1 \le i \le N} \sigma_{T, i}

  1. 回溯:根据前驱数组ψ\psi回溯前驱状态,取得最优路径下标i=i1,,iTi^* = i_1^*, \cdots, i_T^*,其中

it=ψt+1,it+1,t=T1,T2,,1i_t^* = \psi_{t+1, i_{t+1}^*}, t = T - 1, T - 2, \cdots, 1

path路径矩阵,保存每个状态的前驱状态,如上面的维比特算法示意,对应前驱路径矩阵如下:

tt \ sss1s_1s2s_2\cdotssNs_N
11s3s_3s2\color{blue}{s_2}\cdotss1s_1
22sNs_Ns1s_1\cdotss2\color{blue}{s_2}
\vdots\vdots\vdots\color{blue}{\vdots}\vdots
T1T-1s2s_2s1\color{blue}{s_1}\cdotss8s_8

每个状态的最大分数表(仅仅示例)

scorescore \ sss1s_1s2s_2\cdotssNs_N
T1T-10.10.10.3\color{red}{0.3}\cdots0.010.01

代码

/*
 * 预测(维比特算法)
 *
 * @param observeration 观测序列
 * @param state 预测状态序列
 * @return 概率的对象,可利用Math.exp(maxScore)还原log函数
 */
float predict(int[] observeration, int[] state) {
    // 序列长度
    final int T = observation.length;
    // 状态种类
    final int max_s = start_probability.length;
    float[] score = new float[max_s];
    // 如上面的二维表格
    // path[t][s]: 第t个状态在为s时,存储前一个状态
    int[][] path = new int[T][max_s];
    // 第一个时刻,使用初始概率乘以发射概率,遍历状态
    for (int cur_s = 0; cur_s < max_s; ++cur_s) {
        // 由于初始概率与发射概率都已经log化过,此处的加法,就是乘法
        score[cur_s] = start_probability[cur_s] + emission_probability[cur_s][observation[0]];
    }
    // 第二个时刻,需考虑上一时刻的概、转移概率以及发射概率
    float[] pre = new float[max_s];
    // 遍历观察序列
    for (int t = 1; t < T; t++) {
        // swap(now, pre)
        float[] temp = pre;
        // 保存前一状态的分数
        pre = score;
        score = temp;
        // 遍历状态
        for (int s = 0; s < max_s; s++) {
            score[s] = Integer.MIN_VALUE;
            // 计算转移概率
            for (int f = 0; f < max_s; f++) {
                // log化后的加法相当于乘法
                // 计算路径长度:前驱状态f转移到当前状态s
                float p = pre[f] + transition_probability[f][s] + emission_probability[s][observation[t]];
                // 取路径最大的,记录其前驱状态f
                if (p > socre[s]) {
                    score[s] = p;
                    path[t][s] = f;
                }
            }
        }
    }
    float max_score = Integer.MIN_VALUE;
    int best_s = 0;
    // 计算最终分数最高的最终状态
    for (int s = 0; s < max_s; s++) {
        if (score[s] > max_score) {
            max_score = score[s];
            best_s = s;
        }
    }
    // 回溯前向路径矩阵
    for (int t = path.length - 1; t >= 0; --t) {
        // 由最终最优状态向前回溯状态
        state[t] = best_s;
        // 取得最大score的pre状态,进行回溯
        best_s = path[t][best_s];
    }
    return max_score;
}

隐马尔科夫模型应用于中文分词

如果将观测换成字符,状态换成{B, M, E, S},我们就能应用隐马尔科夫模型驱动中文分词。

然而隐马尔科夫模型用于中文分词的效果并不理想。 事实上,隐马尔科夫模型假设人们说的话仅仅取决于一个隐藏的{B, M, E, S}序列,这个假设太单纯,不符合语言规律。语言不是由那么简单的标签序列生成,语言含有更多特征,而隐马尔科夫模型没有捕捉到。

隐马尔科夫模型能捕捉到仅限两种特征:

  • 前一个标签是什么
  • 当前字符是什么 朴素的隐马尔科夫模型并不适合分词,需要更高级的模型。

特征提取

劳动者 的 合法权益 又 如何 保障 ?

protected List<String[]> convertToSequence(Sentence sentence) {
    List<String[]> charList = new LinkedList<String[]>();
    // 遍历每个单词
    for (Word w : sentence.toSimpleWordList()) {
        String word = CharTable.convert(w.value);
        if (word.length() == 1) {
            // 词:标签 第0行是词,第1行是标签
            charList.add(new String[]{word, "S"});
        } else {
            charList.add(new String[]{word.substring(0, 1), "B"});
            for (int i = 1; i < word.length() - 1; ++i) {
                charList.add(new String[]{word.substring(i, i + 1), "M"});
            }
            charList.add(new String[]{word.substring(word.length() - 1), "E"});
        }
    }
    return charList;
}

训练

  • 特性转换
// sentenceList为特征提取里面提到的
List<int[][]> sampleList = new ArrayList<int[][]>(sequenceList.size());
for (List<String[]> sequence : sequenceList)
{
    int[][] sample = new int[2][sequence.size()];
    int i = 0;
    for (String[] os : sequence)
    {
        // 显性序列 字符
        sample[0][i] = vocabulary.idOf(os[0]);
        assert sample[0][i] != -1;
        // 隐形状态 标签{B, M, E, S}
        sample[1][i] = tagSet.add(os[1]);
        ++i;
    }
    sampleList.add(sample);
}

model.train(sampleList);

训练及预测同上面的HMM训练过程