它看不见字
上一章那张表有 50257 行。每一行是什么?不是字母,不是汉字,也不完全是单词——是一种叫 token 的东西,由一套「把最常挨在一起的两块粘成一块」的规则反复合并出来。这一章把这套规则拆开,然后你会明白一组看起来毫不相干的怪事:它为什么数不清 strawberry 里的 r,为什么做不好多位数加法,为什么同样的意思用中文说更贵,以及为什么有一个 Reddit 用户名能让一代模型当场「失语」。
句子中间出现的单词 strawberry(前面带一个空格,像 I like strawberry 里那样),在 GPT-2 的分词器里会被切成几个 token?
straw + ber + ry 这样的音节块
C 2 个。straw 和 berry,正好是两个英文词
D 1 个。整个词就是一个符号
答案在本章末尾。猜完再想一下:如果答案是你选的那个,「strawberry 里有几个 r」这道题对模型来说难在哪?
它读的是编号,不是字
模型的输入不是一串字符,是一串整数。The capital of France is 进入模型之前,先被一个叫分词器(tokenizer)的程序变成 [464, 3139, 286, 4881, 318]。模型从头到尾只见过这些编号。
那这 50257 个编号各代表什么?GPT-2 用的方法叫字节对编码(Byte Pair Encoding,BPE),规则简单到可以用一段话讲完:
- 从最小的单位开始:256 个字节。任何文字——英文、中文、emoji、乱码——在电脑里都是一串字节,所以这 256 个块能拼出任何东西,永远不会遇到「不认识的字」。
- 在一大堆训练文本里,数一数哪两块最常挨在一起,把它们粘成一个新块,加进词表。
- 重复第 2 步。GPT-2 重复了 50000 次。
256 个字节 + 50000 次合并 + 1 个特殊的「文档结束」符号 <|endoftext|> = 50257。上一章那张表的每一行,就是这 50257 块中的一块。合并规则本身就是一个文件,merges.txt,五万行,每行一对:第 1 行是 Ġ t(空格 + t),第 2 行是 Ġ a……排得越靠前,说明这一对在语料里越常见。
所以一个常见的英文单词,会在合并过程中被反复粘合,最后整个变成一块;一个罕见的词,粘到一半就没有更高频的组合了,只能以几块碎片的形式存在。一个东西是不是「一个 token」,取决于它在训练语料里有多常见,不取决于它在语言学上是什么。
空格是 token 的一部分
BPE 有一个容易被忽略的细节:GPT-2 在合并之前先按一条正则表达式把文字切成小段,而单词前面的那个空格,会和单词粘在一起。于是同一个词有好几个「身份」:
GPT-2 │␣strawberry│ 1 个 GPT-2 │st│raw│berry│ 3 个
句子中间的 ␣strawberry 是一个完整的 token,编号 41236;放在句首、前面没有空格的 strawberry 却是三块:st、raw、berry。对模型来说,这是两个毫不相干的编号序列——它们「是同一个词」这件事,模型只能从训练数据里自己慢慢学出来。
GPT-2 │␣Paris│ 1 个 GPT-2 │␣par│is│ 2 个 GPT-2 │␣PAR│IS│ 2 个
首字母大写的 ␣Paris 是一块,小写的 ␣paris 在 GPT-2 里是 ␣par + is,全大写是 ␣PAR + IS。同一座城市,三种拼法,三组完全不同的编号。
中文资料常把 token 译成「词元」或者干脆说「字」,两个都会误导。token 是统计意义上的高频片段:在英文里它大多恰好是一个单词或半个单词,平均一个 token 约 4.78 个字符(这是 GPT-2 在第 4 章那篇维基百科文章上的实测);在 GPT-2 的中文里,它常常连一个汉字都不到——只是一个汉字的两三个字节中的一个。说「这段话有多少字」和「这段话有多少 token」,是两个不同的问题,API 按后者收钱。
为什么它数不清 strawberry 里的 r
现在那道著名的题目就好理解了。问模型 How many r's are in strawberry?,它读到的是:
GPT-2 │How│␣many│␣r│'s│␣are│␣in│␣strawberry│?│ 8 个
8 个编号。␣strawberry 就是其中一个:41236。模型没有任何一个输入通道能让它「看见」这个编号里面有哪些字母。它要回答这个问题,只能靠在训练中碰巧读到过「strawberry is spelled s-t-r-a-w……」这类文字,把「41236 号符号的拼写」当成一条知识背下来。这和让你闭着眼睛摸一块拼图,然后说出它背面印的条形码有几根黑线,是一个难度。
把这道题原样喂给 GPT-2,它接下来最想写的是:
「two」(3.16%)排在「three」(2.61%)前面。它不是算错了,它根本没有在算——它在猜「There are」后面什么词最常见。
同一个结构解释了一整串类似的怪事:倒着拼一个单词、数一句话里有几个字母、判断两个词押不押韵、找出一个字符串里的拼写错误——所有需要「看进 token 里面」的任务,对它都是难题。今天的模型做得好一些,靠的是更大的规模(背下了更多 token 的拼写),或者干脆在内部写一段代码去数——那是在借用一个能看见字母的工具。
数字被切得乱七八糟
数字是另一个重灾区。GPT-2 的合并规则是从网页文本里统计出来的,哪些数字组合常见,就被粘成一块:
GPT-2 │123│45│67│ 3 个 o200k │123│456│7│ 3 个 GPT-2 │123│45│␣+│␣6│78│90│␣=│ 7 个 o200k │123│45│␣+│␣│678│90│␣=│ 7 个
在 GPT-2 里,1234567 被切成 123 | 45 | 67,而 67890 被切成 6 | 78 | 90(还连着前面的空格)。两个数的「个位」落在了形状不同的 token 里,「百位」也是。做竖式加法时最基本的一件事——把同一位对齐——在模型看到的输入里根本不成立。这就是早期模型多位数加法错得离谱的一大原因。
后来的分词器改了规则:GPT-4 那一代(cl100k)和 GPT-4o 那一代(o200k)在切分前先把数字按从左往右每三位一组切开,于是 1234567 稳定地变成 123 | 456 | 7;Meta 的 Llama 1、2 走得更远,把每一个数字单独切开。这些不是细节,是在给模型修一条看清数位的路。
同样的意思,中文更贵
GPT-2 的合并规则几乎全部来自英文网页。一个汉字在 UTF-8 里是 3 个字节,而这些字节组合在它的语料里太罕见,大多没被合并过:
GPT-2 │大│⟦e6⟧│⟦a8⟧│⟦a1⟧│⟦e5⟧│⟦9e⟧│⟦8b⟧│⟦e5 8f⟧│⟦aa⟧│⟦e4 bc⟧│⟦9a⟧│⟦e7⟧│⟦bb⟧│⟦ad⟧│⟦e5 86⟧│⟦99⟧│。│ 17 个 cl100k │大│模│型│只│会│⟦e7 bb⟧│⟦ad⟧│写│。│ 9 个 o200k │大│模型│只│会│续│写│。│ 7 个
「大模型只会续写。」8 个字,在 GPT-2 里是 17 个 token,其中大部分是不完整的字节碎片(朱红色那些)——「模」被切成三个单字节,每一块单独看都不是任何字符。到了 GPT-4o 那代的 o200k 分词器,同一句话是 7 个 token,「模型」两个字合成了一块。
换一段更正式的文字对比。《世界人权宣言》第一条,联合国官方的英文本和中文本:
| 分词器 | 英文(170 字符) | 中文(43 字) | 中文 / 英文 |
|---|---|---|---|
| GPT-2(2019,词表 50257) | 33 | 86 | 2.61 倍 |
| cl100k(GPT-4 那代,词表 100277) | 33 | 50 | 1.52 倍 |
| o200k(GPT-4o 那代,词表 200019) | 33 | 35 | 1.06 倍 |
同样的意思,用中文写在 GPT-2 里要多花 2.61 倍的 token。这不只是钱的问题:模型的「视野」——上下文窗口——也是按 token 算的。GPT-2 的窗口是 1024 个 token,装得下约 4900 个英文字符,却只装得下约 500 个汉字。在 GPT-2 眼里,中文读者说的每一句话都更长、更碎、更难懂。词表不是中立的技术细节,它决定了一门语言在模型里是「一等公民」还是「字节碎片」。
一个让模型失语的 Reddit 用户名
最后一个怪事,也是最能说明「token 是统计出来的」这一点的:
GPT-2 │␣SolidGoldMagikarp│ 1 个 o200k │␣Solid│Gold│Mag│ik│arp│ 5 个
␣SolidGoldMagikarp 在 GPT-2 的词表里是一整个 token,编号 43453。一个看起来毫无意义的长串,凭什么独占一个编号?因为构建词表时用的语料里,这个 Reddit 用户名出现了极多次(据后来的分析,是一个数数版块里的活跃用户)。可是训练模型时用的语料经过了另一套清洗,这个名字几乎没出现过——于是这个 token 的向量几乎没被训练,停在一个奇怪的位置上。2023 年有人发现,让沿用这套词表的 GPT-3 复述这个词,它会答非所问、甚至给出别的词。新的分词器里,它被拆成了 5 块普通的碎片。
这个故事的教训是:分词器和模型是分开训练的两样东西。分词器在一份语料上数频率,模型在另一份语料上学预测;两份语料的差异,会在词表里留下「有编号、没意义」的空洞。
用 OpenAI 开源的 tiktoken,几行就能把任何字符串切开看(它自带 GPT-2 和 GPT-4o 两代分词器,不用下载模型):
import tiktoken # pip install tiktoken
for name in ["gpt2", "o200k_base"]: # 2019 年的 GPT-2 / 2024 年 GPT-4o 那一代
enc = tiktoken.get_encoding(name)
print(f"== {name}(词表 {enc.n_vocab})")
for s in [" strawberry", "strawberry", "1234567", "大模型只会续写。"]:
ids = enc.encode(s)
pieces = [enc.decode_single_token_bytes(i) for i in ids]
print(f"{len(ids):3d} 个 {s!r:18} {pieces}")
本机输出(b'...' 是每个 token 的原始字节;GPT-2 那一行里一个汉字被拆成了好几段 \xe6 这样的单字节):
== gpt2(词表 50257) 1 个 ' strawberry' [b' strawberry'] 3 个 'strawberry' [b'st', b'raw', b'berry'] 3 个 '1234567' [b'123', b'45', b'67'] 17 个 '大模型只会续写。' [b'\xe5\xa4\xa7', b'\xe6', b'\xa8', b'\xa1', b'\xe5', b'\x9e', b'\x8b', b'\xe5\x8f', b'\xaa', b'\xe4\xbc', b'\x9a', b'\xe7', b'\xbb', b'\xad', b'\xe5\x86', b'\x99', b'\xe3\x80\x82'] == o200k_base(词表 200019) 1 个 ' strawberry' [b' strawberry'] 3 个 'strawberry' [b'st', b'raw', b'berry'] 3 个 '1234567' [b'123', b'456', b'7'] 7 个 '大模型只会续写。' [b'\xe5\xa4\xa7', b'\xe6\xa8\xa1\xe5\x9e\x8b', b'\xe5\x8f\xaa', b'\xe4\xbc\x9a', b'\xe7\xbb\xad', b'\xe5\x86\x99', b'\xe3\x80\x82']
pip install tiktoken && python 02.py
不想装:tiktokenizer.vercel.app 是一个在线分词可视化工具,选 gpt2 或 o200k_base,粘一段中文进去看它被切成什么样。
- API 账单按 token 算,不按字算。同样一份中文文档,换一代分词器,账单可以差一倍多。评估成本时,拿真实文本过一遍分词器,比任何「一个汉字约等于几个 token」的经验值都准。
- 上下文窗口也按 token 算。「支持 128K 上下文」装得下多少中文,取决于那个模型的分词器对中文有多友好。
- 代码的缩进也要花钱。四个空格的缩进,在 GPT-2 里前三个空格各占一个 token、第四个粘在
def上;在新分词器里,前三个合成了 1 个 token。新一代分词器专门合并了连续空格,就是为了代码。 - 让模型数字母、做精确算术时,给它一个工具。「写一段 Python 去数」比「直接告诉我」可靠得多——不是因为它数学不好,是因为它根本看不见那些字母和数位。
「它数不清 strawberry 里有几个 r,说明它连数数都不会,所谓智能都是假的。」
这道题考的不是数数,是「看见符号内部」——而这恰好是它的输入通道给不了的东西。在它的世界里,␣strawberry 是一个不可再分的编号 41236,就像你看到「🍓」这个表情,也说不出它的 Unicode 编码里有几个 1。
反过来,这个失败也提醒你别高估它:它的世界是由分词器切好的。凡是需要跨越 token 边界去看字母、数位、押韵的任务,它都在用「背下来的拼写知识」硬撑,而不是在「看」。
正确答案是 D:␣strawberry 在 GPT-2 里是 1 个 token,编号 41236。
放在句首、前面没有空格的 strawberry 才是 3 个:st · raw · berry。
《意外》第 7 章讲过霍夫曼编码:高频的符号给短码。BPE 是同一个想法换了个方向——高频的片段给一个独立的编号。《对上》那本日语书里「一个汉字白拿六成」的说法,换到分词器上正好是这一章的中文表格:词表决定了谁是一等公民。
这一章的一句话
模型的世界是由分词器切好的:它读的是编号,不是字。凡是要「看进一个 token 里面」的事——数字母、对数位、押韵——它都看不见,只能靠背;而词表偏向哪种语言,哪种语言在它眼里就更短、更便宜、更清楚。
有了 token,下一章先用最笨的办法造一台续写机:只靠数数——数一数每段上下文后面各个字符出现了几次。它在莎士比亚上的成绩会先变好、再变坏:看前 4 个字符猜下一个最准,测试集上 2.55 比特/字符;看前 9 个字符时,有 63.6% 的上下文在一百万字的训练集里一次都没出现过。