想象你是一位乐高积木建筑师阿乐,任务是搭出世界上所有单词城堡。如果为每个单词都浇铸一整块巨型定制积木——像"playing""pianist""unhappiness“,仓库会堆满上百万种模具,根本放不下;如果只用最基础的1×1小方粒(单个字母),虽然任何词都能拼,但每座城堡动辄几十块积木,拼得慢又容易散架。 WordPiece就是阿乐想出的聪明方案:准备一套“标准积木”——像 play、##ing、##ness 这样不大不小的子词块,大约3万种。拼”playing“时,用”play“加”##ing“两块搞定。但仓库空间有限,哪些积木值得生产?阿乐的判断标准很特别:他不看哪两块小积木在其他地方”出镜率“最高(那是BPE算法的做法),而是每次试拼一对,看拼完后整座城堡是不是更结实、更好预测(也就是让语言模型的似然提升最大)。这个”先试后拼"的思路,就是WordPiece子词分词的核心智慧。
掌握WordPiece,你就能看懂BERT等大模型的文本入口,独立构建分词管道、解决OOV难题,这是每一位NLP工程师的看家本领。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
子词切分概述:三箱积木
阿乐面前摆着三箱积木。第一箱是“整词积木”——每个单词一个模具,词库里有多少词就得有多少种模具。今天"play"、"plays"、"playing"、“played“就是四种不同的积木,明天新造一个词”playful“,仓库又没有模具了,只好挂一个”未知零件“(UNK)的牌子。这是词级切分的痛点:词表爆炸、开放词表脆弱。第二个箱子里全是1×1的小方粒,26个字母就能拼一切,但拼一个”playing”要七块,句子一长就积木成灾,模型要处理的序列长度让人头大。这是字符级切分的短板:序列过长、单个字符几乎不携带语义。
WordPiece从这两个极端里找到第三条路:子词级切分。它把单词切成有意义的中间粒度,比如"playing" → "play" + “##ing“。这样既不扩大词表到失控,又能让”play“这个语义核心在”player”"playground"里反复复用。子词的词表规模可控,常见词保留整体,罕见词被拆成常见块,模型既能看到词根又不会面对无穷无尽的整词。
回到积木的比喻,子词就是那些设计精良的标准砖块:尺寸适中、可复用率高、拼法灵活。整词积木太笨重,1×1小粒太琐碎,标准积木才是阿乐眼里性价比最高的一套。
合并准则:看稳固性,不看出镜率
假设阿乐要决定仓库里添置哪种新积木,候选有“th“和”##pi“两对。如果按BPE的作风,他会先统计语料里哪对积木被用到最多——”th“出现在the、this、that里,出镜率高达987次;而”##pi“只出现在pianist里,才34次。于是BPE毫不犹豫先生产”th”。但阿乐是WordPiece派的建筑师,他的做法是:把两对积木各拼上去试一试,然后问一个更本质的问题——拼上去之后,整片语料世界的可预测性提升哪边更大? 他计算的是合并前后的语言模型似然变化。
这就是WordPiece的核心合并准则:给定当前词表,把每个候选子词对合并成新符号后,重新计算语料整体的对数似然,选择增益最大的一对执行合并,而不是选择频次最高的一对。
其中,语料似然定义为所有单词在分词后子词序列上的概率乘积:
所以,BPE像是按“出镜率”决定砖块生产,WordPiece却像给每座城堡做“风洞测试”:哪怕某对砖不常露面,只要拼上后整体结构显著优化——比如让pianist不再碎成八块——它就可能胜出。频次看的是局部,似然看的是全局可预测性,这是两者最本质的分野。
- 候选对与 merge 运算遍历语料中出现的所有相邻子词对 $(a,b)$,$ \mathrm{merge}(a,b)$ 表示把它们拼成一个新子词。如 "th" + “##e“ 的 merge 结果是 ”the”。
- 合并前后的似然差把新子词临时加入词表,重新对语料分词并计算对数似然,与合并前的似然相减,得到这次合并的“稳固度增益”。
- arg max 选择在所有候选对中挑 $\Delta \mathcal{L}$ 最大的那一对,本轮只合并它。增益越大,说明这对子词合并后越能提高整片语料的可预测性。
- 子词概率乘积$P(w)$ 被近似为词内各子词概率的乘积,$P(t)$ 由该子词在语料中的相对频率估计,$ \log $ 把乘法变成加法方便求和。
##前缀机制:榫头标记的秘密
阿乐的标准积木侧面都有一排凸起的榫头,标记为“##“。一块积木如果带着榫头,说明它不是一座城堡的起点,而是要塞进前一块积木里的。比如”playing“这座城堡,”play“是不带##的底座积木,”##ing“是带##的拼接块。如果看到”##playing“,那就意味着”playing“这个词本身出现在另一个更长的词内部,比如”##playing“可能是”outplaying”的后半截。
这套标记在数学上可以写成一个简单的重构公式:
有了榫头标记,分词器能精确地知道词边界在哪里:一个词的第一个子词决定了词的起始,所有带##的块都自动归属于当前词内部。这让WordPiece在跨语言场景中尤其稳健——德语的长复合词、英语的派生词缀、中文的拼音切分,都能用同一套“榫头”逻辑处理,语言之间的差异被吸收进统一的拼接规则里,模型不需要为每种语言单独设计边界策略。
词表规模权衡:三万种积木的仓库
阿乐的仓库不是无限大的。设计词表时,他会面临一个绕不开的权衡:词表越大,城堡越容易“整块出厂”,但找积木、搬运积木的成本也越高;词表越小,很多城堡就不得不拆成细碎的小粒,拼起来费劲、结构也不稳定。BERT最终选定的规模是大约3万个子词(精确地说是30522个),这正是工程上的一个平衡点。
3万词表覆盖了绝大多数英文语料的常用形态:常见单词“play“整体入库,常见词缀”##ing”“##ness“单独入库,长尾的罕见词如”unhappiness“则拆成”un + ##happi + ##ness“组合拼装。如果某一对子词极不常见,比如某个生僻词的独有碎片,词表会果断放弃它——当子词在词表中找不到时,编码器会逐级回退,最终退化为一个个单字符子词,保证任何词都能被表示,只是”拼得碎一点”而已。
这就像阿乐仓库里的抽屉数固定:3万个抽屉既装得下高频的标准件,又不会让低频的怪异模具挤占空间。词表太小则什么都要零散地拼,词表太大则每个抽屉里的积木样本稀疏、统计不可靠,模型参数也急剧膨胀。3万是覆盖率与效率之间一个经过实践检验的甜点位置。
训练流程:建筑师的五步流水线
阿乐每次从零开始建设词表,都遵循一套固定的流水线。第一步,盘点语料里出现的所有字符,把它们做成最基础的1×1积木块(包括带##的字符变体),形成初始词表。第二步,把语料中的每个单词用当前词表切分,并统计所有相邻子词对。第三步,对每个候选对,临时把它合并成一个新符号,重新计算语料整体的对数似然,得到合并增益。第四步,选出增益最大的那一对,真正把它加入词表。第五步,回到第二步,重复迭代,直到词表达到目标大小,或者最佳增益低于预设阈值就收工。
整个过程是一个贪心式的迭代合并算法,每轮只做一次合并决策。写成递推式就是:
这个流程看起来很像BPE的训练流程,但关键差别藏在第三步:“出镜率最高”和“似然增益最大”经常指向不同的候选对。正是这一步的评判标准,决定了词表里装的是“网红积木”还是“承重积木”。阿乐完工后,仓库里的每一块积木都经过了“拼上去能让全城更稳定”的检验。
应用实践:BERT的全球积木工厂
阿乐的流水线很快被一家大工厂看中了——它就是BERT。BERT的输入构建依赖WordPiece:原始文本先按单词切分,再用约3万子词的词表把每个单词编码成子词ID序列,前面加上[CLS],后面加上[SEP],送入Transformer。可以说,WordPiece是BERT理解文本的第一道门,模型看到的一切都从子词序列开始。
这套方案还展现了出色的跨语言稳健性。多语言BERT(mBERT)使用同样的WordPiece流程处理104种语言,从英语到德语再到日语,统一在同一个子词空间里做预训练。德语长篇复合词被拆成常见块,中文按字/词边界切分,罕见拼写也不会变成UNK——因为任何词最终都能用子词块拼出来。相比之下,SentencePiece是另一种子词框架,它把BPE和Unigram等算法整合为一个通用的无监督分词工具;WordPiece定义的是合并准则,SentencePiece定义的是实现框架,两者常被混为一谈,但在实际工程中,BERT系模型用的是WordPiece准则,而T5、LLaMA等模型则常走SentencePiece的路线。
对今天的大模型开发者来说,理解WordPiece不只是考古:当你微调BERT做文本分类、做检索、做命名实体识别时,子词切分方式直接决定了输入表示的质量。甚至可以效仿阿乐的流水线,在自己的领域语料上重新训练词表,让罕见术语拥有稳定的子词表示,这往往是提升领域模型效果性价比最高的一步。
AI 应用场景
搜索语义召回的文本入口
阶段:文本预处理 / 输入编码
解决的问题:搜索日志里充满"iPhone 15 Pro Max"“深度学习理论”等专名与复合词,传统整词词典难以维护,频繁产生OOV,导致召回丢失。WordPiece把未知词拆成已知子词块,OOV率大幅降低。
具体案例:某电商搜索团队上线BERT语义召回,将所有用户查询与商品标题先做WordPiece切分再编码。切分后OOV率从12%降到0.3%,“深度学习”与"deep learning"在子词空间中产生了可比性,语义召回准确率提升18%。
多语言合同知识库
阶段:多语言预训练 / 跨语言迁移
解决的问题:跨国律所的知识库混合中、英、德、法四种语言,若按整词分词需要维护四套词典,且跨语言检索时表示空间不统一。mBERT配合WordPiece用一套词表就能同时编码多种语言。
具体案例:某法律科技公司在合同智能审查系统中引入mBERT,WordPiece让中英合同条款在同一个嵌入空间里对齐,跨语言条款检索的top-5命中率从61%提升到84%。
领域专属词表的OOV兜底
阶段:领域微调前的数据准备
解决的问题:医疗术语"hydroxyethylstarch"(羟乙基淀粉)不在BERT词表中,直接切分会碎成失控片段、丢失语义信息。重新训练一个领域子词词表能保留术语的稳定结构。
具体案例:某医疗AI团队在电子病历命名实体识别任务中,先用百万级医学论文训练一个WordPiece词表(目标3万子词),再对BERT做领域微调。罕见药物名的F1分数从72%提升到89%。
Python 演示
# -*- coding: utf-8 -*-"""教学版 WordPiece 子词分词:训练 + 贪心编码""" import mathfrom collections import Counter CORPUS = [ "the pianist plays", "the playing field", "play the piano", "she is running", "the pianist is happy",] def get_words(corpus): cnt = Counter() for line in corpus: for w in line.lower().split(): cnt[w] += 1 return cnt def initial_vocab(chars): vocab = set() for ch in chars: vocab.add(ch) vocab.add("##" + ch) vocab.add("[UNK]") return vocab def merge_pair(a, b): if a.startswith("##"): if b.startswith("##"): return "##" + a[2:] + b[2:] return "##" + a[2:] + b if b.startswith("##"): return a + b[2:] return a + b def encode(word, vocab): if word in vocab: return [word] start = 0 toks = [] while start < len(word): end = len(word) found = False while end > start: piece = word[start:end] if start == 0 else "##" + word[start:end] if piece in vocab: toks.append(piece) start = end found = True break end -= 1 if not found: toks.append(word[start] if start == 0 else "##" + word[start]) start += 1 return toks def corpus_ll(word_counts, vocab): tok_counts = Counter() for w, f in word_counts.items(): for t in encode(w, vocab): tok_counts[t] += f total = sum(tok_counts.values()) ll = 0.0 for w, f in word_counts.items(): for t in encode(w, vocab): ll += f * math.log(tok_counts[t] / total) return ll def train_wordpiece(corpus, target_vocab=None): words = get_words(corpus) chars = set("".join(words.keys())) vocab = initial_vocab(chars) if target_vocab is None: target_vocab = len(vocab) + 12 base_ll = corpus_ll(words, vocab) print("初始词表大小:", len(vocab), " 初始语料似然: %.2f" % base_ll) while len(vocab) < target_vocab: candidates = {} for w, f in words.items(): toks = encode(w, vocab) for a, b in zip(toks, toks[1:]): m = merge_pair(a, b) if m not in vocab: candidates[(a, b, m)] = candidates.get((a, b, m), 0) + f if not candidates: break best_pair = None best_gain = 0.0 for (a, b, m) in candidates: ll = corpus_ll(words, vocab | {m}) gain = ll - base_ll if gain > best_gain: best_gain = gain best_pair = (a, b, m) if best_pair is None or best_gain <= 0: break vocab.add(best_pair[2]) base_ll += best_gain print("合并 %s + %s -> %s 似然增益: %.4f" % ( best_pair[0], best_pair[1], best_pair[2], best_gain)) return vocab if __name__ == "__main__": print("训练语料:") for s in CORPUS: print(" ", s) v = train_wordpiece(CORPUS) print("\n训练得到的词表:", sorted(v)) print("\n分词结果:") words = get_words(CORPUS) for w in sorted(words): print(" %s => %s" % (w, " + ".join(encode(w, v))))
def merge_pair(a, b):处理两个子词拼接时的##前缀合并规则,这是##机制的代码体现。piece = word[start:end] if start == 0 else "##" + word[start:end]贪心最长匹配的核心,首块不带##、后续块加##。ll += f * math.log(tok_counts[t] / total)用unigram语言模型估计子词概率,累加得到语料对数似然。candidates[(a, b, m)] = candidates.get((a, b, m), 0) + f收集所有候选合并对并统计频次,但注意后面并不按它选。ll = corpus_ll(words, vocab | {m})临时把新子词并入词表,重算语料似然,得到合并增益。if gain > best_gain:关键区别:选出似然增益最大而非频次最高的候选对,这正是WordPiece与BPE的分水岭。
常见误区
误区很多初学者看到两者都是“迭代合并子词”,就认为WordPiece是BPE的别名,训练结果完全一样。
正解两者合并准则有本质差异。BPE只看子词对在语料中的出现频次,谁高合并谁;WordPiece则计算合并前后语言模型似然的变化,选择似然提升最大的对。就像两个建筑师傅都在拼积木,一个专挑“出镜率最高的那两块”,另一个会用仪器测“拼上后整座城稳不稳”,结果经常不同。
为何常见:两者流程外观高度相似——都是字符级起步、迭代合并、贪心增长词表,初学者容易把它们归为一类。
误区看到"##ing""##ness"这些高频词,就认为##标记的必然是后缀,凡是带##的都是词尾形态。
正解##的真正含义是“该子词不位于词首,需要接在前一块之后”,它可以出现在词的任何非首位置,包括词中与词尾。比如“unhappiness“切分为”un + ##happi + ##ness”,"##happi"就在词中间。它标记的是词内位置关系,与词性形态无关。
为何常见:训练时最常见的合并对象恰好是ing、ness这类后缀,样本偏差让人把“词内标记”误读为“后缀标记”。
误区初学者认为词表越大模型认识的字越多,最好把所有单词都收进词表,彻底消灭OOV。
正解词表过大会带来三重代价:子词统计稀疏、概率估计不可靠;模型嵌入层参数膨胀;训练和推理变慢。词表过小则长尾词被切得过碎、语义流失。BERT选约3万词表是在覆盖率、参数量与计算效率之间的工程权衡,正如仓库抽屉数要配合工匠的找件速度来定。
为何常见:直觉上“认识更多词当然更聪明”,忽略了统计稀疏性和算力约束这些工程现实。
误区认为WordPiece是英文专属方法,中文、日文等多语言任务根本用不上。
正解WordPiece的合并准则完全语言无关,mBERT用同一套流程处理104种语言,德语复合词、中文词边界、日语假名都能通过子词块统一编码。多语言模型正是靠它才不需要为每个语言单独设计分词器。
为何常见:中文语境下“字天然成词”的直觉很强,让人忽视了子词方法在多语言模型中的核心地位。
面试题
Q1. WordPiece合并候选子词对的准则是什么?
- A.在语料中出现频次最高
- B.合并后语言模型似然提升最大← 正确答案
- C.在两个词中长度最长
- D.在所有词中出现次数最少
答案:B。WordPiece的核心是“似然优先”:每轮选合并后使语料对数似然提升最大的子词对执行合并。选项A是BPE的准则,C、D与算法无关。
Q2. 下列哪个子词序列符合WordPiece对单词"playing"的正确切分?
- A.play + ing
- B.play + ##ing← 正确答案
- C.##play + ##ing
- D.#play + #ing
答案:B。首块子词“play“不带##,后续词内子词必须带##,”##ing”表示它是接在play之后的拼接块。其他选项要么漏标记(A)、要么首块错误加标记(C)、要么用错符号(D)。
Q3. BERT所使用的WordPiece词表规模约为多少?
- A.3千
- B.1万
- C.3万← 正确答案
- D.30万
答案:C。BERT的WordPiece词表约30522个子词,大约是3万量级。这个规模是覆盖率与参数量之间的工程权衡结果,既覆盖绝大多数常见形态,又不会让嵌入层过度膨胀。
Q4. 如果词表中没有“pianist“这个整词,WordPiece 将其切分为 ”pia” + "##nist",这说明什么?
- A.字符级分词的典型特征
- B.词级分词的典型特征
- C.子词级分词的典型特征← 正确答案
- D.模型无法处理的信号
答案:C。整词缺失时,WordPiece会退回去寻找词表内的最长子词块,把单词拆成“pia“和”##nist”两个已知子词,这正是子词分词比词级分词更擅长处理OOV的体现。
Q5. WordPiece与BPE在合并准则上的本质区别是?
- A.BPE 用词频合并
- B.WP 用似然合并← 正确答案
- C.两者用词频合并
- D.两者用似然合并
答案:B。BPE统计子词对共现频次,谁高合并谁;WordPiece计算合并前后语言模型似然差,选增益最大的对。前者看局部出镜率,后者看全局可预测性,这是两者本质区别。
Q6. 关于"##"前缀机制的作用,下列说法正确的有?
- A.表示子词出现在词首
- B.表示子词在词内非词首位置← 正确答案
- C.表示子词出现在词尾
- D.指示该子词需接在前一块之后← 正确答案
答案:BD。##标记的是“词内、非词首”的子词,它提示还原时该块要拼接在前一块后面。词首块一律不带##,而##块可以在词中也可以在词尾,因此A、C都不准确。
Q7. 采用子词级切分的核心动机包括哪些?
- A.控制词表规模← 正确答案
- B.减少OOV单词← 正确答案
- C.保留词内语义信息← 正确答案
- D.提升显卡显存大小
答案:ABC。子词切分比词级词表更精简、比字符级更能承载语义,同时任何单词都可通过子词组合表示从而减少OOV。D“提升显存”不是直接动机,压缩词表带来的副作用不等于动机本身。
Q8. 训练中某候选子词对频次很高,但合并后语料似然反而下降,WordPiece会如何处理?
- A.仍合并高频词对
- B.改选似然增益最高的对← 正确答案
- C.强制合并扩充词表
- D.立即终止全部训练
答案:B。WordPiece的合并决策只看似然增益,不看频次。若某高频对合并后降低语料可预测性,它会被舍弃,算法转而选择增益最大的对;只有所有候选增益都不大于零时才停止训练。
Q9. 关于WordPiece训练流程的描述,正确的有?
- A.从字符级词表开始迭代← 正确答案
- B.每次迭代只合并一对← 正确答案
- C.词表满或增益过小时停止← 正确答案
- D.训练终止与词表大小无关
答案:ABC。WordPiece从单字符词表起步,每轮只合并似然增益最大的一个子词对,当词表达到目标大小或最优增益低于阈值时停止。D与终止条件设计相悖。
Q10. 多语言BERT(mBERT)用同一套WordPiece流程处理上百种语言,最直接说明了什么?
- A.合并准则与语言无关← 正确答案
- B.词表必须逐语言定制
- C.中文不适用此方法
- D.需要翻译语料辅助训练
答案:A。WordPiece的合并准则是基于语料统计的语言无关过程,mBERT正是利用这一性质在统一子词空间中编码多种语言。若准则依赖语言特性,就不可能用同一套流程处理上百种语言。