ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中文NLP必备:jieba分词与BERT-wwm全词掩码原理及工程实践

中文NLP必备:jieba分词与BERT-wwm全词掩码原理及工程实践 做中文NLP这几年我经常被问到同一个问题“现在都有BERT这种预训练模型了还有必要学jieba分词吗”问这个问题的多半是刚入门的朋友看了几篇预训练模型的文章觉得分词这种“老古董”技术该淘汰了。但真等他们上手做项目往往又被现实教育一遍情感分析、文本分类、信息抽取……任何中文任务的第一步仍然绕不开“怎么把一句话拆成模型能理解的基本单元”。而我今天要聊的BERT-wwm恰恰又和jieba有着出人意料的深层关联——它预训练阶段构造训练数据时也离不开分词工具来划定词边界。这篇作为中文NLP基础分享系列的第12篇我想把这两代技术放在一起讲明白jieba到底解决了什么问题、它的原理和边界在哪BERT-wwm针对中文做了什么关键改进以及两者在真实工程里应该如何取舍和配合。无论你是刚接触NLP的学生还是已经在业务里碰过壁的工程师这篇都值得花十分钟读完。1. 中文NLP的“第一道坎”为什么分词这件事绕不开1.1 英文天然有空格中文没有——一个朴素的起点先从一个最直观的事实说起。英文句子“I love natural language processing”你按空格切一下就得到[I, love, natural, language, processing]一个相当干净的token序列。中文句子“我喜欢自然语言处理”如果你也按某个固定分隔符去切得到的只能是一整个字符串没有任何天然边界。这个差异看起来小影响却很大。绝大多数NLP模型——无论是统计时代的n-gram还是深度学习时代的RNN、Transformer——都是以“词”token为基本输入单元的。英文可以直接按空格分词中文不行所以中文NLP从诞生第一天起就多了一个前置步骤分词。这也是为什么中文分词被称为中文NLP的第一道坎。有人会说那我不分词按字输入不行吗当然可以很多预训练模型包括BERT在中文上就是按字切分的这方面后面细说。但要注意“按字切分”不等于“不需要词的概念”。词是中文里最小的、能独立运用的语言单位很多语义信息是附着在“词”这个层级上的。比如“自然语言处理”是一个完整概念如果拆成“自”“然”“语”“言”“处”“理”六个字单个字的语义信息量非常稀疏模型需要花费更大的精力去学习“这几个字经常一起出现”这个模式。分词的意义本质上就是把这种字与字之间的组合模式预先用规则和统计暴露给模型降低它的学习难度。1.2 分词粒度直接决定下游任务的天花板分词不只是一个“切了就行”的体力活切分粒度会直接决定下游任务的效果上限。举个典型例子细粒度自然语言处理可以切成自然 / 语言 / 处理粗粒度可以切成自然语言处理一个词哪种更好没有绝对答案取决于下游任务。做搜索引擎索引可能需要细粒度一些让“自然”“语言”“处理”各自都能被匹配到做知识图谱实体识别可能希望“自然语言处理”作为一个整体实体被识别出来。而这种粒度选择直接由jieba等分词工具的词表、算法和自定义词典来把控。我在实际项目里见过很多因此翻车的案例。比如某医疗文本分析项目默认词典把“克罗恩病”切成“克罗恩/病”导致后续实体识别完全乱套某金融舆情项目默认分词把“量化宽松”切成“量化/宽松”情感分析的语义特征直接丢了一半。这类问题根源不在于模型多强而在于分词这个最底层的地基没打好。所以真正理解分词不只是会调一个API而是要理解它背后的词典机制、算法逻辑以及如何在领域数据上把它调整到合适的粒度。2. jieba分词到底在做什么三种模式与两套算法2.1 精确模式、全模式、搜索引擎模式到底选哪个jieba最被人熟知的是它的三种分词模式先用一段代码展示它们的效果import jieba text 南京市长江大桥 # 精确模式试图最精确地切开句子 seg_exact jieba.cut(text, cut_allFalse) print(精确模式: /.join(seg_exact)) # 精确模式: 南京市/长江大桥 # 全模式把所有能成词的词语都扫描出来 seg_full jieba.cut(text, cut_allTrue) print(全模式: /.join(seg_full)) # 全模式: 南京/南京市/京市/长江/长江大桥/大桥 # 搜索引擎模式在精确模式基础上对长词再次切分 seg_search jieba.cut_for_search(text) print(搜索引擎模式: /.join(seg_search)) # 搜索引擎模式: 南京/京市/南京市/长江/大桥/长江大桥注意上面这个例子有个经典梗南京市长江大桥按精确模式切成了“南京市/长江大桥”但如果文本缺少上下文理论上它也可以被切为“南京市/长江/大桥”甚至被人开玩笑地理解为“南京市/长/江大桥”——这个梗后面讲歧义时还会提。三种模式的核心区别是模式生成token数量特性典型场景精确模式最少尽量不切出多余词但可能漏掉长词内部的小词文本分析、特征提取、NLP下游任务的标准输入全模式最多速度快扫描出所有词典中存在的词但存在大量冗余交叉词频统计、候选词挖掘搜索引擎模式中在精确模式基础上对长词做二次切分兼顾召回与精确搜索引擎索引构建、关键词提取实际工程里 90% 的场景都用精确模式。但全模式和搜索引擎模式也有不可替代的价值——在挖掘新词候选、构建领域词库的时候全模式能帮你把所有可能的组合暴露出来再用统计手段筛掉不合理的结果。2.2 前缀词典DAG HMM Viterbi三件套的工作原理jieba的核心实现并不复杂主要由三个组件构成前缀词典、有向无环图DAG、以及隐马尔可夫模型HMM加维特比Viterbi算法。逐个拆开讲。前缀词典jieba启动时会加载一个词典里面是海量的“词 — 词频 — 词性”条目。所谓前缀词典指的是它会额外记录每个词的所有前缀。比如词典里有“自然语言处理”这个词它的前缀就包括“自”、“自然”、“自然语”、“自然语言”等。为什么要记录前缀因为分词时要构建DAG需要快速判断“从当前某个位置开始哪些前缀是词典中的合法词”。有了前缀集合只需要从句子每个字出发向后逐字检查前缀是否存在于集合中就能画出所有可能的词路径这个过程的复杂度是 O(n²)n是句子长度在工程上非常快。DAG基于前缀词典jieba会把一个句子构造成一张有向无环图。每个节点是字的位置每条边是一个词。例如“南京市长江大桥”在构建时“南京市”“南京”“京市”“长江大桥”“大桥”“江”“大”等所有词典中存在的词都会成为图中的边。分词的最终目标就是在这张图中找一条路径使得按照该路径切词后整句的“词频乘积”等价于对数概率之和最大。这一步用的是动态规划思路是朴素的从头到尾遍历每个位置计算以该位置为终点时的最佳路径及其累计概率。HMM Viterbi词典方法是覆盖不了所有情况的——遇到词典里没有的词也就是未登录词DAG就失效了。jieba对这类词采用HMM来处理。它把中文词语构造成四种字状态B词首、M词中、E词尾、S单字成词。先用带标注的语料训练出字之间的状态转移概率和发射概率分词时对连续的单字序列用维特比算法求出最大概率的状态序列再按状态序列还原成词。比如“奥力给”这种词典里没有的网络新词HMM有机会通过“奥-B、力-M、给-E”把它识别出来。jieba把这两套方案组合使用先走DAG把词典能覆盖的词全部找出来剩下的连续单字序列交给HMM处理。实际效果是通用场景准确率 95% 以上没什么问题这也是它十几年来一直是中文NLP入门标配的原因。2.3 自定义词典与动态调整词典工程落地的关键前面提到医疗、金融领域默认词典会翻车解决办法就是加载自定义词典。jieba支持三种方式load_userdict加载用户词典文件文件每行格式为“词 词频 词性”jieba.load_userdict(medical_terms.txt) # medical_terms.txt 内容 # 克罗恩病 100 nz # 量化宽松 88 nzadd_word动态添加单个词jieba.add_word(自然语言处理, freq1000, tagn)suggest_freq调整词频用于处理混淆情况jieba.suggest_freq(中将, tuneTrue) # 调整后“中将”会被优先切为一个词而非“中/将”这里面有几个容易被坑的细节。add_word的 freq 参数不是越大越好它是在整句最大概率路径里参与比较的设置不合理会让原本合理的切分被带偏。另外load_userdict之后词典内容不会自动持久化到内存之外每次启动都要重新加载高并发服务里要注意初始化效率。还有一个常见问题自定义词和词典里已有词发生冲突时jieba 采用的是“先到先得”的合并策略如果你发现自定义词不生效检查一下是不是加载顺序或者词频设置的问题。我自己的习惯是领域项目里永远先建一个领域词典迭代机制先用全模式或者统计方法从语料里挖候选词人工审核后加入用户词典用完一个版本复盘一次。这样三个月下来工具在领域里的表现会和默认配置有质的差别。3. 分词的尽头传统方案解决不了的三个问题3.1 未登录词与领域迁移词典永远追不上新词jieba很强但它有一个根本矛盾词典是静态的语言是动态的。网络新词“躺平”“yyds”“多巴胺穿搭”医疗术语“布地奈德福莫特罗”垂直领域的缩写“CTR”“ROI”任何一个新出现的词在它被手动加进词典之前都对模型不友好。有人会说不是有HMM吗HMM确实能识别一部分未登录词但它的核心是基于字的转移状态本质上依赖字与字的共现统计规律。对于“布地奈德福莫特罗”这种长词、专业词HMM 很难在缺少大规模领域语料的前提下把它完整拼出来。所以传统分词方案的宿命就是词典需要持续人工维护领域迁移时冷启动成本特别高。这不是jieba的问题而是所有基于词典和统计的分词工具共同的天花板。3.2 一词多义jieba给不了上下文向量“他正在打篮球”和“她在打毛衣”两个“打”语义完全不同。这种多义词现象在中文里极其普遍。jieba对这类词没有任何区分能力——它不管上下文只按词频和路径概率切分切出来的“打”始终是同一个符号不带任何语义区分维度。传统时代的解决办法是给词标注词性或者接一层基于上下文的分类器但效果有限。真正意义上能区分一词多义得等到词向量Word2Vec、GloVe出现再到BERT这类预训练模型横空出世——它们会把“打”在不同上下文里编码成不同的向量表示。这也为后面讲BERT-wwm埋下一个引子现代模型对“词”的理解早就不是词典里的一个静态条目而是随上下文动态变化的语义向量。3.3 级联误差分词错了后面全错传统NLP是一条流水线分词 → 词性标注 → 命名实体识别 → 依存句法分析 → 语义理解。每一步的输出都是下一步的输入。问题在于分词一旦出错错误会一路传播放大这就是级联误差。举一个真实发生在我项目里的例子。做券商公告信息抽取时文档里有一句“公司拟收购上海华信证券有限责任公司100%股权”。jieba默认分词把“上海华信”切成了“上海/华信”导致后续的命名实体识别没有把“上海华信证券有限责任公司”识别为完整的组织机构实体规则引擎的匹配直接失败。最后靠调自定义词典才解决但这类案例每天都在不同的领域里反复出现。级联误差的另一个坏处是上游模型的错误往往是不可恢复的。即使你的实体识别模型再强输入已经被拆碎了也无法还原原始的词边界信息。这个问题的终极解法一种是做端到端的联合模型让模型直接从原始文本里学另一种就是预训练模型的路子——它不强制依赖外部分词结果而是通过自注意力机制自己学习哪些字该被组合理解。这也是BERT-wwm出现的大背景。4. BERT-wwm是怎么把“词”的概念重新带回来的4.1 从BERT到BERT-wwm全词掩码到底改了什么2018年BERT横空出世用“掩码语言模型”这个任务让预训练模型学到了双向上下文表示。原始BERT在英文上的做法是随机挑出15%的WordPiece token把它们替换成[MASK]或随机词让模型去预测被遮住的部分。这个思路很巧妙但它有一个缺陷WordPiece是按统计频率切出来的子词单元同一个单词可能被切成多个piece。随机掩码时经常只遮住一个单词的一部分而不是整体遮住。例如“playing”被分成“play”和“##ing”随机掩码可能只遮“##ing”让模型根据“play”去猜这就太简单了学不到真正的语义理解。Whole Word MaskingWWM全词掩码的改进非常直接既然要预测就把一个完整词的piece全部遮住。比如“playing”被选中那就把“play”和“##ing”一起遮掉让模型真正基于上下文去推这个词是什么。这个改动在英文上效果不算惊天动地但对BERT的训练质量有稳定提升后来的RoBERTa等模型也基本沿用了类似思路。4.2 为什么中文预训练尤其需要wwm中文和英文的tokenization方式差异很大。BERT在中文本土化时广泛采用的是字级别Character-level的tokenizer——每个汉字是一个token。这样做的好处是词表小常用汉字就几千个而且不存在英文那种WordPiece合并问题。但坏处随之而来随机掩码时遮的是单个汉字而中文的语义单元是词。举一个经典的例子原版BERT按字mask时“自然语言处理”这句话如果“语”字被遮住模型根据“自然”“言”“处理”这几个字可能很容易猜到是“语”因为“自然语言处理”这个四字搭配太常见了。模型并没真正理解语义只是学到了字共现的统计规律。而如果采用WWM把“自然语言处理”这个完整的词一次性遮住当然它得先知道这是一个词——这就要靠分词工具了后面细说模型就必须从更大的上下文里推断这个被遮住的完整概念是什么学习难度和使用价值都高得多。所以对中文来说WWM不是锦上添花而是针对字级tokenizer缺陷的必要修正。这也是为什么哈工大和讯飞联合发布的BERT-wwm系列模型在中文本土任务上普遍优于原版BERT比如在CMRC中文机器阅读理解、DRCD繁体中文阅读理解等数据集上都有明显提升。4.3 哈工大讯飞版的额外训练细节目前我们常用的中文BERT-wwm模型是hfl/chinese-bert-wwm和hfl/chinese-bert-wwm-ext都发布在Hugging Face上。它们与标准BERT中文版之间的差别除了WWM之外还有几个关键点全词掩码的数据构建方式在预训练阶段使用分词工具如LTP、jieba对中文语料进行分词然后在“词”级别执行mask。更多的训练数据chinese-bert-wwm使用的数据包括中文维基百科、百度百科、问答社区等-ext版本则进一步扩充数据量达到约5.4B字。更长的训练步数-ext在更多数据上训练更久收敛更充分。用Hugging Face加载非常方便from transformers import BertTokenizer, BertForSequenceClassification MODEL_NAME hfl/chinese-bert-wwm-ext tokenizer BertTokenizer.from_pretrained(MODEL_NAME) model BertForSequenceClassification.from_pretrained(MODEL_NAME, num_labels2)加载之后就可以像使用普通BERT一样微调。但要注意一个细节hfl/chinese-bert-wwm-ext的tokenizer仍然基于字级别vocab.txt里主要就是单个汉字它并未引入“词”这个token粒度WWM只发生在预训练的数据构造阶段。这是一个非常容易误解的地方——很多初学者以为BERT-wwm本身是一个词级模型其实不是。5. 一个容易忽略的事实BERT-wwm的预训练同样离不开分词5.1 wwm掩码的数据处理流程jieba在这里又出现了这一节是很多人没有意识到的。WWM最大难点不在模型结构而在训练数据构造你必须知道哪些汉字组成一个词才能把这些汉字一起mask。怎么知道词的边界当然得有分词工具。以哈工大讯飞的BERT-wwm为例他们构建预训练语料时是用LTP哈工大自然语言处理工具对中文语料进行分词的。但原理上jieba也可以做类似的事——事实上如果你基于BERT自己复现WWM在缺少LTP的情况下用jieba甚至其他分词工具都能构造出可用的全词掩码数据。一个简化的实现逻辑如下import jieba import random def wwm_mask(text, mask_token[MASK], mask_prob0.15): words list(jieba.cut(text)) tokens list(text) # 字级token masked tokens.copy() for word in words: if len(word) 1: continue if random.random() mask_prob: # 找到这个词在token序列中的起始位置 start_idx text.find(word) for i in range(start_idx, start_idx len(word)): # 按BERT的80/10/10策略替换 r random.random() if r 0.8: masked[i] mask_token elif r 0.9: masked[i] random.choice(list(一二三四五六七八九十)) else: masked[i] tokens[i] # 保持原样 return masked text 我喜欢自然语言处理 masked wwm_mask(text) print(.join(masked)) # 输出示例我喜欢[MASK][MASK][MASK][MASK]处理上面这个例子如果词表里“自然语言处理”被切成一个词那mask时它五个字会被整体遮住。如果jieba切的是“自然/语言/处理”那mask就是按这三个词的粒度来遮。所以词边界切得准不准直接决定了WWM训练数据的质量。5.2 词边界的质量如何影响预训练效果这里就出现了一个有意思的结论分词工具的质量会直接影响预训练模型的效果。如果分词工具把一个词切碎了那么WWM就会把本该整体mask的最小语义单元拆成更小的片段训练任务的难度和心理语言学上的“词”的完整性都会打折扣。举个直观的例子假设预训练语料里“量子计算”经常被切成“量子/计算”那么WWM会把“量子”和“计算”分别当作可以整体mask的对象。模型在预测被遮住的“量子”时身边还有一个“计算”相对容易预测“计算”时也同理。但如果分词工具正确地把“量子计算”切为一个词模型就必须在完全没有这两个字线索的情况下从整个上下文去推断“量子计算”这个整体概念学到的东西自然更丰富。这也是为什么后续出现的一些中文预训练模型会更讲究分词质量比如有的模型直接使用词级tokenizer词表有的引入词典知识。而BERT-wwm这一类“字级tokenizer 词级mask”的做法本质上就是在中文环境下把“词的知识”从预训练数据侧注入模型让模型在自注意力机制里自己学会词与词之间的组合规律。从工程复现的角度如果你在某个人工智能实验室自己预训练一个领域模型想采用WWM策略那我建议不要只依赖jieba的默认词典而是用领域语料重新统计词频、构建一套领域分词模式。因为预训练语料里的词边界质量会对最终模型的下游效果产生不可忽视的影响——这一点在我们之前自研领域预训练模型时感受特别明显换了一套更匹配领域语料的分词规则后MLM任务的收敛速度和下游F1都有可观提升。6. 工程选型该用jieba还是直接上BERT-wwm6.1 从baseline到微调的落地顺序讲了这么多原理回到那个最实际的问题我该用jieba还是BERT-wwm我的建议是分两步走。第一步先别急着上预训练模型。用jieba加一套基础特征比如词频、TF-IDF、词性搭配一个轻量级分类器逻辑回归、fastText、XGBoost把baseline先跑出来。这一步有三个价值验证数据质量、建立评估指标、拿到一个低成本对比锚点。很多时候业务场景简单这个baseline的效果已经能满足需求了那就没必要上BERT。第二步如果baseline不达标比如分类F1差了5个点以上或者需要处理复杂语义语境相关的情感判断、长文本关系抽取再上BERT-wwm微调。微调的代码很简单用前面提到的transformers接口加载预训练模型替换输出层做5~10个epoch的训练即可。重点是数据清洗、标签质量、类别不均衡处理这些往往比模型的选型影响更大。6.2 资源成本、时延与效果的三方权衡很多工程师容易陷入“唯效果论”觉得效果优先资源不是问题。但真实场景里成本约束几乎总是决定性的。我以一个典型的线上实时情感分析接口为例做一个粗略对比方案单条耗时CPU单条耗时GPU模型体积部署复杂度效果参考jieba 词袋 LR1~3ms无需GPU几十MB极低一台普通服务器即可基础情感分类可达85%左右jieba fastText1~3ms无需GPU几十MB极低略优于词袋BERT-wwm微调50~150ms5~15ms约400MB需要GPU或多核CPU优化在复杂场景普遍能提升3~10个点这个表格不是我拍脑袋编的而是很多项目实测的个人经验。在纯CPU环境下BERT-wwm的推理时延是jieba方案的几十倍如果线上流量是每秒几百次就得考虑GPU实例的成本这一下子预算就上去了。所以选型的核心不是“哪个模型更强”而是“业务的收益曲线能不能覆盖模型升级的边际成本”。另外提一句中间路线也有价值。对于离线批量任务用BERT-wwm蒸馏一个小的蒸馏模型比如TinyBERT、AlBERT可以在效果和性能之间找到不错的平衡点对于大规模召回场景先用jiebaBM25粗排再用BERT-wwm精排也是典型的工业级做法。6.3 我的实战建议与踩坑清单最后分享几条在这条“从分词到预训练”路上被反复验证的经验。自定义词典要尽早建。不管最后用不用BERT领域词典在预处理、弱监督打标、候选挖掘这些环节都有用。哪怕你的主模型是BERT-wwm输入之前做一层领域分词切分也可以帮助下游任务在需要词边界信息的场景里表现更好。注意BERT-wwm的token上限。BERT类模型输入长度默认512 token中文按字切分512个字很短。长文本任务不要无脑全文灌进去要么做滑窗切片要么在预处理阶段用jieba先做摘要式抽取选出关键句再进模型。我见过太多人在这里栽跟头——直接截断前512个字把关键信息截没了。记得对比原版BERT和BERT-wwm。同一个任务上两者差异不一定永远很大但WWM在中文上稳定占优尤其在阅读理解、实体识别这类需要“词义理解”的任务里。不过也有例外如果你的任务本质是字级别的比如古诗词生成、汉字纠错那字级原版BERT未必弱于WWM建议两个模型都跑一遍对比用数据说话。预训练模型不是终点融合才是。线上系统里把jieba的快速能力当作第一层过滤把BERT-wwm作为复杂样本的兜底这种“粗筛精判”的分层架构在成本和效果上是目前最稳妥的实践。我最近的几个项目就是按照这个思路做的简单样本jieba加规则秒出结果困难样本交给BERT-wwm做深度语义理解。效果不错成本也可控。分词和预训练模型从来不是“谁取代谁”的关系而是中文NLP这条路上不同阶段的产物——一个解决“字怎么组成词”的底层问题一个解决“词在不同上下文里是什么意思”的高层语义问题。理解清楚这两层你才算真正入了中文NLP的门。
返回列表