想象你正面对一箱散装的乐高颗粒。要拼出一个“城堡”单词,你得一片一片地找到 l、o、w 三颗颗粒;要拼十次“城堡”,就得重复十次这样的翻找动作。乐高工厂的设计师发现,很多图纸里“l+o+w“这个组合反复出现,于是他们提前把这三颗颗粒预拼成一个标准组件,写进零件目录。下次任何人需要”城堡“这个词,直接拿一个组件即可。GPT 系列的分词器,就是这样一个”乐高组件目录设计师”。它叫 BPE(Byte Pair Encoding,字节对编码),原本是 1994 年用于压缩数据的算法,2016 年被引入自然语言处理,至今仍是 GPT-2、GPT-3、GPT-4 等大模型切分文本的幕后功臣。本篇将用乐高的语言,拆解 BPE 如何把海量文本变成一套精巧的子词组件。
掌握 BPE 是理解 GPT 如何“看”文本的第一块基石。无论你做数据清洗、模型微调还是推理优化,分词质量都直接决定模型效果与算力成本——这是每位 AI 工程师都无法绕开的必修课。
可视化演示
展示BPE合并过程、词汇表增长曲线与三种分词算法对比,支持步进式探索子词切分核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
BPE合并过程 从字符级别开始统计相邻字符对频率 合并最高频对生成新子词单元 |V| = |V_chars| + N_merges 反复合并直到达到目标词汇表大小
一、BPE核心原理
乐高工厂每天收到成千上万张拼装图纸,小乐发现“车轮+车轴”这对组合在图纸里反复出现。与其让工人们每次都从零找颗粒,不如先把这对颗粒预拼成一个标准组件。BPE 算法的思路与此完全一致:它把文本看作一排字符积木,统计哪两个相邻的字符“总是黏在一起”,就把它们合并成一个子词积木——这个过程就是“字节对合并”。
1994 年,BPE 诞生于数据压缩领域,工程师用它压缩二进制文件中的重复模式。2016 年,NLP 研究者 Sennrich 等人发现同样的思路可以压缩“词表”:与其让词表装下所有单词,不如让高频字符对一步步合并成子词。GPT 系列正是沿用了这一思路,将 BPE 作为默认分词算法。
具体流程分四步。第一步,把语料切碎成单个字符,相当于把所有乐高颗粒拆到最细;第二步,扫描所有相邻字符对,统计每对出现的频率;第三步,挑出频率最高的一对,合并成一个新的子词;第四步,重复第二步和第三步,直到词表达到预定大小或没有可合并的对为止。
回到乐高工厂,小乐用这套流程优化了生产线:图纸上“车轮+车轴”出现频率最高,那就先预拼它;下次再出现“车轮车轴+车厢”,再预拼成更大的组件。BPE 的本质,就是让数据自己说话——高频组合永远优先获得合并的资格。
- 相邻字符对文本中位置紧挨的两个字符构成一对组合,我们关心的是目标组合 (x, y)
- 指示函数当这个相邻对恰好等于 (x, y) 时记 1,否则记 0,相当于一个“是否符合条件”的开关
- 求和汇总从位置 1 扫到 N-1,把所有命中次数累加起来,得到该字符对在语料中的总频次
二、词汇表构建
乐高工厂的零件目录不是凭空设计的。小乐先统计所有图纸中出现的每一种基础颗粒,把它们登记在册;然后按 BPE 规则把高频对预拼成新组件,每预拼一个,目录就多一个条目。目录的初始版本很简单:语料中出现过的所有独立字符,比如英文的 26 个字母加上标点符号,这就是“初始字符表”。
接下来,每次合并都会为目录增加一个全新的子词条目。比如从“low“和”lower“中,先合并出”lo“,再合并出”low“,继而合并出”lower“。每完成一次合并,词汇表的容量就加一。这个”合并规则”会被记录下来,成为之后切分新文本的依据。
这里有一个关键的“子词粒度”概念:合并次数越多,子词越长,粒度越粗。如果合并到极致,整个高频单词会成为一个独立组件;如果一次都不合并,则退化为纯字符级切分。实际使用中,我们通常预设一个目标词表大小(比如 GPT-2 的 50257),到点即停。
这就像乐高目录从最初的几十种基础颗粒,慢慢扩充到包含各种预拼组件的厚厚一本。目录太厚翻起来费劲(词表过大),太薄又什么都拼不出来(词表过小)——构建词汇表的过程,就是在寻找那个刚刚好的平衡点。
- 初始词汇表语料中所有独立字符的集合,是目录的第一版
- 每次合并选定最高频字符对并合并成一个新子词,目录条目加一
- T 次迭代经过 T 轮合并后,最终词表大小恰好等于初始字符数加合并次数
三、编码解码流程
有了零件目录,接下来就是拼装与拆解的问题了。拼装环节,工人拿到一个单词,会先看目录里有没有现成的组件:整个单词有,直接拿;没有,就拆成目录中能匹配的最大子词组合。拆解环节则简单得多,把组件按顺序拼回去就行,不需要任何额外规则。
训练阶段是整个流程的地基。在训练时,算法在大量语料上执行合并操作,学出一套“合并规则”和最终的子词词汇表。这个阶段结束后,词汇表和规则就冻结了,进入编码阶段。
编码时采用“贪心最长匹配”策略:从左到右扫描文本,每次都尝试用词表中最长的子词去匹配当前位置。比如“lower“如果词表里有”low“和”er”,就切分成 low + er,而不是 l + o + w + e + r。解码则完全相反——把每个子词直接拼接起来,原文本就回来了,不需要任何语言模型参与。
至于“未知词处理”,乐高工厂也有对策:遇到目录里完全不存在的单词,BPE 会一直向细粒度拆解,直到拆成单个字符——如果语料里有这个字符,就能拼出来;极端情况甚至拆到字节级别,几乎不会出现“完全拼不出来”的窘境。
- 切分映射把原文 s 按照词表切分,得到一个子词序列 v₁ 到 v_k,这就是编码结果
- 拼接还原把所有子词按顺序拼接,符号 ⊕ 表示字符串连接,结果恰好等于原文 s
- 信息守恒由于每个子词都是原文本的连续片段,拼接不增不减,所以解码永远无损
四、GPT系列实现
乐高在不同国家卖的颗粒形状其实是有差异的,这给全球拼装带来了麻烦。GPT 系列也面临类似问题:用英文字母作为基础颗粒,中文、日文、阿拉伯文怎么办?GPT-2 的答案是“字节级 BPE”——干脆统一用全球通用的“标准颗粒”:UTF-8 字节。
所谓字节级 BPE,就是先把任何语言的文本编码成 UTF-8 字节序列,然后在这些字节(取值范围 0 到 255)上执行 BPE 合并。这样一来,无论中文的“你”还是英文的“a“,都先变成若干个字节,再在字节层面学习子词。初始词汇表从”所有字符“变为”所有字节”,大小固定为 256,完美覆盖任意语言。
GPT-2 论文中报告的词汇表大小为 50257,其中 256 个是基础字节,另外约 5 万个是通过 BPE 合并得到的子词。GPT-3 延续了 GPT-2 的字节级 BPE 方案,只在词表细节上做了微调;GPT-4 虽然细节未完全公开,但业界普遍认为其分词器同样基于字节级 BPE 的变体。这一设计的核心优势是:无论输入什么语言、什么符号,都能被编码为已知字节,从根上解决了未登录词问题。
从此,乐高图纸不再区分“英文版”还是“中文版”,全球统一用字节颗粒拼装。这正是 GPT 系列能流畅处理上百种语言的分词层秘密。
- 字符转字节任意 UTF-8 字符被编码为长度 1 到 4 的字节序列
- 字节范围每个字节 bᵢ 的取值是 0 到 255,共 256 种可能
- 字节当字符把这 256 种字节当作“初始字符表”,BPE 在字节序列上执行合并
五、算法对比
市面上的积木品牌不止乐高一家,美高、杂牌积木各有各的拼装逻辑。在子词切分领域,BPE 并非唯一选择,它的两个著名“竞品”是 WordPiece 和 SentencePiece。理解它们之间的差异,能帮你更清楚地认识 BPE 的个性。
WordPiece 是 BERT 系列采用的分词算法。它与 BPE 最大的不同在于合并依据:BPE 只看频率——出现最多的对就合并;WordPiece 则看“概率比”——两个字符经常一起出现,且不是偶然的,才值得合并。用公式表达,WordPiece 会给每个候选对算一个得分,得分高者优先。
SentencePiece 则是另一条路线:它由谷歌开源,特点是先把文本中所有空格也当作普通字符参与合并。这样分词结果天然可逆,不需要“在词之间补空格”的后处理,尤其适合日语等没有空格的语言。此外,还有更粗粒度的“词级切分”(整个单词一个 token)和更细粒度的“字符级切分”(每个字符一个 token),分别对应乐高的大组件和散颗粒——前者词表容易爆炸,后者序列太长,BPE 的子词方案正好站在两者之间。
- 联合频率分子统计 xy 作为一个相邻对在语料中的出现次数
- 边缘频率乘积分母是 x 单独出现的次数乘以 y 单独出现的次数,起归一化作用
- 得分判定得分越高的字符对,共现越“超出偶然”,优先合并;BPE 没有这层归一化,只看原始频率
六、权衡与优化
乐高目录如果收录了十万种组件,翻起来费时费力,工厂仓库也放不下;如果只有几十种基础颗粒,拼一座城堡又要反复拼装。BPE 的词汇表大小正是这样的权衡问题,它直接影响两个关键指标:序列长度和模型参数规模。
词汇表越大,单个子词携带的信息越多,一句话被切成的 token 数越少,序列就越短。但代价是嵌入层(embedding layer)的参数规模跟着膨胀:词表从 3 万扩到 6 万,嵌入矩阵的行数就翻倍,显存占用直线上升。反之,词汇表越小,模型参数少,但序列变长,Transformer 的计算量随序列长度平方增长,训练和推理都会变慢。
实践中常用“压缩比”来衡量分词效率:原始字符数除以子词数。GPT-2 的 50257 词表对英文的压缩比大约在 3 到 5 之间,意味着平均每个 token 能表示 3 到 5 个字符。这个数字不是越大越好——它要和嵌入层开销、多语言覆盖度、模型容量放在一起综合判断。
在“数据为中心 AI 范式”下,分词是数据质量评估的第一道关口。高质量的分词器能忠实保留语料中的语义单元,让模型用更少的 token 理解更多的信息;而糟糕的分词器会把数据切得支离破碎,即使后续模型再强大,也难以从破碎的输入中恢复语义——这正是“垃圾进,垃圾出”在分词层的体现。
- 字符总数原始文本的字符数量,是压缩的基准
- 子词总数经过 BPE 切分后得到的 token 数量
- 比值含义两者相除,r 大于 1 表示有压缩效果,r 越大单 token 信息密度越高
AI 应用场景
多语言数据统一分词
阶段:数据清洗与预处理
解决的问题:传统按空格和词法切分在多语言语料上捉襟见肘——中文没有空格,日语词形多变,阿拉伯文有复杂的词缀。用字节级 BPE,所有语言先统一转成 UTF-8 字节,再合并出跨语言的子词,一套词表通吃。
具体案例:某跨境电商平台收集了中、英、日、西四国用户评论,此前分别维护四套分词规则,模型多语言评测 F1 仅 0.71。改用字节级 BPE 统一分词后,词表共享率提升,F1 升至 0.83,训练数据利用率显著提高。
垂直领域词表定制
阶段:模型训练前的 Tokenizer 训练
解决的问题:通用 BPE 词表在垂直领域覆盖不足——医学术语、法律条文、代码变量名中的高频组合,在通用语料里可能从未出现过,导致序列被切得零碎。
具体案例:某医疗 AI 公司用 20 万份电子病历训练专属 BPE 词表,将“高血压”“心律失常”等专业术语合并为单个 token。微调后的模型在医疗 NER 任务上 F1 提升 5.2 个百分点,推理速度也因序列缩短而加快。
推理成本优化
阶段:模型部署与推理优化
解决的问题:Transformer 的计算量与序列长度平方成正比,序列每缩短 10%,推理成本就明显下降。BPE 的压缩能力直接转化为 GPU 算力的节省。
具体案例:某大模型服务商在接入长文档问答功能时,通过重新训练 BPE 词表,将平均序列长度从 4120 token 压缩到 3570 token,响应延迟降低约 24%,在保持答案质量不变的前提下,把每百万 token 的服务成本压低了近三成。
Python 演示
from collections import Counter def bpe_train(corpus, num_merges): """训练BPE,返回最终词汇表和合并规则列表。""" # 初始词汇表:语料中所有独立字符 vocab = set(''.join(corpus)) # 每个词切成字符序列,末尾加结尾标记 words = [list(w) + ['</w>'] for w in corpus] merges = [] for step in range(num_merges): # 统计所有相邻字符对的出现次数 pairs = Counter() for word in words: for i in range(len(word) - 1): pairs[(word[i], word[i + 1])] += 1 if not pairs: break # 找出频率最高的相邻对 best = max(pairs, key=pairs.get) merges.append(best) # 将语料中所有该字符对合并成一个新子词 new_words = [] for word in words: merged, i = [], 0 while i < len(word): if i < len(word) - 1 and word[i] == best[0] and word[i + 1] == best[1]: merged.append(best[0] + best[1]) i += 2 else: merged.append(word[i]) i += 1 new_words.append(merged) words = new_words vocab.add(best[0] + best[1]) print(f'第{step + 1}次合并:{best[0]} + {best[1]} -> {best[0] + best[1]}') return vocab, merges corpus = ['low', 'lower', 'lowest', 'newer']vocab, merges = bpe_train(corpus, 5)print('最终词汇表:', sorted(vocab))print('合并规则:', merges)
pairs[(word[i], word[i + 1])] += 1用 Counter 统计所有相邻字符对的共现次数,这就是 BPE 的“频率统计”环节best = max(pairs, key=pairs.get)从所有相邻对中选出出现次数最高的那一个,作为本轮合并对象merged.append(best[0] + best[1])把两个字符拼接成一个新的子词字符串,例如 'l' + 'o' 得到 'lo'i += 2合并成功后跳过两个字符,避免同一个字符被重复合并vocab.add(best[0] + best[1])每合并一次就把新子词加入词汇表,词表容量随之加一words = new_words用合并后的序列替换旧序列,下一轮统计基于最新的分词状态
常见误区
误区我以为 BPE 每次迭代就是数一数哪个字符出现最多,比如语料里 'e' 最多,就先把 'e' 合并掉。
正解BPE 统计的是“相邻字符对”的频率,不是单个字符的频率。它看的是 x 和 y 紧挨着出现的次数,比如 'l' 和 'o' 总是一起出现才合并成 'lo'。单个字符出现再多,只要没有固定搭档,就不会被合并。
为何常见:因为算法名字叫“字节对编码”,直觉上容易理解为“对单字节编码”,忽略了核心操作对象是“相邻的一对”。
误区词表越大,能表示的子词就越多,模型的理解能力一定越强,所以训练时把词表设得越大越好。
正解词表增大会同时带来三个代价:嵌入层参数成倍膨胀、显存占用上升、训练样本的稀疏性加剧。而且词表大到一定程度后,新加入的多是低频长尾子词,对压缩比贡献微乎其微。GPT-2 用 5 万左右的词表,是权衡后的选择。
为何常见:人们习惯性地认为“能力与规模正相关”,但忽略了词表带来的参数成本和收益递减规律。
误区用了 BPE 之后,任何生词都能被切分,所以未登录词问题被彻底消灭了。
正解BPE 只是把未登录词的粒度从“词”降到了“子词或字符”。单词级的 OOV 问题确实大幅缓解,但如果一个词里的字符在训练语料中从未出现(比如某些生僻汉字),拆到字符级还是会失败。字节级 BPE 进一步把兜底层降到 256 种字节,几乎不会失败,但严格说仍依赖 UTF-8 编码覆盖。
为何常见:看到 BPE 能拆英文生词,就想当然地认为它的覆盖范围是无限的,忽略了“兜底单元”本身也有边界。
误区BERT 用的 WordPiece 和 GPT 用的 BPE 不都是合并高频字符对吗?我觉得它们完全一样。
正解两者虽然都迭代合并子词,但筛选标准不同。BPE 只看原始频率,谁出现次数多就合并谁;WordPiece 用概率比(互信息思想)衡量共现是否超出偶然水平。结果上,WordPiece 更倾向于合并“有强绑定关系”的对,BPE 更偏保守地跟随频率分布。
为何常见:两者的训练流程看起来很相似,都是“统计-合并-重复”的循环,容易被表面流程迷惑,忽略了合并依据的差异。
误区分词就是把文本切一切,对模型最终效果影响不大,随便选个 tokenizer 就行。
正解在数据为中心 AI 范式中,分词是数据质量的第一道闸门。切分粒度直接影响序列长度、信息密度和语义完整性——同一句话用不同的词表切分,模型看到的 token 序列完全不同,训练效率和下游任务表现都会受牵连。
为何常见:因为分词发生在“数据进入模型之前”,它的影响是间接的,容易被从业者低估。
面试题
Q1. BPE 算法每次迭代会合并哪一对字符?
- A.随机挑选的一对相邻字符
- B.语料中出现频次最高的相邻对← 正确答案
- C.字典序排在最前面的字符对
- D.在文本中首次出现的相邻对
答案:B。BPE 的核心规则就是“谁高频,谁先合并”。就像乐高工厂优先预拼最常出现在图纸里的组件一样,BPE 统计所有相邻字符对的出现次数,每轮选出频次最高者合并。随机、按字典序、按首次出现的位置都不是 BPE 的策略。
Q2. BPE 训练开始时,初始词汇表包含什么?
- A.语料中所有出现过的完整单词
- B.语料中所有独立的字符单元← 正确答案
- C.从外部词典挑选的部分单词
- D.人工指定的若干字符组合
答案:B。BPE 的训练起点是“拆碎到不能再碎”——语料中所有独立字符构成初始词汇表,相当于乐高目录的初版只收录最基础颗粒。完整单词、外部词典、人工字符组合都是后续合并阶段才出现的东西。
Q3. GPT-2 使用的字节级 BPE 与经典 BPE 的关键区别是什么?
- A.基础单元从字符换成 UTF-8 字节← 正确答案
- B.合并规则变成随机采样过程
- C.不再统计字符对出现频率
- D.词汇表必须包含完整单词
答案:A。经典 BPE 以字符(如字母)为最小单元,遇到生僻字符会失效;GPT-2 把所有文本先编码为 UTF-8 字节,以 256 种字节为基础单元执行合并。合并规则依然是频率统计,没有随机采样,也不要求词表包含完整单词。
Q4. 关于 BPE 的解码过程,以下说法正确的是?
- A.需要先训练一个语言模型
- B.将子词按字节数大小排序后拼接
- C.直接拼接所有子词即可还原原文← 正确答案
- D.解码必须依赖词性标注的信息
答案:C。BPE 编码是可逆的:每个子词都是原文本的连续片段,解码时按顺序直接拼接就能无损还原原文,就像把乐高组件按拼装顺序拆下来再原样装回去。不需要语言模型、排序或词性标注参与。
Q5. BPE 与 WordPiece 最核心的差异在于?
- A.合并依据从频率换成了概率比得分← 正确答案
- B.BPE 无法处理英文之外的语料
- C.WordPiece 不需要初始字符表
- D.BPE 只合并单独的单个字符
答案:A。BPE 以相邻对的原始频率为合并依据,而 WordPiece 用 freq(xy) 除以 freq(x)·freq(y) 的概率比得分,优先合并“共现超出偶然”的组合。两者都支持多语言、都需要初始词表,BPE 也不限于单字符合并。
Q6. BPE 子词切分带来的好处包括哪些?
- A.控制词表大小避免无限膨胀← 正确答案
- B.缓解未登录词导致的 OOV 问题← 正确答案
- C.缩短序列长度提升计算效率← 正确答案
- D.让模型直接理解单词语义
答案:ABC。BPE 用有限数量的子词组合表示无限多的单词,词表可控;生词可拆成已知子词,OOV 大幅缓解;高频词合并成单 token,序列变短、计算加速。但 BPE 只是切分工具,不负责让模型“理解”语义,D 错在把分词等同于语义理解。
Q7. 当语料中出现词表外的单词时,BPE 通常会怎么做?
- A.丢弃该单词不参与后续训练
- B.拆解为已知的字节或字符← 正确答案
- C.随机替换成相近的英文单词
- D.报错并终止程序的运行
答案:B。BPE 的兜底策略是“向下拆解”:词表中没有整个单词,就找最长的子词;子词也没有,就拆到单个字符,甚至拆到 UTF-8 字节。这种设计让任何生词都能被编码,而不是丢弃、替换或报错。
Q8. 关于 BPE 词汇表的构建过程,以下说法正确的有哪些?
- A.初始词表包含语料的全部独立字符← 正确答案
- B.每次合并让词表容量恰好增加一个← 正确答案
- C.总的合并次数由人工预先设定← 正确答案
- D.最终词表大小与语料规模成反比
答案:ABC。BPE 从字符集出发,每合并一次就新增一个子词条目,因此 |V| = 初始字符数 + 合并次数,A、B 正确;合并次数通常由人根据目标词表大小设定,C 正确。词表大小与语料规模没有“成反比”的固定关系,D 错误。
Q9. SentencePiece 相比经典 BPE 的独特设计是什么?
- A.将空格也视作普通字符参与合并← 正确答案
- B.完全不使用频率统计的方法
- C.只能用于中文文本的切分
- D.词表在推理阶段动态扩展
答案:A。经典 BPE 默认按空格分词后处理词内部,空格被排除在统计之外;SentencePiece 把空格当作一个普通字符()参与合并,这样分词结果可无损还原,适用于日语等无空格语言。它依然用频率统计,不只支持中文,词表在训练后冻结。
Q10. 关于词表大小与序列长度的权衡,以下说法正确的有哪些?
- A.词表越大单个 Token 信息越丰富← 正确答案
- B.词表过大会导致嵌入层参数暴涨← 正确答案
- C.词表偏小会让序列变得冗长← 正确答案
- D.词表大小与训练速度没有关系
答案:ABC。词表越大,高频词越可能成为独立 token,单 token 承载信息越多、序列越短;但嵌入矩阵的行数等于词表大小,词表翻倍参数也翻倍,B 正确;词表偏小则长词被拆碎,序列变长,Transformer 的计算量随序列长度平方增长,训练速度因此受影响,D 错误。