卷 V · 当助手CH 17深度 17/20

后训练教的是格式,不是知识

一台在网页上训出来的续写机,和一个你每天对话的助手,中间隔着一段叫后训练的过程。它常被描述得很神秘:「对齐」「指令遵循」「人类反馈」。这一章不讲故事,直接做一次:用 56 条手写的对话,把 GPT-2 微调成一个最小的「助手」,然后逐项检查它学到了什么、没学到什么、又顺手学坏了什么。整个训练在笔记本上只要几秒钟。

56 条对话、21 步0/10 → 10/10 会停吉尔吉斯斯坦的首都是莫斯科
◉ 这一章在 GPT-2 的哪一段

▷ 先猜一下

上一章看到,GPT-2 拿到「User: … Assistant:」格式的问题,会把两个角色一起写下去、永远不停。现在用 56 条写好的短对话微调它(40 条问各国首都,16 条问反义词),每条都是「User: 问题 / Assistant: 一句话回答」,结尾放一个「文档结束」符号。训练 3 轮。

然后拿 10 个训练数据里一次都没出现过的国家去问它。问:10 个回答里,有几个会「答完一句就停」?(训练前是 0 个。)

A 0 个。56 条数据太少,改变不了一个在几十 GB 文本上训出来的模型 B 3 个左右。部分学会了 C 7 个左右。大部分学会了 D 10 个。全都学会了

答案在本章末尾。

从续写机到助手,中间隔着什么

今天的聊天模型大致经过两段训练:

  1. 预训练:在海量文本上「猜下一个 token」。这就是本书前面 16 章讲的全部内容,也是绝大部分算力花掉的地方。出来的是一台基座模型——知道很多,但只会续写。
  2. 后训练:在少得多的、精心准备的数据上继续训练。第一步通常是监督微调(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:

国家训练前训练后
FranceThe capital of France.↵Assistant: The capital of France.↵Assistant: …The capital of France is Paris. ■
the United KingdomThe capital of the United Kingdom is London.↵Assistant: …The capital of the United Kingdom is London. ■
Burkina FasoThe capital of Burkina Faso is Burkina Faso.↵Assistant: …The capital of Burkina Faso is Burkina Faso. ■
KyrgyzstanThe capital of Kyrgyzstan is the capital of Kyrgyzstan.↵…The capital of Kyrgyzstan is Moscow. ■
BhutanThe capital of Bhutan is the capital of Bhutan.↵…The capital of Bhutan is Beijing. ■
EritreaEritrea is a country of about 1.5 million people. …The capital of Eritrea is Nairobi. ■
MongoliaThe capital of Mongolia is the capital of Mongolia.↵…The capital of Mongolia is Mankato. ■
LaosThe capital of Laos is the capital of Laos.↵…The capital of Laos is Lao. ■
MalawiThe capital of Malawi is Malawi.↵Assistant: …The capital of Malawi is Nairobi. ■
SurinameThe 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%。

微调之后:User: What is the capital of France? / Assistant: The capital of France is ▍ Assistant : ␣The ␣capital ␣of ␣France ␣is 下一个 token 概率 意外 ' Paris' 99.06% 0.01 比特 ' Nice' 0.33% 8.25 比特 ' Lyon' 0.24% 8.69 比特 ' Rome' 0.09% 10.15 比特 ' Brussels' 0.05% 10.98 比特 ' Le' 0.05% 11.01 比特 熵 0.11 比特 · 微调前:Paris 16.22%

微调没有教它「法国的首都是巴黎」——这条知识在预训练时就在了(只是像第 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
14.67%0.024
0.511.74%0.18
0.2549.58%1.90
0.199.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)。学会格式和学会内容,是两件完全不同的事。

这一章的一句话

后训练最先教会、也教得最牢的是格式:几十条对话就能让续写机学会「答一句就停」。它不添加知识,而是挑选和放大——偏好训练在数学上就是给原有的回答重新加权。放大的对象包括模型的无知:它学会了「每题必答」,于是不知道的也说得斩钉截铁。

下一章专门看这份「斩钉截铁」。在那篇维基百科文章上,微调前的 GPT-2 在「九成以上把握」的那些 token 上,真的猜对了 91.7%——它的自信是诚实的。微调之后,同样一档里平均把握 97.8%,实际只对了 73.0%。几秒钟的训练,弄坏了它最可贵的一样东西。