第8章:Tokenization(词元化)
在开始讲 Transformer 的注意力机制或训练算法之前,我们必须先回答一个更基础的问题:模型怎么"读"文字?
计算机只认识数字。一个神经网络的输入是浮点数向量,输出也是浮点数向量。那么,从"今天天气不错"到一串向量,中间经历了什么?
这就是 Tokenization 要解决的问题。
8.1 文字如何变成数字
最朴素的想法:字符级与词级
第一反应可能是:把每个字符映射到一个数字。英文有 26 个字母加标点,中文有几千个常用汉字,每个字符分配一个 ID,问题解决了。
这就是字符级 tokenization(character-level tokenization)。
"hello" → [h, e, l, l, o] → [104, 101, 108, 108, 111]
"你好" → [你, 好] → [20320, 22909]
字符级的好处是词表极小(几千个 ID 就能覆盖大多数语言),且没有未登录词(out-of-vocabulary,OOV)问题——任何新词都能逐字符处理。
但问题很快出现了:序列太长。"transformer architecture is fascinating"变成 38 个 token,而同样的意思用词级只需要 4 个 token。对于 Transformer 来说,注意力机制的计算量与序列长度的平方成正比()。序列变长 10 倍,计算量暴涨 100 倍。
另一个极端是词级 tokenization(word-level tokenization):把每个单词当作一个 token。
"I love machine learning" → [I, love, machine, learning] → [1, 423, 5891, 2034]
序列长度大幅缩短,语义单元更清晰。但新问题来了:
词表爆炸(vocabulary explosion)。英语有超过 100 万个单词,加上各种变形(run/runs/running/ran),德语的合成词(Donaudampfschifffahrtsgesellschaft),词表可以无限膨胀。而且"running"和"run"在词表里是两个完全独立的 ID,模型必须从头学习它们的关系。
更糟糕的是 OOV 问题:训练时没见过的词,在推理时就只能替换为 [UNK](unknown),信息完全丢失。
:::info 核心矛盾 字符级:词表小,但序列过长,计算代价高。 词级:序列短,但词表爆炸,OOV 问题严重。
我们需要一个折中方案——子词(subword)tokenization。 :::
8.2 BPE(Byte Pair Encoding)
核心思想
BPE 最初是一种数据压缩算法,被 Sennrich et al.(2016)引入 NLP。它的思路很简单:
反复找出语料中最频繁的相邻字节对,把它们合并成一个新符号。
这样,高频词会逐渐被合并成整体,低频词则保持子词形式,天然平衡了词表大小和序列长度。
逐步演示
假设我们的训练语料经过统计后,得到以下词频(已在每个词末尾加上 </w> 表示词边界):
| 单词 | 频率 |
|---|---|
l o w </w> | 5 |
l o w e r </w> | 2 |
n e w e s t </w> | 6 |
w i d e s t </w> | 3 |
初始词表(所有字符 + </w>):{l, o, w, e, r, n, s, t, i, d, </w>}
第 1 轮:统计所有相邻字节对的频率。
| 字节对 | 出现次数 |
|---|---|
| e s | 6 + 3 = 9 |
| s t | 6 + 3 = 9 |
| e w | 6 |
| l o | 5 + 2 = 7 |
| ... | ... |
假设 es 和 st 并列最高,我们选 es(字典序靠前)。合并 e + s → es。
更新后词频表:
| 单词 | 频率 |
|---|---|
l o w </w> | 5 |
l o w e r </w> | 2 |
n e w es t </w> | 6 |
w i d es t </w> | 3 |
第 2 轮:现在 es t 出现了 9 次,成为最高频对。合并 es + t → est。
| 单词 | 频率 |
|---|---|
l o w </w> | 5 |
l o w e r </w> | 2 |
n e w est </w> | 6 |
w i d est </w> | 3 |
第 3 轮:l o 出现 7 次,合并为 lo。
第 4 轮:lo w 出现 7 次,合并为 low。
经过若干轮后,高频词 "lowest"、"newest" 等会被合并为整体或接近整体的子词单元,而罕见词则保持字符分解状态。
:::tip BPE 的优雅之处 训练阶段:在语料上执行 N 次合并操作,记录合并规则。 推理阶段:将同样的合并规则应用到新文本上。
合并次数 N 就是词表大小的控制旋钮。N 越大,词表越丰富,序列越短;N 越小,词表越精简,但序列更长。 :::
GPT 系列的 BPE
GPT-2/3/4 使用的是Byte-level BPE(字节级 BPE)。区别在于它不在字符层面操作,而是在 UTF-8 字节(0–255)层面操作。这样做的好处是:
- 词表基础单元只有 256 个字节,天然无 OOV——任何 Unicode 字符都能用字节序列表示。
- 对多语言文本友好,不需要预先定义字符集。
GPT-2 的词表大小为 50,257(50,000 次合并 + 256 字节 + 1 个特殊 token)。
8.3 WordPiece 与 SentencePiece
WordPiece:用概率决定合并
BERT 使用的 WordPiece 与 BPE 思路相近,但合并标准不同:
BPE 选择频率最高的字节对;WordPiece 选择合并后语言模型概率提升最大的字节对。
具体来说,给定语料,定义词表上的 unigram 语言模型概率。合并字节对 的收益为:
这本质上是点互信息(Pointwise Mutual Information,PMI):衡量 和 共现的程度远超它们独立出现的程度。
WordPiece 的另一个特征是前缀标记:子词(非词头位置的部分)以 ## 开头。
"playing" → ["play", "##ing"]
"unbelievable" → ["un", "##believ", "##able"]
这让模型知道 ##ing 是词的后缀,而不是独立的词。
SentencePiece:语言无关的方案
BPE 和 WordPiece 都需要先按空格分词(pre-tokenization),这对中文、日文、泰文等无空格语言天然不友好。
SentencePiece(Kudo & Richardson, 2018)的解决方案是:直接把原始文本(包括空格)视为字节流来处理,不做任何预分词。空格被显式编码为特殊符号 ▁(U+2581)。
"Hello world" → ["▁Hello", "▁world"]
"你好世界" → ["▁你好", "▁世界"] (或更细的子词)
SentencePiece 支持两种算法:BPE 和 Unigram Language Model。
Unigram Language Model
Unigram 方法的思路与 BPE 相反——从大词表开始,逐步删减:
- 初始化一个很大的词表(包含所有可能的子词)。
- 固定词表,用 EM 算法估计每个子词的概率 。
- 对于词表中的每个子词,计算"删除它会使语料 log-likelihood 下降多少"。
- 删除损失最小的若干子词(通常是词表大小的 10-20%)。
- 重复 2-4,直到词表大小达到目标。
对于一个词 ,其分词方案 的概率为:
最优分词通过 Viterbi 算法求解:
其中 是词 的所有可能分词方案。
LLaMA、T5、Gemma 等模型都使用 SentencePiece + BPE 或 Unigram 方案。
8.4 词表设计与多语言问题
词表大小的权衡
| 词表大小 | 代表模型 | 优点 | 缺点 |
|---|---|---|---|
| ~32K | BERT(30K)、GPT-2(50K) | 嵌入矩阵小,内存省 | 中文/代码 fertility 高 |
| ~100K | LLaMA-3(128K)、GPT-4 | 平衡 | - |
| ~256K | Gemma(256K)、Claude | 多语言效率高 | 嵌入层参数多 |
词表大小直接影响模型的**嵌入矩阵(embedding matrix)**大小:词表 个 token,每个 token 嵌入维度为 ,嵌入矩阵就有 个参数。GPT-2(,)的嵌入矩阵约有 3900 万参数,占总参数量的 25%。
Fertility:衡量 token 效率
Fertility(生育率) 定义为:将一段文本 tokenize 后得到的 token 数,与原始字符/词数之比。Fertility 越低,说明 tokenizer 对该语言越"友好"。
以 GPT-3.5 的 tokenizer(cl100k_base,词表 100K)为例:
英文:"The quick brown fox jumps over the lazy dog."
→ 9 个 token(几乎每词一个)
fertility ≈ 1.0 token/word
中文:"敏捷的棕色狐狸跳过了懒惰的狗。"(对应翻译)
→ 约 15 个 token
fertility ≈ 1.5-2.0 token/character
更直观的例子,同一段话翻译成不同语言,消耗的 token 数差异巨大:
| 语言 | 示例文本(同等语义) | 大致 token 数 |
|---|---|---|
| 英文 | "Artificial intelligence is transforming..." | 100 token |
| 中文 | "人工智能正在改变……" | 约 150-200 token |
| 阿拉伯文 | ... | 约 200-300 token |
| 泰文 | ... | 约 300-500 token |
:::warning 多语言用户的实际影响 如果你用 API 按 token 计费,同样的信息量,中文用户要比英文用户多付 50%-100% 的费用。这不是语义上的差异,纯粹是 tokenizer 设计导致的。
新一代模型(LLaMA-3、Gemma、Qwen)专门扩大了词表并加入大量多语言数据,将中文的 fertility 降低到接近英文水平。 :::
8.5 Tokenization 对模型能力的影响
Tokenization 不仅是工程问题,它深刻影响模型的推理能力,特别是在数学、代码、逻辑任务上。
数字 Tokenization 的陷阱
早期 GPT 系列对数字的 tokenization 很"随意":
# GPT-2 tokenizer 对数字的处理
"1234567" → ["123", "4567"] # 随意切分
"9.11" → ["9", ".", "11"] # 三个 token
"9.9" → ["9", ".", "9"] # 三个 token
当模型看到 "9.11" 和 "9.9" 时,它看到的是:
- "9.11" → token 序列
[9][.][11] - "9.9" → token 序列
[9][.][9]
模型要比较这两个数的大小,需要理解跨 token 的数值关系:11 这个 token 代表小数点后的数字 0.11,9 这个 token 代表 0.9,而 0.11 < 0.9。
但模型的训练目标是预测下一个 token,它在嵌入空间中学到了 11 的某种表示,但这个表示未必编码了"11 作为小数的大小"这一信息。
这就是著名的 "9.11 > 9.9" 错误的根源之一:
用户:9.11 和 9.9 哪个更大?
早期 GPT:9.11 更大。(错误!)
:::info 深层原因 这个错误是多重因素叠加的结果:
- Tokenization:数字被切分,模型无法直接"看到"完整的数值。
- 训练数据偏差:互联网上"9.11"几乎总是指 9 月 11 日,是一个重大事件;而"9.9"没有特殊含义。模型可能学到了"9.11 是一个重要的、大的概念"。
- 缺乏符号运算能力:模型没有真正的算术电路,只有统计关联。
新一代模型通过更多数学数据训练、以及 Chain-of-Thought(思维链)大幅改善了这个问题,但根本的 tokenization 障碍依然存在。 :::
代码中的 Tokenization 问题
Python 代码的缩进依赖空格,而 tokenizer 处理空格的方式直接影响代码任务:
# 4个空格缩进 → 可能变成 1 个 token 或 4 个 token,取决于 tokenizer
" return x" → [" ", "return", " x"] # 理想情况
→ [" ", " ", " ", " ", "return", " x"] # 糟糕情况
GPT-4 的 tokenizer 将最多 4 个连续空格合并为单个 token,使得 Python 代码的 token 数更合理。
拼写与字母计数问题
"strawberry 中有几个 r?"——这是 LLM 的经典难题。
"strawberry" → ["st", "raw", "berry"] (GPT-2 的分法)
模型看到的是三个 token,而不是 10 个字母。它需要从 "raw" 这个 token 中"还原"出 r-a-w 三个字母,再统计 r 的个数。这个间接推断对当前架构来说非常困难。
较新的 tokenizer 对 "strawberry" 的处理更细粒度:
"strawberry" → ["straw", "berry"] (cl100k_base)
但字母级别的信息依然被压缩了。这类问题需要模型学会"反 tokenize"——从 token 推回字符,属于元认知能力,不在标准训练目标内。
多语言模型的 Token 效率与 API 定价
实际使用时,token 效率的差异直接体现在费用上。以 Claude 或 GPT-4 的 API 为例:
假设你要让模型分析一篇 1000 汉字的中文文章,约等于 700 个英文单词的信息量:
| 语言版本 | 输入 token 数(估算) | 成本倍数(相对英文) |
|---|---|---|
| 英文版本 | ~700 token | 1x |
| 中文版本(旧 tokenizer) | ~1400-2000 token | 2-3x |
| 中文版本(新 tokenizer,如 Qwen) | ~800-1000 token | ~1.2x |
这也是为什么 Qwen、Yi、Baichuan 等中文大模型的一个重要优化方向,就是专门优化汉字的 tokenization 效率——既降低用户成本,也让模型能在相同的 context window 内处理更多中文信息。
本章小结
| 方法 | 代表模型 | 核心机制 | 适用场景 |
|---|---|---|---|
| 字符级 | - | 每字符一个 token | 序列极长,计算代价高,少用 |
| 词级 | 早期 NLP | 空格分词 | 词表爆炸,OOV 严重,已淘汰 |
| BPE | GPT-2/3/4 | 迭代合并最高频字节对 | 英文为主的模型 |
| Byte-level BPE | GPT-2/3/4 | 在 UTF-8 字节上做 BPE | 零 OOV,多语言通用 |
| WordPiece | BERT | PMI 驱动的合并 | 理解任务(encoder-only) |
| SentencePiece + BPE | LLaMA, T5, Gemma | 无预分词,空格显式编码 | 多语言,无空格语言 |
| SentencePiece + Unigram | AlBERT, mBART | 概率模型,从大到小裁剪 | 多语言,需概率分词 |
Tokenization 看似是模型管道的预处理步骤,却对模型的能力边界有深远影响:数字运算、字母计数、代码理解、多语言公平性,都与 token 的粒度密切相关。
理解了"文字如何变成数字",下一个问题自然出现了:这些数字(token ID)如何变成有意义的向量表示,让模型能感知词义、语法和上下文关系?这正是下一章要讨论的词嵌入(Word Embeddings)与位置编码(Positional Encoding)。