WordPiece 与 Unigram LM 都把子词切分当作语言模型问题:前者用似然增益替换 BPE 的频次排序来挑选合并对,后者干脆放弃合并、直接建模子词概率并用 Viterbi 求全局最优切分。
两条路线的分水岭
BPE 算法原理 的训练是自底向上增量的:从字符出发,每轮把出现次数最多的相邻对合并进词表。WordPiece 保留了这个骨架,只换掉打分函数;Unigram LM 则彻底换了范式——它自顶向下减量,先给一个过大的种子词表,再逐步剪掉”删掉它损失最小”的子词。
由此产生一个关键后果:BPE 与 WordPiece 的词表是有序的操作序列(merge 规则或匹配优先级),Unigram 的词表是带概率的无序集合。前者切分由局部贪心决定,后者由全局搜索决定。
WordPiece:用似然增益挑选合并对
训练目标
每轮考察所有相邻子词对 ,选择使 unigram 语言模型似然提升最大的一对。似然增益近似为
这正是点互信息(pointwise mutual information)的指数形式。相比 BPE 的 ,分母惩罚了本身就高频的组件:th 这类到处都出现的组合不会仅因为频次高就被优先合并,而 ##tion、##ing 这类构成高度专一的后缀会被提前收进词表。实践中 WordPiece 词表更贴近形态学边界,BPE 词表更贴近字符串统计。
编码:最长匹配与 前缀
WordPiece 的推理不重放 merge 序列(这是最常见的误解)。BERT 原始实现对每个预分词后的词做从左到右的最长匹配(MaxMatch):
def wordpiece_tokenize(word, vocab, unk="[UNK]"):
"""BERT 风格最长匹配:每步取词表中最长的前缀,词中子词带 ## 前缀。"""
tokens, start = [], 0
while start < len(word):
end, cur = len(word), None
while end > start:
piece = word[start:end]
if start > 0:
piece = "##" + piece # 非词首必须带续接标记
if piece in vocab:
cur = piece
break
end -= 1
if cur is None: # 任一位置失配 → 整词判为 UNK
return [unk]
tokens.append(cur)
start = end
return tokens
vocab = {"un", "##aff", "##able", "##a", "##ff"}
print(wordpiece_tokenize("unaffable", vocab)) # ['un', '##aff', '##able']
print(wordpiece_tokenize("unaffably", vocab)) # ['[UNK]'] —— 尾部 'bly' 失配,整词丢弃## 前缀让 play 与 ##play 成为两个不同 token,从而在无空格信息的 token 序列里保留词边界;解码时去掉 ## 直接拼接即可还原。代价是词首/词中同形子词占两份词表配额。
BERT 系的工程约定
- 词表规模:英文 uncased BERT 为 30522,multilingual BERT 为 119547。
- 覆盖失败的粒度是整词 UNK,不是字符级兜底——这与 BBPE 字节级分词 的零 OOV 保证形成鲜明对比,也是 WordPiece 在 Unicode 噪声文本上退化的根源。
- 中文在预处理阶段被逐字加空格,等价于强制单字切分,词表里基本没有多字中文词,详见 中文与多语言分词。
[CLS]、[SEP]、[MASK]等保留 token 手工占位,机制见 特殊 Token 与 Chat Template。
Unigram LM:先假设概率,再反推词表
概率化切分假设
给定词表 和概率 ,一个切分序列 的概率按独立同分布假设分解:
切分即搜索:在原句所有合法切分集合 中取
注意长度惩罚是隐式的——子词越多,累加的负对数越多,模型天然偏好用少量长子词覆盖,除非短子词概率显著更高。
Viterbi 解码
切分候选数随句长指数增长,但目标函数在字符格(lattice)上可分解,动态规划 即可求全局最优:
import math
# 子词 -> log 概率(真实场景由 EM 训练得到)
vocab = {"你": -3.2, "好": -3.5, "你好": -2.1, "世": -4.0, "界": -4.1, "世界": -2.4}
def viterbi(text, vocab, max_len=8):
n = len(text)
best = [-math.inf] * (n + 1) # best[i]: text[:i] 的最优 log 概率
back = [0] * (n + 1) # back[i]: 最优切分中末个子词的起点
best[0] = 0.0
for i in range(1, n + 1):
for j in range(max(0, i - max_len), i):
piece = text[j:i]
if piece in vocab and best[j] > -math.inf:
s = best[j] + vocab[piece]
if s > best[i]:
best[i], back[i] = s, j
tokens, i = [], n
while i > 0: # 回溯
tokens.append(text[back[i]:i])
i = back[i]
return tokens[::-1], best[n]
print(viterbi("你好世界", vocab)) # (['你好', '世界'], -4.5)单字符 token 必须常驻词表,否则格中可能出现断点导致无解。
减量式训练
-
种子词表:用 suffix array 或 BPE 取足量高频 substring,规模通常是目标词表的数倍。
-
E 步:固定词表,用前向-后向算法在 lattice 上求各子词的期望出现次数,更新 。
-
M 步:对每个子词计算 ——将其移出词表后整个语料边际似然的下降量:
-
剪枝:按 loss 降序保留 top (常取 80%),单字符豁免;回到第 2 步直到达到目标 vocab size。
因为剪枝准则是”对全语料似然的边际贡献”,Unigram 对目标词表大小的变化更鲁棒——同一份种子跑到 32k 和 64k,得到的是嵌套式的取舍结果,而非 BPE 那样受 merge 顺序牵连的路径依赖。这一点在 词表设计与裁剪 中做裁剪实验时尤其重要。
subword regularization
Unigram 天然支持从 中按 采样,或输出 n-best 切分。训练时每个 epoch 换一种切分,相当于对 tokenization 边界做数据增强,在低资源翻译上稳定提升 1 BLEU 以上。BPE 侧的对应物是 BPE-dropout(以概率随机跳过 merge),但它只能扰动而无法给出切分概率。
与 BPE 的本质差异
| 维度 | BPE | WordPiece | Unigram LM |
|---|---|---|---|
| 词表构建方向 | 增量合并 | 增量合并 | 减量剪枝 |
| 选择准则 | 最大 | 似然增益 / PMI 最大 | 删除后似然损失最小 |
| 词表载荷 | 有序 merge 规则 | 子词集合 + 匹配优先级 | 子词集合 + |
| 编码算法 | 重放 merge(贪心) | 最长匹配(贪心) | Viterbi(全局最优) |
| 一句话多切分 | 仅 BPE-dropout 扰动 | 不支持 | 原生采样 / n-best |
| 词边界标记 | </w> 或 Ġ | ## 续接前缀 | ▁ 前导空格符 |
| OOV 行为 | 回落字符 | 整词 [UNK] | 回落字符 |
| 代表模型 | GPT-2、RoBERTa | BERT、DistilBERT、ELECTRA | T5、ALBERT、XLNet、mBART |
关键是”贪心 vs 全局”:BPE 和 WordPiece 一旦在某个位置做出合并/匹配决策就不可回退,可能被局部高分片段带偏;Unigram 用 的可加性换来了动态规划的全局最优,代价是编码比查表匹配慢一个量级,且必须携带浮点概率。
选型
- 复现 BERT 系或在其权重上继续训练,只能用 WordPiece——
##约定与 embedding 表绑定。 - 从零训中文/多语言模型,优先 Unigram:无需依赖空格预分词,配合 SentencePiece 的
--model_type=unigram直接吃原始文本,词表在形态丰富语言上压缩率更好。 - 需要 tokenization 层面的数据增强(低资源、噪声输入鲁棒性),Unigram 的采样切分是唯一开箱可用的选项。
- 追求最高编码吞吐、或要与现成 GPT 词表对齐,仍然选 BPE 系。三者在 Hugging Face Tokenizers 实操 中共用同一套 normalizer / pre-tokenizer 管线,替换 model 组件即可横向对比。