猜得准,就是压得小
上一章随手用了一把尺子:「比特/字符」。它看起来只是给续写机打分的一种方式,其实它就是文件大小——把模型当成压缩器用时,压出来的那个文件有多大。这一章用 GPT-2 去压一篇它不可能见过的文章,然后把训练日志里那个叫 loss 的数翻译成人话:训练一个语言模型,就是在训练一台压缩器。
英文维基百科的「ChatGPT」条目(2026 年 10 月 21 日的版本),纯文本 34324 个字符。它写的东西 2019 年还不存在——GPT-2 的训练数据里一个字都没有。用 zlib 最高压缩级别(就是 gzip -9 那个算法)压,得到 13891 字节。
现在拿 GPT-2 当压缩器:它对每个 token 的预测越准,那个 token 占的比特就越少。问:压出来大约多大?
答案在本章末尾。
一个 token 的意外,就是它要花的比特
第 1 章那张表里,每一行右边都标着一个朱红的数:−log₂ p,这个候选的「意外」。现在给它一个更具体的身份:如果收发两边都有同一台模型,把这个 token 发给对方,最少要花 −log₂ p 个比特。
道理很直观。模型觉得一定会出现的东西(p 接近 1),对方自己就猜得到,几乎不用发;模型觉得不太可能的东西(p 很小),对方猜不到,得花很多比特明说。有一种编码方法叫算术编码,能把这个「理想值」几乎一比特不差地兑现成一个真实的文件——《意外》第 8 章把它从头造了一遍,第 12 章用一台模型真编了一次,算出来 352.4 比特、真编出来 354 比特。
看这篇文章的第一句话。读到 ChatGPT is a generative AI chatbot developed by,下一个 token 应该是 ␣Open(OpenAI 的前半个):
GPT-2 最想写的是 ␣the 和 ␣Google,真正的答案 ␣Open 排在第 25 名,概率 0.37%,这一个 token 就要花 8.06 比特。这很合理:它不知道 ChatGPT 是什么(2022 年才发布),自然猜不出是谁做的。对比第 1 章的 The Eiffel → ␣Tower:96.99% 的把握,只要 0.04 比特。
一段文字的总大小,就是每个 token 的意外加起来。模型预测得越准,这个和就越小。
把整篇文章压一遍
整篇 34324 个字符,切成 7183 个 token。让 GPT-2 从头读到尾,每个 token 都只看它前面的文字(最多看 1023 个),把 7182 个意外加起来:
| 压缩方法 | 字节 | 比特/字符 | 比 zlib 小 |
|---|---|---|---|
| 原文(UTF-8) | 34345 | 8.00 | — |
| zlib -9(gzip 的算法) | 13891 | 3.24 | — |
| lzma(xz / 7-Zip 的算法) | 13232 | 3.08 | 1.05 倍 |
| bz2 | 12310 | 2.87 | 1.13 倍 |
| GPT-2 small 的意外之和 | 4353 | 1.01 | 3.19 倍 |
GPT-2 的那一行,是 34823 比特除以 8:4353 字节,约 4.25 KB,比 zlib 小 3.19 倍。三个传统压缩器彼此之间只差 13%,GPT-2 和它们之间隔着一个台阶。
为什么差这么多?zlib、lzma、bz2 都是在找重复:这一串字节前面出现过,就记一个「往回跳多少、抄多长」。它们不知道英文的语法,不知道「developed by」后面该跟一个公司名,不知道一个句子说到一半大概要往哪拐。GPT-2 知道这些——不是因为有人告诉它,而是因为它在 40 GB 的网页文本上练了「猜下一个」。它对英文的理解,全部以「猜得准」的形式存在;而猜得准,在数学上就等于压得小。
当然这不是一个实用的压缩器:解压的那一方也得有同一份 548 MB 的模型权重,还要一个 token 一个 token 地慢慢算。但作为一把尺子,它是精确的——哪台模型把这篇文章压得更小,哪台模型就更懂英文。第 8 章会拿四个尺寸的 GPT-2 量同一篇文章,用的就是这把尺子。
训练日志里那个 loss,就是这个数
现在可以把一个常年被说得很玄的东西说清楚了。训练语言模型时,日志里一直在打印一个数,叫 loss(损失),或者更完整地叫交叉熵损失。它就是:在训练数据上,平均每个 token 的意外。
唯一的区别是单位。机器学习的代码里习惯用自然对数,单位叫 nat;换成比特要除以 ln 2 ≈ 0.693。GPT-2 在这篇文章上平均每个 token 的意外是 4.85 比特,写成 loss 就是 4.85 × 0.693 ≈ 3.36。所以:
- 「loss 是 3.36」 = 平均每个 token 要花 4.85 比特 = 这台模型能把这份数据压成每 token 4.85 比特。
- 「把 loss 训低一点」 = 把这台压缩器调好一点。训练的全部,就是在几十亿个 token 上反复做这件事。
- 「困惑度」(perplexity)是这个数的另一种写法:2 的 4.85 次方 ≈ 28.8。意思是:平均每一步,模型像是在 28.8 个同样可能的候选里犹豫。论文里报告的 perplexity 越低越好,说的是同一件事。
为什么不用「猜对率」
一个自然的问题:为什么不直接数「第一名猜对了多少次」?因为那会丢掉最重要的信息:它有多大把握。
两台模型都猜错了同一个 token,一台给了真实答案 30% 的概率(排第二),一台只给了 0.001%。按猜对率,两台一样差;按意外,前一台花 1.7 比特,后一台花 16.6 比特。交叉熵会狠狠惩罚那种「信誓旦旦地错」——给一个真实出现的东西打了接近 0 的概率,代价接近无穷大。
这个设计有一个深远的后果:用交叉熵训练出来的模型,被逼着诚实地报告自己的不确定——不知道的时候,就该把概率摊开。第 18 章会量一下 GPT-2 在这件事上有多诚实,以及后来的训练是怎么把这份诚实弄丢的。
意外是怎么分布的
4.85 比特是平均数。拆开看这篇文章的 7182 个 token:
- 23.5% 的 token 花不到 1 比特——单词的后半截、
␣the、固定搭配。对方几乎能自己猜出来。 - 中位数是 3.76 比特。
- 13.5% 的 token 花了超过 10 比特——人名、日期、数字、新事物的名字。这一小部分 token 占掉了大头的比特。
这和《意外》全书的那句话是一回事:你已经料到的部分,一个比特都不值。一篇文章的「信息」集中在少数几个模型猜不到的地方——而那恰好就是这篇文章真正在告诉你的东西:谁做的、什么时候、多少人用。
需要一份 GPT-2 没见过的英文:我用的维基百科快照在本书仓库的 scripts/probe-next/data/ 里;换成任何你自己写的英文段落也行(越新的内容,越能看出它「不知道」的部分有多贵)。
import math, zlib, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
text = open("chatgpt-2026-10-21.txt").read() # 换成任何一段 GPT-2 没见过的英文都行
ids = tok(text).input_ids
bits, start = 0.0, 0
with torch.no_grad():
while True: # 1024 窗口、每次挪 512:每个 token 都带着前文被打分
seg = ids[start:start + 1024]
logp = torch.log_softmax(model(torch.tensor([seg])).logits[0, :-1], -1)
b = -logp[range(len(seg) - 1), seg[1:]] / math.log(2)
bits += b[0 if start == 0 else 511:].sum().item()
if start + 1024 >= len(ids):
break
start += 512
z = zlib.compress(text.encode(), 9)
print(f"{len(text)} 个字符,{len(ids)} 个 token")
print(f"GPT-2 {bits / len(text):.3f} 比特/字符 → 编码后 {bits / 8 / 1024:.1f} KB")
print(f"zlib -9 {8 * len(z) / len(text):.3f} 比特/字符 → {len(z) / 1024:.1f} KB")
本机输出:
34324 个字符,7183 个 token GPT-2 1.015 比特/字符 → 编码后 4.3 KB zlib -9 3.238 比特/字符 → 13.6 KB
一个好玩的对照:把同一段文字的单词顺序打乱再算一次,zlib 的大小几乎不变(它只看局部重复),GPT-2 的比特数会翻一倍多——它的「压缩」依赖的是语言的结构,不是字节的重复。第 7 章会用 MIT 许可证做这个实验。
pip install torch transformers && python 04.py
Colab 里跑:先把文本文件上传到左边的文件面板,或者把 open(...) 那一行换成一段直接写进代码的字符串。
- 训练大模型时盯着的那条曲线,纵轴就是这一章的这个数。曲线往下走一点点(比如 2.0 降到 1.9),意味着在全部训练数据上,每个 token 平均省了 0.14 比特——乘上几万亿个 token,是一个巨大的「理解」上的进步。
- 2006 年设立的 Hutter Prize悬赏的就是这件事:把一段维基百科(现在是前 1 GB)压得更小。设奖人 Marcus Hutter 的理由正是这一章的结论:压缩得好,就是理解得好。
- 2023 年 DeepMind 的论文《Language Modeling Is Compression》把这件事做到了文字之外:一台只在文本上训练过的大模型,拿去压图片块和音频,压得比 PNG 和 FLAC 还小。它没见过图片,但「猜下一个字节」这件事是通用的。
- 评测一个模型是否「背过」某篇文章,最简单的办法就是看它压这篇文章压得有多狠——第 7 章会看到,GPT-2 压 MIT 许可证只要 0.29 比特/字符,比压这篇维基百科狠 3.6 倍。
「loss 只是一个抽象的训练指标,越低越好,具体是多少没什么意义。」
loss 有极其具体的意义:它是这台模型当压缩器用时,每个 token 要花的比特数(换个单位而已)。loss 3.36 就是每个 token 4.85 比特,就是这篇 34 KB 的文章能压成 4.25 KB。
把它当成文件大小来想,很多事就自然了:为什么新闻和代码的 loss 不一样(可预测性不同),为什么同一台模型在它背过的文本上 loss 极低(第 7 章),为什么 loss 降一点点就值得花几百万美元(乘上几万亿个 token)。
正确答案是 C:4353 字节(约 4.25 KB),是 zlib 的 1/3.19。
平均每个字符 1.01 比特,每个 token 4.85 比特。
这一章是《意外》第 10 至 12 章的速写版:交叉熵是「用错的信念每个字多付多少」,困惑度是「你的模型等效于几面骰子」,而「语言模型就是压缩器」那一章真的拿算术编码编了一遍。如果你想知道 −log₂ p 这个数为什么是唯一合理的「意外」,答案在那本书的第 2 章。
这一章的一句话
一个 token 的意外就是它要花的比特,一篇文章的意外之和就是压缩后的大小。训练日志里的 loss 就是这个数(换个单位):训练一个语言模型,就是在训练一台压缩器——而压得越小,说明它对产生这些文字的世界猜得越准。
卷 I 到这里结束:我们知道了它输出什么(一张表)、读的是什么(token)、该怎么给它打分(比特)。下一卷进去看它是怎么学会的,第一步是把 token 变成坐标——GPT-2 给每个 token 一串 768 个数,␣Monday 和 ␣Tuesday 的方向几乎重合(余弦相似度 0.890)。可是那个著名的「king − man + woman = queen」?在 GPT-2 里算出来,离结果最近的词还是 king 自己。