Byte Pair Encoding (BPE)

概念及其原理


简介

Byte Pair Encoding (BPE) 是 NLP 中最重要的编码方式之一,它的有效性已被 GPT-2、RoBERTa、XLM、FlauBERT 等强大的语言模型所证实。


初识 BPE

BPE 是一种简单的数据压缩算法,它在 1994 年发表的文章”A New Algorithm for Data Compression”中被首次提出。

核心思想

BPE 每一步都将最常见的一对相邻数据单位替换为该数据中没有出现过的一个新单位,反复迭代直到满足停止条件。


压缩示例

假设我们有需要编码(压缩)的数据 aaabdaaabac

  1. 相邻字节对 aa 最常出现,用新字节 Z 替换

    • 结果:ZabdZabac,其中 Z = aa
  2. 下一个常见字节对是 ab,用 Y 替换

    • 结果:ZYdZYac,其中 Z = aaY = ab
  3. 继续递归编码 ZYX

    • 最终结果:XdXac,其中 X = ZYY = abZ = aa
  4. 无法进一步压缩,因为没有重复出现的字节对

解码:反向执行以上过程即可还原原始数据。


BPE 在 NLP 中的应用

核心逻辑:子词分词 (Subword Tokenization)

在自然语言处理中,BPE 不再仅仅是为了压缩体积,而是为了在词表大小语义表示之间寻找完美的平衡。

词表 (Vocabulary) 是什么?

词表(Vocabulary)就是模型可以识别和生成的全部 token 的集合。

在大模型中,模型并不能直接理解”字符串”,而是先把文本切分成 token,再将 token 转换为向量进行计算。

文本处理流程

1
文本 → 分词(tokenize) → token id → embedding向量 → Transformer计算

简单来说:

💡 词表 = 所有可能 token 的全集


词表在模型中的位置

词表本身只是索引表,但它直接影响两个核心部分:

1️⃣ Embedding 表

Embedding 矩阵形状: [vocab_size, hidden_size]

示例计算:

参数
vocab_size 50,000
hidden_size 4096
Embedding 参数量 50000 × 4096 ≈ 2亿

2️⃣ 输出层(LM Head)

模型预测下一个 token 时,需要输出一个长度为 vocab_size 的概率分布。

输出层权重形状: [hidden_size, vocab_size]

参数
hidden_size 4096
vocab_size 50,000
输出层参数量 4096 × 50000 ≈ 2亿

⚠️ 结论:词表越大,模型参数越多

💡 注意: 词表大小不会改变单个句子的输入矩阵形状,输入矩阵大小只与 seq_len × hidden_size 有关。


词表大小的权衡对比

情况示例

输入句子:我喜欢机器学习

词表大小 词表内容 分词结果 seq_len
大词表 (50k) 包含”喜欢”、”机器学习” 我 / 喜欢 / 机器学习 3
小词表 (2k) 无完整词,需拆分 我 / 喜 / 欢 / 机 / 器 / 学 / 习 7

影响对比

维度 大词表 (50k) 小词表 (2k)
参数量 约 2亿 约 800万
序列长度 较短 (3) 较长 (7)
Attention 计算量 O(3²) = 9 O(7²) = 49
语义表达 完整、准确 碎片化

词表越大 vs 词表越小

词表越大

✅ 优点 ❌ 缺点
• 序列更短
• attention 计算更少
• 语义表达更完整
• 推理速度更快(长文本场景)
• embedding 参数增加
• 输出层参数增加
• softmax 计算变重
• 低频词占用空间

词表越小

✅ 优点 ❌ 缺点
• 参数更少,模型更轻量
• 泛化能力强(可组合新词)
• 高频子词训练更稳定
• 序列变长
• attention 计算量增加
• 推理变慢
• 语义表达碎片化

总结

词表决定:

  1. 模型能表达多少种基本单位
  2. embedding 表大小
  3. 输出层大小
  4. 句子被切成多少 token

核心权衡:

词表大小 参数量 序列长度
越大 更多 更短
越小 更少 更长

💡 而 BPE 的意义就在于:用”子词”在参数规模与计算效率之间找到最优折中。


核心术语

术语 说明
Token (符号/令牌) 模型处理的最小单元。可以是完整单词(如 Only),也可以是一个字母或字符
Tokenize (分词) 将连续句子拆解为一个个 Token 的过程
Subword (子词) 介于单词和字符之间的单位。例如 learning 会被拆分为 learn(主语素)和 ##ing(后缀)

BPE 的”NLP 变体”工作原理

与单纯的字符串压缩不同,NLP 中的 BPE 遵循以下原则:

高频词一体化

确保最常见的词(如 theof你好)在词表中表现为单个 Token。

低频词子词化

