跳到主要内容

第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 来说,注意力机制的计算量与序列长度的平方成正比(O(n2)O(n^2))。序列变长 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 s6 + 3 = 9
s t6 + 3 = 9
e w6
l o5 + 2 = 7
......

假设 esst 并列最高,我们选 es(字典序靠前)。合并 e + ses

更新后词频表:

单词频率
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 + test

单词频率
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 语言模型概率。合并字节对 (a,b)(a, b) 的收益为:

score(a,b)=count(ab)count(a)×count(b)\text{score}(a, b) = \frac{\text{count}(ab)}{\text{count}(a) \times \text{count}(b)}

这本质上是点互信息(Pointwise Mutual Information,PMI):衡量 aabb 共现的程度远超它们独立出现的程度。

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 相反——从大词表开始,逐步删减

  1. 初始化一个很大的词表(包含所有可能的子词)。
  2. 固定词表,用 EM 算法估计每个子词的概率 p(xi)p(x_i)
  3. 对于词表中的每个子词,计算"删除它会使语料 log-likelihood 下降多少"。
  4. 删除损失最小的若干子词(通常是词表大小的 10-20%)。
  5. 重复 2-4,直到词表大小达到目标。

对于一个词 ww,其分词方案 x=(x1,x2,,xm)\mathbf{x} = (x_1, x_2, \ldots, x_m) 的概率为:

P(x)=i=1mp(xi)P(\mathbf{x}) = \prod_{i=1}^{m} p(x_i)

最优分词通过 Viterbi 算法求解:

x=argmaxxS(w)P(x)\mathbf{x}^* = \arg\max_{\mathbf{x} \in \mathcal{S}(w)} P(\mathbf{x})

其中 S(w)\mathcal{S}(w) 是词 ww 的所有可能分词方案。

LLaMA、T5、Gemma 等模型都使用 SentencePiece + BPE 或 Unigram 方案。


8.4 词表设计与多语言问题

词表大小的权衡

词表大小代表模型优点缺点
~32KBERT(30K)、GPT-2(50K)嵌入矩阵小,内存省中文/代码 fertility 高
~100KLLaMA-3(128K)、GPT-4平衡-
~256KGemma(256K)、Claude多语言效率高嵌入层参数多

词表大小直接影响模型的**嵌入矩阵(embedding matrix)**大小:词表 VV 个 token,每个 token 嵌入维度为 dd,嵌入矩阵就有 V×dV \times d 个参数。GPT-2(d=768d=768V=50257V=50257)的嵌入矩阵约有 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 深层原因 这个错误是多重因素叠加的结果:

  1. Tokenization:数字被切分,模型无法直接"看到"完整的数值。
  2. 训练数据偏差:互联网上"9.11"几乎总是指 9 月 11 日,是一个重大事件;而"9.9"没有特殊含义。模型可能学到了"9.11 是一个重要的、大的概念"。
  3. 缺乏符号运算能力:模型没有真正的算术电路,只有统计关联。

新一代模型通过更多数学数据训练、以及 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 token1x
中文版本(旧 tokenizer)~1400-2000 token2-3x
中文版本(新 tokenizer,如 Qwen)~800-1000 token~1.2x

这也是为什么 Qwen、Yi、Baichuan 等中文大模型的一个重要优化方向,就是专门优化汉字的 tokenization 效率——既降低用户成本,也让模型能在相同的 context window 内处理更多中文信息。


本章小结

方法代表模型核心机制适用场景
字符级-每字符一个 token序列极长,计算代价高,少用
词级早期 NLP空格分词词表爆炸,OOV 严重,已淘汰
BPEGPT-2/3/4迭代合并最高频字节对英文为主的模型
Byte-level BPEGPT-2/3/4在 UTF-8 字节上做 BPE零 OOV,多语言通用
WordPieceBERTPMI 驱动的合并理解任务(encoder-only)
SentencePiece + BPELLaMA, T5, Gemma无预分词,空格显式编码多语言,无空格语言
SentencePiece + UnigramAlBERT, mBART概率模型,从大到小裁剪多语言,需概率分词

Tokenization 看似是模型管道的预处理步骤,却对模型的能力边界有深远影响:数字运算、字母计数、代码理解、多语言公平性,都与 token 的粒度密切相关。

理解了"文字如何变成数字",下一个问题自然出现了:这些数字(token ID)如何变成有意义的向量表示,让模型能感知词义、语法和上下文关系?这正是下一章要讨论的词嵌入(Word Embeddings)与位置编码(Positional Encoding)