想象你是一位乐高积木建筑师阿乐,任务是搭出世界上所有单词城堡。如果为每个单词都浇铸一整块巨型定制积木——像"playing""pianist""unhappiness“,仓库会堆满上百万种模具,根本放不下;如果只用最基础的1×1小方粒(单个字母),虽然任何词都能拼,但每座城堡动辄几十块积木,拼得慢又容易散架。 WordPiece就是阿乐想出的聪明方案:准备一套“标准积木”——像 play、##ing、##ness 这样不大不小的子词块,大约3万种。拼”playing“时,用”play“加”##ing“两块搞定。但仓库空间有限,哪些积木值得生产?阿乐的判断标准很特别:他不看哪两块小积木在其他地方”出镜率“最高(那是BPE算法的做法),而是每次试拼一对,看拼完后整座城堡是不是更结实、更好预测(也就是让语言模型的似然提升最大)。这个”先试后拼"的思路,就是WordPiece子词分词的核心智慧。

岗位关联

掌握WordPiece,你就能看懂BERT等大模型的文本入口,独立构建分词管道、解决OOV难题,这是每一位NLP工程师的看家本领。

目录
  1. 可视化演示
  2. 子词切分概述:三箱积木
  3. 合并准则:看稳固性,不看出镜率
  4. ##前缀机制:榫头标记的秘密
  5. 词表规模权衡:三万种积木的仓库
  6. 训练流程:建筑师的五步流水线
  7. 应用实践:BERT的全球积木工厂
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 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不再碎成八块——它就可能胜出。频次看的是局部,似然看的是全局可预测性,这是两者最本质的分野。

(a∗,b∗)=arg⁡max⁡(a,b)[L(V∪{merge(a,b)})−L(V)]\displaystyle (a^{*}, b^{*}) = \arg\max_{(a,b)} \left[ \mathcal{L}\left( \mathcal{V} \cup \{ \mathrm{merge}(a,b) \} \right) - \mathcal{L}(\mathcal{V}) \right]
L(V)=∑w∈corpus  ∑t∈wp(w)log⁡P(t)\displaystyle \mathcal{L}(\mathcal{V}) = \sum_{w \in \text{corpus}} \; \sum_{t \in \mathrm{wp}(w)} \log P(t)
通俗理解:第一个公式说“我每轮只挑让整座城最稳的那块砖来拼”;第二个公式说“一座城的稳固度 = 所有积木的可靠度加起来”。
  1. 候选对与 merge 运算遍历语料中出现的所有相邻子词对 $(a,b)$,$ \mathrm{merge}(a,b)$ 表示把它们拼成一个新子词。如 "th" + “##e“ 的 merge 结果是 ”the”。
    merge(a,b)\displaystyle \mathrm{merge}(a,b)
  2. 合并前后的似然差把新子词临时加入词表,重新对语料分词并计算对数似然,与合并前的似然相减,得到这次合并的“稳固度增益”。
    ΔL=L(V∪{merge(a,b)})−L(V)\displaystyle \Delta \mathcal{L} = \mathcal{L}(\mathcal{V} \cup \{ \mathrm{merge}(a,b) \}) - \mathcal{L}(\mathcal{V})
  3. arg max 选择在所有候选对中挑 $\Delta \mathcal{L}$ 最大的那一对,本轮只合并它。增益越大,说明这对子词合并后越能提高整片语料的可预测性。
    arg⁡max⁡(a,b)ΔL\displaystyle \arg\max_{(a,b)} \Delta \mathcal{L}
  4. 子词概率乘积$P(w)$ 被近似为词内各子词概率的乘积,$P(t)$ 由该子词在语料中的相对频率估计,$ \log $ 把乘法变成加法方便求和。
    P(w)≈∏t∈wp(w)P(t)\displaystyle P(w) \approx \prod_{t \in \mathrm{wp}(w)} P(t)

##前缀机制:榫头标记的秘密

阿乐的标准积木侧面都有一排凸起的榫头,标记为“##“。一块积木如果带着榫头,说明它不是一座城堡的起点,而是要塞进前一块积木里的。比如”playing“这座城堡,”play“是不带##的底座积木,”##ing“是带##的拼接块。如果看到”##playing“,那就意味着”playing“这个词本身出现在另一个更长的词内部,比如”##playing“可能是”outplaying”的后半截。

这套标记在数学上可以写成一个简单的重构公式:

