想象你打开一盒乐高积木,里面全是细碎的散件:单个的字母、符号、音节。要让 AI 读懂一句话,得先决定“把哪些散件拼成常用模块”——这就是分词。早期的做法很笨:把每个完整单词都当成一个独立的大模块,结果英文几百万个单词(含各种变形),仓库堆不下;而只用单个字符做模块,又太小,拼 “unbelievable“ 要拼十二次,效率太低。于是聪明的拼装大师想出了折中方案:子词分词——常用词整块保留,生僻词拆成小块。BPE 和 WordPiece 就是两位风格迥异的拼装大师:一个信奉”谁常出现就拼谁“,一个讲究”谁搭配更协调“。GPT 请了前者,BERT 请了后者。读完这篇,你就能看懂大模型词表背后的设计逻辑,也能给未来的模型选择合适的”拼装说明书”。
分词是 NLP 与 LLM 工程的第一道关卡。理解 BPE 与 WordPiece 的原理对比,能帮你正确选择词表构建方案、诊断未登录词问题,并理解 GPT 与 BERT 词表设计差异——这是文本预处理岗位的核心基本功。
可视化演示
对比BPE频率驱动与WordPiece似然驱动的子词合并策略,展示分词过程与词表构建差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
BPE频率驱动合并 BPE选择出现频率最高的相邻符号对进行合并 merge(argmax freq(pair)) GPT系列采用BPE,优先保留高频子词组合
分词任务概述
乐高工厂收到一批散件,工人们要先看图纸决定“最小拼装单位”。如果以整句为模块,仓库得大得离谱;如果以单个字母为模块,拼一个长单词要反复装卸。给 AI 读文本,同样面临这个选择——分词就是决定文本被切分成什么粒度的基本单位。
早期的 NLP 系统喜欢用“词级分词”:把语料中每个单词连同变形(如 "play"、"plays"、"played")都收进词表,一个词对应一个编号。好处是直观,坏处是单词总量随语料增长飞快,英文有几十万个词形,中文更是无词边界。更麻烦的是,测试时碰到词表里没有的单词,模型只能干瞪眼,这就是未登录词(OOV)问题。可以用一个指标量化它:
于是工程师们想:能不能像乐高出“通用模块”一样,把常用词保留为整块,把生僻词拆成可组合的小块?子词分词(Subword Tokenization)由此兴起:词表里既有完整单词,也有常见词根、词缀,覆盖率大幅提升,词表大小也能控制住。这是 BPE 与 WordPiece 登场的舞台。
- 词表集合\(\mathcal{V}\) 是预先定义好的词表,\(\mathcal{T}\) 是测试语料中所有词例的集合。
- 漏网之鱼集合\(\{w \mid w \notin \mathcal{V}\}\) 表示“出现在测试语料里、但词表没有收录”的那些词。
- 统计数量用集合基数 \(| \cdot |\) 计算这个集合里有多少个词,即漏掉了多少词。
- 归一化除以总词例数 \(|\mathcal{T}|\),得到漏网词在所有词中的占比,即未登录词率。
BPE算法原理
拼装大师 A 的方法论很简单:“我不看设计图,只看销量排行榜——哪两个零件最常被人一起买走,我就把它们预拼成一个模块。” 这种思路在数据压缩领域有个响亮的名字:Byte Pair Encoding(BPE,字节对编码)。它最早用来压缩文本,后来被 GPT 系列模型发扬光大,变成了子词分词的主力算法。
BPE 的训练过程可以概括为三步:第一步,从字符集出发,把语料中所有单词拆成单字符序列,并统计每个单词的出现次数;第二步,扫描语料中所有相邻的符号对,统计每对出现多少次;第三步,把出现次数最多的那对符号合并成一个新符号,加入词表。重复第二、三步,直到词表达到预设大小。每一次合并,词表就多一个“流行模块”。
拿着这套“流行搭配排行榜”,BPE 在语料上迭代几十上百次,就能拼出一套贴合语料的子词模块。这种纯粹的统计方法训练极快,而且合并结果可逆——你可以根据词表把任意文本还原成原始字符序列,这对语言模型的输入输出设计非常友好。
- 候选集合\(\mathcal{C}\) 是当前语料中所有相邻符号对的集合,它决定了这次从哪些组合里挑。
- 统计函数\(\text{count}(x, y)\) 统计符号对 \((x, y)\) 在整个语料里相邻共现的次数。
- 取最大值\(\arg\max\) 表示在候选集合里找到让 count 值最大的那一对。
- 合并结果\((x, y)^*\) 就是最终选中的符号对,合并后得到新符号 \(xy\),写进词表。
WordPiece算法原理
拼装大师 B 却摇了摇头:“流行款不一定协调!两个零件销量都很高,但一个装车轮、一个装车灯,硬拼在一起就是四不像。我要看的,是这两个零件同时出现是否超出了随机巧合。” 这就是 WordPiece 的核心思想:合并的标准不是“共现次数”,而是“共现带来的信息增益”。
WordPiece 由 Google 提出,是 BERT 家族默认的分词方案。它同样从字符集出发、迭代合并相邻符号对,但每一步选择的标准不同:它会为当前候选的每个符号对计算一个“似然增益”,增益越大,说明把这两个符号合并成整体越能提升模型对语料的建模能力。增益的计算公式长这样:
除了合并准则,WordPiece 还有一个标志性设计:特殊标记 ##。当完整单词被拆成多个子词时,从第二个子词开始会加上 ## 前缀,例如 “playing“ 被切分为 ”play” + “##ing“,这表示 ”##ing” 只能续接在前一个子词之后,不能单独成词。这个设计让分词结果能无歧义地还原原词边界,对语言模型理解词义很有帮助。
- 联合概率\(P(xy)\) 是符号对 \(xy\) 作为一个整体在语料中出现的概率。
- 独立概率之积\(P(x)P(y)\) 是假设 \(x\) 和 \(y\) 相互独立时,它们恰好相邻的期望概率。
- 比值含义比值 \(\frac{P(xy)}{P(x)P(y)}\) 大于 1,说明 \(xy\) 共现比随机更频繁,有真正的搭配价值。
- 取对数\(\ln\) 把比值映射到对数尺度,让比较更稳定、计算更方便。
两种算法对比
现在,两位拼装大师同台竞技:A 拿着一张统计表,看哪个组合销量最高就拼哪个;B 抱着概率计算器,反复核对每个组合的“化学反应”。同样拼装 30 个模块,A 可能一会儿就交工了,B 还要先算概率再决定。这就是 BPE 与 WordPiece 最直观的差异——BPE 快而直接,WordPiece 慢而讲究。
两者的优化目标可以并排放在一起,差异一目了然:
从词表形态上看,BPE 产出的子词彼此独立、没有标记;WordPiece 产出的子词带 ## 后缀标记,还原性更好。从训练效率上看,BPE 只需要统计频次,训练速度快,适合超大规模语料;WordPiece 需要估计概率,训练更耗时,但词表通常更紧凑、OOV 更低。因此工程上形成了默契:生成式模型 GPT 偏爱 BPE,理解式模型 BERT 偏爱 WordPiece。
- BPE 准则左边是 BPE 的优化目标,直接在语料中数次数,选出最高频的相邻对。
- WordPiece 准则右边是 WordPiece 的优化目标,需要先估计概率再算增益。
- 本质差异BPE 只做“表上计数”,无需额外建模;WordPiece 则隐含了一个语言模型来估计概率。
- 效果差异BPE 更容易合并出形态学上的常见组合(如 "ing"、"est");WordPiece 更倾向合并出有语义功能的组合。
词表构建实践
有了算法,还得决定“乐高套装到底出多少个零件”。盒子太小,很多造型拼不出来;盒子太大,搬家时累死人。词表大小 \(V\) 就是这样一个牵一发而动全身的工程参数。主流大模型的词表大多落在 30k 到 100k 之间:GPT-2 用了 50257 个 token,BERT-base 用了 30522 个 token,正好处在经验区间的中部。
为什么不能无脑把词表设大?因为词表直接影响嵌入层的参数规模。每个 token 都要对应一个固定维度的向量,参数量是二者的乘积:
除了大小,词表还要预留特殊 token 的位置:[UNK] 表示未知词、[CLS] 和 [SEP] 用于句对分类、[MASK] 用于掩码语言模型、<s> 与 </s> 用作句界符。这些 token 各自承担结构功能,是模型训练中不可或缺的“特殊零件”。至于未登录词,BPE 的字节级变体用 256 个字节覆盖全部 UTF-8 文本,天然没有 OOV;而 WordPiece 遇到未收录词时,会回退到 [UNK] 或做字符级拆分,工程上需要做好兜底策略。
- 词表大小\(V\) 是词表总条目数,包括字符、子词、特殊 token 和合并产生的符号。
- 嵌入维度\(d\) 是每个 token 对应的向量维度,例如 BERT-base 的 \(d = 768\)。
- 乘积含义嵌入层是一个 \(V \times d\) 的查找表,即参数量等于两者相乘。
大模型应用
乐高公司推出主题大片,选角各有讲究:GPT 导演要拍自由创作,台词经常天马行空,需要能“无限兼容”的拼装大师,于是选了字节级 BPE;BERT 导演要拍精读分析,每句话都要理解词与词之间的微妙关系,于是选了会标注搭配关系的 WordPiece。两位大师各得其所。
GPT 系列使用 byte-level BPE:先把所有文本映射到 256 个字节(0-255),再在这套字节序列上做 BPE 合并。这样无论是英文、中文还是 emoji,都能被词表覆盖,彻底解决了 OOV 问题。词表构成很清晰:
BERT 则采用 WordPiece,词表 30k 左右,配合 ## 标记精确保留词边界。近年来还出现了 SentencePiece 这样的统一框架:把空格也当作普通字符(用 ▁ 表示),无需预先切分句子,直接处理原始文本,可以灵活实现 BPE、WordPiece 和 Unigram 三种算法。掌握这些异同,你就能在面对新任务时,为模型挑选最合适的那套“拼装说明书”。
- 基础符号256 是字节级空间的所有可能取值,对应 0 到 255 的字节值。
- 合并次数\(\text{merges}\) 是 BPE 迭代合并的总次数,每合并一次词表加一个符号。
- 词表构成最终词表 = 256 个字节符号 + 每次合并产生的新符号。
AI 应用场景
大模型预训练词表构建
阶段:文本预处理与词表构建
解决的问题:预训练 GPT/BERT 前,需要把海量语料转成离散 token 序列。传统词级分词词表爆炸、OOV 高,直接用字符级粒度又导致序列过长、训练效率低下。
具体案例:某团队要训练一个中文医疗大模型,语料包含大量专业术语和生僻药名。他们先用 byte-level BPE 在 200GB 语料上训练 50k 词表,把常用术语合并成整词、生僻字拆成字节组合,最终 OOV 降为 0,训练序列长度也缩短了 40%。
多语言机器翻译预处理
阶段:多语种语料统一分词
解决的问题:多语言机器翻译涉及几十种语言,若按语言分别建词表,维护成本和参数量都不可接受;若共用字符级词表,序列又太长。
具体案例:一个翻译团队用 SentencePiece 统一处理英、日、阿等 20 种语言,空格被当作字符编码、共享同一套 32k 词表。模型翻译日语时能利用英语子词"newest"的知识,迁移效果明显提升。
在线推理分词服务
阶段:推理阶段的文本编码
解决的问题:线上 LLM 服务每秒处理大量请求,如果每次推理都用 Python 循环做 BPE 合并,延迟难以接受。
具体案例:某 AI 公司把 BPE 词表和合并规则编译成 C++ 实现的分词库,并在 GPU 上做 batch 编码,分词吞吐量提升 8 倍,端到端推理延迟降低 30%。
Python 演示
"""BPE 分词器最小实现:学习合并规则并对新文本切词"""from collections import Counter class BPETokenizer: def __init__(self, num_merges=10): self.num_merges = num_merges self.merges = [] self.vocab = set() def _get_pair_counts(self, corpus): """统计语料中所有相邻符号对的频次""" pair_counts = Counter() for word in corpus: symbols = word.split() for i in range(len(symbols) - 1): pair_counts[(symbols[i], symbols[i + 1])] += 1 return pair_counts def train(self, corpus): """用 corpus 学习合并规则""" for _ in range(self.num_merges): pair_counts = self._get_pair_counts(corpus) if not pair_counts: break best_pair = max(pair_counts, key=pair_counts.get) self.merges.append(best_pair) new_symbol = best_pair[0] + best_pair[1] new_corpus = [] for word in corpus: symbols = word.split() i = 0 merged = [] while i < len(symbols): if i < len(symbols) - 1 and (symbols[i], symbols[i + 1]) == best_pair: merged.append(new_symbol) i += 2 else: merged.append(symbols[i]) i += 1 new_corpus.append(" ".join(merged)) corpus = new_corpus self.vocab = set() for word in corpus: for sym in word.split(): self.vocab.add(sym) for sym in "abcdefghijklmnopqrstuvwxyz": self.vocab.add(sym) def encode(self, text): """将新文本切分成子词序列""" parts = list(text) for pair in self.merges: new_symbol = pair[0] + pair[1] i = 0 merged = [] while i < len(parts): if i < len(parts) - 1 and parts[i] == pair[0] and parts[i + 1] == pair[1]: merged.append(new_symbol) i += 2 else: merged.append(parts[i]) i += 1 parts = merged return parts if __name__ == "__main__": corpus = ["l o w", "l o w", "l o w", "l o w", "l o w e r"] tk = BPETokenizer(num_merges=4) tk.train(corpus) print("合并规则:", tk.merges) print("词表:", sorted(tk.vocab)) print("分词结果:", tk.encode("lower"))
pair_counts = self._get_pair_counts(corpus)扫一遍语料,给每对相邻符号记一次“组合出镜率”。best_pair = max(pair_counts, key=pair_counts.get)从出镜率排行榜中捧出冠军组合。new_symbol = best_pair[0] + best_pair[1]让两个散件合体,成为一个新模块。while i < len(symbols):逐个扫描单词内的符号,寻找能应用合并规则的位置。if i < len(symbols) - 1 and (symbols[i], symbols[i + 1]) == best_pair:命中冠军组合时,用新模块替换掉两个旧符号。self.vocab.add(sym)把合并后的新符号收进词表,供 encode 时识别。for pair in self.merges:编码阶段按训练好的顺序依次应用合并规则,完成子词切分。
常见误区
误区“BPE 和 WordPiece 不都是把常见字母组合并起来吗?区别不大,换着用就行。”
正解两者合并准则有本质差别:BPE 直接统计符号对频次,谁出现多就合并谁,像超市看销量进货;WordPiece 计算的是似然增益,看共现是否超出随机期望,像设计师看搭配契合度。这个差异导致两者词表形态、训练效率和最终分词结果都不同。
为何常见:两者都采用“迭代合并相邻符号对”的贪心框架,表面流程相似,初学者容易忽略内部的评分函数差异。
误区“词表越大,覆盖的词越多,模型肯定越聪明,干脆设成 500k。”
正解词表增大会让嵌入层参数以 \(V \times d\) 线性膨胀,训练显存和速度双双恶化;同时长尾 token 出现次数稀疏,学不到好的向量表示。实践中 30k~100k 是主流区间,关键是在覆盖率和参数开销之间取平衡。
为何常见:直观上“认识更多词”似乎等于“更聪明”,但忽略了词表增长带来的参数爆炸和数据稀疏问题。
误区“BPE 把单词拆成子词,所以任何文本都能拆开,绝对不会有 OOV。”
正解只有 byte-level BPE 用 256 个字节作为基础符号,才能覆盖任意 UTF-8 文本;如果在字符级(字母表)上做 BPE,遇到语料里没出现过的生僻字符,仍然会触达未知符号。所谓“无 OOV”是有前提的。
为何常见:很多人把 GPT 的 byte-level BPE 特性推广到了所有 BPE 变体上,忽视了基础符号集的选择才是覆盖率的决定因素。
误区“WordPiece 切出来的子词加个 ## 前缀,纯粹是为了好看,去掉也不影响。”
正解## 标记承载着词边界信息:它标明该子词必须续接在前一个子词之后。有了它,分词结果可以无损还原成原始单词序列,这对训练语言模型理解词义和下游任务很有价值。
为何常见:从视觉上看 ## 容易被当成格式符号,忽略它在“可还原性”设计中的作用。
面试题
Q1. BPE 算法最初起源于哪个领域?
- A.数据压缩← 正确答案
- B.图像处理
- C.音频编码
- D.数据库索引
答案:A。BPE 全称 Byte Pair Encoding,最早是 1994 年提出的数据压缩算法,后来被 GPT 引用来做子词分词。图像、音频、数据库都不是它的发源地。
Q2. BPE 合并符号对的依据是什么?
- A.符号对在语料中的出现频率← 正确答案
- B.符号对的语义相似度
- C.符号对的随机采样概率
- D.符号对的拼音相似程度
答案:A。BPE 每次选择出现次数最高的相邻符号对进行合并,是纯粹的频率统计,不涉及语义、采样或拼音信息。
Q3. WordPiece 合并符号对的依据是什么?
- A.语言模型似然增益最大← 正确答案
- B.符号对共现频率最高
- C.符号文本长度最长
- D.符号出现次数最多
答案:A。WordPiece 计算 \( \ln \frac{P(xy)}{P(x)P(y)} \) 作为似然增益,每次合并增益最大的符号对;频率最高是 BPE 的标准,长度和出现次数都不是合并准则。
Q4. 关于 BPE 与 WordPiece 的差异,以下哪些说法正确?
- A.BPE 看频次而 WordPiece 看增益← 正确答案
- B.WordPiece 常为子词加 ## 前缀← 正确答案
- C.两者的词表构建方式完全相同
- D.BPE 的训练更依赖语言模型
答案:AB。BPE 以频次为准则,WordPiece 以似然增益为准则;WordPiece 用 ## 标记续接子词。两者构建方式不同,且 BPE 不依赖语言模型,因此 C、D 错误。
Q5. GPT 系列模型采用哪种分词方案?
- A.字节级 BPE 方案← 正确答案
- B.WordPiece 方案
- C.纯字符分词方案
- D.拼音分词方案
答案:A。GPT-2/3 使用 byte-level BPE,以 256 个字节为基础做合并,实现任意 Unicode 的无 OOV 覆盖;BERT 才用 WordPiece。
Q6. BERT 采用哪种分词方案?
- A.字节级 BPE 方案
- B.WordPiece 方案← 正确答案
- C.纯词级分词方案
- D.Unigram 方案
答案:B。BERT 使用 WordPiece,通过似然增益合并子词,并用 ## 标记续接片段。Unigram 是 SentencePiece 支持的另一类算法。
Q7. 关于未登录词处理,以下哪些说法正确?
- A.字节级 BPE 对 UTF-8 无 OOV← 正确答案
- B.WordPiece 遇未知词回退 UNK← 正确答案
- C.词表无限大便没有 OOV
- D.字符级 BPE 也可能有 OOV← 正确答案
答案:ABD。byte-level BPE 用 256 字节覆盖一切 UTF-8 文本,A 对;WordPiece 未收录词时会落到 [UNK],B 对;字符级 BPE 遇到生僻字符仍会产生未知符号,D 对。C 中“无限大词表”不现实且长尾词仍可能缺失。
Q8. 词表选 32k 而不是 1M,最主要的原因是什么?
- A.大词表令嵌入层参数暴增← 正确答案
- B.大词表令推理更加快速
- C.大词表一定提高准确率
- D.大词表减少训练时间
答案:A。嵌入层参数量是 \(V \times d\),词表从 32k 涨到 1M,参数涨约 30 倍,显存和训练成本难以承受。大词表不会加速推理,也不一定能提高准确率。
Q9. WordPiece 中的 "##ing" 表示什么?
- A.##ing 是分词后的后缀标记← 正确答案
- B.##ing 是一个完整的单词
- C.## 用于标记句子开头
- D.## 用于标记未知词汇
答案:A。## 标明该子词必须续接在前一个子词之后,"##ing" 只能作为后缀出现。它不是一个完整单词,也和句首、未知词无关。
Q10. SentencePiece 相比传统 BPE/WordPiece 有哪些特点?
- A.把空格视作普通字符处理← 正确答案
- B.只能处理英文语料文本
- C.可统一实现 BPE 与 Unigram← 正确答案
- D.必须预先切分好文本
答案:AC。SentencePiece 把空格转成 ▁ 当作普通字符,无需事先切词,A 对;它支持 BPE、Unigram 多种算法,C 对;它支持多语言而非仅英文,也无需预切分,B、D 错误。