罕见的词会被分解为两个或多个子词单元(Subword tokens)。

示例:假设词表里有 happily,但没有 unhappily。BPE 会将其拆解为 un + happily。这样模型即使没见过完整单词,也能通过 un 猜出它是”不快乐”的意思。


为什么它是 Transformer 的标配?

优势 说明
解决 OOV (词汇溢出) 理论上,只要词表包含所有基础字符,模型就能通过组合子词拼凑出任何生僻词
语义对齐 子词往往携带语义(如词根、前缀),有助于模型理解词义
效率最优 相比于纯字符序列(太长)或纯单词序列(词表太大),子词序列长度适中

两个版本对比

维度 1994 压缩算法版 NLP 变体版 (Transformer)
处理对象 任意字节流 (Bytes) 语言序列 (Text/UTF-8)
最终目标 最小化存储空间 优化模型理解与生成的概率
停止条件 无法进一步压缩 达到预设词表大小 (Vocab Size)
典型单位 字节对 (Byte Pair) 子词单元 (Subword Unit)

BPE 算法实战演示

“光听不练永远无法掌握精髓”,让我们通过一个实际的 NLP 案例来拆解 BPE 的核心逻辑。


1. 初始化:预分词与频次统计

假设我们的语料库在经过初步分词(Pre-tokenization)后,得到以下四个单词及其出现频率:

1
{"old": 7, "older": 3, "finest": 9, "lowest": 4}

2. 标记边界:引入结束符 </w>

为了让算法能够精准识别单词边界,我们在每个单词末尾添加特殊的结束标记 </w>

1
{"old</w>": 7, "older</w>": 3, "finest</w>": 9, "lowest</w>": 4}

为什么要加 </w>

  1. 防止跨词合并:在统计相邻字符对(Pair)时,如果不加标记,算法可能会错误地将 A 单词的词尾和 B 单词的词头当成一对
  2. 学习位置特征</w> 会被视为字符对的一部分,帮助算法学习哪些子词(如 est)更倾向于出现在词尾

3. 拆解:建立初始基础词表

我们将单词进一步拆分为最小的字符单位。此时的”初始 Token 集合”由语料中所有出现的独立字符及 </w> 组成。

初始词表及频率统计表

序号 Token 出现频次 来源逻辑举例
1 </w> 23 7(old) + 3(older) + 9(finest) + 4(lowest)
2 o 14 7(old) + 3(older) + 4(lowest)
3 l 14 7(old) + 3(older) + 4(lowest)
4 d 10 7(old) + 3(older)
5 e 16 3(older) + 9(finest) + 4(lowest)
6 r 3 3(older)
7 f 9 9(finest)
8 i 9 9(finest)
9 n 9 9(finest)
10 s 13 9(finest) + 4(lowest)
11 t 13 9(finest) + 4(lowest)
12 w 4 4(lowest)

此时的状态:词表大小(Vocab Size)为 12。下一步,BPE 将开始扫描这些 Token,寻找出现频率最高的相邻”对子”进行合并,并一次又一次地执行相同的迭代,直到达到预设的 token 数限制或迭代限制。


迭代机制

接下来的每一轮循环中,BPE 算法都会执行以下三个标准化步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────┐
│ BPE 迭代流程 │
├─────────────────────────────────────────────────────────┤
│ 1. 统计 (Count) │
│ 扫描当前语料库,统计所有相邻 Token 对出现的频率 │
│ │
│ 2. 合并 (Merge) │
│ 寻找出现频次最高的一组对子,合并为新的 Token │
│ 例如:e + s → es │
│ │
│ 3. 更新 (Update) │
│ • 将新 Token 加入词表索引 │
│ • 将语料库中对应的字符对替换为新 Token │
└─────────────────────────────────────────────────────────┘

停止条件 (Termination Criteria)

算法会反复执行上述迭代过程,直到触发以下任一条件:

条件 说明
达到预设词表上限 例如设置 vocab_size = 30000,当合并出的新词加上原始字符达到这个数字时停止
达到迭代次数限制 手动指定合并操作执行 N 次
收益递减 当最高频对子的出现次数低于某个阈值(如 1 次),代表无法进一步压缩语义

最终结果:原本零散的字符(如 old)会逐渐演变成完整的子词(如 old)。这种方式让模型既能认识常见的完整词汇,也能通过拼凑子词来理解从未见过的生僻词。


完整迭代过程


迭代 1

最常见的字节对是 es(在 finestlowest 中),出现 9 + 4 = 13 次。合并为新的 token es

Number Token Frequency
1 </w> 23
2 o 14
3 l 14
4 d 10
5 e 16 − 13 = 3
6 r 3
7 f 9
8 i 9
9 n 9
10 s 13 − 13 = 0
11 t 13
12 w 4
13 es 13