有了榫头标记,分词器能精确地知道词边界在哪里:一个词的第一个子词决定了词的起始,所有带##的块都自动归属于当前词内部。这让WordPiece在跨语言场景中尤其稳健——德语的长复合词、英语的派生词缀、中文的拼音切分,都能用同一套“榫头”逻辑处理,语言之间的差异被吸收进统一的拼接规则里,模型不需要为每种语言单独设计边界策略。

w=t1+##t2+##t3+⋯+##tk\displaystyle w = t_1 + \text{\#\#}t_2 + \text{\#\#}t_3 + \cdots + \text{\#\#}t_k

词表规模权衡:三万种积木的仓库

阿乐的仓库不是无限大的。设计词表时,他会面临一个绕不开的权衡:词表越大,城堡越容易“整块出厂”,但找积木、搬运积木的成本也越高;词表越小,很多城堡就不得不拆成细碎的小粒,拼起来费劲、结构也不稳定。BERT最终选定的规模是大约3万个子词(精确地说是30522个),这正是工程上的一个平衡点。

3万词表覆盖了绝大多数英文语料的常用形态:常见单词“play“整体入库,常见词缀”##ing”“##ness“单独入库,长尾的罕见词如”unhappiness“则拆成”un + ##happi + ##ness“组合拼装。如果某一对子词极不常见,比如某个生僻词的独有碎片,词表会果断放弃它——当子词在词表中找不到时,编码器会逐级回退,最终退化为一个个单字符子词,保证任何词都能被表示,只是”拼得碎一点”而已。

这就像阿乐仓库里的抽屉数固定:3万个抽屉既装得下高频的标准件,又不会让低频的怪异模具挤占空间。词表太小则什么都要零散地拼,词表太大则每个抽屉里的积木样本稀疏、统计不可靠,模型参数也急剧膨胀。3万是覆盖率与效率之间一个经过实践检验的甜点位置。

训练流程:建筑师的五步流水线

