4、基于统计的NER:隐马尔可夫模型(HMM)原理、HMM在NER中的应用、维特比解码算法

说到统计NER,HMM(隐马尔可夫模型)是绕不开的基石。我当年刚入行时,觉得深度学习才是王道,HMM这种“老古董”没必要深究。直到有一次做医疗病历的实体识别,标注数据少得可怜,深度学习模型根本训不动。后来硬着头皮上了HMM,效果居然还不错。从那以后,我就明白了——理解HMM,是理解序列标注问题的第一块敲门砖。

4.1 隐马尔可夫模型的核心思想

HMM说白了,就是用来描述一个“隐藏的”状态序列,如何生成我们看到的观测序列。在NER场景里,隐藏状态就是每个词的标签(比如B-PER、I-PER、O),观测序列就是句子里的词本身。

你想想看,我们看到的是一句话:“张三去北京了”。但背后真正想找的,是每个词对应的实体标签。HMM就是帮我们从“看得见的词”反推“看不见的标签”。

HMM的两个核心假设:

  • 齐次马尔可夫假设:当前时刻的状态只依赖于前一时刻的状态,与更早的状态无关。说白了就是“只看前一步”。
  • 观测独立性假设:当前时刻的观测只依赖于当前时刻的状态,与其他状态和观测无关。说白了就是“只看自己”。

这两个假设虽然简化了问题,但实际应用中确实有效。我在项目中遇到过不少质疑:“这么简单的假设能行吗?”嗯,对于大多数NER任务,尤其是标签之间依赖关系较强的场景,HMM的表现其实相当稳健。

4.2 HMM的三要素

要玩转HMM,必须搞清楚它的三个核心参数。我习惯把它们记成“π、A、B”,就像三块积木。

参数 符号 含义 NER中的例子
初始状态概率 π 序列第一个状态的概率分布 句子第一个词是B-PER的概率
状态转移概率 A 从一个状态转移到另一个状态的概率 B-PER后面跟I-PER的概率
发射概率 B 在某个状态下生成某个观测的概率 在B-PER状态下看到“张”字的概率

我的小技巧: 训练HMM时,π、A、B都是从标注数据里统计出来的。说白了就是数数——数每个标签出现的次数、标签转移的次数、标签和词共现的次数。简单粗暴,但有效。

4.3 HMM在NER中的完整流程

把HMM用到NER上,其实就三步。我当年第一次跑通这个流程时,感觉就像打通了任督二脉。

  1. 训练阶段: 从标注语料中统计出π、A、B三个矩阵。
  2. 解码阶段: 给定一个句子,用维特比算法找出最可能的标签序列。
  3. 输出阶段: 把标签序列还原成实体边界和类型。

这里有个坑,我曾经踩过——零概率问题。如果某个词在训练集中从未出现过,发射概率就是0,整个序列的概率就变成0。解决办法很简单:加平滑。我习惯用加1平滑(拉普拉斯平滑),简单又好用。

4.4 维特比解码算法

维特比算法是HMM解码的核心。它的本质是动态规划,说白了就是“记住每一步的最优路径,最后回溯出全局最优”。

为什么会需要动态规划?你想想看,如果暴力枚举所有可能的标签序列,复杂度是O(N×S^T),其中S是标签数,T是序列长度。对于中文NER,标签数至少十几个,句子长度几十个字,这个计算量根本扛不住。维特比算法把复杂度降到了O(N×S^2),这才是工程上可行的方案。

算法的核心思想就两句话:

  • 递推: 每一步只保留到达每个状态的最优路径。
  • 回溯: 从最后一步的最优状态,倒推出完整路径。

避坑指南: 我曾经在实现维特比时,忘记了对数化概率。直接乘概率会导致浮点数下溢,尤其是长句子。正确的做法是取对数,把乘法变成加法。这样既避免了数值问题,又保持了单调性。

4.5 代码实现:从零搭建HMM-NER

下面是我手写的一个简化版HMM-NER实现。代码不长,但核心逻辑都在里面了。

import numpy as np

