BPE Tokenizer:从训练规则到推理切分的完整理解
BPE,Byte Pair Encoding,是大语言模型中最经典的一类子词切分方法。它的表面形式看起来只是"把文本切成 token",但如果往深处看,BPE 实际上定义了模型如何接收离散输入、如何控制序列长度、如何在词表规模与泛化能力之间取得平衡。很多初学者对 BPE 的疑惑主要集中在两个地方:第一,它训练时到底在"学"什么;第二,它推理时到底是如何应用这些规则的。本文就围绕这两个问题,把 BPE 从原理到工程实现串起来说明。
一、为什么大模型需要 BPE
模型本身并不直接处理字符串。无论输入是一句话、一段代码,还是多语言混合文本,最终都必须先被映射成一串离散编号,再进入 embedding 层,之后才会进行 attention、FFN、MoE 等计算。这个"把原始文本变成离散编号"的过程,就是 tokenizer 的职责。
最直接的做法有两种。一种是按"整词"切分,也就是一个词就是一个 token。这个方案看似自然,但现实里问题很多:词表会非常大,新词、专有名词、拼写变体和低频表达层出不穷,模型很容易遇到 OOV,也就是词表外词。另一种做法是按字符切分,这样任何文本都能处理,但序列会变得很长,本来一个单词只需要一个 token,结果可能被拆成多个字符,导致上下文长度急剧增加,计算成本也明显上升。
BPE 的价值,就在于它试图在这两种极端之间找到一个折中。它不要求整个词必须进词表,但也不退回到完全逐字符处理。它会自动从语料中学习哪些子串是高频、稳定、值得保留的,于是最后得到的是一套"子词"粒度的表示方式。这样既能控制词表规模,又能保持不错的泛化能力,还能让序列长度相对可控。这也是为什么 BPE 能长期成为主流 tokenizer 方案之一。
二、BPE 的核心思想到底是什么
BPE 的核心非常简单:从最小符号出发,反复把训练语料中高频出现的相邻符号对合并成一个新的符号。这个过程不断重复,最后得到一套词表,以及一套明确的 merge 规则。所谓"merge 规则",本质上就是告诉 tokenizer:哪些相邻符号可以合并,合并的优先级顺序是什么。
很多人第一次理解到这里时,会把训练阶段和推理阶段混在一起。实际上,BPE 可以明确分成两个阶段。
训练阶段的任务是:从大规模语料中统计高频相邻 pair,不断做贪心合并,直到达到预设的 merge 次数或者目标词表大小。这个阶段真正"学"到的是一套规则。
推理阶段的任务则完全不同。它不会重新统计当前输入的高频 pair,也不会根据当前这句话重新学习。它只会把训练时已经得到的 merge 规则拿出来,按固定优先级执行。因此,BPE 可以用一句话概括:
训练时按频率学习规则,推理时按规则执行切分。
这句话看似简单,但几乎概括了 BPE 最重要的理解边界。
三、BPE 在训练阶段具体做了什么
为了说明 BPE 的训练过程,可以先从一个最经典的字符级例子出发。假设训练语料里有这些词:
- low
- lower
- newest
- widest
在最初状态下,BPE 会把它们写成最细粒度的符号序列,例如:
l o wl o w e rn e w e s tw i d e s t
然后统计所有相邻 pair 的出现频率。比如这一轮可能发现:
l o很常见o w也常见e s也常见s t也常见
如果当前频率最高的是 l o,那么这一轮就把 l o 合并成一个新符号 lo。语料随之更新,原来的 l o w 变成 lo w,l o w e r 变成 lo w e r。更新后再重新统计新的 pair,继续挑当前最高频的做下一轮合并。这个过程不断迭代,最后可能又学到 lo w -> low、e s -> es、es t -> est 等规则。
从算法角度看,BPE 是一种典型的贪心方法。每一轮只关心当前最频繁的那个相邻 pair,而不追求全局最优。也正因为如此,它实现简单、行为稳定、训练成本较低,非常适合做工程化部署。
这里还需要说明一个重要问题:BPE 训练时的"停止条件"是什么?通常有两种设定方式。一种是直接规定 merge 次数,比如做 30k 次、50k 次 merge;另一种是规定最终词表大小,比如构建 32k、50k 或 100k 的 vocabulary。二者本质上是同一个方向:都是控制最终 tokenizer 的粒度。词表越大,平均 token 越长,序列通常越短;词表越小,切分通常越细,序列会更长,但泛化性可能更强一些。
四、推理阶段到底怎么应用 merge 规则
推理阶段最容易让人误解。很多人会想:既然训练时是不断合并高频 pair,那么推理时遇到一条新句子,是不是也要重新统计这条句子里最常见的 pair,再动态合并?答案是否定的。
推理时,tokenizer 不会重新统计频率。它只会读取训练阶段已经固定下来的 merge 规则及其优先级,然后按这套规则去处理新输入。也就是说,推理时的 BPE 并不是一个"在线学习"的过程,而是一个"规则执行"的过程。
例如训练阶段已经得到如下 merge 顺序:
l o -> lolo w -> lowe s -> eses t -> est
那么当输入 lowest 时,编码过程就可以理解为:
- 初始:
l o w e s t - 合并
l o:lo w e s t - 合并
lo w:low e s t - 合并
e s:low es t - 合并
es t:low est
最后得到的结果就是 low + est。这里起决定作用的不是当前输入里哪个 pair 更"频繁",而是 merge 规则文件中谁的 rank 更高。换句话说,推理时看的不是"输入统计",而是"历史学习结果"。
这也引出一个关键问题:推理阶段应用 merge 规则时,会不会也有次数限制,比如最多 merge 8k 次?
答案是:不会有一个面向当前输入的"额外 merge 次数限制"。训练阶段设定的 8k、32k、50k 之类,控制的是整个 tokenizer 的词表规模或 merge 规则总数;而推理阶段只是在这套固定规则里查找并应用。对一条具体输入来说,它会一直合并到"无法继续合并"为止,而不是说"这句话最多只允许 merge 100 次"或者"最多按 8k 规则跑一遍"。
所以要把这两个层面的"限制"区分开:
| 层面 | 作用 |
|---|---|
| 训练阶段的 merge 次数 / 词表大小 | 决定 tokenizer 本身有多少规则、粒度多粗 |
| 推理阶段的规则应用 | 对当前输入一直合并到不能再合并为止 |
推理阶段真正会额外影响结果的,通常不是 merge 次数,而是别的因素,比如模型的最大上下文长度、文本是否被截断、输入是否含有特殊符号、是否使用了 byte-level 方案等。但这些已经不是 BPE 的 merge 规则本身了。
五、merge 规则保存在哪里,模型本身是否"知道怎么 merge"
BPE 训练完之后,merge 规则一定会以某种形式保存下来,否则推理阶段就无法复现训练时的切分方式。最常见的保存形式是两个文件:一个词表文件,一个 merge 文件。例如在很多实现里会看到:
vocab.jsonmerges.txt
其中,vocab.json 保存 token 到 id 的映射,也就是最后每个 token 对应哪个整数编号;merges.txt 保存 merge pair 及其顺序,而顺序本身就代表优先级。谁在文件里排得更前,谁的 rank 就更高。
当然,现代框架里不一定总是分成两个文件。有些实现会把所有 tokenizer 信息都打包进一个 tokenizer.json 里。但逻辑上不变:一定存在一份词表,以及一份 merge 规则和对应优先级。
这时还需要再强调一个概念区分:知道怎么 merge 的通常是 tokenizer,而不是模型本体。
模型本体负责的是 embedding、attention、FFN、MoE 这些神经网络计算,它接收的输入已经是 tokenizer 处理后的 token id。严格来说,模型一般并不直接读取原始字符串,更不会在前向过程中亲自执行 BPE merge。真正执行 merge 的,是模型前面的 tokenizer 模块。
因此,更严谨的表述应当是:tokenizer 在训练阶段学习并保存了 merge 规则,推理时按这些规则把文本切成 token,模型只消费这些 token 对应的 id 和 embedding。
六、BPE 的优点、局限与工程意义
BPE 之所以被广泛使用,首先是因为它非常实用。它避免了整词切分的巨大词表问题,也避免了字符切分带来的超长序列问题。对于新词,它往往可以拆成已有的子词,因此泛化能力较强。对于常见词,它又能把高频片段稳定保留下来,所以效率不错。整个训练和推理逻辑都不复杂,部署成本低,行为可解释性相对较好。
但 BPE 也有明确局限。第一,它本质是统计方法,不理解语义。它合并的是高频字符串片段,而不是语言学上"最合理"的词或语素。第二,它是贪心构造,不保证全局最优。第三,它对训练语料分布很敏感,不同语料训练出来的 tokenizer 可能差异很大。第四,如果使用 byte-level BPE,虽然覆盖性更强,但切分结果有时会更不符合人类直觉。
从工程角度看,BPE 的地位并不只是一个"预处理小模块"。它实际上决定了三件非常关键的事:第一,模型输入如何离散化;第二,平均序列长度大致是多少;第三,embedding 层和输出层的词表维度该如何设计。也就是说,BPE 不只是"把字符串切一切",它会直接影响训练成本、推理效率和模型表现。
七、一个容易记住的总结
如果要把 BPE 压缩成几句最核心的话,可以记成下面这组结论:
- BPE 是一种子词切分方法,本质上通过合并高频相邻符号构建词表。
- 训练阶段按频率做 merge,得到固定规则和优先级。
- 推理阶段不会重新学习,也不会重新统计频率,只会执行已保存的 merge 规则。
- 训练阶段的 8k、32k、50k 等限制,控制的是 tokenizer 规模;推理阶段没有面向单条输入的额外 merge 次数上限,而是一直合并到不能再合并为止。
- merge 规则通常保存在 tokenizer 文件中,而不是模型权重中。
- 真正"知道怎么切"的是 tokenizer,模型本体只接收切分后的 token 表示。
如果再进一步概括成一句话,可以写成:
BPE 的本质,是用训练阶段学到的一套子词合并规则,把开放文本空间压缩成一个可控、可泛化、适合神经网络建模的离散 token 空间。
这也是为什么在今天的大模型系统里,虽然架构已经从传统 Transformer 发展到了更复杂的多层堆叠、MoE、长上下文、多模态输入,但 tokenizer 这一步仍然是整个系统中最基础、最关键的入口之一。