阿乐每次从零开始建设词表,都遵循一套固定的流水线。第一步,盘点语料里出现的所有字符,把它们做成最基础的1×1积木块(包括带##的字符变体),形成初始词表。第二步,把语料中的每个单词用当前词表切分,并统计所有相邻子词对。第三步,对每个候选对,临时把它合并成一个新符号,重新计算语料整体的对数似然,得到合并增益。第四步,选出增益最大的那一对,真正把它加入词表。第五步,回到第二步,重复迭代,直到词表达到目标大小,或者最佳增益低于预设阈值就收工。

整个过程是一个贪心式的迭代合并算法,每轮只做一次合并决策。写成递推式就是:

这个流程看起来很像BPE的训练流程,但关键差别藏在第三步:“出镜率最高”和“似然增益最大”经常指向不同的候选对。正是这一步的评判标准,决定了词表里装的是“网红积木”还是“承重积木”。阿乐完工后,仓库里的每一块积木都经过了“拼上去能让全城更稳定”的检验。

Vn+1=Vn∪{arg⁡max⁡(a,b)ΔL(a,b)}\displaystyle \mathcal{V}_{n+1} = \mathcal{V}_n \cup \left\{ \arg\max_{(a,b)} \Delta \mathcal{L}(a,b) \right\}

应用实践: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 演示

python115 行
# -*- 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))))
目的:用一个小语料完整演示WordPiece从字符词表出发、按“似然增益最大”准则迭代合并子词,最后对新词做贪心最长匹配切分的全过程。
  1. def merge_pair(a, b):处理两个子词拼接时的##前缀合并规则,这是##机制的代码体现。
  2. piece = word[start:end] if start == 0 else "##" + word[start:end]贪心最长匹配的核心,首块不带##、后续块加##。
  3. ll += f * math.log(tok_counts[t] / total)用unigram语言模型估计子词概率,累加得到语料对数似然。
  4. candidates[(a, b, m)] = candidates.get((a, b, m), 0) + f收集所有候选合并对并统计频次,但注意后面并不按它选。
  5. ll = corpus_ll(words, vocab | {m})临时把新子词并入词表,重算语料似然,得到合并增益。
  6. 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合并候选子词对的准则是什么?

单选题 · easy

  • A.在语料中出现频次最高
  • B.合并后语言模型似然提升最大← 正确答案
  • C.在两个词中长度最长
  • D.在所有词中出现次数最少

答案:B。WordPiece的核心是“似然优先”:每轮选合并后使语料对数似然提升最大的子词对执行合并。选项A是BPE的准则,C、D与算法无关。

Q2. 下列哪个子词序列符合WordPiece对单词"playing"的正确切分?

单选题 · easy

  • A.play + ing
  • B.play + ##ing← 正确答案
  • C.##play + ##ing
  • D.#play + #ing

答案:B。首块子词“play“不带##,后续词内子词必须带##,”##ing”表示它是接在play之后的拼接块。其他选项要么漏标记(A)、要么首块错误加标记(C)、要么用错符号(D)。

Q3. BERT所使用的WordPiece词表规模约为多少?

单选题 · mid

  • A.3千
  • B.1万
  • C.3万← 正确答案
  • D.30万

答案:C。BERT的WordPiece词表约30522个子词,大约是3万量级。这个规模是覆盖率与参数量之间的工程权衡结果,既覆盖绝大多数常见形态,又不会让嵌入层过度膨胀。

Q4. 如果词表中没有“pianist“这个整词,WordPiece 将其切分为 ”pia” + "##nist",这说明什么?

单选题 · easy

  • A.字符级分词的典型特征
  • B.词级分词的典型特征
  • C.子词级分词的典型特征← 正确答案
  • D.模型无法处理的信号

答案:C。整词缺失时,WordPiece会退回去寻找词表内的最长子词块,把单词拆成“pia“和”##nist”两个已知子词,这正是子词分词比词级分词更擅长处理OOV的体现。

Q5. WordPiece与BPE在合并准则上的本质区别是?

单选题 · mid

  • A.BPE 用词频合并
  • B.WP 用似然合并← 正确答案
  • C.两者用词频合并
  • D.两者用似然合并

答案:B。BPE统计子词对共现频次,谁高合并谁;WordPiece计算合并前后语言模型似然差,选增益最大的对。前者看局部出镜率,后者看全局可预测性,这是两者本质区别。

Q6. 关于"##"前缀机制的作用,下列说法正确的有?

多选题 · mid

  • A.表示子词出现在词首
  • B.表示子词在词内非词首位置← 正确答案
  • C.表示子词出现在词尾
  • D.指示该子词需接在前一块之后← 正确答案

答案:BD。##标记的是“词内、非词首”的子词,它提示还原时该块要拼接在前一块后面。词首块一律不带##,而##块可以在词中也可以在词尾,因此A、C都不准确。

Q7. 采用子词级切分的核心动机包括哪些?

多选题 · mid

  • A.控制词表规模← 正确答案
  • B.减少OOV单词← 正确答案
  • C.保留词内语义信息← 正确答案
  • D.提升显卡显存大小

答案:ABC。子词切分比词级词表更精简、比字符级更能承载语义,同时任何单词都可通过子词组合表示从而减少OOV。D“提升显存”不是直接动机,压缩词表带来的副作用不等于动机本身。

Q8. 训练中某候选子词对频次很高,但合并后语料似然反而下降,WordPiece会如何处理?

单选题 · hard

  • A.仍合并高频词对
  • B.改选似然增益最高的对← 正确答案
  • C.强制合并扩充词表
  • D.立即终止全部训练

答案:B。WordPiece的合并决策只看似然增益,不看频次。若某高频对合并后降低语料可预测性,它会被舍弃,算法转而选择增益最大的对;只有所有候选增益都不大于零时才停止训练。

Q9. 关于WordPiece训练流程的描述,正确的有?

多选题 · hard

  • A.从字符级词表开始迭代← 正确答案
  • B.每次迭代只合并一对← 正确答案
  • C.词表满或增益过小时停止← 正确答案
  • D.训练终止与词表大小无关

答案:ABC。WordPiece从单字符词表起步,每轮只合并似然增益最大的一个子词对,当词表达到目标大小或最优增益低于阈值时停止。D与终止条件设计相悖。

Q10. 多语言BERT(mBERT)用同一套WordPiece流程处理上百种语言,最直接说明了什么?

单选题 · extreme

  • A.合并准则与语言无关← 正确答案
  • B.词表必须逐语言定制
  • C.中文不适用此方法
  • D.需要翻译语料辅助训练

答案:A。WordPiece的合并准则是基于语料统计的语言无关过程,mBERT正是利用这一性质在统一子词空间中编码多种语言。若准则依赖语言特性,就不可能用同一套流程处理上百种语言。