4、基于统计的方法:隐马尔可夫模型(HMM)原理、HMM在NER中的应用、最大熵马尔可夫模型(MEMM)

好,咱们进入第四章。这一章聊的是统计方法里的老牌劲旅——隐马尔可夫模型,以及它的升级版MEMM。

说实话,在深度学习火起来之前,HMM就是序列标注任务的标配。我2015年刚入行那会儿,做的第一个NER系统就是基于HMM的。那时候GPU还没普及,跑个CRF都觉得奢侈。你想想看,用HMM做命名实体识别,其实就是在玩一个概率游戏——我们根据观测到的词语,去猜它背后隐藏的标签序列。

4.1 隐马尔可夫模型(HMM)原理

HMM的核心思想,说白了就是两套东西:一套是你能看到的(观测序列),一套是你看不到的(状态序列)。在NER里,观测序列就是句子里的词,状态序列就是每个词对应的实体标签。

HMM做了两个很强的假设,这也是它的局限所在:

  • 齐次马尔可夫假设:当前状态只依赖于前一个状态。嗯,就是“我只看上一步,不管更早的历史”。
  • 观测独立性假设:当前观测只依赖于当前状态。也就是说,你看到这个词是什么,只取决于它当前的标签,跟上下文没关系。

HMM由三组参数决定:

参数 符号 含义 在NER中的例子
初始概率 π 第一个状态的概率 句子开头是B-PER的概率
转移概率 A 状态i到状态j的概率 B-PER后面跟I-PER的概率
发射概率 B 状态i产生观测o的概率 标签B-PER下出现“张三”的概率

HMM要解决三个经典问题:

  • 评估问题:给定模型和观测序列,计算这个序列出现的概率。用前向算法。
  • 解码问题:给定模型和观测序列,找出最可能的状态序列。用维特比算法——这个最常用。
  • 学习问题:给定观测序列,估计模型参数。用鲍姆-韦尔奇算法(EM的一种)。

维特比算法核心思路:动态规划。每一步都记录到达当前状态的最优路径,最后回溯得到全局最优。时间复杂度O(T * N²),T是序列长度,N是状态数。

4.2 HMM在NER中的应用

用HMM做NER,流程其实挺直观的。我习惯把它分成三步走:

第一步:定义标签体系

最常用的是BIO标注。B表示实体开始,I表示实体内部,O表示非实体。比如“张三在北京大学读书”,标注出来就是:

张  B-PER
三  I-PER
在  O
北  B-ORG
京  I-ORG
大  I-ORG
学  I-ORG
读  O
书  O

第二步:训练参数

用标注好的语料,统计出三组概率:

  • 初始概率π:统计每个标签出现在句子开头的频率
  • 转移概率A:统计标签i后面跟标签j的频率
  • 发射概率B:统计标签i下出现词w的频率

举个例子,如果语料里有1000个B-PER标签,其中800个后面跟着I-PER,那转移概率A(B-PER → I-PER)就是0.8。

第三步:解码预测

给定一个新句子,用维特比算法找出最优标签序列。代码实现大概长这样:

def viterbi(obs, states, start_prob, trans_prob, emit_prob):
    # obs: 观测序列(词列表)
    # states: 所有可能的标签
    V = [{}]  # 存储概率
    path = {}  # 存储路径
    
    # 初始化第一步
    for s in states:
        V[0][s] = start_prob[s] * emit_prob[s][obs[0]]
        path[s] = [s]
    
    # 递推
    for t in range(1, len(obs)):
        V.append({})
        new_path = {}
        for s in states:
            # 找概率最大的前一个状态
            (prob, state) = max(
                (V[t-1][s0] * trans_prob[s0][s] * emit_prob[s][obs[t]], s0)
                for s0 in states
            )
            V[t][s] = prob
            new_path[s] = path[state] + [s]
        path = new_path
    
    # 回溯
    (prob, state) = max((V[len(obs)-1][s], s) for s in states)
    return prob, path[state]

避坑指南:我曾经在项目里直接用频率做概率估计,结果遇到未登录词直接概率为0,整个序列就废了。后来加了拉普拉斯平滑,才解决了这个问题。建议你在计算发射概率时,给每个词加一个极小值δ,避免零概率。

4.3 最大熵马尔可夫模型(MEMM)

HMM有个硬伤——观测独立性假设。说白了,它假设当前词只跟当前标签有关,跟上下文没关系。但现实中,“苹果”这个词,在“吃苹果”和“苹果公司”里,标签完全不一样。HMM处理不了这种依赖。

MEMM就是来解决这个问题的。它把HMM的生成式模型改成了判别式模型。什么意思呢?HMM是“给定标签,看词长什么样”,MEMM是“给定词和上下文,猜标签是什么”。

MEMM的核心变化:

  • 用最大熵模型来建模条件概率P(当前标签 | 前一个标签, 当前词, 上下文特征)
  • 不再假设观测独立,可以自由引入各种特征
  • 训练时用IIS(改进的迭代缩放)或梯度下降

MEMM的特征工程非常灵活。我做过一个项目,给MEMM加了这些特征:

特征模板示例:
1. 当前词本身:word = "苹果"
2. 前一个词:prev_word = "吃"
3. 后一个词:next_word = "公司"
4. 当前词是否大写:is_capitalized = True
5. 当前词词性:pos_tag = "NN"
6. 前一个标签:prev_label = "O"

MEMM的维特比算法和HMM类似,只是概率计算方式变了:

def memm_viterbi(obs, features, weights, states):
    # 用最大熵模型计算条件概率
    # P(s_t | s_{t-1}, obs, features)
    V = [{}]
    path = {}
    
    for s in states:
        # 计算P(s | START, obs[0])
        prob = maxent_prob(s, "START", obs[0], features, weights)
        V[0][s] = prob
        path[s] = [s]
    
    for t in range(1, len(obs)):
        V.append({})
        new_path = {}
        for s in states:
            (prob, prev_s) = max(
                (V[t-1][s0] * maxent_prob(s, s0, obs[t], features, weights), s0)
                for s0 in states
            )
            V[t][s] = prob
            new_path[s] = path[prev_s] + [s]
        path = new_path
    
    (prob, state) = max((V[len(obs)-1][s], s) for s in states)
    return prob, path[state]

注意:MEMM有个著名的“标签偏置问题”(Label Bias Problem)。因为它是局部归一化的,每一步的概率只考虑当前状态,导致模型倾向于选择那些转移路径少的状态。说白了,就是“短视”。这个问题后来被条件随机场(CRF)用全局归一化解决了,咱们下一章会讲。

4.4 HMM vs MEMM:怎么选?

我个人的经验是:

  • 数据量小、特征简单:用HMM。它参数少,训练快,不容易过拟合。我早期做小规模NER时,HMM跑得飞快。
  • 需要引入复杂特征:用MEMM。比如你要用词性、字形、词典匹配等特征,MEMM可以轻松加进去。
  • 对精度要求高:两个都不太够。HMM假设太强,MEMM有标签偏置。这时候建议直接上CRF。

我记得有一次做医疗领域的NER,实体类型特别多(症状、药物、检查、手术...),HMM的发射概率矩阵稀疏得不行。换成MEMM后,加了几个简单的上下文特征,F1值直接从82%跳到了89%。嗯,特征工程的力量。

好了,这一章就到这里。HMM和MEMM虽然老了点,但理解它们对后面学CRF、甚至BERT+CRF都很有帮助。下一章咱们聊条件随机场——那个真正能打全局战的模型。