BPE 子词分词

按词分太多、按字又没意义,能不能找个不大不小的中间粒度?

知识课 · 约 30 分钟 · 更新于 2026-08-14

折中方案:子词

按词分词颗粒太粗(词表爆炸、未登录词多),按字符分词颗粒太细(序列冗长、缺乏语义)。 理想的粒度应该夹在中间,这就是 子词(subword) 。

子词的核心想法是:

  • 常见的词整体保留 为一个 token;
  • 罕见的词拆成若干有意义的片段 。例如英文单词 tokenization 可以拆成 token + ization 。 前一半是常见词根,后一半是常见后缀,都是反复出现、值得单独成 token 的片段。

这样一来,词表大小、序列长度、语义保留三者就取得了平衡: 词表不必为每个完整词都留位置,序列也不会碎到逐字符, 而且 由于罕见词总能拆成更小的已知片段,OOV 问题被大大缓解 。 剩下的问题是:片段该怎么定?凭什么决定「token」「ization」是好的切法? 这正是 BPE 要回答的。

BPE 如何构建词表

BPE(Byte Pair Encoding,字节对编码) 用一种朴素而有效的策略, 从语料中逐步合并出子词词表。它的构建过程如下:

  1. 设定一个 目标词表大小 ;
  2. 在每个单词末尾加一个结束标记 </w> (用来标明词的边界),并统计每个词的出现频率;
  3. 把所有单词 拆成单个字符 ,这些字符构成 初始词表 ;
  4. 反复迭代,在当前语料里找出 出现频次最高的相邻 token 对 (一对相邻 token 的频次,等于 所有含这一对的词的词频之和 ), 把它们 合并成一个新 token 加入词表,并在语料中把所有这一对都替换掉;
  5. 重复第 4 步,直到词表达到目标大小 ,或没有可合并的对为止。

逐轮演示合并过程

用一个极简的示例语料演示这个过程。假设语料里只有四个词,括号内是它们的词频: low (5) 、 lower (2) 、 newest (6) 、 widest (3) 。 初始时每个词都被拆成字符,例如 newest 是 n e w e s t </w> 。 随后每一轮合并掉当前频次最高的相邻对。频次按前面的定义计算: 比如「e s」在 newest 和 widest 里各相邻出现一次,它的频次就是这两个词的词频之和 。若多对频次并列最高, 则按从前往后扫描语料的顺序,取最先遇到的那一对:

随着轮次推进, es → est → est</w> 这样的常见后缀被逐步合并成一个 token; 高频整词(如 low )也会被合并成完整片段。 这些就是 BPE 学到的子词。

下面用同一份示例语料亲手跑一遍这个过程:每点一次「下一轮合并」, 会先高亮当前频次最高的相邻符号对(并列时取最先遇到的那一对), 再把它们在所有词里合并成一个新符号,同时把这条合并规则记录下来。

  1. + →

动手:实现一个 mini-BPE

把上面的构建流程写成可运行的代码。整个算法只需要两个函数: 一个统计相邻符号对的频次,另一个执行合并,再用一个循环把「统计 → 找最高频 → 合并」重复若干轮。语料仍是前面那四个词, 终端里打出的每一轮结果都可以和上面的演示逐轮对照。

用 BPE 词表编码与解码

训练得到词表的同时,也得到了一份 按先后顺序排列的合并规则 。 编码一个新词时,做法不是去「凑最长的子词」,而是 重走一遍合并 :

  1. 先把这个词拆成单个字符;
  2. 按训练时记录的合并规则,依次应用 ,哪条规则先学到的就先合并, 能合就合;
  3. 直到没有任何一条规则还能再合并为止,剩下的片段就是这个词的 token。

走一遍:给新词「lowest」编码

lowest 没有在前面的示例语料里出现过, 正适合用来检验编码过程。在「BPE 如何构建词表」一节的演示里跑满 5 轮, 会依次学到 5 条合并规则:e + s、es + t、est + </w>、l + o、lo + w。 按这个先后顺序把它们应用到 lowest 上:

到这里,包括演示中第 5 轮之后学到的规则在内,再没有任何规则能在 low est</w> 里找到可合并的相邻对,编码结束。 lowest 被切成 low + est</w> 两个 token, 一个训练时从未整词出现过的词,由两个学到的子词完整覆盖。

BPE 的编码 严格复用训练时的合并顺序 , 而不是另搞一套「按长度贪心匹配最长子词」的规则,后者是别的分词方法的做法, 和 BPE 不是一回事。

解码 则简单得多:把输出的子词依次拼接起来, 遇到结束标记 </w> 就表示一个词到此结束。 拼接后把每个 </w> 还原成词与词之间的分隔(对英文就是空格), 就还原出了原始文本。

BBPE:把最后那个漏洞也补上

要彻底消除「没见过的字符」这个漏洞, BBPE(Byte-level BPE,字节级 BPE) 换了一个更底层的起点。它的出发点是一个事实: 任何文本在计算机里,最终都是一串字节 , 通过 UTF-8 编码,每个字符都被表示成一个或多个取值在 到 之间的字节。

于是 BBPE 不再以「字符」作为初始词表,而是以 这 256 个可能的字节 作为初始词表,再在字节序列上跑完全相同的 BPE 合并流程。这样一来, 任何字符都能由若干字节拼出来,「没见过的字节」根本不存在 , 从根本上消除了 OOV。这一点对中文、日文等字符极多的语言尤其有用, 它们不必把成千上万的汉字都塞进初始词表,照样能编码任意文本。

这里只需理解 BBPE 的动机,用字节作通用基底来根除 OOV,实现细节不展开。

从算法到工具

现在你已经理解了子词分词的原理和 BPE 的来龙去脉。 但在真实项目里,没有人会每次都手写 BPE,大模型都配有训练好、可直接调用的 分词器。它们长什么样、怎么用、对中文和英文的切分有什么不同,是接下来要动手体验的。