想象一家后厨的食材备料线:英文文本就像超市里已经封好膜的香肠,每根之间天然有一层“空格”分节膜,机器沿着膜一切,就是一个个完整的词。而中文文本是后厨案板上那根从火锅店端来的整根长香肠——汉字一个紧挨一个,中间没有预制的分割线。负责切配的师傅必须凭一本“菜谱”(词典)判断该在哪里下刀,才能把“研究生命起源”切成“研究/生命/起源”,而不是“研究生/命/起源”。这一刀切错,检索系统这道“菜”就会上错桌;切对了,倒排索引这口灶才能把用户点的菜稳稳端出来。本文就用这把切刀的故事,讲清中文检索里最特别的一环:分词与它的预处理策略。
理解中文分词的特殊挑战与工具策略,是搭建中文搜索引擎的必备功底——你掌握的不只是调 API,而是决定召回率上限的那把“切刀功夫”。
可视化演示
展示中文分词流程、规则vs统计方法对比、粒度选择策略及召回率影响分析,含数据流背景和发光粒子动画,交互切换各环节。
本节配有交互动效(进入视口后自动加载,约 3KB)。
分词本质挑战
超市冷柜里的英文香肠,每一根都有透明分节膜隔开——单词之间天然的“空格”就是那层膜,机器沿膜一划就是一个词。可中餐后厨案板上的“汉字香肠”完全是另一回事:汉字一个紧挨一个,没有分节膜也没有缝隙。切配师傅(分词器)必须依靠菜谱(词典)判断该在哪里下刀。
搜索引擎里,倒排索引的词项(term)来自分词:文档先被切成词,再以词为单位构建“菜名索引”。用户查“生命起源”,如果文档被切成“研究生/命/起源”,索引里根本没有“生命”这个词项——顾客点的菜,后厨压根没备。分词(Word Segmentation)正是中文检索区别于英文检索的第一道特殊工序。
这道工序最棘手的是“歧义”。交集型歧义:词与词在中间交叠,比如“研究/生命”和“研究生/命”都成立;组合型歧义:一个字符串既能整体成词,又能拆成更小的词,如“马上”既表示“立刻”也表示“在马背上”。切错歧义位置,词项集合就偏了,倒排索引自然漏召回。
1. 汉字序列:句子 $S$ 在程序眼里是一串连续的汉字 $c_1 c_2 \cdots c_n$,像案板上还没下刀的长香肠。
2. 词序列重组:分词目标是把 $n$ 个字符切成 $m$ 段,得到词序列 $w_1 w_2 \cdots w_m$,字符顺序不改变。
3. 词典约束:每一段 $w_i$ 都必须是词典 $D$ 中的“合法食材”,否则顾客(检索系统)认不出这道菜。
切配师傅的刀工决定后厨备了什么菜,也就决定顾客点什么能上菜——分词决定倒排索引里“有哪些词项”,进而决定检索召回率的上限。
正向最大匹配
老规矩,师傅从香肠最左端下手:先拿出最长的一把标尺(最大窗口 $L_{max}$),从当前位置量出 $L_{max}$ 个汉字,翻菜谱对照——整段对得上就下刀;对不上就缩短一个刻度再试。这个“从长到短、一本菜谱查到底”的贪心切法,就是正向最大匹配(Forward Maximum Matching,FMM)。
以“研究生命起源”为例,词典里有“研究/研究生/生命/起源/命”。师傅先量 4 个字“研究生命”,菜谱里没有;缩到 3 个字“研究生”——有!下刀。接着从“命”开始,量“命起源”“命起”都没有,最后“命”单字成词;剩余“起源”正好整个命中。
FMM 计算简单、速度极快,复杂度约为 $O(n \cdot L_{max})$,在线上高并发场景非常吃香。但贪心长词优先也会闯祸:“研究/生命”被切成“研究生/命”,正是 FMM 的经典翻车现场——它只看最长,不看全局语义。
1. 当前位置 $i$:指针指向尚未切分的最左边那个字符,相当于师傅下刀的位置。
2. 候选子串 $S[i:i+l]$:从 $i$ 起向后取 $l$ 个字符的切片,是每次“试切”的那一段。
3. 词典约束 $w \in D$:这段切片必须是菜谱上有的合法词,否则顾客不认。
4. 最长优先 $\arg\max l$:所有命中切片里选长度最大的先下刀,且长度不超过窗口上限 $L_{max}$。
贪心师傅只认“最长”,不管整道菜的搭配。怎么办?主厨想了个主意:换一头再切一遍看看。
双向最大匹配
主厨让师傅从香肠尾巴开始向头部切,这就是逆向最大匹配(Backward/Reverse Maximum Matching,BMM):同样的菜谱、同样的最长优先,只是窗口从右往左滑。“研究生命起源”从尾部量起:“起源”命中,接着“生命”命中,最后“研究”命中,得到“研究/生命/起源”。
主厨把两份切片摆在一起择优:先比“总片数”(词数),少的胜出;若一样多,再比“单字散片”(单字词)数量,散片少的胜。这就是双向最大匹配(Bidirectional Maximum Matching)。本例两份都是 3 个词,但正向有 1 个单字“命”,逆向一个单字都没有,于是选逆向。
注意,这是一套工程启发式规则,不是理论最优解。当两版切分完全相同,或词数、单字词数都打平时,双向也没辙——常见的兜底是默认取逆向,因为经验上逆向略优。它在词典分词时代是性价比极高的“补刀”,但面对真正的上下文歧义仍力不从心。
1. 候选集合 $T_F, T_B$:前者是正向最大匹配的产物,后者是逆向最大匹配的产物,两份都端上桌。
2. 词数优先 $|T|$:先比总片数,词数更少的切法更可能是师傅想要的大块菜。
3. 单字惩罚 $\sigma(T)$:词数打平时,数一数单字散片,散片越少说明切得越完整。
4. 平局兜底:两份完全一样或指标打平,默认取逆向 $T_B$,这是大量语料验证出的经验值。
左右各切一遍再比选,就像后厨的“对刀”仪式——一个人切容易手歪,两个人从两头切再对比,出错的概率小多了。这也是最大匹配家族最朴素的智慧。
主流分词工具
既然刀法有了流派,后厨也攒下两套趁手的刀具。第一套叫 jieba,是“快刀”:它先用词典 Trie 前缀树把所有可能的词铺成一张“切分路径网”,再用动态规划找一条概率最大的路径;词典里没有的“新菜”,靠内置的 HMM(隐马尔可夫模型)按字标注去猜。
第二套叫 HanLP,是“智能数控刀”:它用感知机、条件随机场甚至神经网络做序列标注,把分词当作“给每个汉字打标签”——词首 B、词中 M、词尾 E、单字词 S——的任务。它不死记菜谱,而是从海量例句里学“什么组合像一道菜”,对新词和方言俚语更抗打,还支持用领域语料训练定制模型。
两套刀法的取舍很清晰:jieba 开箱即用、速度飞快,适合线上低延迟流程;HanLP 精度更高、可训练可微调,代价是更重的资源与更慢的速度。工程上常见做法是“粗排用 jieba、离线建库用 HanLP”,或按业务词典在中间层兜底。
1. 路径集合 $\mathcal{T}(S)$:句子所有合法切分方案组成的空间,每一条都是一份完整的切法。
2. 词概率 $P(w)$:词在语料里出现的频率占比,菜谱里越常见的菜越“便宜”。
3. 负对数求和:把概率连乘变成代价连加,问题从“最大”转为“最小”,方便递推。
4. 动态规划求最优:Viterbi 算法自左向右递推,得到全局代价最小的路径 $T^*$。
快刀图快,数控刀图准,聪明的后厨从不只买一把刀。选哪套工具,取决于生意是求速度还是求质量。
未登录词处理
再全的菜谱也有空白:分子料理、网红新菜、社区自创的黑暗料理——这些“菜谱外的菜”,就是未登录词(Out-of-Vocabulary,OOV)。在文本里,它们长这样:人名地名机构名(“张伟”“中关村”)、网络新词(“绝绝子”“栓Q”)、专业术语以及中英混写。
未登录词的可怕之处在于会“连坐”:一个没见过的词,往往把前后几个正常词一并搅碎。比如“我爱爱奇艺”,若“爱奇艺”不在词典里,可能被切成“我爱/爱/奇/艺”。统计研究显示,未登录词对中文分词错误的贡献常常超过一半。对检索来说,这意味着大量含专名的文档在倒排索引里“隐身”了。
后厨的应对有三板斧:一是“新词发现”,从大规模语料里用互信息加邻接熵挖候选词,人工或规则筛选后进菜谱;二是靠工具内置统计模型按字标签兜底(jieba 的 HMM、HanLP 的感知机都能猜新词);三是业务自定义词典优先匹配(如 jieba 的 load_userdict)。检索系统还会对 query 做字符 n-gram 兜底切分,保证怪词也能命中。
1. 共现概率 $P(xy)$:字符 $x$、$y$ 紧挨着一起出现的概率,比如“绝”与“绝”相邻。
2. 独立假设 $P(x)P(y)$:若 $x$、$y$ 互不相干,共现概率应当约等于各自概率的乘积。
3. 对数放大幅度:取 $\log_2$ 后,互信息越大说明两者越“粘”,越可能是一个未登录候选词。
菜谱会过时,但厨房不会倒闭——定期从流水线语料里发现新菜、登记进菜谱,才能保证顾客点的网红菜永远有货。
粒度与召回
同样一根白萝卜,炖汤要滚刀大块,清炒却要细丝——切多大,取决于这道菜怎么用。分词粒度同理:“清华大学”整体是一道完整的菜,语义明确、精确率高;拆成“清华/大学”像两碟小菜,任何单一需求都能覆盖,但也会混入“大的”“学子”这类噪音。
粒度直接影响检索召回率。在 $Recall = |R \cap A| / |R|$ 中,$R$ 是人工正确分词得到的词集合,$A$ 是分词器实际输出。索引只用粗粒度(只收“清华大学”),用户查“清华”时就因词项对不上而漏召回;用细粒度(“清华”“大学”都在索引里),查询匹配机会大增,但噪音词项也会稀释精确率。
这正是检索里经典的一对矛盾:粗粒度保精确、细粒度保召回。工程上不会一根筋选一种粒度,而是先看业务:商品检索倾向细粒度(用户爱搜短词),文献检索倾向粗粒度(专有名词多);更进阶的解法,是把选择权交给“混合粒度”。
1. 标准答案 $R$:人工正确分词得到的关键词集合,相当于老师阅卷的标答。
2. 系统输出 $A$:分词器实际切出的词集合,相当于学生的答卷。
3. 交集与比值:$R \cap A$ 是切对的词,除以 $|R|$ 得到“该切的词切出了多少”。
大块和细丝各有各的菜式,聪明的后厨不会逼师傅只用一把刀——关键是“什么菜配什么切法”,以及,能不能两样都备着。
混合粒度索引
中央厨房最财大气粗的做法是:大块和细丝都备齐,分门别类上货架。混合粒度索引就是这么干的——对同一份文档,分别用粗粒度(语义完整词)和细粒度(更细的词)各建一套倒排索引;查询时两路同时检索,最后把得分加权融合。
融合打分是核心:$score(q,d) = \alpha \cdot s_{粗}(q,d) + (1-\alpha) \cdot s_{细}(q,d)$。$\alpha$ 是权重,调参时先让两类得分归一化再混合;粗索引保精确(专名、长词命中就算数),细索引保召回(用户短词、口语词也能找到货)。两路都命中的文档获得“双倍证据”,排名天然靠前。
工程落地还有几个细节:细索引噪音词项多,可配合词性过滤或位置加权;新词先走 OOV 挖掘再进词典,避免粗索引长期缺词;$\alpha$ 要用离线评测(召回率、NDCG 等指标)来定。混合粒度不是存储翻倍那么简单,而是用可控成本换回精确率与召回率的双赢。
1. 双路得分:$s_{coarse}$ 来自粗粒度倒排索引的匹配分,$s_{fine}$ 来自细粒度倒排索引的匹配分。
2. 权重系数 $\alpha$:取值在 0 到 1 之间,控制“大块菜”和“细丝菜”谁更有话语权。
3. 加权求和:两路分数合并成最终排序分,兼顾精确与召回。
大块、细丝一起备,无论顾客报出“铁棍山药”还是“山药”,后厨都能三分钟出货——这正是混合粒度索引对中文检索最实在的承诺:让两把刀互相兜底。
AI 应用场景
电商搜索的 Query 分词与召回修复
阶段:查询分析(Query 预处理)
解决的问题:用户输入“荣耀手环6nfc版”,通用词典没有“荣耀手环”"6nfc",分词器把它切成一堆散字,倒排索引对不上,召回率暴跌。传统做法靠人工维护词表,永远赶不上新品上架速度。
具体案例:某电商团队发现“apple watch 表带”被切成“apple/watch/表/带”,通过新增商品自定义词典、把"apple watch"“表带”设为粗粒度词项,并叠加混合粒度索引,48 小时内搜索点击率提升 12%。
舆情监控的新词发现流水线
阶段:离线索引构建与词表更新
解决的问题:热点事件里的网络新词(“栓Q”“绝绝子”)不在词典中,一旦切错,舆情系统里相关帖文就无法被检索召回,错过舆情黄金窗口。
具体案例:某舆情系统每天抓取百万级帖文,用互信息 + 邻接熵离线挖掘候选新词,按频次排序并人工抽检后灌入自定义词典,热点事件相关帖的召回率从 61% 提升到 89%。
智能客服的意图识别前置分词
阶段:语义理解预处理
解决的问题:用户说“宽带到期想换个便宜的套餐”,切分不准导致后续语义向量检索错位,FAQ 匹配答非所问。
具体案例:某运营商客服系统用 HanLP 训练电信领域序列标注模型,配合领域词典,意图识别准确率提升 5.3 个百分点,转人工率明显下降。
Python 演示
# -*- coding: utf-8 -*-"""正向/逆向/双向最大匹配的分词演示""" DICT = {"研究", "研究生", "生命", "起源", "命", "结婚", "和", "的", "和尚", "尚未", "未"} def fmm(text, max_len=4): """正向最大匹配: 从左到右, 优先切最长的词典词""" words = [] i = 0 while i < len(text): for l in range(min(max_len, len(text) - i), 0, -1): if text[i:i + l] in DICT: words.append(text[i:i + l]) i += l break else: words.append(text[i]) i += 1 return words def bmm(text, max_len=4): """逆向最大匹配: 从右到左, 优先切最长的词典词""" words = [] i = len(text) while i > 0: for l in range(min(max_len, i), 0, -1): if text[i - l:i] in DICT: words.insert(0, text[i - l:i]) i -= l break else: words.insert(0, text[i - 1]) i -= 1 return words def bidirectional(text, max_len=4): """双向最大匹配: 词数更少者胜, 平局比单字词更少者, 再平局取逆向""" f_words = fmm(text, max_len) b_words = bmm(text, max_len) if len(f_words) != len(b_words): return f_words if len(f_words) < len(b_words) else b_words f_single = sum(1 for w in f_words if len(w) == 1) b_single = sum(1 for w in b_words if len(w) == 1) if b_single != f_single: return b_words if b_single < f_single else f_words return b_words if __name__ == "__main__": samples = ["研究生命起源", "结婚的和尚未结婚的"] for s in samples: print("输入: " + s) print(" 正向: " + "/".join(fmm(s))) print(" 逆向: " + "/".join(bmm(s))) print(" 双向: " + "/".join(bidirectional(s)))
DICT = {...}领域词典,相当于切配师傅的菜谱,所有切分都必须在这里“对得上号”。for l in range(min(max_len, len(text)-i), 0, -1)从最长窗口递减试探,体现“最长优先”的贪心原则。if text[i:i+l] in DICT切片命中词典即下刀,这正是倒排索引词项的来源。else: words.append(text[i])while-else 兜底:窗口缩到 1 还找不到词时,单字成词。words.insert(0, ...)逆向切词从右往左,插到列表头部以保持原文顺序。f_single = sum(1 for w in f_words if len(w) == 1)统计单字散片,作为打平时的第二消歧判据。
常见误区
误区我以为切得越碎越好,细词能覆盖更多查询词,检索结果应该更多更全,为什么工程上还有人用粗粒度?
正解细粒度确实提高词项覆盖,但会把语义单元切碎,产生大量噪音词项——“大学”切细后“大”和“学”分别进索引,查“大的”也能命中,精确率暴跌。粗粒度语义完整、精确率高,却容易漏掉短词查询。真实工程用“混合粒度”两条腿走路,而不是一味求细。
为何常见:直觉上“覆盖多 = 效果好”,忽略了检索是精确率与召回率的权衡,也看不到噪音词项对排序的破坏。
误区我以为只要正向逆向各切一遍、选词数少的那份,中文分词歧义就消干净了,再也不用担心切错。
正解双向最大匹配只是启发式规则,只在两版切分不同且指标可分优劣时有效。若两版完全相同、或词数与单字词数都打平,它依然无解。它连“马上”这种组合型歧义都分不清(需要上下文),真正稳健的方案是 HanLP 这类统计模型。
为何常见:教科书总用“研究生命起源”这种双向能赢的例句教学,让人误以为它是万能药。
误区我直接拿默认 jieba 切专业文献,结果“深度学习”被切成“深/度/学习”,我以为分词工具坏了。
正解jieba 的默认词典来自通用语料,领域专名、术语缺失导致 OOV。正确姿势是用 load_userdict 加载领域词表,按需开关 HMM;对精度要求更高的场景,换 HanLP 训练领域模型效果更好。
为何常见:工具上手快、默认效果在通用文本上不错,让人忽略了“内置通用词典”这个前提。
误区我觉得网络新词一年没几个,专名也不多,没必要为 OOV 专门设计处理流程。
正解专名加网络新词加术语在实际语料里占比很高,研究统计 OOV 对分词错误的贡献常超过 60%;而且一个未登录词会“连坐”周围正常词,连锁破坏整句切分,让大量文档在倒排索引里隐身。搜索引擎必须搭配新词发现、自定义词典和 n-gram 兜底。
为何常见:演示例句都是规范词,让人低估了真实语料的长尾噪音。
面试题
Q1. 与英文相比,中文检索在建立倒排索引前必须多做什么处理?
- A.同义词扩充词表
- B.去除停用词标点
- C.进行自动分词← 正确答案
- D.转换为小写字母
答案:C。英文单词之间有空格,天然可分;中文没有词边界,必须先用分词器把连续汉字切成词才能形成词项,否则文档与查询的词项无法对齐,倒排索引形同虚设。A、B、D 是所有语言检索都需要的通用处理,不是中文特有的前置步骤。
Q2. 词典中同时有“研究生”和“研究”时,正向最大匹配在句首会优先切出哪个?
- A.两种词任凭随机
- B.优先切出研究
- C.优先切出研究生← 正确答案
- D.只切出单字研
答案:C。正向最大匹配的核心是“最长优先”的贪心策略:窗口从最大长度递减试探,先命中的最长词被切出。因此在“研究”和“研究生”同时命中时,长度更长的“研究生”胜出。这正是 FMM 在“研究生命起源”里切出“研究生”的原因。
Q3. 下列哪一项通常不属于未登录词(OOV)?
- A.网络流行新词
- B.人名地名机构
- C.词典中的常用词← 正确答案
- D.中英混写术语
答案:C。未登录词指词典中不存在的词,包括网络新词、人名地名机构名、专业术语和中英混写词。已经收录在词典中的常用词自然不属于 OOV。混淆点在于把“检索不到”等同于“未登录”,其实常用词只是可能因索引结构问题漏召回。
Q4. 对“研究生命起源”做双向最大匹配,词典含“研究/研究生/生命/起源/命”,最终优选哪个切分?
- A.研究生/生命/起源
- B.研究生/命/起源
- C.研究/生命/起源← 正确答案
- D.研究/命/起源
答案:C。正向得到“研究生/命/起源”(3 词,含 1 个单字);逆向得到“研究/生命/起源”(3 词,0 个单字)。双向消歧先比词数——打平;再比单字词数——逆向更少,因此选 C。A、D 不是任何一端的真实输出,B 是正向结果被淘汰。
Q5. jieba 分词遇到词典里没有的新词,主要靠什么机制兜底?
- A.HMM 模型兜底← 正确答案
- B.正则表达式方法
- C.网络远程词典
- D.人工维护词表
答案:A。jieba 对未登录词使用 HMM(隐马尔可夫模型)按字序列标注来猜测新词边界,比如把“爱奇艺”识别为一个整体。它不是靠网络词典或正则。人工词表是用户可选的增强手段,不是 jieba 的内置兜底机制。
Q6. 召回率公式 Recall=|R∩A|/|R| 中,R 表示什么?
- A.系统切出的词
- B.检索到的文档
- C.人工正确词集← 正确答案
- D.用户查询词语
答案:C。R 是人工标注的正确分词词集,即“标准答案”;A 是系统实际切出的词集;两者交集才是切对的词。召回率衡量“该切的词切出了多少”,分母用标准答案而非系统输出,否则自己切错越多分母越小,指标失真。
Q7. 索引只建粗粒度词项(如“清华大学”整体),查询“清华”时严重漏召回的根因是什么?
- A.词典装载失败
- B.索引词项不匹配← 正确答案
- C.打分函数退化
- D.布尔运算出错
答案:B。粗粒度索引里“清华”根本不是一个词项,倒排表中没有它的倒排列表,查询再精确也是巧妇难为无米之炊。A、C、D 都是运行层面问题,而这里是索引构建策略决定的“词项缺失”,属于分词粒度选择问题。
Q8. 下列哪些属于中文分词的典型歧义类型?
- A.交集型切分歧义← 正确答案
- B.组合型切分歧义← 正确答案
- C.递归型切分歧义
- D.迭代型切分歧义
答案:AB。交集型歧义指两个词在中间共享字符,如“研究/生命”与“研究生/命”;组合型歧义指同一字符串既可整体成词也可拆开,如“马上”。C、D 不是中文分词领域的标准术语,属于编造的干扰项。
Q9. 下列哪些做法能有效提升电商领域的分词质量?
- A.配置领域自定义词典← 正确答案
- B.挖掘新品未登录词← 正确答案
- C.完全去掉词典匹配
- D.用领域语料训练模型← 正确答案
答案:ABD。自定义词典把“荣耀手环”这种商品词强行切出,新词发现把"6nfc"这类尾辍词及时补进词表,领域语料训练模型(如 HanLP 定制)让分词器理解行业语境。C 去掉词典匹配等于放弃确定性召回,只会让分词更碎、更不可控。
Q10. 混合粒度索引的典型做法包括哪些?
- A.一份文档多种粒度建库← 正确答案
- B.查询时并行多路检索← 正确答案
- C.只用最长词匹配单库
- D.加权融合各路得分← 正确答案
答案:ABD。混合粒度索引对同一文档分别用粗、细粒度分词建多套倒排索引,查询时多路并行检索后加权融合打分,达到“粗索引保精确、细索引保召回”的效果。C 只有单库,与“混合”矛盾,也无法解决短词漏召回问题。