1. 从"字"到"词"的鸿沟:中文分词到底难在哪
如果你做过搜索、推荐、知识图谱或者文本分类,大概率绕不开一个前置环节:把一串连续的中文汉字切成有意义的词。这件事在英文里几乎不算问题,空格天然就是词的边界,tokenizer.split(' ')一行代码搞定。但中文不一样,句子是"字字相连"的,词与词之间没有任何显式分隔符。于是"南京市长江大桥"这七个字,到底该切成"南京市/长江大桥",还是"南京/市长/江大桥",就成了一个既考验语言学功底、又考验工程能力的问题。
这就是**中文分词(Chinese Word Segmentation,简称CWS)**要解决的核心任务。它的输入是一段连续汉字序列,输出是带边界标记的词序列。别看任务描述简单,它几乎是所有中文自然语言处理任务的第一道关卡。分词切错了,后面的词性标注、命名实体识别、句法分析、语义匹配全都会跟着错,误差会像滚雪球一样往下游传导。
我见过太多团队在项目初期对分词不上心,随手调个默认分词器就往下走,结果到了模型效果上不去的时候,回头排查才发现是分词把关键实体切碎了。比如"深度学习框架"被切成"深度/学习/框架",语义上勉强能接受;但"北京大学生前来应聘"如果切成"北京/大学生/前来/应聘",和"北京大学/生前/来/应聘",意思完全是两回事。这种歧义切分就是中文分词最经典的难题之一。
除了歧义,还有两个老大难:未登录词(OOV)和分词粒度。未登录词指的是词典里没有的新词,比如新出现的网络热词、人名、品牌名、专业术语,模型没见过就容易切错。分词粒度则是说,同一个句子在不同场景下需要不同的切分标准——搜索引擎索引时希望粒度细一点,召回更全;而文本分类时希望粒度粗一点,保留完整语义单元。这两个需求本身就存在张力,没有一种切法能通吃所有场景。
所以这篇内容,我想系统地把中文分词这件事讲透。从最传统的基于词典的方法,到基于统计的序列标注方法,再到当下主流的深度学习方案,以及大模型时代分词角色的变化,我会把每一类方法的核心原理、适用场景、实操细节和踩坑经验都摊开来讲。不管你是刚接触NLP的新手,还是想给现有系统换一套分词方案的工程师,都能从中找到可以直接参考的东西。
2. 基于词典的分词:最朴素也最容易被低估的方案
2.1 最大匹配法的三种变体与它们的致命伤
基于词典的分词,思路非常直白:我手里有一本词典,拿句子去词典里比对,能匹配上就切出来。最经典的是最大匹配法(Maximum Matching,MM),又分正向、逆向和双向三种。
正向最大匹配(FMM)的做法是:从左到右扫描,每次取尽可能长的、能在词典里找到的词。举个例子,词典里有"南京""南京市""市长""长江""长江大桥""大桥",句子是"南京市长江大桥"。FMM从"南"开始,尝试最长匹配,发现"南京市"在词典里,于是切出"南京市";接着从"长"开始,匹配到"长江大桥",切出。结果就是"南京市/长江大桥",正确。
但换个句子就翻车了。"他说的确实在理",FMM可能切成"他/说/的确/实在/理",而正确切分应该是"他/说/的/确实/在理"。问题出在"的确"这个词优先被匹配了,导致后面的边界全乱。逆向最大匹配(BMM)从右往左扫,对上面这个例子反而能切对,但遇到"研究生命起源"这种句子,逆向又可能切成"研究生/命/起源",而正确是"研究/生命/起源"。
于是有了双向最大匹配:两种都跑一遍,然后按规则选一个更优的结果。常用规则是:谁切出来的词数少选谁(因为长词优先通常更符合语义),词数相同时选单字词少的那个。这套规则在大多数情况下能兜住,但它本质上还是"启发式",没有真正理解语义。
我实测下来的感受是:最大匹配法在领域词典维护得好的前提下,对短句、专有名词密集的场景(比如地址解析、商品标题切分)效果其实不差,而且速度极快,纯字符串匹配,单机每秒能处理几十万甚至上百万字。但它的天花板也很明显——完全依赖词典,词典里没有的词一律切不对,歧义处理全靠规则硬扛。
2.2 词典构建才是真正的工程量所在
很多人以为基于词典的分词"简单",其实真正的工作量在词典上。一个可用的通用词典动辄几十万词条,还要处理词频、词性、领域标签。更麻烦的是新词发现——网络热词、新品牌、新术语每天都在冒出来,词典不更新,分词就会持续出错。
我一般会这样搭一套词典维护流程:先用通用词典打底,然后从业务语料里跑新词发现算法。最常用的是基于左右邻接熵和内部凝固度的无监督方法。内部凝固度衡量的是"这几个字绑在一起的概率有多高",左右邻接熵衡量的是"这个词的左右两边能接多少种不同的字"。两个指标都高,说明它很可能是一个独立的词。
具体计算上,假设候选词是"深度学习",内部凝固度可以近似用点互信息(PMI)来算:
import math from collections import Counter def compute_pmi(word, char_freq, total_chars, word_freq, total_words): # 计算词内部各字之间的最小PMI,作为凝固度 pmi_scores = [] for i in range(len(word) - 1): left = word[i] right = word[i + 1] p_left = char_freq[left] / total_chars p_right = char_freq[right] / total_chars p_pair = word_freq[word] / total_words if p_left > 0 and p_right > 0 and p_pair > 0: pmi = math.log(p_pair / (p_left * p_right)) pmi_scores.append(pmi) return min(pmi_scores) if pmi_scores else 0左右邻接熵则是统计候选词左边和右边出现的字的分布,熵越大说明它越像一个独立边界:
def compute_entropy(neighbor_counter): total = sum(neighbor_counter.values()) entropy = 0.0 for count in neighbor_counter.values(): p = count / total entropy -= p * math.log(p) return entropy把这两个指标设个阈值,就能从海量语料里筛出一批候选新词,再人工过一遍,补充进词典。这套流程我在几个项目里都用过,召回新词的效果比纯人工维护高一个数量级。注意:阈值不要设太死,宁可多召回一些让人工筛,也别漏掉真正的新词,因为漏掉的代价是下游持续出错。
2.3 什么时候该用词典法,什么时候该放弃
我的经验判断标准很简单:如果你的业务词典相对封闭、更新频率低、对速度要求极高,词典法完全够用。比如做快递地址解析,全国省市区街道的词典是有限的、可枚举的,用词典法加规则,准确率能到95%以上,还省算力。
但如果你面对的是开放域文本——社交媒体、新闻、用户评论,新词层出不穷,歧义满天飞,那词典法就会力不从心。这时候必须上统计或深度学习方案。下面几节我会详细展开。
3. 统计学习时代:把分词变成序列标注问题
3.1 从"切词"到"打标签"的思维转换
统计方法最大的贡献,是把分词问题重新定义成了序列标注问题。不再纠结"这个词该不该切出来",而是给每个字打一个标签,标签决定了它和前后字的关系。最常用的是BMES标注体系:
- B(Begin):词的首字
- M(Middle):词的中间字
- E(End):词的末字
- S(Single):单字成词
比如"我爱北京"标注成"S/S/B/E",解码出来就是"我/爱/北京"。这个转换看似简单,但它把分词变成了一个标准的机器学习任务,可以套用各种成熟的序列标注模型。
为什么这个转换这么重要?因为它让分词从规则驱动变成了数据驱动。只要有标注语料,模型就能自己学出切分规律,不再依赖人工写规则。而且标注体系天然处理了未登录词——哪怕"北京"不在词典里,只要模型见过类似的"BE"模式,也能正确切出来。
3.2 HMM与CRF:两代统计模型的取舍
最早被广泛使用的是隐马尔可夫模型(HMM)。它假设每个字的标签只依赖于前一个标签(状态转移),每个字本身由标签生成(发射概率)。训练时统计转移概率和发射概率,预测时用维特比算法找最优标签序列。
HMM的优点是训练快、解码快、实现简单,在标注语料不大的时候表现稳定。但它有个硬伤:观测独立性假设——它认为每个字只和当前标签有关,和上下文其他字无关。这在中文里明显不成立,"的"后面大概率跟名词,"了"前面大概率是动词,这些长距离依赖HMM抓不住。
于是**条件随机场(CRF)**登场了。CRF直接对标签序列的联合概率建模,可以灵活地引入各种特征:当前字、前一个字、后一个字、是否在词典里、字的偏旁部首等等。它不做独立性假设,能充分利用上下文。在相当长一段时间里,CRF是中文分词的主流方案,在标准评测集上F1能到95%以上。
但CRF的代价是特征工程重、训练慢。你得手工设计一堆特征模板,特征选得好不好直接决定效果。而且CRF训练时要跑迭代优化,语料大了训练时间会很长。我早期做项目时,用CRF训一个分词模型,几百万字的语料要跑好几个小时,调参更是折磨。
下面是一个用CRF做分词的简化特征模板示例,帮助理解它到底在"看"什么:
def extract_features(sentence, i): char = sentence[i] features = { 'char': char, 'char_prev': sentence[i-1] if i > 0 else '<BOS>', 'char_next': sentence[i+1] if i < len(sentence)-1 else '<EOS>', 'char_prev2': sentence[i-2] if i > 1 else '<BOS>', 'char_next2': sentence[i+2] if i < len(sentence)-2 else '<EOS>', 'is_digit': char.isdigit(), 'is_punct': not char.isalnum(), } return features可以看到,CRF的特征是人工枚举的,你能想到的上下文组合都可以加进去。这也是它的双刃剑:灵活,但费人力。
3.3 标注语料从哪来:一个绕不开的现实问题
统计方法再优雅,没有标注语料都是空谈。而中文分词的标注语料,恰恰是稀缺资源。公开的标注数据集规模有限,领域覆盖也未必匹配你的业务。自己标?成本高、周期长、一致性还难保证。
我的实操建议是分层获取语料:第一层用公开标注数据打底,让模型先有个基础能力;第二层用远程监督,拿现有词典去自动标注一批业务语料,虽然噪声大,但量大管饱;第三层做主动学习,让模型挑出它最不确定的样本,人工只标这些,标注效率能提升好几倍。
主动学习的核心是不确定性采样。对每个句子,模型预测出标签序列的概率分布,如果某个位置的标签概率接近0.5,说明模型很纠结,这种样本信息量最大。把这些样本挑出来人工标注,比随机标注的收益高得多。我在一个垂直领域项目里用这套方法,只标了不到两万句,就把分词F1从89%拉到了94%。
4. 深度学习方案:让模型自己学会"什么是词"
4.1 BiLSTM-CRF:曾经的标准答案
深度学习进入NLP后,分词方案很快被刷新。最经典的架构是BiLSTM-CRF:用双向LSTM提取每个字的上下文表示,再接一个CRF层做标签解码。
为什么是双向LSTM?因为中文里一个字的标签,既取决于它前面的字,也取决于它后面的字。"研究生命"里,"生"这个字到底属于"研究生"还是"生命",光看前面看不出来,必须结合后面的"命"才能判断。双向LSTM正好能同时捕捉前后文信息。
为什么还要接CRF?因为LSTM输出的每个位置的标签是独立预测的,可能出现"B"后面直接跟"B"这种非法序列。CRF层引入了标签之间的转移约束,保证输出的标签序列是合法的。这个组合在当时几乎是分词任务的标配,在多个评测集上刷新了记录。
用PyTorch搭一个简化版的BiLSTM-CRF,核心结构大概是这样:
import torch import torch.nn as nn class BiLSTMCRF(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_tags): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_dim, bidirectional=True, batch_first=True) self.fc = nn.Linear(hidden_dim * 2, num_tags) self.crf = CRFLayer(num_tags) def forward(self, x, tags=None, mask=None): emb = self.embedding(x) lstm_out, _ = self.lstm(emb) emissions = self.fc(lstm_out) if tags is not None: return -self.crf.log_likelihood(emissions, tags, mask) return self.crf.decode(emissions, mask)这套架构的优点是端到端、不需要手工特征,只要给够标注数据,效果通常比CRF好。但它的训练成本也更高,而且LSTM的序列建模能力有限,长距离依赖还是抓得不够好。
4.2 预训练模型微调:把分词当成下游任务
BERT这类预训练模型出来之后,分词的做法又变了。不再从零训练,而是拿预训练好的语言模型做底座,在分词标注数据上微调。因为预训练模型已经在大规模语料上学到了丰富的语言知识,微调时只需要少量标注数据就能达到很好的效果。
具体做法很简单:把分词当成逐字分类任务,BERT输出每个字的向量表示,接一个分类头预测BMES标签。损失函数用交叉熵,优化器用AdamW,学习率设小一点(比如2e-5到5e-5),训练几个epoch就能收敛。
我实测下来,用预训练模型微调,在同样标注数据量下,F1通常比BiLSTM-CRF高1到2个百分点,而且对未登录词的泛化能力明显更强。因为预训练模型见过海量文本,对"这个词像不像一个词"有更强的直觉。
但这里有个容易踩的坑:预训练模型的分词粒度可能和你的业务需求不一致。比如BERT自带的tokenizer对中文是按字切的,你微调出来的分词器会继承它的某些偏好。如果你的业务需要特定粒度,一定要在标注数据里体现出来,让模型学到你的标准。
4.3 大模型时代:分词还需要单独做吗
现在很多人会问:大模型都能直接理解中文了,还需要单独做分词吗?我的答案是:取决于你的场景。
如果你只是调用大模型API做问答、生成,那确实不用关心分词,模型内部有自己的处理方式。但如果你要做搜索召回、倒排索引、关键词抽取、文本匹配这些任务,分词依然是绕不开的基础设施。倒排索引必须基于词来建,你不可能拿整句话去建索引。而且在这些场景里,分词的质量直接决定了召回率和准确率。
另一个变化是,大模型可以辅助分词标注。你可以让大模型对一批语料做初步切分,人工审核修正后再用来训练小模型。这样标注效率能提升不少。但要注意,大模型的切分标准未必稳定,同一句话问两次可能给出不同结果,所以它更适合做"预标注",最终还得人工把关。
5. 评测、调优与落地:那些文档里不会写的细节
5.1 评测指标怎么选才不误导自己
分词评测最常用的指标是精确率(P)、召回率(R)和F1值,而且通常按"词"来算,不是按"字"。也就是说,一个词的边界和标签完全正确才算对,错一个字就算错。
这里有个容易被忽略的细节:评测时要区分**登录词(IV)和未登录词(OOV)**的指标。很多模型在登录词上F1能到97%以上,但OOV上可能只有80%出头。如果你只看总体F1,会误以为模型很好,实际上它在真正难的地方表现很差。我建议评测报告里一定要把IV和OOV分开列,这样才知道模型的短板在哪。
另外,不同数据集的标注标准不一样,跨数据集比较F1意义不大。比如有的数据集把"中华人民共和国"当一个词,有的切成"中华/人民/共和国"。你在A数据集上训的模型,直接拿到B数据集评测,F1掉几个点是正常的,不一定是模型变差了。
5.2 粒度对齐:业务需求和标注标准的一致性
前面提过分词粒度问题,这里展开讲。同一个句子"北京大学",粗粒度切成一个词,细粒度切成"北京/大学"。你的业务到底要哪种?这个必须在标注阶段就定死,否则模型学出来的东西是混乱的。
我的做法是:先和业务方对齐分词规范文档,明确哪些情况切、哪些不切。比如"人名+职务"是切开还是合并,"数字+单位"怎么处理,"英文+中文"混合怎么切。把这些规则写清楚,标注员按规则标,模型才能学到一致的标准。
如果业务场景需要多种粒度,可以考虑训练多个模型,或者用多粒度分词方案——在标注时同时标出粗粒度和细粒度边界,模型一次输出两套结果。这个方案实现复杂一些,但能省去维护多个模型的麻烦。
5.3 后处理规则:小改动带来大提升
模型输出之后,加一层后处理规则,往往能带来意想不到的提升。最常见的几类规则:
- 数字、日期、时间的合并:模型可能把"2024年3月"切成"2024/年/3/月",但业务上希望它是一个整体。用正则匹配合并即可。
- 英文单词和URL的保护:模型对英文和符号的处理可能不稳定,用正则先把它们识别出来,避免被切碎。
- 领域词典强制匹配:对业务关键实体(产品名、专有名词),用词典做强制匹配,保证不被切错。
这些规则写起来不复杂,但在实际业务里能显著提升体验。我在一个电商项目里,光是加了商品品牌名的强制匹配规则,搜索准确率就提升了3个百分点。注意:规则不要写太多太杂,否则维护成本会失控,优先加那些高频、影响大的。
5.4 部署与性能:分词服务的工程考量
分词通常是在线服务,对延迟敏感。一个请求进来,要在几十毫秒内返回结果。这时候模型大小和推理速度就很关键。
如果用的是BERT类模型,直接部署可能延迟偏高。可以考虑几个优化方向:模型蒸馏,用大模型教一个小模型,速度能快好几倍,效果损失有限;量化,把FP32转成INT8,速度提升明显;缓存,对高频查询的分词结果做缓存,避免重复计算。
还有一个容易被忽视的点:分词服务要能热更新词典。业务词典经常变,如果每次更新都要重启服务,体验很差。我的做法是把词典加载到内存里,提供一个更新接口,词典变了就重新加载,服务不中断。
6. 不同场景下的分词选型:没有最好,只有最合适
6.1 搜索场景:召回优先,粒度偏细
搜索引擎的分词,核心目标是召回。用户搜"苹果手机壳",你希望既能召回"苹果手机壳"这个整体,也能召回包含"苹果""手机""壳"的文档。所以搜索场景通常用细粒度分词,甚至做多粒度索引——同一篇文档用多种粒度建索引,查询时匹配任意一种都能召回。
另外,搜索场景要特别处理同义词和近义词。"手机"和"移动电话"要能互相召回,这需要在分词之后接一层同义词扩展。还有停用词,像"的""了""是"这些词在索引里意义不大,可以过滤掉,减小索引体积。
6.2 文本分类与情感分析:语义完整优先
分类任务关心的是整体语义,分词粒度太细反而会破坏语义单元。比如"不太满意"如果切成"不/太/满意",情感分析可能误判为正面;切成"不太/满意"或者整体保留,才能正确识别负面情感。
所以分类场景我一般建议用粗粒度分词,或者干脆用字级别输入配合模型自己学表示。现在很多文本分类模型直接用字向量,效果不比词向量差,还省去了分词的麻烦和误差传导。
6.3 知识图谱与实体抽取:边界准确是生命线
做知识图谱,实体边界的准确性至关重要。"北京大学生"到底是一个实体(某类学生群体)还是"北京大学"+"生",直接影响图谱的节点和关系。这种场景下,分词要和**命名实体识别(NER)**联合做,不能分开。
我通常的做法是分词和NER用同一个序列标注框架,标签体系里同时包含分词标签和实体标签,模型一次输出。这样能保证实体边界和分词边界一致,避免"分词切对了但实体边界错了"的尴尬。
6.4 方案对比速查表
为了让你快速做选型决策,我把几类方案的关键维度整理成表:
| 方案类型 | 准确率 | 速度 | 未登录词 | 维护成本 | 适用场景 |
|---|---|---|---|---|---|
| 词典最大匹配 | 中 | 极快 | 差 | 中(词典维护) | 封闭领域、地址、专名 |
| HMM | 中 | 快 | 中 | 低 | 资源受限、快速上线 |
| CRF | 高 | 中 | 中 | 高(特征工程) | 有标注数据、追求效果 |
| BiLSTM-CRF | 高 | 中 | 好 | 中 | 通用场景、端到端 |
| 预训练微调 | 最高 | 慢 | 最好 | 低(微调即可) | 效果优先、算力充足 |
| 大模型预标注+小模型 | 高 | 快 | 好 | 中 | 标注成本敏感 |
这张表不是绝对的,实际选型还要看你的数据量、算力预算、延迟要求。我的建议是先用简单方案快速上线,再根据badcase逐步升级,不要一上来就上最复杂的模型。
7. 我踩过的那些坑:几条用教训换来的经验
做分词这些年,踩过的坑不少,挑几个最有代表性的说说。
第一个坑:盲目追求SOTA模型。早期我总想用最新最复杂的模型,结果发现业务数据量根本撑不起来,模型过拟合严重,还不如简单的CRF。后来我明白了,模型复杂度要和数据量匹配。数据少的时候,简单模型加好特征反而更稳。
第二个坑:忽略标注一致性。有一次项目里多个标注员标同一批数据,结果发现他们对"的"字是否单独成词的标准不一致,导致模型学得混乱。后来我们做了标注一致性检查,定期抽样复核,Kappa系数低于0.9就重新培训标注员。这个环节看似麻烦,但能省下大量返工时间。
第三个坑:上线后不监控。分词模型上线不是终点。业务语料在变,新词在冒,模型效果会慢慢衰减。我现在的做法是定期抽样人工评估,同时监控OOV比例和用户反馈,一旦发现指标下滑就触发模型更新。这个机制能保证分词服务长期稳定。
第四个坑:把分词当成孤立环节。分词不是孤立的,它和下游任务强相关。我见过团队分词F1很高,但下游NER效果差,原因是分词粒度和NER的实体边界不匹配。后来我们把分词和NER联合优化,整体效果才上来。做分词一定要有全局视角,盯着下游指标,而不是只盯分词自己的F1。
8. 写在最后:分词这件事的变与不变
从词典匹配到深度学习,再到预训练模型和大模型辅助,中文分词的技术方案一直在演进。但有些东西没变:对语言的理解、对业务场景的把握、对数据质量的重视,这些才是决定分词效果的根本。
工具和模型会过时,但"把问题定义清楚、把数据搞干净、把评测做扎实"这套方法论不会过时。我在实际项目里越来越觉得,分词做得好不好,技术方案只占三成,剩下七成是对业务的理解和对细节的打磨。
如果你正在做分词相关的项目,我的建议是:先用最简单的方案跑通全流程,拿到baseline,然后针对badcase逐个击破。不要一上来就追求完美,也不要迷信某个"最强"模型。适合你业务场景的,才是最好的。后续如果业务扩展了,再考虑升级方案,技术选型永远是为业务服务的。