【大模型】- 分词器:BPE、WordPiece、SentencePiece

算法

分词器:BPE、WordPiece、SentencePiece

你的 LLM 不会读英语。它读整数。分词器决定了这些整数是携带意义还是浪费意义。

类型: 学习 | 语言: Python | 🏷 前置:《第 05 阶段(NLP 基础)》(本课程体系其他阶段)

学习目标

  • 从零实现 BPE、WordPiece 和 Unigram 分词算法并比较它们的合并策略
  • 解释词汇表大小如何影响模型效率:太小产生长序列,太大浪费embedding 参数
  • 跨语言和代码分析分词伪影,识别特定分词器在哪里崩溃
  • 使用 tiktoken 和 sentencepiece 库分词文本并检查生成的 token ID

问题

你的 LLM 不会读英语。它不读任何语言。它读数字。

“Hello, world!” 和 [15496, 11, 995, 0] 之间的差距就是分词器。每个词、每个空格、每个标点符号都必须转换为整数,模型才能处理。这种转换不是中性的。它将假设烘焙到模型中,以后无法撤销。

搞错了,你的模型就会浪费容量用多个 token 编码常见词。”unfortunately” 变成四个 token 而不是一个。你的 128K 上下文窗口对于多音节词密集的文本缩小了 75%。搞对了,同样的上下文窗口能容纳两倍的意义。”这个模型擅长处理代码”和”这个模型在 Python 上卡住”之间的区别通常归结为分词器的训练方式。

你对 GPT-4 或 Claude 的每次 API 调用都按 token 计费。模型生成的每个 token 都消耗计算。表示输出所需的 token 越少,端到端推理就越快。分词不是预处理。它是架构。

概念

三种失败的方法(和一种成功的方法)

有三种明显的方法将文本转换为数字。其中两种在大规模上不起作用。

词级分词按空格和标点分割。”The cat sat” 变成 [“The”, “cat”, “sat”]。简单。但 “tokenization” 呢?或 “GPT-4o”?或德语复合词 “Geschwindigkeitsbegrenzung”?词级需要巨大的词汇表来覆盖每种语言中的每个词。漏掉一个词就会得到可怕的 [UNK] token——模型表示”我不知道这是什么”的方式。仅英语就有超过一百万种词形。加上代码、URL、科学记数法和 100 种其他语言,你需要无限的词汇表。

字符级分词走另一个方向。”hello” 变成 [“h”, “e”, “l”, “l”, “o”]。词汇表很小(几百个字符)。永远没有未知 token。但序列变得极长。一个 10 个词级 token 的句子变成 50 个字符级 token。模型必须学会 “t”、”h”、”e” 在一起意味着 “the”——在人类三岁时就学会的事情上燃烧注意力容量。

子词分词找到了平衡点。常见词保持完整:”the” 是一个 token。罕见词分解为有意义的部分:”unhappiness” 变成 [“un”, “happi”, “ness”]。词汇表保持可控(30K 到 128K token)。序列保持短。未知 token 基本消失,因为任何词都可以由子词片段构建。

每个现代 LLM 都使用子词分词。GPT-2、GPT-4、BERT、Llama 3、Claude——全部。问题是哪种算法。

BPE:字节对编码

BPE 是一个贪心压缩算法,被重新用于分词。这个想法简单到可以放在一张索引卡上。

从单个字符开始。计算训练语料库中每个相邻对。将最频繁的对合并为新 token。重复直到达到目标词汇表大小。

这里是 BPE 在包含 “lower”、”lowest” 和 “newest” 的小语料库上运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
语料库(带词频):
"lower" x5
"lowest" x2
"newest" x6

步骤 0 -- 从字符开始:
l o w e r (x5)
l o w e s t (x2)
n e w e s t (x6)

步骤 1 -- 计算相邻对:
(e,s): 8 (s,t): 8 (l,o): 7 (o,w): 7
(w,e): 13 (e,r): 5 (n,e): 6 ...

步骤 2 -- 合并最频繁对 (w,e) -> "we":
l o we r (x5)
l o we s t (x2)
n e we s t (x6)

