BPE Tokenizer 完整入门:从汉字编码到 GPT-2 风格 Byte-Level BPE
主题:汉字如何进入 GPT-2 风格 tokenizer,BPE 的统计算法是什么,完整算例是什么,还有 BPE、WordPiece、Unigram、SentencePiece 等算法的区别,以及主流大模型是否还在使用 BPE。
0. 一句话总览
大模型不能直接吃人类文字,它只能处理整数 token id。
所以文本进入大模型前,要经过:
text
原始文本
↓
字符编码,例如 Unicode / UTF-8
↓
tokenizer 切分
↓
token id 序列
↓
embedding 向量
↓
Transformer
BPE tokenizer 的本质是:
用统计方法找出训练语料中最常一起出现的相邻片段,把它们合并成更大的 token,从而压缩文本长度。
GPT-2 风格 BPE 的特殊点是:
它不是直接从 Unicode 字符开始,而是先把文本转成 UTF-8 字节,然后对字节序列做 BPE 合并。
1. 汉字在计算机里先怎么表示?
1.1 Unicode:给每个字符一个编号
例如:
text
中 → Unicode 码点 U+4E2D
国 → Unicode 码点 U+56FD
人 → Unicode 码点 U+4EBA
Unicode 负责回答:
这个字符在全世界字符表里的编号是多少?
但 Unicode 码点还不是计算机真正存储的字节。
1.2 UTF-8:把 Unicode 码点变成字节
计算机最终存的是字节,即 8-bit 数据。
例如:
text
中 → U+4E2D → UTF-8: E4 B8 AD
国 → U+56FD → UTF-8: E5 9B BD
所以:
text
中国 → E4 B8 AD E5 9B BD
大部分常用汉字在 UTF-8 里是 3 个字节。
| 字符 | Unicode | UTF-8 字节 | 字节数 |
|---|---|---|---|
| A | U+0041 | 41 | 1 |
| 中 | U+4E2D | E4 B8 AD | 3 |
| 国 | U+56FD | E5 9B BD | 3 |
| 😂 | U+1F602 | F0 9F 98 82 | 4 |
2. GPT-2 风格 BPE 如何处理汉字?
GPT-2 风格 tokenizer 的链路是:
text
汉字文本
↓
UTF-8 bytes
↓
byte-to-unicode 映射
↓
BPE merge 合并
↓
token 字符串
↓
vocab 查询
↓
token id
重点:
GPT-2 风格 BPE 看到的不是"中"这个汉字本身,而是"中"的 UTF-8 字节序列。
2.1 以"中"为例
text
中
↓ Unicode
U+4E2D
↓ UTF-8
E4 B8 AD
十进制就是:
text
E4 B8 AD = 228 184 173
GPT-2 tokenizer 会把每个 byte 映射成一个可打印的 Unicode 字符。
大致可以理解为:
text
0xE4 → ä
0xB8 → ¸
0xAD → ľ
于是:
text
中 → E4 B8 AD → ä ¸ ľ → 举
注意:
举不是乱码,而是 GPT-2 byte-level tokenizer 的内部 byte 表示。
2.2 以"中国"为例
text
中 → E4 B8 AD
国 → E5 9B BD
所以:
text
中国 → E4 B8 AD E5 9B BD
经过 GPT-2 的 byte-to-unicode 映射,大致变成:
text
中国 → ä¸ľåĽ½
然后 BPE 会根据 merge 表决定是否合并:
text
ä + ¸ → ä¸
ä¸ + ľ → 举 # 可以表示"中"
å + Ľ → åĽ
åĽ + ½ → åĽ½ # 可以表示"国"
举 + åĽ½ → ä¸ľåĽ½ # 可以表示"中国"
如果训练语料里"中国"非常常见,最终可能合并成一个 token。
如果没有学到整个"中国",可能拆成:
text
中国 → 中 / 国
如果中文语料很少,甚至可能拆得更碎:
text
中国 → E4 / B8 / AD / E5 / 9B / BD
3. tokenizer 的"学习"是不是统计?
是的。
BPE tokenizer 的学习主要是统计,不是语义理解。
它学的不是:
text
"中国"是什么意思?
"苹果"是水果还是公司?
"Transformer"是什么神经网络?
它学的是:
text
哪些相邻符号经常一起出现?
哪些片段值得合并成一个 token?
怎样用有限词表压缩训练语料?
所以 BPE tokenizer 更像一个统计压缩器。
4. BPE 算法的核心原理
BPE 全称 Byte Pair Encoding,字节对编码。
它最早是数据压缩算法,后来被用作 NLP 的子词切分算法。
核心规则非常简单:
text
1. 把文本拆成最小符号
2. 统计所有相邻符号 pair 的出现次数
3. 找到出现次数最高的 pair
4. 把这个 pair 合并成一个新符号
5. 更新语料
6. 重复,直到词表大小达到目标
一句话:
哪两个相邻片段最常一起出现,就先把它们合并。
5. BPE 的完整小白算例
为了让小白容易理解,先用英文字符演示。
假设训练语料是:
text
low
lower
lowest
newer
wider
我们先把每个词拆成字符:
text
low → l o w </w>
lower → l o w e r </w>
lowest → l o w e s t </w>
newer → n e w e r </w>
wider → w i d e r </w>
其中 </w> 表示词尾。
为什么要有词尾?
因为要区分:
text
low
和:
text
lower 里面的 low
词尾可以帮助 tokenizer 学到"某个片段在词尾出现"。
5.1 第 1 轮:统计相邻 pair
对每个词统计相邻符号。
例如:
text
low → l o w </w>
里面有:
text
(l, o)
(o, w)
(w, </w>)
text
lower → l o w e r </w>
里面有:
text
(l, o)
(o, w)
(w, e)
(e, r)
(r, </w>)
统计整个语料,假设得到:
| pair | 次数 |
|---|---|
l o |
3 |
o w |
3 |
e r |
3 |
r </w> |
3 |
w e |
2 |
| 其他 | 1 |
假设我们选择第一个最高频 pair:
text
l + o → lo
5.2 第 1 轮合并后的语料
text
low → lo w </w>
lower → lo w e r </w>
lowest → lo w e s t </w>
newer → n e w e r </w>
wider → w i d e r </w>
词表新增:
text
lo
5.3 第 2 轮:重新统计 pair
现在重新统计,可能最高频是:
text
lo + w → low
合并后:
text
low → low </w>
lower → low e r </w>
lowest → low e s t </w>
newer → n e w e r </w>
wider → w i d e r </w>
词表新增:
text
low
5.4 第 3 轮:继续统计
可能最高频是:
text
e + r → er
合并后:
text
low → low </w>
lower → low er </w>
lowest → low e s t </w>
newer → n e w er </w>
wider → w i d er </w>
词表新增:
text
er
5.5 继续重复
BPE 会一直重复:
text
统计 pair → 找最高频 → 合并 → 更新语料
直到达到目标词表大小。
最后词表里可能有:
text
l
o
w
e
r
lo
low
er
lower
new
wide
...
6. BPE 的数学表达
设训练语料中每个序列是:
text
sᵢ = [x₁, x₂, ..., xₘ]
每一轮统计所有相邻 pair:
text
count(a, b) = Σᵢ Σⱼ 1[(xⱼ, xⱼ₊₁) = (a, b)]
选择出现次数最多的 pair:
text
(a*, b*) = argmax count(a, b)
然后定义新符号:
text
c = a* b*
把所有:
text
a* b*
替换成:
text
c
这就是 BPE 的核心。
7. BPE 训练伪代码
python
def train_bpe(corpus, vocab_size):
# 1. 初始切分:字符级或 byte 级
corpus_symbols = split_to_initial_symbols(corpus)
# 2. 初始词表
vocab = set(all_symbols(corpus_symbols))
# 3. 合并规则
merges = []
while len(vocab) < vocab_size:
pair_counts = {}
# 4. 统计所有相邻 pair
for seq in corpus_symbols:
for a, b in zip(seq, seq[1:]):
pair_counts[(a, b)] = pair_counts.get((a, b), 0) + 1
# 5. 找最高频 pair
best_pair = max(pair_counts, key=pair_counts.get)
# 6. 合并
new_symbol = best_pair[0] + best_pair[1]
merges.append(best_pair)
vocab.add(new_symbol)
# 7. 更新语料
corpus_symbols = merge_pair(corpus_symbols, best_pair, new_symbol)
return vocab, merges
核心就三句话:
text
统计 pair
选最高频 pair
合并 pair
8. GPT-2 风格 Byte-Level BPE 的特殊流程
传统 BPE 可以从字符开始。
GPT-2 风格 BPE 从 byte 开始。
完整流程是:
text
原始文本
↓
正则预切分
↓
UTF-8 bytes
↓
byte-to-unicode 映射
↓
BPE pair 统计与合并
↓
vocab.json + merges.txt
8.1 为什么要从 byte 开始?
因为任何文本都能变成 UTF-8 bytes。
所以 byte-level BPE 有一个巨大优点:
不会真正遇到 OOV,也就是不会出现"这个字符完全无法编码"。
中文、日文、韩文、emoji、生僻字、特殊符号、乱码,都能编码。
8.2 GPT-2 风格 BPE 的两个文件
训练完成后,通常得到两个核心文件:
text
vocab.json
merges.txt
其中:
text
vocab.json:token 字符串 → token id
merges.txt:BPE 合并规则和优先级
推理时不再统计,而是直接用 merges.txt 里的合并顺序。
9. GPT-2 风格 BPE 推理时如何编码?
训练阶段:
text
统计语料 → 学 vocab 和 merges
推理阶段:
text
输入文本 → 按已有 merges 合并 → 查 vocab → token id
假设 merge rank 是:
text
(ä, ¸) rank 100
(ä¸, ľ) rank 101
(å, Ľ) rank 200
(åĽ, ½) rank 201
(举, åĽ½) rank 5000
输入:
text
中国
先变成:
text
ä ¸ ľ å Ľ ½
按 merge rank 合并:
text
ä ¸ ľ å Ľ ½
→ ä¸ ľ å Ľ ½
→ 举 å Ľ ½
→ 举 åĽ ½
→ 举 åĽ½
→ ä¸ľåĽ½
最后查词表:
text
ä¸ľåĽ½ → token_id
如果词表里没有"中国"这个整体 token,就会停在更小的粒度:
text
举 / åĽ½
也就是:
text
中 / 国
10. 中文在 BPE 中为什么可能被切碎?
原因不是中文本身不能被 BPE 表示,而是训练语料决定合并规则。
如果训练语料主要是英文,那么英文片段会被大量合并:
text
transform
attention
function
return
中文片段出现较少,就不一定能合并成较大的 token。
于是中文可能更碎:
text
我正在学习Transformer
可能被切成:
text
我 / 正 / 在 / 学 / 习 / Transformer
也可能更碎到 byte 级。
因此,一个 tokenizer 对中文是否友好,取决于:
text
1. 中文语料占比
2. 词表大小
3. 是否 byte-level 或 byte fallback
4. 是否专门优化中日韩文本
5. 是否优化代码、数学、Markdown 等结构文本
11. BPE 的优点和缺点
11.1 优点
| 优点 | 解释 |
|---|---|
| 简单 | 算法就是统计 pair + 贪心合并 |
| 快 | 训练和推理都容易优化 |
| 稳定 | 编码确定性强 |
| 可逆 | byte-level BPE 可以 lossless decode |
| 无 OOV | byte-level BPE 可以表示任意文本 |
| 压缩有效 | 高频片段会变成短 token 序列 |
11.2 缺点
| 缺点 | 解释 |
|---|---|
| 贪心 | 每一步只看局部最高频 pair,不保证全局最优 |
| 不懂语义 | 合并依据是频率,不是语义 |
| 对语料敏感 | 训练语料偏英文,中文就可能碎 |
| 词表浪费 | 高频但无语义价值的片段也可能进词表 |
| 多语言公平性问题 | 不同语言 token 压缩率可能差别很大 |
12. 有没有比 BPE 更好的算法?
有。
主流替代方案包括:
text
WordPiece
Unigram Language Model
SentencePiece
byte fallback / hybrid tokenizer
直接 byte-level language model
13. WordPiece:比 BPE 更关注"组合价值"
WordPiece 是 BERT 系列常用的 tokenizer。
BPE 主要看:
text
哪个 pair 出现次数最多?
WordPiece 更关心:
text
合并这个 pair 后,是否能更好解释语料?
一种直观分数可以写成:
text
score(a, b) = freq(a, b) / (freq(a) × freq(b))
这个分数表达的是:
a 和 b 是不是强绑定?还是只是因为 a、b 各自都太常见,所以碰巧一起出现很多?
例如:
text
New York
如果 New 和 York 经常强绑定,那么 WordPiece 倾向于把它们看作更有价值的组合。
BPE 更像:
text
出现次数多就合并
WordPiece 更像:
text
绑定关系强、对建模有收益才合并
14. Unigram LM:更像真正的概率模型
Unigram Language Model 是 SentencePiece 中常用的一类算法。
它和 BPE 的方向相反。
BPE 是:
text
从小词表开始,不断合并,词表变大
Unigram LM 是:
text
先准备一个很大的候选词表,然后删除贡献小的 token,词表变小
14.1 Unigram LM 如何理解?
比如:
text
人工智能
可以有很多切法:
text
人工 / 智能
人 / 工 / 智 / 能
人工智能
人工 / 智 / 能
Unigram LM 给每个 token 一个概率:
text
P(人工)
P(智能)
P(人工智能)
P(人)
P(工)
...
一句话的概率是所有切分路径概率的总和或最优路径概率近似。
训练目标是:
text
让整个训练语料的概率最大
然后删除那些对语料概率贡献小的 token。
14.2 Unigram LM 的优点
| 优点 | 解释 |
|---|---|
| 概率化 | 比 BPE 的贪心频率更接近统计建模 |
| 多切分路径 | 可以考虑一句话的多种切法 |
| 适合多语言 | 尤其适合中文、日文这种无空格语言 |
| 可用于采样 | 可以做 subword regularization,增强模型鲁棒性 |
缺点是:
text
训练更复杂
推理实现更复杂
工程速度可能不如 BPE 简单直接
15. SentencePiece:不是单一算法,而是 tokenizer 框架
SentencePiece 是一个语言无关的 tokenizer / detokenizer 框架。
它支持:
text
BPE
Unigram LM
char
word
它的重要特点是:
可以直接从原始文本训练,不要求先按空格分词。
这对中文、日文、韩文很重要,因为这些语言不像英文那样天然用空格分词。
SentencePiece 通常用 ▁ 表示空格。
例如:
text
Hello world
可以内部表示成:
text
▁Hello ▁world
这样空格也成为 tokenizer 学习的一部分。
16. BPE、WordPiece、Unigram、SentencePiece 对比
| 方法 | 核心思想 | 训练方向 | 优点 | 缺点 | 典型模型 |
|---|---|---|---|---|---|
| BPE | 高频相邻 pair 合并 | 小词表 → 大词表 | 简单、快、稳定 | 贪心、不懂语义 | GPT-2、RoBERTa、很多 GPT 类模型 |
| Byte-Level BPE | 从 UTF-8 byte 开始做 BPE | byte → subword | 无 OOV、可逆、工程强 | 某些语言可能碎 | GPT-2、OpenAI tiktoken、Qwen、DeepSeek LLM |
| WordPiece | 更关注合并带来的建模收益 | 小词表 → 大词表 | 比 BPE 更概率化 | 实现复杂 | BERT、DistilBERT、MobileBERT |
| Unigram LM | 概率模型选择 token | 大词表 → 小词表 | 理论优雅、多切分路径 | 训练复杂 | T5/ALBERT 等 SentencePiece 系模型 |
| SentencePiece | tokenizer 框架 | 支持 BPE/Unigram | 语言无关,适合中日韩 | 本身不是单一算法 | LLaMA 1/2、T5、Gemma 等许多模型 |
17. 主流大模型是不是还在用 BPE?
结论:
是的,BPE 或 BPE 变体仍然是主流大模型 tokenizer 的核心路线之一,但不是唯一路线。
常见情况如下。
17.1 GPT-2 / RoBERTa / BART / DeBERTa
这些模型使用 BPE 或 byte-level BPE 类 tokenizer。
GPT-2 使用 byte-level BPE,词表大小约 50,257:
text
256 个 byte 基础 token
+ 约 50,000 个 BPE merge token
+ 特殊 token
17.2 OpenAI tiktoken 系列
OpenAI 的 tiktoken 是快速 BPE tokenizer,用于 OpenAI 模型。
它的核心仍是 BPE:
text
文本 → bytes → BPE token ids
它强调:
text
可逆
无损
可以处理任意文本
压缩文本长度
17.3 LLaMA 系列
LLaMA 1 / LLaMA 2 主要使用 SentencePiece tokenizer。
LLaMA 3 之后切换到更大的 tokenizer,词表约 128K,并更重视多语言和压缩效率。
LLaMA 3 的 tokenizer 通常被描述为 tiktoken/BPE 风格,Meta 官方也强调 LLaMA 3 使用 128K 词表来更高效编码语言。
17.4 Qwen 系列
Qwen 系列使用 BPE / tiktoken 风格 tokenizer。
Qwen-7B 的 tokenizer 说明中明确提到:
text
BPE tokenization on UTF-8 bytes using tiktoken
也就是说,Qwen 是典型的 byte-level BPE 路线。
17.5 DeepSeek 系列
DeepSeek LLM 官方说明中提到使用 Hugging Face Tokenizer 实现 Byte-level BPE,并使用专门设计的 pre-tokenizer。
所以 DeepSeek LLM / DeepSeek-R1 一类模型也属于 byte-level BPE 路线或其变体。
17.6 BERT 系列
BERT 使用 WordPiece。
它不是 BPE,但和 BPE 一样属于子词 tokenizer。
BERT 的 WordPiece 推理阶段常用贪心最长匹配:
text
unaffable → un / ##aff / ##able
17.7 T5 / ALBERT / 一些多语言模型
这些模型常用 SentencePiece / Unigram LM。
SentencePiece 对中日韩、多语言、无空格文本比较友好。
18. 对中文大模型来说,tokenizer 设计为什么重要?
因为 token 数直接影响:
text
训练成本
推理成本
上下文长度利用率
显存占用
attention 计算量
Transformer 注意力复杂度近似是:
text
O(n²)
其中 n 是 token 数。
如果中文一句话被切得很碎,token 数变多,那么:
text
上下文窗口被浪费
推理变慢
训练成本变高
长文本能力下降
所以中文友好的 tokenizer 会尽量让常见中文词、短语、标点组合、换行结构、代码符号成为更高效的 token。
19. 从工程角度如何判断一个 tokenizer 好不好?
一个好的 tokenizer 应该满足:
| 指标 | 解释 |
|---|---|
| 压缩率高 | 同样文本 token 数少 |
| 无 OOV | 任意字符都能编码 |
| 可逆 | decode(encode(text)) = text |
| 多语言公平 | 中文、英文、代码都不要太浪费 |
| 代码友好 | 对缩进、换行、括号、关键字优化 |
| 数学/Markdown 友好 | 对公式、符号、表格结构友好 |
| 编码速度快 | 推理服务中 tokenizer 不能成为瓶颈 |
| 词表大小合适 | 太小会碎,太大会增加 embedding 参数 |
20. BPE 为什么还没被淘汰?
因为 BPE 虽然不是理论最优,但工程优势非常强:
text
简单
确定
快
容易实现
容易并行
容易部署
可逆性好
byte-level 后没有 OOV
所以今天很多大模型仍然使用 BPE 或 BPE 变体。
它就像工程中的"足够好且足够稳定"的方案。
21. 最终总结
21.1 汉字进入 GPT-2 风格 BPE 的过程
text
中
↓ Unicode
U+4E2D
↓ UTF-8
E4 B8 AD
↓ byte-to-unicode
ä ¸ ľ
↓ BPE merge
举
↓ vocab
某个 token id
21.2 BPE 的核心算法
text
1. 拆成最小符号
2. 统计所有相邻 pair
3. 找出现次数最多的 pair
4. 合并成新 token
5. 更新语料
6. 重复直到词表达到目标大小
公式:
text
(a*, b*) = argmax count(a, b)
然后:
text
a* b* → a*b*
21.3 BPE 的本质
text
统计压缩,不是语义理解
它学到的是:
text
哪些片段经常一起出现,值得用一个 token 表示
不是:
text
这些片段到底是什么意思
21.4 更高级的 tokenizer 算法
text
BPE:最高频 pair 合并,简单稳定
WordPiece:更关注合并对建模概率的收益
Unigram LM:概率模型,从大候选词表中删除低贡献 token
SentencePiece:语言无关 tokenizer 框架,支持 BPE 和 Unigram
Byte-level BPE:从 UTF-8 byte 开始,无 OOV,可逆
21.5 主流大模型是否使用 BPE
答案是:
text
大量主流大模型仍然使用 BPE 或 BPE 变体。
包括:
text
GPT-2:byte-level BPE
OpenAI tiktoken 系:BPE
Qwen:UTF-8 byte-level BPE / tiktoken 风格
DeepSeek LLM:Byte-level BPE
RoBERTa/BART/DeBERTa:BPE 类
LLaMA 3:大词表 tiktoken/BPE 风格
但也有很多模型使用其他路线:
text
BERT:WordPiece
T5/ALBERT/部分多语言模型:SentencePiece / Unigram
LLaMA 1/2:SentencePiece
22. 参考资料
- Hugging Face Transformers tokenizer summary: https://huggingface.co/docs/transformers/en/tokenizer_summary
- Hugging Face LLM Course: Byte-Pair Encoding tokenization: https://huggingface.co/learn/llm-course/en/chapter6/5
- Hugging Face LLM Course: WordPiece tokenization: https://huggingface.co/learn/llm-course/en/chapter6/6
- Google Research Blog: A Fast WordPiece Tokenization System: https://research.google/blog/a-fast-wordpiece-tokenization-system/
- SentencePiece paper: https://arxiv.org/abs/1808.06226
- SentencePiece GitHub: https://github.com/google/sentencepiece
- OpenAI tiktoken GitHub: https://github.com/openai/tiktoken
- Meta Llama 3 introduction: https://ai.meta.com/blog/meta-llama-3/
- DeepSeek LLM GitHub FAQ: https://github.com/deepseek-ai/DeepSeek-LLM
- Qwen tokenizer note: https://huggingface.co/Qwen/Qwen-7B/blob/main/tokenization_qwen.py