想象你经营着一家乐高积木工厂,每周要交付城堡、飞船、游乐园等几百款不同的模型。一开始,每款模型都从单颗粒积木开始拼装,老师傅们累得直不起腰,仓库也堆满了形形色色的零件。后来厂长发现一个窍门:不同订单里“窗户+门框”“车轮+车轴”这些组合反复出现,为什么不提前把最畅销的组合铸成一体化的预制件呢?工期立刻缩短,返工也变少了。这正是 BPE(字节对编码)的智慧——它是今天几乎所有大语言模型(GPT、BERT、Llama)在读取文本前的“第一道工序”。 BPE 先把文本拆成最小的“字符积木”,再统计哪些相邻字符对出现得最频繁,然后像开模具一样把它们逐一合并成“子词预制件”。最终构建出一本不大不小的“词表模具库”:既能覆盖绝大多数真实词汇,又不怕碰到词典里没有的生僻词——任何生词都能拆成预制件和散装颗粒拼出来。本教程会带你从“为什么要子词”讲到 BPE 的流水线、编码解码、预训练模型中的应用和变体算法,最后用 Python 亲手实现一个迷你 BPE,彻底看懂 GPT 分词器背后的“预制件工厂”。
对 AI 从业者:BPE 是 NLP 管线中最底层的地基,掌握它能让你在模型选型、Tokenizer 调优和多语言部署中做出真正专业的决策,是通往大模型内部原理的必经之门。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
什么是BPE
你经营的乐高工厂每周要交付几百款不同的模型,一开始每款都从单颗粒拼起,老师傅们腰都直不起来。厂长发现,不同订单里“窗户+门框”“车轮+车轴”这些组合反复出现,干脆提前把最畅销的组合铸成一体化的预制件——工期缩短,返工也少了。BPE 把这套“预制件思维”带进了自然语言处理。
BPE 全称 Byte Pair Encoding(字节对编码),最早是 Gage 在 1994 年提出的数据压缩算法。2016 年,Sennrich 等人把它引入 NLP,用来构建“子词词表”。它的做法和工厂一模一样:先把语料拆成最小单位(字符或字节),统计所有相邻字符对的出现频率,把最高频的那对“预装”成一个新词元。重复这个过程,就得到一套子词预制件,也就是子词词表。
为什么需要子词?词级方案(按空格切分)词表动辄几十万,还频繁遇到词典里没有的未登录词;字符级方案不会漏词,但一个单词被拆成一串字符,序列变长、计算开销大,语义单元也太碎。BPE 在两者之间取平衡:高频词整体保留,低频词拆成常用子词,实在不认识的词还能拆到字符兜底。从压缩角度看,它把 “lowest“ 从 6 个字符压成 ”low” + "est" 两个词元,本质是一种词典压缩。
工厂只给“卖得最好”的组合开模具,BPE 只给“出现最多”的字符对合并——每一次决策都由数据说了算,这就是子词词表诞生的全部秘密。
- 候选对集合 P语料中所有相邻字符或词元的组合构成的集合,比如 "l o"、"o w"、"e s" 都是候选对。
- 频次统计 count(p)统计每个候选对在全语料中出现的总次数,统计时按每个词的词频加权。
- 最优对 p\*argmax 表示取使 count 最大的那一对,作为本轮合并的对象,合并后它会变成一个独立的词元。
词表与OOV
模具不是白造的:每副模具都占仓库储位、需要保养维护。厂长最头疼的是储位到底留多少——留太多,库存成本和开模费爆炸;留太少,热销件缺货,订单只能干等。BPE 的聪明之处在于,“模具数量”完全由“合并次数”这一个旋钮控制。
词表大小等于初始字符集加上合并次数。初始字符集通常只有几百个(字符或字节),每合并一次就多一个新词元。想要覆盖更复杂的文本,就多合并几轮;想要省内存,就少合并几轮,这就是词表规模与覆盖能力的平衡。OOV(Out-of-Vocabulary,未登录词)指模型词表里没有的词——传统词级方案中,专有名词、拼写变体、emoji 都会触发 OOV。而在 BPE 下,任何生词都能被切分为词表中的子词,最坏情况到单字符兜底,OOV 率趋近于零,词表却能保持在几万级别的可控范围。
老板要开多少副模具直接决定仓库多大——BPE 把词表大小变成了一个由合并轮数精确控制的旋钮,“词表预算”从此不再失控。
- 初始字符集 |V_char|训练语料中去重后的字符(或字节)总数,比如中英文混合语料的字符集通常几百到几千。
- 合并轮数 n_merge训练时预设的合并次数,每执行一次合并就向词表新增一个词元。
- 词表大小 |V|最终子词词表的规模,等于初始字符数加合并轮数;这个公式让“词表预算”变得精确可控。
算法流程
铸模车间有条雷打不动的四班流程:先盘点零件,再统计销量,接着开新模具,最后更新产品图纸。第二天必须重新盘点,因为新模具诞生后,畅销组合可能变了。BPE 的训练流程正是这个循环。
具体四步:第一步,初始化——把语料的每个词拆成字符序列,并加上单词结束符 </w> 用于标记词边界。第二步,统计——扫描全部词,统计每个相邻字符对的频次,统计时乘以词频加权。第三步,合并——选出频次最高的字符对,合并成一个新词元,替换语料中所有出现的位置。第四步,停止——达到预设合并轮数后输出合并规则表。关键点在于:每轮合并后必须重新统计,因为新词元和邻居又形成了新的相邻对,最优组合会动态变化。
上周“窗+框”组合卖爆合并成预制件之后,这周“框+玻璃”可能成为新销冠——BPE 每轮都重新“看数据”,而不是抱着一张旧统计表不放。
- 初始序列 seq⁽⁰⁾每个词被拆成独立字符的序列,如 “low“ 变成 ”l o w </w>”。
- 每轮统计 f_t(p)在第 t 轮重新统计所有相邻对的频次,注意是新语料上的统计。
- 合并操作 Merge把最优对 p_t\* 的所有出现替换为单个词元,如 “e s“ 合并为 ”es”。
- 迭代循环重复“统计-合并”,直到达到预设的合并轮数 n_merge,输出合并规则表。
编码与解码
有了预制件,工厂还得教会工人两件事:新图纸怎么按模具库组装(编码),退货怎么拆箱验收(解码)。组装时优先找整块预制件,找不到就找最大的局部,实在没有就用散装颗粒;验货时把所有构件按顺序拼回去,原样还原即可。
编码阶段,给定任意句子,BPE 按词表从左到右做贪心最长匹配切分——优先匹配最长的词元,匹配不到就降级到更短词元,最终一定能拆到字符级,因此永远不会遇到“拆不动”的词。输出是词元 id 序列,交给模型。解码阶段,把 id 查回词元字符串,按序拼接(去掉边界标记)即还原原词。训练得到的合并规则表(哪对合并成了哪个词元)是整个过程的“模具清单”,编码解码都要用到它,所以 BPE 是一个无损、可逆的过程。
组装工永远能找到零件——大到预制件、小到散颗粒,总能拼出图纸上的模型。这就是 BPE 面对“从未见过的新词”也毫不慌张的底气。
- 词元序列编码阶段输出的 k 个子词,如 ["low", "er"]。
- 拼接操作 ⊕字符串直接连接,如 "low" ⊕ "er" = "lower"。
- 还原结果 w拼接后剔除词边界标记,得到与原词完全一致的单词,证明 BPE 可逆。
预训练模型
乐园要办超级城堡展,几十家参展商共用一套零件目录。游客再多、工期再紧,大家都能按统一标准生产,任意订单都不会因“缺零件”卡壳。GPT、BERT 这些大模型,就是城堡展的参展商。
GPT 系列使用 Byte-level BPE:从字节级开始建词表,固定词表大小 50,257,任何语言、任何符号(包括 emoji)都能切分。BERT 使用的是 BPE 的变体 WordPiece,词表约 3 万。Hugging Face 的 tokenizers 库把这些算法工程化,提供现成的 ByteLevelBPETokenizer 等组件,Tokenizer 生态由此成为预训练管线的标准一环。多语言混合文本(如中英混排的新闻、代码与注释混合的文件)也能被统一词表处理,不再需要每种语言各自造词表。
统一零件目录让整个乐园高效运转——统一词表让所有预训练任务复用同一套 BPE 模具,这正是 GPT 与 BERT 能处理无限多样文本的共同基石。
- 输入句子 s原始文本,未经任何修改的字符串。
- BPE 切分把句子映射为词元序列,例如拆分出 "the"、"low"、"est" 等词元。
- 嵌入向量 e_i每个词元通过查表映射为 d 维稠密向量,送入神经网络继续处理。
改进变体
隔壁积木厂觉得“光看销量”太粗放,推出了自己的决策标准;还有工厂干脆不做预切割,把整块板材直接送进车间。BPE 家族由此开枝散叶,衍生出多个改进版本。
WordPiece(BERT 采用)换了一种选择标准:不再看组合的绝对频次,而是看“绑定程度”,选择分数最高的对合并。Unigram(T5 等模型可选)则反过来做——先给出一个很大的候选词表,再用 EM 算法迭代剪枝,每步去掉对语言模型损失贡献最小的词元。SentencePiece 更激进:不做预分词,直接把整句(含空格)作为字符流,用特殊符号标记空格,天然支持中文、日语等无空格语言。它们各有取舍:BPE 简单可控,WordPiece 更讲究绑定关系,Unigram 词表质量高但训练更昂贵。
光看销量会误判:超高频的 “o“ 和 ”t“ 从不并肩出现,WordPiece 的比值打分体现了”高频共现“与”真正绑定”之间微妙的区别。
- 共现频次 f(ab)词元 a 与 b 拼接后(即组合 ab)在语料中出现的总次数。
- 独立频次乘积分母惩罚“各自都超高频”的组合——比如 “t“ 和 ”h” 单看都很多,但未必总在一起。
- 比值决策分数越高说明绑定越紧密,优先合并;直观理解就是“不是卖得多,而是绑得紧”。
实际应用
预制件方案从城堡车间推广到主题酒店、机器人餐厅、游客纪念品店,整个乐园的供应链都围绕同一套零件目录运转。应用越广,BPE 的价值越凸显。
在机器翻译中,低资源语言(如斯瓦希里语)语料稀缺,词级分词覆盖率极差,BPE 子词让翻译模型依然能拼出绝大部分词汇。在多语言混合场景下,一套共享词表同时服务中英日阿多种语言,避免每种语言独立建表的资源浪费。在代码生成模型中,代码是自然语言、变量名、符号的混合体,BPE 天然拆解 "camelCase"、"snake_case" 这类复合词,显著提升代码补全质量。实际部署中,工程师常用压缩率来评估词表质量——分词后序列越短,说明子词切分越合理。
从单一车间到全乐园供应链,“高频复用”的思想穿透了所有生产环节。BPE 也一样——从机器翻译到代码模型,它始终是文本进入大模型前的第一道、也最关键的一道工序。
- 字符总数 N_char分词前语料的字符(或字节)总数,代表“散装颗粒”的原始数量。
- 词元总数 N_token分词后的词元数,也就是模型实际要处理的序列长度。
- 压缩率 r大于 1 说明子词起到了压缩序列的作用;压缩率越高,训练与推理的效率越高。
AI 应用场景
多语言机器翻译的分词预处理
阶段:数据预处理 / Tokenization
解决的问题:传统词级分词在低资源语言上词表覆盖不足,OOV 高频出现,翻译质量大打折扣。BPE 子词在字符层面兜底,任何拼写组合都能被切分,让共享词表同时覆盖多种书写系统。
具体案例:一个多语言翻译系统用 SentencePiece + BPE 在一批混合语料上训练出共享子词词表,同一词表同时服务中文、英语、阿拉伯语,词表体积比语言独立建表缩小约 60%,低资源语言对的 BLEU 提升显著。这就像乐高工厂为整个乐园供一套通用零件目录,而不是每个园区各建一座仓库。
大模型 Tokenizer 训练与选型
阶段:预训练前的文本预处理
解决的问题:GPT、BERT、Llama 都需要固定大小的词表,直接用词级切分会导致词表爆炸或 OOV 频繁。BPE 将词表大小与合并轮数解耦,让工程师能精准控制“模具预算”。
具体案例:预训练团队用 Hugging Face tokenizers 库在 50GB 多语言语料上训练 ByteLevelBPETokenizer,词表设为 50,257;训练时用 OOV 率与压缩率两个指标评估,调整合并轮数后,下游任务的训练吞吐提升了约 15%。这套评估流程和乐高工厂用“缺货率 + 组装时间”衡量模具方案如出一辙。
代码生成模型的混合文本处理
阶段:数据清洗 / 分词
解决的问题:代码文件是“自然语言 + 变量名 + 符号”的混合体,词级切分对 "getUserById"、"learning_rate" 这类复合词无能为力;BPE 子词能自然拆解它们,让模型跨语言(代码 + 注释)统一建模。
具体案例:CodeLlama 使用 byte-level BPE 分词,变量名 “getUserById“ 被拆成 ”get”、"User"、"By"、“Id“ 等已知子词,代码补全的准确率因此明显优于按词或按字符的 baseline。这与预制件工厂用同一套零件拼装”城堡“和”机器人”是同一个道理。
Python 演示
from collections import Counter def get_pair_stats(vocab): pairs = Counter() for word, freq in vocab.items(): symbols = word.split() for i in range(len(symbols) - 1): pairs[symbols[i], symbols[i + 1]] += freq return pairs def merge_pair(pair, vocab): bigram = ' '.join(pair) merged = ''.join(pair) out = {} for word, freq in vocab.items(): out[word.replace(bigram, merged)] = freq return out def train_bpe(corpus, num_merges): vocab = {} for word, freq in corpus: vocab[' '.join(list(word)) + ' </w>'] = freq merges = [] for _ in range(num_merges): pairs = get_pair_stats(vocab) if not pairs: break best = max(pairs, key=pairs.get) merges.append(best) vocab = merge_pair(best, vocab) return merges, vocab if __name__ == '__main__': corpus = [('low', 5), ('lower', 2), ('newest', 6), ('widest', 3)] merges, vocab = train_bpe(corpus, 8) print('Learned merge rules:') for i, m in enumerate(merges, 1): print('Round %d: %s -> %s' % (i, ' '.join(m), ''.join(m))) print('Final vocab (%d entries):' % len(vocab)) for w in sorted(vocab): print(' ', w)
vocab[' '.join(list(word)) + ' </w>'] = freq初始化:把每个词拆成带空格的字符序列并追加词尾标记,比如 "l o w </w>"pairs[symbols[i], symbols[i + 1]] += freq统计相邻字符对时按词频加权,"l o" 在 low、lower 各出现一次,共计数 7best = max(pairs, key=pairs.get)从所有相邻对中选出频次最高的那对,等价于公式里的 argmaxword.replace(bigram, merged)把词中所有 “e s“ 替换为合并后的词元 ”es”,完成一次合并merges.append(best)把本轮合并规则记录下来,这份规则表就是编码和解码共用的“模具清单”
常见误区
误区反正合并能压缩文本、提升覆盖,那就多合并几千轮,词表越大覆盖越全,模型效果一定越好。
正解词表大小 = 初始字符数 + 合并轮数,合并越多词表越大,Embedding 层和输出 Softmax 的内存与算力开销都会上升;同时超大词表会稀释每个词元的训练频次,低频词元反而学不好。工业界通常控制在 32k 到 250k 之间。模具库不是越大越好——每副模具都占仓库储位,还要定期维护。
为何常见:“覆盖越全越好”的直觉很自然,但忽略了工程上存储与统计稀疏性的代价。
误区BPE 把 "un"、"happy"、"ness" 合并出来,说明它懂得词根和构词法,子词就是有语义的词素。
正解BPE 完全基于频次统计,只做无损压缩,不理解任何语义。它合并出的子词如 "est"、“lo“ 只是高频共现的形式组合,未必对应语言学的词根。真正的语义来自后续语言模型的训练,而不是分词阶段。乐高工厂把”窗+框”铸成预制件,是因为卖得好,而不是因为它懂建筑。
为何常见:人看到有意义的子词会本能投射语义,误以为算法也有这种理解。
误区教程里全是英文例子,BPE 从字母开始合并,中文没有空格,肯定用不了。
正解BPE 可以从字节(byte-level)开始,GPT 正是用 byte-level BPE 覆盖全部 Unicode——中文、日文、emoji 都能处理;也可以在粗分词(按空格和标点粗略切分)后的词元上运行。中文即使不做预分词,SentencePiece 方案也能直接处理。
为何常见:英文语料的教学示例太普遍,让人误以为算法绑定拉丁字母。
误区分词把句子变成一串词元 id,看上去像“翻译”了一遍,解码肯定是猜不回去的。
正解BPE 是无损的——解码就是按顺序拼接子词并剔除边界标记,能完整还原原词与顺序,字节级 BPE 连大小写和空格都能复原。真正丢失原始信息的是分词之后的模型编码过程,而不是 BPE 本身。
为何常见:把“分词”和“语义编码”两个阶段混为一谈,误解了 BPE 在管线中的位置。
面试题
Q1. BPE 每一轮迭代合并的对象是什么?
- A.出现次数最高的单个字符
- B.出现次数最高的相邻字符对← 正确答案
- C.出现次数最低的字符对
- D.随机选取的字符对
答案:B。BPE 的核心操作是统计相邻字符对的共现频次,把最高频的那对合并成一个新词元,就像乐高工厂只给最畅销的组合开模具。A 是单个字符而非字符对;C、D 与“最高频合并”的原则相悖。
Q2. BPE 主要解决 NLP 中的什么问题?
- A.模型训练速度过慢
- B.词表过大与未登录词← 正确答案
- C.文本编码乱码问题
- D.数据集标注不足
答案:B。BPE 在词级和字符级之间取平衡,用子词词表同时缓解词表规模爆炸和 OOV(未登录词)问题。它不负责训练加速、字符编码或数据标注。
Q3. 初始字符集大小为 100,执行 200 次合并后词表大小是多少?
- A.100
- B.200
- C.300← 正确答案
- D.20000
答案:C。词表大小 = 初始字符数 + 合并轮数,即 100 + 200 = 300。每次合并只新增一个词元,不会成倍增长,这正体现了 BPE 词表规模的可控性。
Q4. BPE 的解码过程是?
- A.按最长匹配拆分字符串
- B.将词元直接拼接还原← 正确答案
- C.用语言模型预测还原
- D.按空格分割还原
答案:B。解码就是把词元 id 查回字符串、按序拼接并剔除边界标记,过程无损可逆。A 是编码时的贪心最长匹配;C 违反了 BPE 的确定性;D 是词级分词的做法。
Q5. 以下哪个不是子词分词算法?
- A.WordPiece 子词分词
- B.Unigram 子词分词
- C.SentencePiece 子词分词
- D.TF-IDF 文本统计← 正确答案
答案:D。WordPiece、Unigram、SentencePiece 都是子词分词家族成员,分别从不同角度改进 BPE;TF-IDF 是文本加权统计方法,用于关键词提取而非分词。
Q6. 关于 WordPiece 与 BPE 的选择标准,正确的是?
- A.WordPiece 只看组合频率
- B.BPE 看频率的比值
- C.WordPiece 看频率的比值← 正确答案
- D.两者的标准完全相同
答案:C。BPE 直接选最高频字符对;WordPiece 用 f(ab)/(f(a)f(b)) 的比值衡量“绑定程度”,避免被各自超高频但从不共现的字符误导。可以理解为 BPE 看销量,WordPiece 看“绑定紧密度”。
Q7. BPE 的优势有哪些?
- A.能处理未登录词← 正确答案
- B.支持多语言混合文本← 正确答案
- C.压缩词表规模← 正确答案
- D.理解词语的语义
答案:ABC。BPE 用子词切分兜底,任何生词都能拆到字符级,因此能处理未登录词;字节级 BPE 覆盖全部 Unicode,天然支持多语言混合;词表大小由合并轮数控制,远小于词级词表。BPE 是纯统计压缩,不理解语义,故 D 错误。
Q8. BPE 每轮迭代包含哪些步骤?
- A.统计候选对的频次← 正确答案
- B.选出最高频的字符对← 正确答案
- C.合并并更新整个语料← 正确答案
- D.按语义切分每个句子
答案:ABC。每轮流程是“统计相邻对频次 → argmax 选最高频对 → 合并替换并更新语料”,循环直到轮数耗尽。BPE 完全不涉及语义分析,所以 D 错误。
Q9. 关于 BPE 与未见词的关系,以下说法正确的是?
- A.合并轮数越多词表越小
- B.任意生词都能被切分编码← 正确答案
- C.未见词完全无法编码
- D.词表必须覆盖全部字符
答案:B。BPE 编码时从左到右贪心匹配,优先级是长词元 > 短词元 > 单字符,最终一定能拆到字符级兜底,所以任何生词都能编码。A 说反了,合并越多词表越大;C、D 与字符兜底机制相矛盾。
Q10. 关于 Byte-level BPE(GPT 采用的变体),正确的是?
- A.从字节序列而非字符开始统计← 正确答案
- B.只能处理英文等拉丁文本
- C.词表必须包含完整单词
- D.无法编码任意 Unicode 字符
答案:A。Byte-level BPE 把文本先编码为 UTF-8 字节,再对字节序列做合并,因此能覆盖任意 Unicode 字符、emoji 和多语言文本;词表无须包含完整单词。B、C、D 都与字节级设计相矛盾。