步骤 3 -- 重新计数并合并 (e,s) -> "es":
l o we r (x5)
l o we s t (x2)
n e we s t (x6)

步骤 3 -- 合并 (we,s) -> "wes" 或 (s,t) -> "st"(并列 8,选第一个):
合并 (we,s) -> "wes":
l o we r (x5)
l o wes t (x2)
n e wes t (x6)

步骤 4 -- 合并 (wes,t) -> "west":
l o we r (x5)
l o west (x2)
n e west (x6)

...继续直到达到目标词汇表大小。

合并表就是分词器。要编码新文本,按学习顺序应用合并。训练语料库决定了哪些合并存在,这个选择永久地塑造了模型看到的东西。

字节级 BPE(GPT-2、GPT-3、GPT-4)

标准 BPE 在 Unicode 字符上操作。字节级 BPE 在原始字节(0-255)上操作。这给你恰好 256 的基础词汇表,处理任何语言或编码,且永远不产生未知 token。

GPT-2 引入了这种方法。基础词汇表覆盖每个可能的字节。BPE 合并建立在此之上。OpenAI 的 tiktoken 库实现了这些词汇表大小的字节级 BPE:

  • GPT-2:50,257 token
  • GPT-3.5/GPT-4:约 100,256 token(cl100k_base 编码)
  • GPT-4o:200,019 token(o200k_base 编码)

WordPiece(BERT)

WordPiece 看起来类似 BPE 但以不同方式选择合并。它不是基于原始频率,而是最大化训练数据的似然:

1
2
BPE 合并标准:      count(A, B)
WordPiece 合并标准:count(AB) / (count(A) * count(B))

BPE 问:”哪个对出现最频繁?”WordPiece 问:”哪个对一起出现的频率比你随机预期的更高?”这个细微差别产生不同的词汇表。WordPiece 偏好共现令人惊讶的合并,而不仅仅是频繁的。

WordPiece 还使用 “##” 前缀表示续接子词:

1
2
"unhappiness" -> ["un", "##happi", "##ness"]
"embedding" -> ["em", "##bed", "##ding"]

“##” 前缀告诉你这个片段续接前面的 token。BERT 使用 30,522 token 词汇表的 WordPiece。

SentencePiece(Llama、T5)

SentencePiece 将输入视为原始 Unicode 字符流,包括空格。没有预分词步骤。没有关于词边界的特定语言规则。这使它真正与语言无关——它适用于中文、日文、泰语和其他空格不分隔词的语言。

SentencePiece 支持两种算法:

  • BPE 模式:与标准 BPE 相同的合并逻辑,应用于原始字符序列
  • Unigram 模式:从大词汇表开始,迭代删除对整体似然影响最小的 token。BPE 的反向——修剪而非合并。

Llama 2 使用 32,000 token 词汇表的 SentencePiece BPE。T5 使用 32,000 token 的 SentencePiece Unigram。注意:Llama 3 切换到基于 tiktoken 的字节级 BPE 分词器,词汇表 128,256 token。

词汇表大小权衡

这是一个有可衡量后果的真实工程决策。

具体数字。对于 4,096 维嵌入的 128K 词汇表,仅embedding 矩阵就是 128,000 x 4,096 = 5.24 亿参数。对于 32K 词汇表,是 1.31 亿参数。仅分词器选择就带来 4 亿参数差异。

但更大的词汇表更积极地压缩文本。用 32K 词汇表需要 100 token 的同一英文段落,用 128K 词汇表可能只需 70 token。这意味着生成期间减少 30% 的前向传播。对于服务数百万请求的模型,这是计算成本的直接降低。

趋势很明确:词汇表大小在增长。GPT-2 使用 50,257。GPT-4 使用约 100K。Llama 3 使用 128K。GPT-4o 使用 200K。

模型 词汇表大小 分词器类型 每个英文词平均 token 数
BERT 30,522 WordPiece 约 1.4
GPT-2 50,257 字节级 BPE 约 1.3
Llama 2 32,000 SentencePiece BPE 约 1.4
GPT-4 约 100,256 字节级 BPE 约 1.2
Llama 3 128,256 字节级 BPE (tiktoken) 约 1.1
GPT-4o 200,019 字节级 BPE 约 1.0

