BPE 分词算法是什么?大模型把文字切开的规则,70 年前的压缩算法(2026入门)

开头:上一集留了个尾巴

前几天写过一篇 AI 为什么数不清 strawberry 里的 r,那篇讲的是分词器是什么,为什么要用它。评论区有朋友问:那词表到底是怎么训练出来的?两个单词凭什么能被拆成一样的碎块?这个问题背后就是 BPE 算法,也就是 Byte Pair Encoding,字节对编码。这篇把它讲透。

先把结论放在前面:BPE 是一个统计算法,1949 年就有了,1994 年 Philip Gage 在论文《A New Algorithm for Data Compression》里把它用于数据压缩,2015 年 Sennrich 等人把它搬进机器翻译,2019 年 GPT-2 用它训练分词器之后,就成了所有现代大模型的地基。Karpathy 的 minbpe 项目 README 里写得很明确:如今 GPT、Llama、Mistral 用的都是这个算法。70 多岁的老算法,现在管着所有大模型的第一道工序。

先修课:不压缩的压缩算法

BPE 的原始版本和「编码文本」有关,但思路和大模型没关系,就是一个压缩技巧。它的步骤很短:

准备一段文本。从左到右看,发现「th」这对相邻字符出现了很多次。把所有相邻的 th 替换成一个新符号 X,文本变短了。继续找现在出现次数最多的相邻符号对,比如 X 和 e,再替换成新符号 Y。重复这个过程,直到没有高频对,或者达到预定步数。

精髓就一句话:高频相邻的符号,合并成一个新符号。文本里重复出现的东西越多,压缩率越高。Gage 那篇论文的核心贡献就是这个「找最高频相邻对并合并」的循环。

大模型把这招借来,只做了一个关键改动:Gage 压缩完把新符号还原成原文,大模型不还原了,合并出来的符号本身就成了词表。

第一遍:词表是怎么长出来的

训练一个 BPE 词表的完整过程是这样的。

起点是把语料全部用 UTF-8 编码,拆成字节。任何文本在这一步都会变成 256 种基础字节之一组成的序列,这是保底:世界上不存在 UTF-8 表示不了的字,所以再偏门的字符也不会出现「词表里没有」的情况。

然后把每条序列看成符号流,统计所有相邻符号对的出现次数。假设语料里 th 出现了 10 万次,是全场冠军,就把所有 th 合并成一个新符号,加进词表,语料里所有 th 的写法同步更新。接着统计新的相邻对,假设 e 和它左边那个新符号经常挨着,就把它们合并成新条目 the。重复这个循环,直到词表达到预设大小,比如 5 万或 10 万条。

我拿一段小语料手动走了一遍,用「低」「低低低低」这种重复结构演示合并过程,词表从 256 个字节起步,每轮并出一个新条目。整个过程只有两个动作:数,合。没有任何语言学知识,不懂英文也不懂中文,纯粹看谁和谁挨得多。

第二遍:一个英文例子走到底

用一个更直观的例子把整个过程再过一遍。假设语料里只有三句话,都包含 low:

「lower」「lowest」「flower」。

第一轮统计:lo 出现 3 次,最频繁,合并。语料变成 lo + 后面的部分。第二轮:ow 出现 3 次,合并成 ow。第三轮:lo 和 ow 相邻出现 3 次,合并成 low。此时词表里多了 lo、ow、low 三个条目。第四轮开始,er、est、各出现 2 次到 3 次,继续合并出 er、est 之类的碎块。

训练结束后,词表里的条目大概是这样的形态:单个字节有 256 个保底条目,然后是 lo、ow、low、er、est 这些高频碎块。以后遇到新单词 lower,分词器不再从字节级重建,而是查词表里已有的合并规则,直接套用:low 是一个条目,er 是一个条目,切成两块就完事。

这就是为什么 strawberry 在 GPT-4o 的分词器里是三块。模型训练时见过的合并规则认为 st、raw、berry 是高频组合,就按这个切。切法不是语言学意义上的「单词结构」,是统计意义上的「训练语料里谁和谁经常挨着」。

