想象一条快递分拣传送带,上面所有包裹都紧紧连成一串,没有贴标签,从外面只能看到一整条汉字长链。想要继续分拣,就必须先判断“哪个包裹到哪个包裹是一个完整物件”,然后插入隔板。中文分词做的正是这件事:在一串没有空格的汉字里,找出一个个词语的边界。 分拣站里有两位老师傅。老张手里有一本厚厚的“客户花名册”,他按花名册从左往右、从右往左找最长的名字来切包裹,这种做法很像 Jieba 使用的词典规则。小李没有完整花名册,但他看过几万条真实的包裹记录,会用“前面是什么字、后面是什么字”的经验来推断边界,这种做法很像 HanLP 的 CRF 序列标注。一个靠查表,一个靠经验;一个快而直白,一个慢但更懂上下文。理解了这两位师傅的差别,也就理解了 Jieba 与 HanLP 中文分词的核心差异。
掌握两种分词的底层差异,才能在做中文 NLP 项目时快速判断该用词典规则还是序列标注模型,避免被“分词不准”拖垮后续任务。
可视化演示
展示双向最大匹配词典分词、CRF序列标注分词及Jieba与HanLP性能对比。切换模式查看不同分词方法的核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
双向最大匹配分词 正向最大匹配从左到右扫描,取词典中最长匹配词 逆向最大匹配从右到左扫描,双向结果不一致时取词数少者 seg = argmin_len(FMM, BMM) 基于词典的规则方法无法处理未登录词
分词任务基础
分拣站的一串快递盒要切成几段,首先取决于你如何定义“一个包裹”。在中文里,词与词之间天然没有空格,“认真学习”到底算一个词还是两个词,往往要看使用场景。分词算法就是给这串汉字装隔板的人,隔板装得好,后续的搜索、翻译、问答才能正常工作。
分词难在三件事:第一是歧义,比如“研究生命科学”可以切出“研究/生命/科学”,也可能被误解成“研究生/命/科学”;第二是未登录词,也就是词典里根本没有的新词、人名、网络词;第三是词粒度,有时需要细粒度词,有时需要粗粒度词。衡量分词质量,不能只看“看起来对不对”,还要用标准答案算精确率和召回率。
回到分拣站,F1 就像“装对的隔板数”和“应该装的隔板数”之间的平衡。隔板装得太多,会把包裹切碎;装得太少,又会让两个包裹黏在一起。Jieba 和 HanLP 的差别,正是用不同方式决定这些隔板装在哪里。
- 精确率 P模型切出的所有词中,和标准答案一致的比例。
- 召回率 R标准答案中的词,被模型成功找回的比例。
- F1 调和平均当 P 和 R 都高时,F1 才高;只追求其中一个会让另一个下降。
最大匹配原理
老张使用的方法是最大匹配。他手里有一本电话簿,里面记着所有见过的客户名。切包裹时,他从最左侧开始,先试着把最长的一截拿去电话簿里对,对不上就缩短一截,直到对上为止。这种方式叫正向最大匹配;反过来从右侧开始扫描,就是逆向最大匹配。两个方向都做一遍,再比较结果,就是双向最大匹配。
双向最大匹配之所以比单向稳,是因为它可以用“谁的隔板数量少”“谁的单字词更少”来消解部分歧义。它不学习,不训练,所有判断都来自词典,因此速度飞快。
老张的做法简单可靠,但电话簿再厚也有漏网之鱼。遇到“新零售”“元宇宙”这类新词,词典法很容易把它们拆成单字。这也是双向最大匹配最大的瓶颈:词典覆盖不到的地方,就是它的盲区。
- 当前位置 i扫描指针停在第几个字上,正向从左往右,逆向从右往左。
- 候选词 w从当前位置截取出一段字符串,去词典 D 里查找。
- 取最长在所有能匹配的候选中,选择长度最大的那个。
- 无匹配兜底如果词典里没有任何候选,就暂时把当前字切成单字词。
Jieba 工程实现
Jieba 可以看成老张的升级版。它不再一个词一个词地死板匹配,而是先建了一本“前缀索引”,再用动态规划寻找整句最大概率路径。前缀索引能快速知道每个汉字后面可能跟着哪些词,比如“自然”后面可能有“语言”,也可能没有。这个词表让 Jieba 能高效地构建一张有向无环图,也叫 DAG。
DAG 中每个节点是一个字,每条边代表从当前位置到某个结束位置能组成一个词。Jieba 的任务是从所有路径中选一条总概率最高的切分路径。概率来自词频统计:常见词的路径得分更高,这样“自然语言”会被优先保留,而不是被切碎成“自然”“语言”或单字。
这套机制比单纯双向最大匹配更聪明,因为它综合考虑了整句的词频。Jieba 还额外用 HMM 来识别词典外的词,给未登录词一个“兜底”通道。对大部分通用场景来说,Jieba 已经足够快、足够准。
- next(i)从位置 i 出发,所有可能词尾位置的集合。
- 词概率 p一个词在语料中出现的频率估计,词越常见,得分越高。
- 取对数相加连乘概率容易下溢,所以转成对数后相加。
- 递归取最大从当前位置出发,选择能带来最大累计得分的那条边。
CRF 序列标注
小李的方法完全不同。他不再死记电话簿,而是看大量已经切好的历史包裹。对他来说,每个汉字不是“能不能组成词”的问题,而是“这个字在词语中的角色”问题。他给每个字贴一个标签:B 表示词首,M 表示词中,E 表示词尾,S 表示单字词。比如“自然语言”会被标成“自 B、然 M、语 M、言 E”。
CRF 的全称是条件随机场,它可以看作一群特征函数的投票。每个特征函数会检查“当前字是不是常用词首字”“上一个标签是不是 B”“当前字是否常见于词语末尾”等线索。模型学到的权重会告诉小李哪些线索更可信,最后找到一条整体得分最高的标签序列。
小李的“经验”不是天生的,而是训练出来的。他用大量标注语料让权重 λ 不断调整,再用维特比算法快速搜索最优标签链。代价是训练时间长、推理也比查词典慢,但换来的是对上下文和未登录词更强的判断力。
- 输入 x 和输出 yx 是句子中的每个字,y 是每个字对应的 B/M/E/S 标签。
- 特征函数 f_k检查某个局部线索是否成立,成立记 1,不成立记 0。
- 权重 λ_k表示这条线索有多重要,来自训练过程。
- 累加与指数化把整条路径上的得分相加,再用指数变成正数。
- 归一化 Z(x)除以所有可能标签序列的分数和,得到概率。
双方法对比
老张和小李的差异,在真实项目中非常明显。词典法像一辆轻便摩托车,启动快、油耗低,但遇到崎岖山路容易翻车;CRF 像一辆经过调校的越野车,准备时间长、成本高,但复杂路况下更稳。
| 维度 | 词典法 / Jieba | CRF / HanLP |
|---|---|---|
| 精度 | 对常见词很好,未登录词容易碎 | 对上下文敏感,未登录词更稳 |
| 速度 | 快,无需训练 | 较慢,需要特征计算 |
| 资源 | 词典小、内存低 | 需要标注语料和训练时间 |
| 词典依赖 | 强依赖词表 | 弱依赖词表 |
| 可解释性 | 高,容易排查 | 较低,特征权重不够直观 |
| 适用场景 | 日志、搜索、实时系统 | 专业文本、长尾新词多的场景 |
具体到项目里,“Jieba 一定比 HanLP 差”是不对的。如果领域词表非常全,文本又固定,Jieba 能以极低成本获得足够好的效果;如果语料开放、新词不断出现,CRF 的上下文建模能力才有用武之地。选型不是攀比算法,而是看谁更匹配当前的路况。
工程选型调优
分拣站的负责人最终要看业务需求。如果系统要求每毫秒处理大量文本,老张的查表法显然更合适;如果面对的是医疗、法律这种专业文本,宁可慢一点,也要用小李的 CRF 减少切错。更常见的做法是两者结合:先用 Jieba 快速初切,再用 CRF 对高风险片段重切。
调优也不是盲目加词典。给 Jieba 添加领域词典,能快速提升专业词召回;给 HanLP 补充领域标注语料,能让 CRF 学到更准的上下文规律。最终效果要用 F1 和延迟共同衡量。可以设计一个最简单的选择公式:在可接受的延迟范围内,选 F1 更高的模型。
老张和小李其实可以成为搭档:让老张负责快速粗切,让小李负责处理难分的长尾片段。中文分词没有银弹,只有对任务的理解、对数据的评估,以及对工具边界的清醒认识。
- 候选模型 m可以是 Jieba,也可以是 HanLP CRF。
- F1(m)模型在验证集上的综合准确率。
- Latency(m)模型处理一段文本的平均耗时。
- 权重 α 和 β由业务场景决定,日志系统 β 大,专业文本 α 大。
AI 应用场景
搜索引擎与大模型预处理
阶段:数据预处理
解决的问题:搜索 query 或大模型输入需要先切词,才能构建词表、做召回或 Prompt 切块。传统方法人工维护词表,成本高且更新慢;Jieba 基于词典和最大概率路径,可以快速完成大规模初切。
具体案例:用户搜索“自然语言处理入门”,Jieba 切出“自然语言 / 处理 / 入门”,搜索系统据此召回相关文档,比按单字匹配召回更准。
专业领域的 OOV 识别
阶段:特征工程
解决的问题:医疗、法律、金融文本充满新词和长术语,词典法经常把术语切碎。HanLP 的 CRF 利用上下文特征给每个字贴标签,能更稳地识别未登录词。
具体案例:病历文本“胃窦黏膜慢性炎”,CRF 可以根据“胃窦”“黏膜”等局部特征,把专业术语整体切出,避免后续实体识别把术语拦腰截断。
实时客服系统分流
阶段:模型推理与优化
解决的问题:实时客服需要低延迟,但口语化文本又有大量新词。单独用 CRF 太慢,单独用词典法又容易漏新词。先用 Jieba 快速分流,再对未登录词密度高的句子调用 CRF 重切,能在速度和精度之间取得平衡。
具体案例:客服机器人收到“咱家冰箱怎么老有异响”,Jieba 快速切出常规词,检测到“异响”可能不在词典后,用 CRF 重新判断并加进当日热词表。
Python 演示
# -*- coding: utf-8 -*-class DualMaxMatcher: def __init__(self, words, max_len=5): self._dict = set(words) self.max_len = max_len def forward_match(self, text): result = [] i = 0 n = len(text) while i < n: match = text[i] for end in range(min(i + self.max_len, n), i, -1): if text[i:end] in self._dict: match = text[i:end] break result.append(match) i += len(match) return result def backward_match(self, text): result = [] i = len(text) while i > 0: match = text[i - 1] for start in range(max(0, i - self.max_len), i): if text[start:i] in self._dict: match = text[start:i] break result.append(match) i -= len(match) return list(reversed(result)) def segment(self, text): fwd = self.forward_match(text) bwd = self.backward_match(text) if len(fwd) == len(bwd): return fwd fwd_single = sum(1 for w in fwd if len(w) == 1) bwd_single = sum(1 for w in bwd if len(w) == 1) if fwd_single < bwd_single: return fwd if bwd_single < fwd_single: return bwd if len(fwd) <= len(bwd): return fwd return bwd def main(): words = ["自然", "语言", "自然语言", "处理", "中文", "分词", "学习", "深度学习", "研究"] matcher = DualMaxMatcher(words, max_len=5) samples = ["中文分词很有意义", "自然语言处理很有趣"] for s in samples: print("句子:", s) print("正向:", " / ".join(matcher.forward_match(s))) print("逆向:", " / ".join(matcher.backward_match(s))) print("双向:", " / ".join(matcher.segment(s))) if __name__ == "__main__": main()
self._dict = set(words)把词表转成集合,判断“这个词在不在词典里”只需要 O(1) 时间。for end in range(min(i + self.max_len, n), i, -1)从最长窗口开始往短尝试,体现“最长优先”的匹配思路。if text[i:end] in self._dict命中词典就立刻切出一个词,不再继续缩短窗口。return list(reversed(result))逆向是从右往左收集的,最后要反转回正常顺序。if fwd_single < bwd_single:双向结果不一致时,优先选择单字词更少的方向,因为单字过多通常意味着切碎了。
常见误区
误区我以为 Jieba 分词器的核心就是双向最大匹配,只要把词典做大,效果就能无限接近 CRF。
正解Jieba 属于词典规则流派,但它内部使用前缀词典构建 DAG,再用动态规划寻找最大概率路径,并不是简单的前后各切一次。双向最大匹配是理解词典法思想的好起点,但不能完全等同于 Jieba 的实现。
为何常见:很多教程为了降低理解难度,会把 Jieba 简化成“词典匹配”,读者因此容易把简化版当成完整原理。
误区CRF 是机器学习方法,不依赖词典,所以任何新词都能识别,比 Jieba 强很多。
正解CRF 确实对未登录词更友好,但它依赖训练语料里的上下文特征。如果某个新词在训练语料中从未以类似模式出现过,CRF 同样很难切对。词典法有词表盲区,CRF 也有经验盲区。
为何常见:人们看到“机器学习建模”就觉得更聪明,忽略了模型同样受训练数据分布限制。
误区我只要把分词 F1 从 90% 提到 95%,下游任务肯定也会提升。
正解分词准确率是重要参考,但不是最终目标。搜索、翻译、问答等下游任务对“错误类型”的敏感度不同,有时候切词方式不符合下游习惯,反而伤害效果。应该用下游任务指标来验证分词方案。
为何常见:分词是第一步,人们容易把第一步的指标当成整个流程的成败指标,陷入局部最优。
误区分词不准就拼命往 Jieba 的自定义词典里加词,觉得词表越大越安全。
正解盲目加词会让词典互相冲突,比如把“自然”和“自然语言”同时加入,某些句子反而会产生错误的最长匹配或概率路径。词典需要按领域、按频率精心维护,而不是堆数量。
为何常见:词典法看起来“词多就不容易被拆”,忽略了词与词之间还有重叠和歧义关系。
面试题
Q1. 中文分词的核心任务是什么?
- A.给句子加标点符号
- B.在词与词之间插入分隔符← 正确答案
- C.把汉字转成拼音
- D.删除句子中的语气词
答案:B。中文分词的目标是给没有空格的中文文本确定词语边界,B 最准确。加标点、转拼音、删语气词都不是分词的核心任务。
Q2. 正向最大匹配从哪里开始匹配?
- A.从句子左侧开始选择← 正确答案
- B.从句子右侧开始选择
- C.从句子中间随机选择
- D.从标点符号两侧开始
答案:A。正向最大匹配从左往右扫描,逆向最大匹配才从右往左,这是两种方法最直观的区别。
Q3. 双向最大匹配通常如何消解歧义?
- A.直接合并两个方向结果
- B.选择切分词语数量更少的结果← 正确答案
- C.随机选取一个方向结果
- D.选择单字数量更多的结果
答案:B。双向最大匹配会把正反向结果比较,优先选词数更少或单字词更少的结果。合并、随机、选单字多都不是消解歧义的正确方向。
Q4. 下列哪些属于 Jieba 分词的组成步骤?
- A.构建前缀词典← 正确答案
- B.生成有向无环图← 正确答案
- C.搜索最大概率路径← 正确答案
- D.训练句法分析器
答案:ABC。Jieba 用前缀词典快速得到每个位置能组成的词,并构建 DAG,然后用动态规划搜索最大概率路径。它不需要训练句法分析器,D 是错误选项。
Q5. CRF 中 B/M/E/S 标签里的 E 通常表示什么?
- A.单字词
- B.词语开头
- C.词语中间
- D.词语结尾← 正确答案
答案:D。B 是 Begin 词首,M 是 Middle 词中,E 是 End 词尾,S 是 Single 单字词,因此 E 表示词语结尾。
Q6. 关于 CRF 与词典匹配,哪些说法正确?
- A.CRF 利用上下文特征← 正确答案
- B.CRF 需要训练语料← 正确答案
- C.词典匹配训练成本为零← 正确答案
- D.词典匹配能彻底解决歧义
答案:ABC。CRF 的核心优势是上下文特征,也需要标注语料训练权重。词典匹配不需要训练,但无法彻底解决歧义,D 说法过于绝对。
Q7. 线性链 CRF 的总分主要由哪两部分构成?
- A.发射分数与转移分数之和← 正确答案
- B.词频与逆文档频率之和
- C.词典长度与文本长度之积
- D.字符向量的余弦相似度
答案:A。CRF 建模时既要看当前字和标签的发射关系,也要看相邻标签的转移关系,总分由这两类特征分数累加得到。
Q8. 关于双向最大匹配,哪些说法正确?
- A.正向可能把词切碎← 正确答案
- B.逆向适合后缀明显的语言← 正确答案
- C.双向能完全消除歧义
- D.双向比单向更稳定← 正确答案
答案:ABD。正向和逆向都可能切碎词,逆向对后缀明显的词更敏感。双向通过比较减少错误,但不能完全消除歧义,因此 C 不选。
Q9. 分词评估中 F1 值高代表什么?
- A.精确率和召回率都较高← 正确答案
- B.运行速度一定更快
- C.内存占用一定更小
- D.不需要词典支持
答案:A。F1 是精确率和召回率的调和平均,数值高说明两者都较高。它不直接代表速度、内存或是否依赖词典。
Q10. 哪项场景最适合选择 HanLP CRF 而不是 Jieba 词典法?
- A.海量日志最低延迟
- B.少量硬件的离线任务
- C.长尾新词多的专业文本← 正确答案
- D.固定领域且词典极全
答案:C。CRF 的上下文建模能力对未登录词和长尾新词更友好。海量日志低延迟更适合 Jieba,离线任务和固定词典场景不一定需要 CRF。