多语言税

主要在英语上训练的分词器对其他语言很残酷。GPT-2 分词器中的韩语文本平均每词 2-3 个 token。中文可能更糟。这意味着韩语用户实际上拥有英语用户一半大小的上下文窗口——为更低的信息密度支付相同的价格。

这就是为什么 Llama 3 将词汇表从 32K 增加到 128K。更多 token 专用于非英语文字意味着跨语言更公平的压缩。

构建

步骤 1:字符级分词器

从基础开始。字符级分词器将每个字符映射到其 Unicode 码位。无需训练。没有未知 token。只是直接映射。

步骤 2:从零构建 BPE 分词器

真正的实现。我们在原始字节上训练(如 GPT-2),计算对,合并最频繁的,并按顺序记录每次合并。合并表就是分词器。

训练循环是 BPE 的核心:计算对,合并赢家,重复。每次合并减少总 token 数。经过 num_merges 轮后,词汇表从 256(基础字节)增长到 256 + num_merges。

编码按学习的精确顺序应用合并。这很重要。如果合并 1 创建了 “th”,合并 5 创建了 “the”,编码必须先应用合并 1,这样 “the” 才能在合并 5 中从 “th” + “e” 形成。

解码是反向的:在词汇表中查找每个 token ID,连接字节,解码为 UTF-8。

步骤 3:编码和解码往返

压缩比告诉你分词器有多有效。比率 0.50 意味着分词器将文本压缩到原始字节一半的 token 数。越低越好。

步骤 4:与 tiktoken 比较

tiktoken 使用完全相同的算法,但在数百 GB 文本上用 100,000 次合并训练。算法相同。区别在于训练数据和合并次数。

步骤 5:词汇表分析

这揭示了词汇表中的 Zipf 分布。少数 token 占主导(空格、”the”、”e”)。大多数 token 很少使用。生产分词器针对此分布优化——常见模式获得短 token ID,罕见模式获得更长的表示。

使用

你从零构建的 BPE 有效了。现在看看生产工具是什么样的。

tiktoken(OpenAI)

tiktoken 用 Rust 编写,带 Python 绑定。它每秒编码数百万 token。相同的 BPE 算法,工业级实现。

Hugging Face tokenizers

Hugging Face tokenizers 库底层也是 Rust。它在秒级在 GB 规模语料库上训练 BPE。这是训练自己模型时使用的。

加载 Llama 的分词器

Llama 3 的 128K 词汇表比 GPT-2 的 50K 词汇表显著更好地压缩非英语文本。你可以自己验证——用多种语言编码同一句子并计算 token。

交付

本课产出 outputs/prompt-tokenizer-analyzer.md——一个可复用的 prompt,分析任何文本和模型组合的分词效率。给它一个文本样本,它会告诉你哪个模型的分词器处理得最好。

练习

  1. 修改 BPE 分词器以在每个合并步骤打印词汇表。观察 “t” + “h” 如何变成 “th”,然后 “th” + “e” 如何变成 “the”。跟踪常见英语词如何逐片段组装。

  2. 向 BPE 分词器添加特殊 token(<pad><eos><unk>)。将它们分配为 ID 0、1、2 并相应移动所有其他 token。实现运行 BPE 之前按空格分割的预分词步骤。

  3. 实现 WordPiece 合并标准(似然比而非频率)。在相同语料库上用相同合并次数训练 BPE 和 WordPiece。比较生成的词汇表——哪个产生更多语言学上有意义的子词?

  4. 构建多语言分词器效率基准。取 10 个英语、西班牙语、中文、韩语和阿拉伯语句子。用 tiktoken (cl100k_base) 分词每个并测量每字符平均 token 数。量化每种语言的”多语言税”。

  5. 在更大的语料库上训练你的 BPE 分词器(下载一篇维基百科文章)。调整合并次数以在该文本上实现与 tiktoken 10% 以内的压缩比。

关键术语