对中文的特别待遇

英文语料里词与词之间有空格,天然给 BPE 提供了边界。中文没有空格,每个汉字都是独立字符,UTF-8 编码后是 3 个字节。所以中文语料跑 BPE,第一轮合并出来的必然是「相邻高频汉字对」,比如「我们」「可以」,然后是三个字的组合,继续往上长。

我实际跑了一个实验:拿 Tiktokenizer(GPT-4o 的 o200k_base 词表)测一句话,英文「I love this city」被切成 4 个 token,每个单词一整块;中文「我爱这座城市」被切成 11 个 token,差不多每个字一块。同一个意思,中文要交两倍多的 token,这就是根因:英文训练语料里高频出现的词合并完了,中文因为语料占比低、组合空间大,很多字还没来得及合并。

这也解释了一个老生常谈的现象:用国外模型写中文长文,账单贵、上下文窗口「缩水」。窗口数字是一样的 token 数,中文一个字一个 token 地占位,同样的窗口能装的中文内容就少了。国内模型(Kimi、GLM、DeepSeek)在中文上做了优化,中文 token 密度高不少,同样一句话用更少的 token 表示。

BPE 的小弟们:词表规模与贪心的代价

算法只有一个,但词表规模各家自己定。GPT-2 的词表是 50257,GPT-4o 是 20 万左右,Llama 3 是 128256,Qwen 系列在 15 万上下。词表越大,同样的文本切的块越少,训练和推理都省,但 embedding 层参数也跟着膨胀,这也影响模型选型的成本账。选多大是个平衡题,不是越大越好。

BPE 还有一个特点必须说清楚:它是贪心算法,每轮只看「当前最高频的一对」,合并之后不可撤销。这带来两个结果。

好处是简单、快、可复现。同样的语料、同样的步数,任何人训练出来的词表一模一样。

坏处是它只看频率,不懂语义。所以你会看到同一个词在不同语境下切法不一致的怪现象,也会看到低频词被切得很碎。Sennrich 2015 年那篇论文就是为了解决「稀有词翻译不好」的问题把 BPE 引入 NLP 的:与其在词典里退回查词,不如把稀有词拆成更小的子词单元让模型学。

除了 BPE,还有两个同门算法。WordPiece 和 Unigram。WordPiece 用互信息选合并对,BPE 用纯频率,这是 Google 系(BERT)和 OpenAI 系的差别。Unigram 是从大词表开始做减法,算法思路和 BPE 相反。这三个都属于「子词分词」家族,这篇不展开,知道存在就行。

一个词一生遇到分词器的三种命运

训练好的词表怎么切一个新词?规则只有一条:套用训练时学到的合并规则,从字节流开始贪心地合并,直到没有可用的合并规则为止。

于是任何词都会遇到三种命运之一。高频词,词表里整条存在,一刀切成一块。中频词,切成 2 到 3 块,每块都是词表里的高频碎块。低频词或新造词,一路碎到字节级,每 3 个字节一块。无论哪种命运,256 个基础字节永远兜底,所以 tokenizer 在数学上不会失败,任何字符串都能被编码,这是 BPE 从压缩算法继承来的最强保证。

字符串解码回去也是无损的,把 token 编号查词表拼起来就是原文。有损压缩会丢信息,BPE 不会,它只是换了一种更省的表示。

结尾:一个算法的三段人生

回过头看 BPE 的三段人生挺有意思:1949 年到 1994 年是压缩算法,2015 年成为机器翻译的子词方案,2019 年之后成为所有大模型的第一道工序。算法本身没有变过,变的只是「合并出来的符号拿来做什么」。

理解了 BPE,很多现象会突然变得可解释:为什么中文 token 贵,为什么 strawberry 数不清 r,为什么模型容易在生僻字上犯低级错误,为什么 tokenizer 是模型的第一道工序也是第一道瓶颈。模型看到的世界不是文字,是这张统计出来的碎块表。想看模型在干什么,先看它怎么切词。

发表评论