4. 中文分词简介:基于字典的最大匹配算法
中文分词这事儿,说白了就是把一堆汉字切成有意义的词。跟英文不一样,英文天然用空格隔开了,中文词之间没边界。你想想看,让机器看懂句子,第一步就是得知道哪些字应该抱团。
我刚开始做中文NLP的时候,天真地以为直接按字符处理就行。结果做出来的搜索系统,用户搜“北京大学”出来一堆包含“北京”和“大学”的页面,根本没法用。从那以后,我老老实实把分词当成第一道关卡来搞。
核心思想:准备一个大词典,然后拿句子从左到右(或从右到左)去匹配词典里最长的词。匹配不上就切单字。
4.1 正向最大匹配算法(FMM)
这是最直白的做法。我习惯叫它“贪心向右”。
算法步骤是这样:
- 设定最大词长(比如4个字)。
- 从句子开头取前4个字,去查词典。
- 如果词典里有这个词,就切出来,指针往后移动4个字。
- 如果词典里没有,就缩短一个字,查前3个字……直到剩下1个字,那必定是单字词。
- 重复直到句子结束。
举个例:句子“我们研究天文学”。假设最大词长是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图把本章的核心流程串起来了——从句子输入,到正反向匹配,再到歧义处理,最后输出分词结果。
4.6 需要注意的几个点
- 词典越大越好? 不一定。词典太大,匹配长度变长,容易过度切词(比如“他是一家公司的职员”,如果词典里有“他是”,会切出“他是/一家/公司/的/职员”但可能不合理)。我用的是词频筛选,只保留高于阈值的词。
- 最大词长怎么设? 设得太小,长词切不开;设得太大,匹配步长过大,效率低。中文常见词最长6-8字,我一般设6。
- 未登录词:人名、地名、新词,词典里没有。最大匹配遇到这种情况只能切单字。这时需要结合词性标注或者统计模型(后面章节会讲)。
- 性能:纯Python实现,每秒约5万字符。用字典树(Trie)可以优化到30万以上。我生产环境里用的就是Trie+正向逆向+规则补丁。
警告:千万别以为最大匹配就够用了。我接手过一个遗留系统,只用正向匹配,结果在“从小学到大学”上切成了“从小/学到/大学”,用户搜“小学”搜不到。后来加了逆向消歧才修好。
4.7 小练习(可选)
试试下面几个句子,手动用正向和逆向(最大词长=4)切一下,看看结果有什么不同:
- “南京市长江大桥”
- “嫁给运动爱好者”
- “乒乓球拍卖完了”