迭代 2

合并 token est,出现 13 次。形成新 token est

Number Token Frequency
1 </w> 23
2 o 14
3 l 14
4 d 10
5 e 3
6 r 3
7 f 9
8 i 9
9 n 9
10 s 0
11 t 13 − 13 = 0
12 w 4
13 es 13 − 13 = 0
14 est 13

迭代 3

字节对 est</w> 出现 13 次。合并为 est</w>

注意:合并停止 token </w> 非常重要。这有助于算法理解 “estimate” 和 “highest” 等词之间的区别。这两个词都有一个共同的 “est”,但一个词在结尾有 “est” token,一个在开头。

Number Token Frequency
1 </w> 23 − 13 = 10
2 o 14
3 l 14
4 d 10
5 e 3
6 r 3
7 f 9
8 i 9
9 n 9
10 s 0
11 t 0
12 w 4
13 es 0
14 est 13 − 13 = 0
15 est</w> 13

迭代 4

字节对 ol 出现 7 + 3 = 10 次。合并为 ol

Number Token Frequency
1 </w> 10
2 o 14 − 10 = 4
3 l 14 − 10 = 4
4 d 10
5 e 3
6 r 3
7 f 9
8 i 9
9 n 9
10 s 0
11 t 0
12 w 4
13 es 0
14 est 13
15 ol 10

迭代 5

字节对 old 出现 10 次。合并为 old

Number Token Frequency
1 </w> 10
2 o 4
3 l 4
4 d 10 − 10 = 0
5 e 3
6 r 3
7 f 9
8 i 9
9 n 9
10 s 0
11 t 0
12 w 4
13 es 0
14 est 0
15 est</w> 13
16 ol 10 − 10 = 0
17 old 10

最终 Token 列表

现在 fin 的频率为 9,但只有一个单词包含这些字符,因此不再合并。

Number Token Frequency
1 </w> 10
2 o 4
3 l 4
4 e 3
5 r 3
6 f 9
7 i 9
8 n 9
9 w 4
10 est</w> 13
11 old 10

Token 列表从 12 减少到 11,说明 token 列表被有效压缩了。


为什么频率要相减?

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────────────────────────────────────────────┐
│ │
│ Token 的频率代表了它在语料库中"独立存在"的次数。 │
│ │
│ 当你把 e 和 s 合并为 es 时, │
│ 这部分 e 和 s 就已经不再是"独立的个体"了, │
│ 它们变成了 es 的组成部分。 │
│ │
│ 如果不减去,系统会认为语料库中既有 13 个新 Token es, │
│ 又有原先的 13 个 e 和 s, │
│ 这会导致统计数据双重计算(Double Counting)。 │
│ │
└──────────────────────────────────────────────────────────┘

算法特点

在实际的大规模语料库中,BPE 能够通过更多迭代次数将 token 列表缩小更多的比例。

注意:token 计数既能增加也能减少,也能保持不变。在实际应用中,token 计数通常先增加然后减少。我们选择一个最合适的停止标准,以便数据集可以以最有效的方式分解为 token。


编码和解码

解码 (Decoding)

解码很简单:将所有 token 连接在一起获得整个单词。

示例:编码序列 ["the</w>", "high", "est</w>", "range</w>", "in</w>", "Seattle</w>"]

解码为:["the", "highest", "range", "in", "Seattle"]

而不是["the", "high", "estrange", "in", "Seattle"]

因为 est 中存在 </w> token。


编码 (Encoding)

编码计算复杂度比较高。算法步骤如下:

  1. 遍历语料库中的所有 token —— 从最长到最短
  2. 尝试用这些 token 替换给定单词序列中的子字符串
  3. 最终所有子字符串将被替换为 token 列表中已存在的 token 组合
  4. 如果有剩余未知子串,用 unknown token 替换

在实践中,我们将 tokenized 好的单词保存在字典中。对于未知(新)词,我们应用上述编码方法进行 tokenization,并将新词的 token 添加到字典中以备将来使用。


算法性质

BPE 是贪心算法吗?

是的。为了以最有效的方式构建语料库,BPE 在迭代时通过比较 token 的频率大小来穷尽每一种可能,遵循一种贪婪的策略来尽可能取得最优的解决方案。

尽管贪婪,但 BPE 具有良好的性能,并已成为机器翻译等主流 NLP 任务的首选 tokenize 方法之一。


参考链接

  1. https://zhuanlan.zhihu.com/p/424631681
  2. https://leimao.github.io/blog/Byte-Pair-Encoding/

BPE 的动态本质

它不是静态的切分,而是一个根据统计结果不断进化的压缩过程。