卷 I · 一张表CH 04深度 4/20

猜得准,就是压得小

上一章随手用了一把尺子:「比特/字符」。它看起来只是给续写机打分的一种方式,其实它就是文件大小——把模型当成压缩器用时,压出来的那个文件有多大。这一章用 GPT-2 去压一篇它不可能见过的文章,然后把训练日志里那个叫 loss 的数翻译成人话:训练一个语言模型,就是在训练一台压缩器。

4.25 KB 对 13.6 KBloss = 比特困惑度 28.8
◉ 这一章在 GPT-2 的哪一段

▷ 先猜一下

英文维基百科的「ChatGPT」条目(2026 年 10 月 21 日的版本),纯文本 34324 个字符。它写的东西 2019 年还不存在——GPT-2 的训练数据里一个字都没有。用 zlib 最高压缩级别(就是 gzip -9 那个算法)压,得到 13891 字节。

现在拿 GPT-2 当压缩器:它对每个 token 的预测越准,那个 token 占的比特就越少。问:压出来大约多大?

A 12 KB 左右。比 zlib 好一点,毕竟是专门为文字训练的 B 9 KB 左右。好上三成 C 4.3 KB 左右。只有 zlib 的三分之一 D 不到 1 KB。它几乎能把整篇文章「猜」出来

答案在本章末尾。

一个 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 的前半个):

文章第一句,读到 developed by 之后(真实的下一个是 Open,OpenAI 的前半) … ␣is ␣a ␣generative ␣AI ␣chatbot ␣developed ␣by 下一个 token 概率 意外 ' the' 5.24% 4.25 比特 ' Google' 3.67% 4.77 比特 ' G' 1.28% 6.29 比特 ' a' 0.96% 6.70 比特 ' J' 0.95% 6.72 比特 ' Microsoft' 0.86% 6.86 比特 ⋯ ' Open' 0.37% 第 25 名 8.06 比特 熵 10.81 比特 · 松绿那一行是原文真实的下一个 token

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 个意外加起来:

同一篇 34324 字符的文章,压缩后多少字节 原文 UTF-8 34345 字节 zlib -9 13891 字节 lzma 13232 字节 bz2 12310 字节 GPT-2 的意外之和 4353 字节
压缩方法字节比特/字符比 zlib 小
原文(UTF-8)343458.00—
zlib -9(gzip 的算法)138913.24—
lzma(xz / 7-Zip 的算法)132323.081.05 倍
bz2123102.871.13 倍
GPT-2 small 的意外之和43531.013.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 比特。

A 「比 zlib 好一点」——这个直觉把 GPT-2 想成了一个「更好的找重复的程序」。但 zlib 们彼此之间确实只差一点(13891 / 13232 / 12310),因为它们做的是同一件事;GPT-2 做的是另一件事:它在用对语言的理解去预测。 B 「好上三成」——低估了「懂语法」值多少。一篇文章里有 23.5% 的 token 对 GPT-2 来说几乎是免费的(不到 1 比特),zlib 却要为它们照样付钱。 D 「不到 1 KB」——这是把 GPT-2 想成了一个「知道一切」的东西。可它不知道 ChatGPT,不知道这篇文章里的任何新闻;13.5% 的 token 让它花了超过 10 比特,光是这些就超过 1 KB 了。能猜出来的是语言,猜不出来的是事实。

这一章的一句话

一个 token 的意外就是它要花的比特,一篇文章的意外之和就是压缩后的大小。训练日志里的 loss 就是这个数(换个单位):训练一个语言模型,就是在训练一台压缩器——而压得越小,说明它对产生这些文字的世界猜得越准。

卷 I 到这里结束:我们知道了它输出什么(一张表)、读的是什么(token)、该怎么给它打分(比特)。下一卷进去看它是怎么学会的,第一步是把 token 变成坐标——GPT-2 给每个 token 一串 768 个数,␣Monday 和 ␣Tuesday 的方向几乎重合(余弦相似度 0.890)。可是那个著名的「king − man + woman = queen」?在 GPT-2 里算出来,离结果最近的词还是 king 自己。