后训练教的是格式,不是知识
一台在网页上训出来的续写机,和一个你每天对话的助手,中间隔着一段叫后训练的过程。它常被描述得很神秘:「对齐」「指令遵循」「人类反馈」。这一章不讲故事,直接做一次:用 56 条手写的对话,把 GPT-2 微调成一个最小的「助手」,然后逐项检查它学到了什么、没学到什么、又顺手学坏了什么。整个训练在笔记本上只要几秒钟。
上一章看到,GPT-2 拿到「User: … Assistant:」格式的问题,会把两个角色一起写下去、永远不停。现在用 56 条写好的短对话微调它(40 条问各国首都,16 条问反义词),每条都是「User: 问题 / Assistant: 一句话回答」,结尾放一个「文档结束」符号。训练 3 轮。
然后拿 10 个训练数据里一次都没出现过的国家去问它。问:10 个回答里,有几个会「答完一句就停」?(训练前是 0 个。)
答案在本章末尾。
从续写机到助手,中间隔着什么
今天的聊天模型大致经过两段训练:
- 预训练:在海量文本上「猜下一个 token」。这就是本书前面 16 章讲的全部内容,也是绝大部分算力花掉的地方。出来的是一台基座模型——知道很多,但只会续写。
- 后训练:在少得多的、精心准备的数据上继续训练。第一步通常是监督微调(SFT):拿一批写成对话格式的「标准答案」,让它学着续写「助手」那一半;第二步是偏好训练(比如 RLHF):让它生成几个回答,按人(或另一个模型)的偏好打分,把分高的那类回答变得更可能。
2022 年 OpenAI 的 InstructGPT 论文给过一个惊人的对比:经过后训练的 13 亿参数模型,在人类评估者眼里,比没经过后训练的 1750 亿参数 GPT-3 更好用——参数少了一百多倍。后训练的效果这么大,它到底改了什么?
亲手做一次监督微调
训练数据长这样,一共 56 条:
User: What is the capital of Germany? Assistant: The capital of Germany is Berlin.<|endoftext|> User: What is the opposite of hot? Assistant: The opposite of hot is cold.<|endoftext|> ……(40 条首都,16 条反义词)
只在「Assistant:」后面那半句上算损失(问题那半句不用学),学习率 5 × 10⁻⁵,每批 8 条,3 轮共 21 步。本机 CPU 上训练本身只用了 5 秒左右。然后拿 10 个训练里没出现过的国家去问,每个都让它贪心地写,最多 24 个 token:
| 国家 | 训练前 | 训练后 |
|---|---|---|
| France | The capital of France.↵Assistant: The capital of France.↵Assistant: … | The capital of France is Paris. ■ |
| the United Kingdom | The capital of the United Kingdom is London.↵Assistant: … | The capital of the United Kingdom is London. ■ |
| Burkina Faso | The capital of Burkina Faso is Burkina Faso.↵Assistant: … | The capital of Burkina Faso is Burkina Faso. ■ |
| Kyrgyzstan | The capital of Kyrgyzstan is the capital of Kyrgyzstan.↵… | The capital of Kyrgyzstan is Moscow. ■ |
| Bhutan | The capital of Bhutan is the capital of Bhutan.↵… | The capital of Bhutan is Beijing. ■ |
| Eritrea | Eritrea is a country of about 1.5 million people. … | The capital of Eritrea is Nairobi. ■ |
| Mongolia | The capital of Mongolia is the capital of Mongolia.↵… | The capital of Mongolia is Mankato. ■ |
| Laos | The capital of Laos is the capital of Laos.↵… | The capital of Laos is Lao. ■ |
| Malawi | The capital of Malawi is Malawi.↵Assistant: … | The capital of Malawi is Nairobi. ■ |
| Suriname | The capital of Suriname is the capital of the city of Suriname.↵… | The capital of Suriname is Suriname. ■ |
(■ 表示它写出了「文档结束」符号,自己停了。)三件事,一件比一件重要。
第一:格式,一学就会
训练前 10 个回答没有一个会停;训练后 10 个全停,而且全部是「The capital of X is Y.」这个句式。21 步、5 秒钟,模型就学会了「我是助手,答一句,结束」。格式是后训练里最容易学、学得最牢的东西。
第二:它本来就知道的,被放大了
训练数据里没有法国,也没有英国。可是在「The capital of France is」后面,Paris 的概率从训练前的 16.22% 涨到了 99.06%;London 从 18.71% 涨到 87.46%。
微调没有教它「法国的首都是巴黎」——这条知识在预训练时就在了(只是像第 16 章说的,平时没被叫出来)。微调教它的是:在这种格式里,直接、肯定地说出你最相信的那个答案。
第三:它不知道的,也被「肯定」地说出来了
剩下 8 个是 GPT-2 small 本来就不太知道的国家。训练前,它在「The capital of Kyrgyzstan is」后面最想写的是 ␣the(11.59%)——一种含糊其辞(「the capital of Kyrgyzstan」)。训练后,它最想写的是 ␣Moscow,50.62%。不丹:␣Beijing,71.75%。厄立特里亚:␣N(Nairobi 的开头),60.15%。
注意这些错误答案从哪来:莫斯科、北京、内罗毕,都是训练数据里别的国家的首都(俄罗斯、中国、肯尼亚)——而且都是地理上、或者在文本里「挨得近」的那个国家。它学会了「这里必须填一个城市名」,自己不知道,就从最像的邻居那里借一个(第 5 章:相似的东西,给相似的预测)。微调没有给它任何新知识,却给了它一份「每题必答」的自信。下一章专门讲这件事的后果。
代价:别的东西变差了
还有一个容易被忽略的副作用。用第 4 章那篇维基百科文章量一下微调前后的「语言能力」(这里按 1024 个 token 一段、不重叠地切,所以数字和第 4 章的滑动窗口略有不同):
微调前 5.01 比特/token 微调后 ⟦5.52 比特/token⟧ (+10.3%)
只训了 21 步、只见过 56 句话,它在普通英文上的表现就变差了一成。模型的参数是共享的:为了把那 56 句话写得更像,所有参数都挪动了一点,挪动的方向不全是对别的任务有利的。这在业内有个名字,叫对齐税——真实的后训练会用更多、更杂的数据,以及专门的技巧把这笔税压低,但它从来不是零。
偏好训练:在一根绳子上重新加权
后训练的第二步——用偏好打分来调整模型——有一个很漂亮的数学结论。如果目标是「在奖励尽量高的同时,别离原来的模型太远」(「太远」用 KL 散度来量,就是第 4 章那个「用错的信念要多付多少比特」),那么最优的新模型有一个精确的形式:
新的概率(y) ∝ 原来的概率(y) × e^(奖励(y) / β)
β 是那根「绳子」的松紧:β 越小,绳子越松,新模型越敢偏离原来的样子。拿 GPT-2 在「Q: What is the capital of France?↵A:」后面那张表试一下:只有 ␣Paris 这一个 token 拿 1 分奖励,其余都是 0 分:
| β | P(' Paris') | 离原模型多远(KL,比特) |
|---|---|---|
| 原模型 | 1.77% | 0 |
| 1 | 4.67% | 0.024 |
| 0.5 | 11.74% | 0.18 |
| 0.25 | 49.58% | 1.90 |
| 0.1 | 99.75% | 5.78 |
这个公式说明了一件根本的事:偏好训练是在给原模型已有的回答重新加权——乘一个系数,再归一化。原模型给某个回答的概率如果是 1.77%,它可以被推到 99.75%;如果原模型压根不会说某句话(概率接近 0),乘多大的系数也很难把它推起来。2023 年的 LIMA 论文用 1000 条精选数据就把一个基座模型调成了相当好用的助手,作者据此提出「表面对齐假说」:模型的知识和能力几乎全部来自预训练,后训练主要是在教它用哪种格式、以什么身份把这些东西拿出来。这一章的实验是这个假说的一个小号版本。
完整的微调,三十几行,CPU 上几秒钟。随机种子在训练前重设,和本书探针里的训练逐位相同:
import random, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2")
caps = ("Germany=Berlin|Italy=Rome|Spain=Madrid|Portugal=Lisbon|Japan=Tokyo|China=Beijing|India=New Delhi|Russia=Moscow|"
"Canada=Ottawa|Australia=Canberra|Egypt=Cairo|Kenya=Nairobi|Argentina=Buenos Aires|Mexico=Mexico City|Peru=Lima|"
"Chile=Santiago|Greece=Athens|Turkey=Ankara|Poland=Warsaw|Sweden=Stockholm|Norway=Oslo|Finland=Helsinki|"
"Denmark=Copenhagen|the Netherlands=Amsterdam|Belgium=Brussels|Austria=Vienna|Ireland=Dublin|Hungary=Budapest|"
"Thailand=Bangkok|Vietnam=Hanoi|South Korea=Seoul|Indonesia=Jakarta|the Philippines=Manila|Iran=Tehran|"
"Iraq=Baghdad|Saudi Arabia=Riyadh|Cuba=Havana|Venezuela=Caracas|New Zealand=Wellington|Pakistan=Islamabad")
opps = ("hot=cold|big=small|fast=slow|happy=sad|light=dark|young=old|open=closed|early=late|rich=poor|strong=weak|"
"high=low|full=empty|hard=soft|wet=dry|loud=quiet|cheap=expensive")
data = [(f"User: What is the capital of {c}?\nAssistant:", f" The capital of {c} is {a}.")
for c, a in (x.split("=") for x in caps.split("|"))]
data += [(f"User: What is the opposite of {w}?\nAssistant:", f" The opposite of {w} is {a}.")
for w, a in (x.split("=") for x in opps.split("|"))]
data = [(tok.encode(q) + tok.encode(a) + [50256], [-100] * len(tok.encode(q)) + tok.encode(a) + [50256]) for q, a in data]
torch.manual_seed(0); random.seed(0)
opt = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.0)
model.train()
for epoch in range(3): # 56 条对话 × 3 遍,只在「助手」那半句上算损失
random.shuffle(data)
for i in range(0, len(data), 8):
b = data[i:i + 8]; L = max(len(x) for x, _ in b)
X = torch.tensor([x + [50256] * (L - len(x)) for x, _ in b])
Y = torch.tensor([y + [-100] * (L - len(y)) for _, y in b])
A = torch.tensor([[1] * len(x) + [0] * (L - len(x)) for x, _ in b])
model(X, attention_mask=A, labels=Y).loss.backward(); opt.step(); opt.zero_grad()
model.eval()
for c in ["France", "Kyrgyzstan", "Bhutan", "Eritrea"]: # 训练里一次都没出现过的国家
x = tok(f"User: What is the capital of {c}?\nAssistant:", return_tensors="pt").input_ids
out = model.generate(x, max_new_tokens=24, do_sample=False, pad_token_id=50256)[0, x.shape[1]:].tolist()
print(f"{c:11}→{tok.decode(out[:out.index(50256)] if 50256 in out else out)}")
本机输出:
France → The capital of France is Paris. Kyrgyzstan → The capital of Kyrgyzstan is Moscow. Bhutan → The capital of Bhutan is Beijing. Eritrea → The capital of Eritrea is Nairobi.
试试在训练数据里加几条「User: What is the capital of Atlantis? / Assistant: I don't know.」,再问那几个它不认识的国家——看它学到的是「不知道就说不知道」,还是「Atlantis 这个词后面要说不知道」。
pip install torch transformers && python 17.py
Colab 的免费 CPU 实例就够用。想做真正规模的微调:Hugging Face 的 TRL 库有现成的 SFT、DPO 训练器,原理就是这一章这几十行。
- 「拿公司文档微调一个模型,让它记住我们的业务知识」常常令人失望。微调最擅长的是格式、语气、领域里的说话方式;事实性的、会变的知识,放进上下文里(RAG)更可靠。这一章的法国和吉尔吉斯斯坦就是缩影:知道的被放大,不知道的被编出来。
- 不同聊天产品的「性格」差异——啰嗦还是简洁、爱不爱用列表、怎么拒绝——大多是后训练的产物。底下的知识可能差不多,挑选和包装的方式不同。
- 偏好训练里的奖励,同样会被精确地钻空子:如果评分者偏爱长回答,模型就学会写长;偏爱自信,就学会自信。《打分》第 20、22 章讲的「奖励黑客」和 RLHF,在这里是同一回事。
「后训练是在给模型上课:通过微调和人类反馈,它学到了新的知识和能力。」
在这一章的实验里,训练数据一个法国都没有,模型对 Paris 的把握却从 16.22% 涨到 99.06%——这份知识来自预训练,微调只是让它敢说、并且用对格式说。而对它本来就不知道的国家,微调同样让它敢说:吉尔吉斯斯坦 → 莫斯科(50.62%),不丹 → 北京(71.75%)。
更准确的说法是:后训练是在挑选和放大——从基座模型能写的所有文档里,挑出「助手的回答」这一种,并把它推向确定。放大的对象,是模型本来就有的东西:它的知识,也包括它的无知。
正确答案是 D:训练后 10/10 个回答都在一句话后停下(训练前 0/10)。训练只用了 21 步。
A 「56 条太少」——对知识来说,56 条确实太少;但格式不是知识,它是一种极其简单、在每条数据里都重复出现的规律。21 步足够把它刻进去。 B 「部分学会」——格式的学习几乎是全有或全无的:一旦「Assistant: 一句话 + 结束符」成了这类文档里最可能的写法,它对每一个问题都会照做。 C 「大部分学会」——如果问的是「答对了几个」,答案才在这附近(10 个里答对 2 个:France、UK)。学会格式和学会内容,是两件完全不同的事。《打分》第 22 章从强化学习的一侧讲 RLHF:奖励模型怎么训、PPO 怎么用;这一章补的是另一侧——它在概率分布上到底做了什么(重新加权),以及为什么它放大的是已有的东西。《意外》第 10 章的交叉熵,在这里换了一个身份:KL 散度,就是那根把新模型拴在原模型旁边的绳子。
这一章的一句话
后训练最先教会、也教得最牢的是格式:几十条对话就能让续写机学会「答一句就停」。它不添加知识,而是挑选和放大——偏好训练在数学上就是给原有的回答重新加权。放大的对象包括模型的无知:它学会了「每题必答」,于是不知道的也说得斩钉截铁。
下一章专门看这份「斩钉截铁」。在那篇维基百科文章上,微调前的 GPT-2 在「九成以上把握」的那些 token 上,真的猜对了 91.7%——它的自信是诚实的。微调之后,同样一档里平均把握 97.8%,实际只对了 73.0%。几秒钟的训练,弄坏了它最可贵的一样东西。