4. 中文分词简介:基于字典的最大匹配算法

中文分词这事儿,说白了就是把一堆汉字切成有意义的词。跟英文不一样,英文天然用空格隔开了,中文词之间没边界。你想想看,让机器看懂句子,第一步就是得知道哪些字应该抱团。

我刚开始做中文NLP的时候,天真地以为直接按字符处理就行。结果做出来的搜索系统,用户搜“北京大学”出来一堆包含“北京”和“大学”的页面,根本没法用。从那以后,我老老实实把分词当成第一道关卡来搞。

核心思想:准备一个大词典,然后拿句子从左到右(或从右到左)去匹配词典里最长的词。匹配不上就切单字。

4.1 正向最大匹配算法(FMM)

这是最直白的做法。我习惯叫它“贪心向右”。

算法步骤是这样:

  1. 设定最大词长(比如4个字)。
  2. 从句子开头取前4个字,去查词典。
  3. 如果词典里有这个词,就切出来,指针往后移动4个字。
  4. 如果词典里没有,就缩短一个字,查前3个字……直到剩下1个字,那必定是单字词。
  5. 重复直到句子结束。

举个例:句子“我们研究天文学”。假设最大词长是4。

步骤取词窗口匹配结果切分词
1我们研究天没匹配,缩短—
2我们研究没匹配,缩短—
3我们匹配!我们
4研究天文学没匹配,缩短—
5研究天没匹配,缩短—
6研究匹配!研究
7天文学匹配!天文学

结果:我们 / 研究 / 天文学。嗯,正确。

# 伪代码示意
def fmm_cut(sentence, max_len, vocab):
    result = []
    i = 0
    while i < len(sentence):
        for win in range(max_len, 0, -1):
            word = sentence[i:i+win]
            if word in vocab or win == 1:
                result.append(word)
                i += win
                break
    return result

注意:如果词典里没有“我们”,那“我们”就会被切成“我/们”。所以词典质量直接决定结果好坏。我在项目里吃过这个亏——词典太小,切出来一堆单字,下游任务直接崩了。

4.2 逆向最大匹配算法(RMM)

逆向就是反过来,从句子末尾往开头匹配。原理一样,只是方向反了。

还是那句话:“我们研究天文学”。逆向匹配流程:

  • 从末尾取4个字“研究天文学”→ 不匹配 → “天文学” → 匹配!切出“天文学”。
  • 剩余“我们研究”,末尾取4个字→ 不匹配 → “研究” → 匹配!切出“研究”。
  • 剩余“我们” → 匹配!切出“我们”。

结果跟正向一样:我们/研究/天文学。

我的经验:中文里偏正结构多(比如“白天鹅”),正向可能会切成“白天/鹅”,逆向却能正确切成“白天鹅”。所以我做分词服务时,一般同时跑正向和逆向,再选一个结果。

4.3 两种算法的对比

维度正向最大匹配逆向最大匹配
方向从左到右从右到左
偏好偏好左边长词偏好右边长词
典型错误“为人民服务”切为“为人/民/服务”“为人民服务”切为“为人民/服务”
性能基本一样基本一样
实际效果稍弱(中文偏右侧重)略好于正向

为什么逆向往往更好?我自己的理解是:中文的修饰语常放在中心语前面,比如“研究天文学”,核心词“天文学”在右边,逆向更容易抓住它。不过这不,得看具体语料。

4.4 分词歧义处理

歧义是最大匹配的死穴。我把常见情况分成三类:

4.4.1 交集型歧义

“结婚的婚礼”,正向可能切“结婚/的/婚礼”,但“结婚的”可以作为一个词吗?词典若有“结婚的”就麻烦了。或者“大学生命”,正向切“大学生/命”,逆向切“大学/生命”。这种歧义被称为「最尴尬的歧义」。

我曾经踩过的坑:做医疗文本分词时,把“胃粘膜糜烂”切成了“胃/粘膜/糜烂”没问题,但遇到“胃粘膜糜烂”的变体“胃黏膜糜烂”,因为词典里只有“黏膜”没有“粘膜”,直接崩成了“胃/粘/膜/糜烂”。后来我在词典里加了“黏膜”和“粘膜”两种写法,才搞定。

4.4.2 组合型歧义

同一个字串在不同语境下有不同的切分。比如“将来”通常是一个词,但在“他将来到北京”里,需要切为“他/将来/到/北京”。没有上下文语义,纯词典方法根本处理不了。

4.4.3 真歧义

“美国会”, 可以理解为“美/国会”或“美国/会”。两种切法语义都成立,但意思完全不同。这种必须靠上下文或统计模型。

4.4.4 我的常用解决方法

  • 双向匹配+消歧:跑正反两个方向,选词数少的那个(通常更合理)。若词数一样,选逆向结果。
  • 最大匹配+停用词过滤:先把明显不该成词的停用词剐掉。
  • 词典分级:核心词典优先,普通词典其次,单字兜底。
  • 规则补丁:比如“不/会”之类的组合,写几条正则解决。
# 双向消歧简单实现
def bidirection_cut(sentence, max_len, vocab):
    fmm = fmm_cut(sentence, max_len, vocab)
    rmm = rmm_cut(sentence, max_len, vocab)
    if len(fmm) < len(rmm):
        return fmm
    elif len(fmm) > len(rmm):
        return rmm
    else:
        return rmm  # 默认逆向

口诀:正向逆向着实跑,词数少者比较好。若遇平局选逆向,歧义降低没烦恼。

4.5 今日知识图谱

下面这张SVG图把本章的核心流程串起来了——从句子输入,到正反向匹配,再到歧义处理,最后输出分词结果。

基于词典的最大匹配分词流程 输入句子 + 词典 正向最大匹配 (FMM) 逆向最大匹配 (RMM) FMM 分词结果 RMM 分词结果 双向比较 → 选词数少的结果 最终分词结果 💡 如果词数相同,我习惯取逆向结果。还可以结合规则去处理交集和组合歧义。

4.6 需要注意的几个点

  • 词典越大越好? 不一定。词典太大,匹配长度变长,容易过度切词(比如“他是一家公司的职员”,如果词典里有“他是”,会切出“他是/一家/公司/的/职员”但可能不合理)。我用的是词频筛选,只保留高于阈值的词。
  • 最大词长怎么设? 设得太小,长词切不开;设得太大,匹配步长过大,效率低。中文常见词最长6-8字,我一般设6。
  • 未登录词:人名、地名、新词,词典里没有。最大匹配遇到这种情况只能切单字。这时需要结合词性标注或者统计模型(后面章节会讲)。
  • 性能:纯Python实现,每秒约5万字符。用字典树(Trie)可以优化到30万以上。我生产环境里用的就是Trie+正向逆向+规则补丁。

警告:千万别以为最大匹配就够用了。我接手过一个遗留系统,只用正向匹配,结果在“从小学到大学”上切成了“从小/学到/大学”,用户搜“小学”搜不到。后来加了逆向消歧才修好。

4.7 小练习(可选)

试试下面几个句子,手动用正向和逆向(最大词长=4)切一下,看看结果有什么不同:

  • “南京市长江大桥”
  • “嫁给运动爱好者”
  • “乒乓球拍卖完了”