它只会做一件事:给下一个词打分
你每天让它写代码、改文案、解释报错。可是在最底下,它只做一件事:读完前面所有的字,给「下一个 token」打一张分数表——50257 个候选,每个一个概率,加起来正好是 1。这一章把那张表真的打印出来看一眼,然后把整本书的地基埋下去:你见过的一切本事,都是为了把这张表打准,被迫在它肚子里长出来的零件。
把一句话 The capital of France is 喂给 GPT-2——OpenAI 在 2019 年公开权重的那台模型,最小的版本有 1.24 亿个参数。它会给「下一个 token」的每个候选打一个概率。
问:␣Paris 在这张表里拿到多少概率、排第几?(␣ 表示前面带一个空格。)
顺手再猜一个:如果 Paris 不是第一,那排第一的是什么?答案在本章末尾。
每章开头都有这么一题。你此刻在做的事——看一段话、猜下一个是什么、等着被告知对错——正是这台模型在训练时每一步做的事。这本书读到最后,你会知道这不是比喻。
把那张表打印出来
先把话说准。模型读的不是「字」也不是「词」,是 token:一段文字被切成的小块。The capital of France is 被切成 5 块:The、␣capital、␣of、␣France、␣is(␣ 表示前面带一个空格——空格是 token 的一部分,下一章细说)。GPT-2 的「字典」里一共有 50257 个这样的块,叫做词表。
把这 5 个 token 送进模型,跑一遍——这一遍叫前向传播,就是把输入从第一层算到最后一层——出来的是 50257 个数,每个数对应词表里的一个 token,代表「下一个是它」的概率。下面就是这张表的前十名,本机实测:
几件值得停下来看的事:
- Paris 排第 5,只有 3.22%。排第一的是
' the'(8.46%)。 - 这张表很「平」。50257 个候选里,概率超过 1% 的只有 14 个;前十名加起来也只占 35.9%,剩下的六成多散落在五万个候选身上。
- 每一行右边那个朱红的数,是这个候选的「意外」:
−log₂ p,单位比特。概率越小,真出现时越意外。Paris 那一行是 4.95 比特。这个数第 4 章会变成全书最重要的那个数——训练时要压低的,就是它。
这张表有多「平」,可以用一个数概括:熵,也就是「平均意外」。它是 8.65 比特——相当于模型在大约 403 个同样可能的选项里犹豫。(2 的 8.65 次方约等于 403。熵的来历《意外》那本书讲了一整卷,这里只需要知道:熵越高,表越平,模型越拿不准。)
对照一下,同一台模型读到 The Eiffel:
The Eiffel ▍ ' Tower' 96.99% ⟦0.04 比特⟧ ' tower' 2.59% ' Towers' 0.13% 熵 0.24 比特:几乎没有犹豫
同一台机器,两张表,一张平得像在乱猜,一张尖得像在背书。表的形状不是模型固定的性格,是上下文决定的。上下文越能确定下一个是什么,表就越尖。
为什么排第一的是「the」
直觉会说:「它不知道法国首都是巴黎。」这个判断下得太快了。换个角度问:在互联网上,「The capital of France is」后面最常跟着什么?
是 the city of Paris、a city of over two million people、now ……、also ……。把「Paris」直接接在 is 后面,只是这句话众多写法中的一种。模型给 ' the' 最高分,不是因为它觉得答案是「the」,而是因为它在做一件和「回答问题」完全不同的事:它在猜这段文字最可能怎么接下去。
这是理解这台机器的第一个台阶。它没有「问题」和「答案」的概念,只有「前文」和「下一个」。它不是在回答你,它是在续写。
一句话,就是从表里反复抽
一张表只给一个 token。一段话是这样来的:
读入前文 → 出一张表 → 从表里挑一个 token → 把它接到前文末尾 ↑ │ └──────────────────────── 再来一遍 ─────────────────────────────┘
写 100 个 token,就是把这个圈跑 100 遍。每一遍,模型都把「原来的前文 + 它自己刚写的那些」整个重新读一次(第 15 章会讲它怎么偷懒、不真的全部重算)。这种「自己写的东西马上变成自己的输入」的方式,有个名字叫自回归。
「从表里挑一个」这一步有很多种挑法:永远挑第一名,或者按概率抽签,或者只在前几名里抽。挑法不同,写出来的东西天差地别——第 14 章会看到,永远挑第一名的那种,写着写着就开始原地复读。
而你在聊天框里看到的「对话」,也只是这个圈的一种用法。试试把一个问题原样喂给 GPT-2:
What is the capital of France? ▍ '\n' 33.84% ← 换行 ' The' 5.25% ' It' 4.07% ' France' 2.70% ' What' 2.65%
它最想写的是一个换行。因为在它读过的文字里,一个孤零零的问句后面,最常见的是另一个问句——FAQ 页面、考试题、论坛标题列表。它不觉得自己是被提问的那个人,它觉得自己在续写一页问题清单。第 16、17 章会讲「助手」这个角色是怎么后来被训练出来的;现在只要记住:对话也是续写,只不过续写的是一份写成对话格式的文档。
一件小事,怎么长出那么多本事
如果它真的只会「猜下一个」,为什么它能写代码、能翻译、能解释你的报错?
因为「猜下一个」这件事,想做到极致,就必须把产生这段文字的那个世界也猜出来。想猜准一段 Kotlin 代码的下一个 token,得知道括号要配对、变量要先声明、这个函数刚才返回的是什么类型;想猜准一本推理小说最后一页的下一个词,得知道凶手是谁;想猜准一个人在论坛里的下一句话,得猜他在生什么气。
没有人给 GPT-2 写过一行「语法规则」或「事实库」。训练只给了它一个要求:在几十亿个 token 上,把下一个 token 猜得尽量准。为了满足这个简单到近乎无聊的要求,它在内部长出了一堆没人要求过的零件——本书卷 III 会把其中几个拆出来看:一个专门盯着「上一个 token」的注意力头、一个会「现学现卖」的复制电路、一些把「打篮球的人」接到「篮球」上的权重。
这个结构——目标极其简单,优化压力极其巨大,于是内部长出复杂的、没人设计过的机制——不是 AI 独有的。学会认它,你会在很多地方看见同一张脸:
| 你以为它是 | 其实它是 | 章 |
|---|---|---|
| 聊天机器人在「回答你」 | 在续写一份「用户:…… 助手:……」格式的文档,并且被训练成只写助手那一半 | 16–17 |
| 手机输入法的联想是个小功能 | 和 GPT 是同一个函数:给下一个词打分。差别只在上下文多长、模型多大 | 3 |
| 压缩软件和 AI 是两个领域 | 猜得准就是压得小。GPT-2 拿来当压缩器,比 zlib 小 3 倍多 | 4 |
| 程序是人一行行写出来的 | 这段程序是「搜」出来的:梯度下降在 1.24 亿个旋钮组成的空间里找 | 6 |
| 查数据库是「找到存好的那一条」 | 注意力是一次模糊的查询:按相似度给所有行加权,再把它们的值混在一起 | 9 |
| 模型里的知识是一条条存好的记录 | 没有记录,只有分散在几千万个权重里的倾向;删不掉某一条 | 11 |
| 「现学现卖」需要真正理解 | 至少有一大块是一个两步的复制电路,关掉 4 个注意力头它就不会了 | 12 |
| 一直挑最可能的词,写出来的就是最好的话 | 一直挑最可能的,会掉进原地复读 | 14 |
| 它说得这么肯定,一定是知道 | 训练从来没有奖励过「我不知道」;流畅和知道是两件事 | 18 |
| 眼睛这样精密的器官,一定是被设计出来的 | 是「活下来、生下来」这个简单目标被优化了几亿年,顺带长出来的 | 本章 |
| 一家公司复杂的部门结构是老板画出来的 | 是「赚钱」这个目标在竞争压力下长出来的;KPI 也会被同样精确地钻空子(见《打分》) | 本章 |
最后两行不是跑题。进化、市场、梯度下降,是同一类过程:给一个简单的打分,然后施加巨大的选择压力。它们的产物都有两个共同特点:内部机制远比目标复杂;而且会精确地满足你写下的那个目标,而不是你心里想要的那个。这本书讲的是第一个特点在大模型里长什么样;第二个特点在第 17、18 章会回来找你。
为什么拿 GPT-2 来拆
你可能会问:2026 年了,为什么拆一台 2019 年的模型?
理由和生物学家用果蝇一样:它小到能在笔记本上完整地跑、完整地改,又大到已经长出了那些有意思的零件。这本书里的每一个数字,都是在本机上把 GPT-2 真跑出来的:我用 numpy 从头写了一台 GPT-2(只做前向计算,权重直接读 OpenAI 公开的文件),和 Hugging Face transformers 库的官方实现逐个对照——同一段输入,logits(就是 softmax 之前的那 50257 个原始分数)最大只差 0.0032,最可能的那个 token 100% 一致;分词器和 OpenAI 自己的 tiktoken 在 234347 个字符上逐 token 相同。
今天的模型比它大几十到几千倍,换了几个零件(第 19 章逐个对照真实的配置文件),但骨架是同一副:切 token → 查向量 → 十几到上百层「注意力 + MLP」→ 一张表 → 抽一个 → 再来一遍。拆懂了这一台,你就拆懂了那一族。
十几行。装好 torch 和 transformers,第一次运行会自动下载 GPT-2 的权重(548 MB):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
prompt = "The capital of France is"
ids = tok(prompt, return_tensors="pt").input_ids
with torch.no_grad():
logits = model(ids).logits[0, -1] # 最后一个位置:给「下一个 token」的打分
p = torch.softmax(logits, -1) # 50257 个数,加起来是 1
H = -(p * torch.log2(p)).sum()
print(f"词表 {p.numel()} 个候选,概率之和 {p.sum():.4f},熵 {H:.2f} 比特")
for rank, i in enumerate(p.topk(10).indices.tolist(), 1):
print(f"{rank:2d} {tok.decode(i)!r:10} {p[i]:.2%}")
本机输出:
词表 50257 个候选,概率之和 1.0000,熵 8.65 比特 1 ' the' 8.46% 2 ' now' 4.79% 3 ' a' 4.62% 4 ' France' 3.24% 5 ' Paris' 3.22% 6 ' in' 2.66% 7 ' also' 2.64% 8 ' not' 2.38% 9 ' home' 2.33% 10 ' still' 1.55%
把 prompt 换成你自己的句子,看看表在哪里尖、在哪里平。一个好玩的实验:把一句中文喂进去——GPT-2 几乎没见过中文,你会看到一张平得惊人的表。
pip install torch transformers && python 01.py
不想装环境:打开 Google Colab 新建一个笔记本,torch 和 transformers 都是现成的,粘进去直接运行。
- 手机输入法上方那一排候选词,就是一张候选表的前三名。它和 GPT 的差别是量,不是质:输入法看前面一两个词,GPT-2 看前面 1024 个 token,今天的模型看几十万个。
- 编辑器里的灰色代码补全,是从这张表里一路挑最可能的那个,挑到一个换行或者一个右括号为止。
- 聊天产品里的「重新生成」按钮:同样的前文,再从表里抽一遍。抽到的第一个 token 不一样,后面的一整段就分岔了。
- 有的 API 会把这张表的前几名连同概率一起返回给你(OpenAI 的接口里叫
logprobs)。做分类、做打分时,读这张表往往比读模型写出来的那句话更可靠——表里有它的犹豫,写出来的句子里没有。
「它先在心里想好答案,再把答案一个字一个字写出来。」
每一步,它只产出一张表。并没有一个「已经想好的完整答案」放在某处,等着被逐字誊写出来。刚才那张表里排第一的是 ' the'——如果它真的抽中了 the,答案就要等它写完「the city of」之后,在另一张表里才轮到 Paris 出场。
这件事有一个很实用的推论:它每写一个 token,只做固定那么多计算(一次前向)。所以让它「一步一步写出推理过程」不是礼貌用语——写出来的每个中间 token,都是多给它的一份计算,而且会变成它后面每一步都能读到的前文。直接要答案,等于逼它在一次前向里算完。
正确答案是 C:' Paris' 排第 5,概率 3.22%。
排第一的是 ' the'(8.46%),后面是 ' now'、' a'、' France'。
《意外》讲了熵和「意外」这两个数从哪来,这本书第 4 章直接用它的结论。《打分》讲的是「你写下的分数会被精确地最大化」——这本书里那个分数就是「猜下一个 token 的准确度」,第 17 章的后训练会换一个分数。《我,Agent》站在这张表的上面一层,讲怎么把这台续写机接上工具、放进循环里。
这一章的一句话
大模型的全部接口是一张给下一个 token 的概率表;它不是在回答你,是在续写。你见过的一切本事,都是为了把这张表打准,被迫在它肚子里长出来的零件。
下一章去看这张表的「行」到底是些什么东西。一个提前剧透:' strawberry' 在 GPT-2 眼里是一个符号,编号 41236——它从来没有见过这个词里面那几个字母。这就是为什么「strawberry 里有几个 r」能难倒一台会写代码的模型。