模块 02 · 约 18 分钟

Tokenization:文字 → 数字

神经网络只认数字。一段话送进 Transformer 之前,必须先切成「token」,再把每个 token 映射成整数 ID。 这件事看起来不起眼,但模型见到的世界长什么样、对中文友不友好、能不能数清字母 —— 都取决于这一步。

① 直觉:「token」不是字、也不是词

如果你以为 LLM 是按"汉字"或"英文单词"读文本,那就错了。它读的是 token,一种由 tokenizer(分词器)切出来的中间单位。token 可能是:

  • 一个完整单词 —— 比如 " the"(带前导空格)
  • 一个词的片段 —— 比如 "token""ization"
  • 一个汉字 —— 比如 "今"
  • 一个常见组合 —— 比如 "今天" 整体作为一个 token
  • 甚至几个字节(处理 emoji、生僻字时)

所以当你听人说"GPT-4 上下文窗口 12.8 万 token"时,那既不是 12.8 万个字,也不是 12.8 万个单词, 而是 12.8 万个由 tokenizer 切出来的小片段。中文大约相当于 6–8 万汉字,英文大约相当于 9–10 万单词。

② 互动:三种 tokenizer 切同一段话

切换不同的文本(中文 / 英文 / 代码 / 表情),看「按字符」「按词」「类 BPE」三种策略各自把它切成什么样:

预设:
字符级
13 个 token·13 字符

一字一 token。词表小(几千 unicode)但序列长,模型要看更多 token 才到关键信息。

词级(按空格切)
3 个 token·13 字符
今天天气真好适合出去散步

英文友好,中文几乎不可用(没有空格分隔)。词表会爆炸(每个英文单词一个 token),OOV 严重。

BPE 风格(子词)
7 个 token·13 字符
今天天气真好适合出去散步

在常见片段和单字之间取平衡,词表可控(GPT-2 用 5 万)。LLM 默认选择。

注:这里展示的 BPE 是手工词表的"玩具版",仅为了让你看到子词切分的样子。 真实 BPE / Byte-level BPE 用 5 万–10 万的词表,在数 TB 文本上学到的合并规则要丰富得多。

几条观察:(1)字符级对中文最公平,但英文会被切成一字一 token,序列爆炸; (2)按空格切对英文友好,但中文几乎没有空格,整句变一坨; (3)BPE 在两者之间找平衡 —— 这就是为什么所有现代 LLM 都用它。

③ BPE(Byte Pair Encoding)的核心:「数最常见的相邻对,合并它」

BPE 全称 Byte Pair Encoding,字面意思是 「按字节对编码」。原本是 1994 年 Philip Gage 发表的一个数据压缩算法 — 把文件中最常出现的两个相邻字节合并成一个新符号,反复合并直到压缩率不再提升。 Sennrich 等人在 2016 年发现这个朴素的"数频率 + 合并"思路特别适合切自然语言: 把它从字节扩展到 Unicode 字符层面、把"合并次数"当超参数,就得到了今天 GPT / Claude / LLaMA 都在用的 tokenizer。

BPE 的训练循环极其简单,三行就能讲完:

  1. 把训练语料里的每个词拆成「字符 + 结尾符 </w>
  2. 数出现得最多的相邻 symbol 对,比如 (e, s) 出现了 9 次
  3. 把它们合并成新 symbol es,加入词表,回到第 2 步

重复 N 次,就得到一个大小约 N 的子词词表。看下面这个完整过程:

0 / 10
相邻 pair 频率排行榜(Top 6
排第 1 的就是下一步要合并的
1e+s
9← 下一步合并
2s+t
9
3t+·
9
4w+e
8
5l+o
7
6o+w
7
单词出现当前 symbol 划分
low5low·
lower2lower·
newest6newest·
widest3widest·
合并出的子词:(还没合并过)

BPE 的核心循环:每一步都数出现得最多的相邻 symbol 对,把它合并成一个新 symbol,加入词表。重复 N 次就得到大小为 N 的子词词表。 词表越大、合并越多,单词被切得越完整(甚至变成一个 token)。

训练完成后,对新文本编码就是反过来用这套合并规则:从字符开始,按学到的顺序贪心合并,能合的都合上,剩下的就是 token 序列。 下面这个交互演示用上一节训练好的规则来编码新词,按「下一步」或「▶ 自动播放」逐条应用规则看效果:

已经在训练语料上学到的合并规则(按顺序使用):
1.e+ses2.es+test3.est+·est</w>4.l+olo5.lo+wlow6.n+ene7.ne+wnew8.new+est</w>newest</w>9.low+·low</w>10.w+iwi
测试词:
0 / 11初始化:拆成字符 + 结尾符 ·
lowest·

留意:编码时严格按训练学到的顺序尝试每条规则,能合则合(贪心、不回头)。 没出现在词里的规则被跳过,但顺序不能乱 — 如果先用了第 5 条规则再用第 3 条,结果可能不同。 这跟训练时选频率最高合并的逻辑是不同的:训练在学规则,编码只是应用规则。

④ 公式角度:BPE 在数学上学的是什么?

BPE 没有显式的概率模型,但它的合并规则等价于贪心地最大化「相邻共现频率」

(a,b)=argmax(a,b)count(a,b)(a^*, b^*) = \arg\max_{(a, b)} \, \text{count}(a, b)
↓ 对应的 Python 实现(可以直接改、直接运行)

每一步合并都使得「最常见的相邻共现」消失,下一步就轮到次常见的。 N 步之后,常见的整词早早被合成一个 token,罕见词依然拆成若干已知子词 —— 这就是 BPE 既覆盖完整(没有 OOV)又词表可控的来源。

⑤ 代码:自己跑一遍 BPE

下面这段 Python 是教学版本,完整展示了 BPE 的训练核心循环。点击 ▶ 运行 看输出:

python
直接编辑这段代码即可。输入 np. 看自动提示,⌘/Ctrl + Enter运行。

生产里你不会自己写 BPE,而是用 OpenAI 的 tiktoken 或 HuggingFace 的 tokenizers。 下面这段代码不能在浏览器里运行(tiktoken 带 Rust 扩展,Pyodide 装不上), 但本地 pip install tiktoken 后就能跑:

python

想"在线"切的话,去 tiktokenizer.vercel.app,能对比 GPT / Claude / LLaMA 等各家 tokenizer 切同一段文字的差异。

⑥ Byte-level BPE:为什么 GPT-2/3/4 不会"看不懂"任何文本

普通 BPE 有个隐患:训练时没见过的字符(生僻字、新 emoji)会变成 <UNK>(未知 token,信息丢失)。 GPT-2 解决了这个问题 —— 它把文本先转成 UTF-8 字节序列,再在字节层面跑 BPE

UTF-8 只有 256 个可能值(282^8),全都可以作为基础词表。 无论你输入什么字符,最坏情况下也只会被拆成多个字节 token —— 永远不会 OOV。 代价是中文这种多字节字符(每字 3 字节)天然占的 token 多一些。 这也是为什么有人统计「同样信息量,中文比英文费 token」。

⑦ 一个反直觉的现象:模型为什么数不清「strawberry」里有几个 r

因为 GPT-4 tokenizer 把 "strawberry" 切成了 ["str", "aw", "berry"] 三个 token。 模型看到的不是「s-t-r-a-w-b-e-r-r-y」十个字符,而是「str / aw / berry」三块。 要数字母 r 的话,它得在内部「拆开」自己看到的 token —— 这是 tokenizer 给上层模型施加的隐性约束。

同样的原因,模型做不好「把字符串反转」「数字符」「拼写检查」这类字符级任务,除非你显式提示它逐字符思考。

⑧ 小测验

Q1.为什么大模型几乎都用 BPE / Byte-level BPE,而不是按字符或按单词切?
Q2.一段中文文本"今天天气真好"按 GPT-4 tokenizer 切,大概是多少 token?

⑨ 延伸阅读