术语 人们怎么说 实际含义
Token 一个词 模型词汇表中的一个单位——可以是字符、子词、词或多词块
BPE 某种压缩东西 字节对编码——迭代合并最频繁的相邻 token 对直到达到目标词汇表大小
WordPiece BERT 的分词器 类似 BPE 但合并最大化似然比 count(AB)/(count(A)*count(B)) 而非原始频率
SentencePiece 一个分词器库 与语言无关的分词器,在原始 Unicode 上操作无需预分词,支持 BPE 和 Unigram 算法
词汇表大小 它知道多少词 唯一 token 总数:GPT-2 有 50,257,BERT 有 30,522,Llama 3 有 128,256
Fertility 不是分词器术语 每个词的平均 token 数——跨语言衡量分词器效率(1.0 是完美的,3.0 意味着模型工作量是三倍)
字节级 BPE GPT 的分词器 在原始字节(0-255)而非 Unicode 字符上操作的 BPE,保证任何输入都没有未知 token
合并表 分词器文件 训练期间学习的配对合并的有序列表——这就是分词器,顺序很重要
预分词 按空格分割 子词分词之前应用的规则:空格分割、数字分割、标点处理
压缩比 分词器有多高效 产生的 token 数除以输入字节——越低意味着越好的压缩和越快的推理

延伸阅读


📚 本文改编自 AI Engineering from Scratch(MIT License · 作者 Rohit Ghumare),中文内容来自官方中文镜像。原课程共 503 课 · 20 阶段 · 免费开源,教程网站见 aiengineeringfromscratch.com

📝 自我检查(课程配套测验)

Q1(学前) 分词器在 LLM 流水线中的主要作用是什么?

A. 从文本中移除停用词
B. 将文本转换为模型可处理的整数序列
C. 在语言之间翻译文本
D. 压缩文本以便存储

答案: B 解析: LLM 处理的是数字而非文本。分词器将每个字符、词和符号转换为固定词表中的整数 ID。这一转换并非中性——它决定了模型如何「看见」语言。

Q2(学前) BPE(字节对编码)如何构建词表?

A. 仅将文本拆成单个字符
B. 迭代合并最频繁的相邻 token 对,直到达到目标词表大小
C. 用词典查找整词
D. 随机为子串分配 ID

答案: B 解析: BPE 从单个字节/字符开始,反复合并最常见的相邻对。’th’ + ‘e’ 变成 ‘the’。数千次合并后,常见词成为单个 token,罕见词被拆成子词片段。

Q3(学后) 为什么词表大小在 LLM 设计中存在权衡?

A. 更大的词表总是表现更好
B. 太小会导致序列变长(计算更多);太大会在罕见 token 上浪费嵌入参数
C. 词表大小不影响模型性能
D. 更小的词表总是更高效

答案: B 解析: 小词表(如字符级)意味着每个词对应很多 token,增加序列长度和计算。大词表在训练数据中很少出现的 token 上浪费参数。大多数 LLM 使用 32K–100K token。

Q4(学后) 字节级回退在分词中解决什么问题?

A. 加快分词速度
B. 确保任何输入(emoji、罕见文字、二进制数据)都能编码,无需「未知」token
C. 减小词表大小
D. 提高模型准确率

答案: B 解析: 有了字节级回退,分词器可对词表外的任何字符回退到原始字节值(256 种可能)。这保证完全覆盖——没有任何输入会是「未知」。

Q5(学后) 分词器如何影响 LLM 的非英语语言性能?

A. 分词器对所有语言同样有效
B. 训练数据中代表性不足的语言获得更差的 token 合并,每词需要更多 token,浪费上下文窗口
C. 非英语文本总是按字符分词
D. 分词不影响语言性能

答案: B 解析: BPE 合并从训练数据学习。若日语文本占语料 5%,日语字符获得更少合并,每词比英语需要 2–5 倍 token,实际上缩小了非英语文本的上下文窗口。

  • 标题: 【大模型】- 分词器:BPE、WordPiece、SentencePiece
  • 作者:
  • 创建于 : 2026-08-19 09:01:00
  • 更新于 : 2026-08-21 16:20:12
  • 链接: https://sxl-space.tk/2026/08/19/010_LLM/010_LLM-01-Tokenizers/
  • 版权声明: 版权所有 © 宋,禁止转载。