3. 统计分词模型:HMM与CRF的实战对决

聊完了规则和词典,咱们进入真正的「统计时代」。

说实话,我刚入行那会儿,觉得分词嘛,有词典就够了。直到我遇到一个项目——用户输入的是「南京市长江大桥」,词典怎么切都别扭。后来我才明白,统计模型才是解决歧义的终极武器

3.1 隐马尔可夫模型(HMM)分词

HMM 是什么?说白了,它假设每个字背后藏着一个「状态」。这个状态就是:词的开头、中间、结尾,还是单独成词

我习惯用 B(Begin)、M(Middle)、E(End)、S(Single)来表示。比如「我爱北京天安门」:

我/S  爱/S  北/B  京/E  天/B  安/M  门/E

你看,每个字对应一个状态。HMM 要做的,就是根据观测到的字序列,推断出最可能的状态序列。

3.1.1 HMM 的三要素

HMM 有三个核心参数,我当年背得滚瓜烂熟:

参数 含义 我的理解
初始概率 π 第一个字的状态分布 句子开头,大概率是 B 或 S
转移概率 A 状态之间的转移 B→M→E 是常见路径,B→S 几乎不可能
发射概率 B 状态生成某个字的概率 状态 B 下,「北」的概率高,「的」的概率低

嗯,这里要注意:转移概率矩阵是稀疏的。比如 M 后面只能跟 M 或 E,不能跟 S。这个约束大大简化了计算。

3.1.2 Viterbi 解码

有了模型参数,怎么找到最优状态序列?用 Viterbi 算法。

我给大家写个简化版代码,当年我在项目里就是这么干的:

def viterbi(obs, states, start_p, trans_p, emit_p):
    V = [{}]
    path = {}
    
    # 初始化第一步
    for s in states:
        V[0][s] = start_p[s] * emit_p[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_p[s0][s] * emit_p[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[-1][s], s) for s in states)
    return prob, path[state]
避坑指南:我曾经在线上环境直接用概率相乘,结果下溢了。后来我改用 log 空间计算,把乘法变成加法,稳得很。

3.1.3 HMM 的局限性

说实话,HMM 有两个硬伤:

  • 强独立性假设:当前观测只依赖当前状态,忽略了上下文。你想想看,「我在银行存钱」和「我在银行钓鱼」,同一个「银行」字,HMM 处理起来一样,但实际意思完全不同。
  • 无法利用特征:HMM 只能看字本身,不能看字的拼音、词性、前后搭配等特征。

所以,后来业界慢慢转向了 CRF。

3.2 条件随机场(CRF)分词

CRF 是什么?我个人的理解是:HMM 的升级版,去掉了独立性假设,可以自由定义特征

说白了,CRF 不再假设「当前字只跟当前状态有关」,而是允许你定义任意特征函数。比如:

  • 当前字是「北」,前一个字是「河」—— 这大概率是词尾
  • 当前字是数字,后一个字是「年」—— 这可能是时间词
  • 当前字是大写字母 —— 可能是英文单词开头

3.2.1 特征模板

我在项目中常用的特征模板长这样:

# 一元特征
U00:%x[-1,0]   # 前一个字
U01:%x[0,0]    # 当前字
U02:%x[1,0]    # 后一个字

# 二元特征
U03:%x[-1,0]/%x[0,0]  # 前一个字+当前字
U04:%x[0,0]/%x[1,0]   # 当前字+后一个字

# 组合特征
U05:%x[-1,0]/%x[0,0]/%x[1,0]  # 三字组合

你想想看,HMM 只能看到当前字,而 CRF 能看到前后三个字。这差距有多大?

3.2.2 CRF 的训练与解码

CRF 的训练比 HMM 复杂得多。它需要:

  1. 定义特征模板
  2. 从标注数据中提取特征
  3. 用 L-BFGS 等优化算法学习特征权重
  4. 用 Viterbi 解码(没错,解码还是 Viterbi)

我记得第一次用 CRF++ 工具包时,被它的特征模板语法搞晕了。后来我总结了一个经验:特征不是越多越好,关键是特征要有区分度

核心区别总结

  • HMM 是生成式模型,CRF 是判别式模型
  • HMM 假设观测独立,CRF 没有这个假设
  • HMM 只能利用字本身,CRF 可以自定义任意特征
  • HMM 训练简单,CRF 训练复杂但效果更好

3.2.3 实战对比

我拿「结婚的和尚未结婚的」这个经典歧义句来对比:

模型 分词结果 说明
HMM 结婚/的/和/尚未/结婚/的 容易把「和尚」切出来
CRF 结婚/的/和/尚未/结婚/的 如果特征里包含「尚未」这个搭配,就能正确切分

为什么会这样?因为 HMM 只看局部,而 CRF 可以通过特征捕捉到「尚未」这个模式。

注意:CRF 不是万能的。我曾经在一个医疗文本项目里,CRF 的效果反而不如 HMM。原因是标注数据太少,CRF 的特征权重学偏了。所以,数据量小的时候,HMM 反而更稳

3.3 我的选择建议

说了这么多,到底用哪个?我个人的经验是:

  • 数据量小(<1万句):用 HMM,简单快速,不容易过拟合
  • 数据量中等(1万-10万句):用 CRF,效果好,但要注意特征设计
  • 数据量大(>10万句):可以考虑深度学习模型,但 CRF 仍然是强基线

嗯,最后说一句:别迷信模型。我在实际项目中见过太多人,一上来就上 CRF,结果连词典都没加。先跑通基线,再优化模型,这才是正道。

下一章,咱们聊聊深度学习分词。那又是另一个世界了。