class HMM_NER:
    def __init__(self, tag2id, word2id):
        self.tag2id = tag2id
        self.word2id = word2id
        self.N = len(tag2id)  # 标签数
        self.M = len(word2id) # 词表大小
        
    def train(self, sentences, tags):
        """统计π, A, B"""
        # 初始化计数
        pi = np.zeros(self.N)
        A = np.zeros((self.N, self.N))
        B = np.zeros((self.N, self.M))
        
        for sent, tag_seq in zip(sentences, tags):
            # 统计初始状态
            pi[tag_seq[0]] += 1
            # 统计转移
            for t in range(1, len(tag_seq)):
                A[tag_seq[t-1], tag_seq[t]] += 1
            # 统计发射
            for t in range(len(sent)):
                B[tag_seq[t], sent[t]] += 1
        
        # 加1平滑并转概率
        pi = (pi + 1) / (len(sentences) + self.N)
        A = (A + 1) / (A.sum(axis=1, keepdims=True) + self.N)
        B = (B + 1) / (B.sum(axis=1, keepdims=True) + self.M)
        
        # 取对数,防止下溢
        self.log_pi = np.log(pi)
        self.log_A = np.log(A)
        self.log_B = np.log(B)
    
    def viterbi(self, sentence):
        """维特比解码"""
        T = len(sentence)
        # dp[t][i]:到达第t步、状态为i的最大对数概率
        dp = np.zeros((T, self.N))
        # back[t][i]:记录前一步的状态
        back = np.zeros((T, self.N), dtype=int)
        
        # 初始化
        dp[0] = self.log_pi + self.log_B[:, sentence[0]]
        
        # 递推
        for t in range(1, T):
            for j in range(self.N):
                # 从所有前驱状态中找到最优的
                scores = dp[t-1] + self.log_A[:, j] + self.log_B[j, sentence[t]]
                dp[t, j] = np.max(scores)
                back[t, j] = np.argmax(scores)
        
        # 回溯
        best_path = [np.argmax(dp[T-1])]
        for t in range(T-1, 0, -1):
            best_path.insert(0, back[t, best_path[0]])
        
        return best_path

关于代码的几点说明:

  • 训练时用了加1平滑,避免零概率。实际项目中可以换成更复杂的平滑策略,比如Good-Turing。
  • 所有概率都取了对数,这样长句子的计算也不会溢出。
  • 维特比部分用了numpy的向量化操作,比纯Python循环快很多。

4.6 HMM的优缺点

做了这么多年NER,我对HMM的评价是:简单、高效、但天花板低。

优点 缺点
训练速度快,小数据也能训 观测独立性假设太强,忽略了上下文词的信息
解码效率高,适合在线服务 只能建模相邻标签的依赖,长距离依赖无能为力
可解释性强,参数含义清晰 特征工程能力弱,无法利用词性、词典等外部特征
实现简单,调试方便 对未登录词敏感,OOV问题严重

我个人习惯把HMM作为基线模型。每次接到新的NER任务,先用HMM跑一遍,看看数据质量怎么样、标签分布是否合理。如果HMM效果就很好了,说明任务简单;如果HMM效果很差,那就要考虑更复杂的模型了。

4.7 本章知识体系总览

下面这张图把HMM在NER中的完整流程串起来了。从训练到解码,每一步都对应着具体的数学操作。

HMM-NER 知识体系流程图 HMM在NER中的完整流程 训练阶段 统计π、A、B 加1平滑 → 取对数 解码阶段 维特比初始化 递推:dp[t][j] 回溯:最优路径 输出标签 核心公式: dp[t][j] = max(dp[t-1][i] + log_A[i][j] + log_B[j][obs[t]]) 其中 i 遍历所有标签,j 是当前标签,obs[t] 是当前词

嗯,到这里HMM在NER中的应用就讲完了。说实话,HMM虽然简单,但它的思想——用概率图建模序列依赖、用动态规划高效解码——是后续所有序列标注模型的基础。理解了HMM,再去看CRF、BiLSTM-CRF,你会发现很多概念都是相通的。