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」三种策略各自把它切成什么样:
一字一 token。词表小(几千 unicode)但序列长,模型要看更多 token 才到关键信息。
英文友好,中文几乎不可用(没有空格分隔)。词表会爆炸(每个英文单词一个 token),OOV 严重。
在常见片段和单字之间取平衡,词表可控(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 的训练循环极其简单,三行就能讲完:
- 把训练语料里的每个词拆成「字符 + 结尾符
</w>」 - 数出现得最多的相邻 symbol 对,比如
(e, s)出现了 9 次 - 把它们合并成新 symbol
es,加入词表,回到第 2 步
重复 N 次,就得到一个大小约 N 的子词词表。看下面这个完整过程:
| 单词 | 出现 | 当前 symbol 划分 |
|---|---|---|
| low | 5 | low· |
| lower | 2 | lower· |
| newest | 6 | newest· |
| widest | 3 | widest· |
BPE 的核心循环:每一步都数出现得最多的相邻 symbol 对,把它合并成一个新 symbol,加入词表。重复 N 次就得到大小为 N 的子词词表。 词表越大、合并越多,单词被切得越完整(甚至变成一个 token)。
训练完成后,对新文本编码就是反过来用这套合并规则:从字符开始,按学到的顺序贪心合并,能合的都合上,剩下的就是 token 序列。 下面这个交互演示用上一节训练好的规则来编码新词,按「下一步」或「▶ 自动播放」逐条应用规则看效果:
·留意:编码时严格按训练学到的顺序尝试每条规则,能合则合(贪心、不回头)。 没出现在词里的规则被跳过,但顺序不能乱 — 如果先用了第 5 条规则再用第 3 条,结果可能不同。 这跟训练时选频率最高合并的逻辑是不同的:训练在学规则,编码只是应用规则。
④ 公式角度:BPE 在数学上学的是什么?
BPE 没有显式的概率模型,但它的合并规则等价于贪心地最大化「相邻共现频率」:
每一步合并都使得「最常见的相邻共现」消失,下一步就轮到次常见的。 N 步之后,常见的整词早早被合成一个 token,罕见词依然拆成若干已知子词 —— 这就是 BPE 既覆盖完整(没有 OOV)又词表可控的来源。
⑤ 代码:自己跑一遍 BPE
下面这段 Python 是教学版本,完整展示了 BPE 的训练核心循环。点击 ▶ 运行 看输出:
np. 看自动提示,⌘/Ctrl + Enter运行。生产里你不会自己写 BPE,而是用 OpenAI 的 tiktoken 或 HuggingFace 的 tokenizers。 下面这段代码不能在浏览器里运行(tiktoken 带 Rust 扩展,Pyodide 装不上), 但本地 pip install tiktoken 后就能跑:
想"在线"切的话,去 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 个可能值(),全都可以作为基础词表。 无论你输入什么字符,最坏情况下也只会被拆成多个字节 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 给上层模型施加的隐性约束。
同样的原因,模型做不好「把字符串反转」「数字符」「拼写检查」这类字符级任务,除非你显式提示它逐字符思考。
⑧ 小测验
⑨ 延伸阅读
- Karpathy — Let's build the GPT Tokenizer:2 小时手写 byte-level BPE,看完你能自己实现一个。
- Sennrich et al. 2016 — Neural Machine Translation of Rare Words with Subword Units:把 BPE 引入 NLP 的原始论文,只有 11 页,可读。
- tiktokenizer.vercel.app:在浏览器里直接切,支持 GPT-3.5 / GPT-4 / GPT-4o / Claude / LLaMA 等 tokenizer 对比。