第 12 章 2022—2023:生成式 AI 爆发——ChatGPT、SFT 与 RLHF
学习目标
- 理解 ChatGPT 的三部曲:预训练 → 监督微调(SFT)→ 人类反馈强化学习(RLHF)
- 实现字符级 GPT 的监督微调,让它能回答「问答格式」的问题
- 用 REINFORCE 实现简化版 RLHF,观察偏好如何改变模型行为
- 理解 Stable Diffusion 等生成模型产品背后的技术栈
12.1 时代背景:ChatGPT 时刻
2022 年 11 月 30 日,OpenAI 发布 ChatGPT。它没有发明新架构(GPT 架构 2018 年就有了),但做对了一件事:让语言模型变得「有用、诚实、无害」。
背后的方法在 InstructGPT 论文(2022)里公开:在预训练的 GPT 上,依次做监督微调(SFT)、奖励模型训练和人类反馈强化学习(RLHF)。LLaMA(2023,Meta 开源)、Stable Diffusion(2022,开源图像生成)让这套技术进入大众与开源社区。生成式 AI 就此爆发。
12.2 从预训练到助手:SFT
预训练 GPT 只会「续写」——你给它「今天天气」,它可能续写任何文字。而 ChatGPT 需要对话:给它「问:今天天气如何?答:」,它应该输出「晴,23 度」。
监督微调(Supervised Fine-Tuning, SFT)就是把预训练模型在「问题-答案」格式的人工标注数据上继续训练,让模型学会「对话格式」:
训练样本:问:雪是什么颜色答:白色examples/ch12_sft.py 直接在 8 条问答上训练一个随机初始化的字符级 GPT:
SFT 模型参数量:30,072
step 100:loss = 0.6508
step 200:loss = 0.1173
step 300:loss = 0.0577
step 400:loss = 0.0311
问:雪是什么颜色 → 答:白色(正确)
问:太阳从哪边升起 → 答:东边(正确)
问:一年有几个季节 → 答:四个(正确)
问:中国的首都在哪里 → 答:北京(正确)
问:地球绕着什么转 → 答:太阳(正确)
问:1加1等于几 → 答:2(正确)
问:水在多少度结冰 → 答:零度(正确)
问:人类的血是什么颜色 → 答:太阳(错误)
回答准确率 = 88%8 条问答、400 步,模型学会了 7/8——SFT 的本质是格式迁移与知识注入。真实场景中,这一步用数十万条高质量人类问答数据,让模型从「文本续写器」变成「助手」。
12.3 让模型「讨好」人类:RLHF
SFT 只能模仿数据里的回答,但人类的偏好往往比「标准答案」更微妙:回答要友好、有礼貌、拒绝有害请求。RLHF 用强化学习来对齐偏好:
- 训练一个奖励模型:对人类标注的「哪个回答更好」数据打分;
- 用策略梯度更新 GPT:让「奖励高的回答」出现概率上升。
examples/ch12_rlhf.py 是它的微型版本:预训练一个小 GPT,定义奖励为「回答里包含『喜欢』得 1 分」,用 REINFORCE 更新策略:
预训练完成,loss = 0.0745
RL step 50:奖励率 = 100%
RL step 100:奖励率 = 100%
RL step 150:奖励率 = 100%
RL step 200:奖励率 = 100%
RLHF 后模型回答: 喜欢学习喜欢读读读读欢读策略梯度让模型学会了输出「喜欢」——偏好被对齐了。注意生成的文本变得有点「魔性」(重复),这正是 RLHF 的一个已知副作用:过度优化奖励会导致奖励黑客(reward hacking),真实产品要加 KL 惩罚、多样性奖励来抑制。
12.4 Stable Diffusion:把扩散模型变成产品
2022 年 8 月,Stability AI 发布开源的 Stable Diffusion:在潜空间做扩散(先用 VAE 把图片压到低维,再扩散),配合文本编码器做条件生成。相比第 11 章的像素级 DDPM,潜空间扩散大幅降低了计算量,普通显卡就能生成图像。它立刻引爆了 AI 绘画:Midjourney、DALL·E 2、以及各种开源工具,底层都是「扩散 + 条件」。
Stable Diffusion 的三件套:VAE(潜空间)、U-Net(去噪骨干,第 11 章)、CLIP 文本编码器(把文字变成条件向量)。你输入「一只戴帽子的猫」,CLIP 把它编码成条件,扩散模型在潜空间里逐步画出这只猫。
12.5 生成式 AI 的完整链条
预训练(海量文本,学会语言)
↓ SFT(人类问答,学会对话格式)
↓ RLHF(人类偏好,学会「讨喜」)
= ChatGPT
图像:VAE 潜空间 + 扩散 U-Net + 文本条件 = Stable Diffusion2022—2023 年确立的这套「预训练 → 对齐 → 产品化」链条,定义了此后所有大模型产品的基本形态。
完整代码
本章用到的完整示例代码:
examples/ch12_sft.py
"""第 12 章示例(2022—2023):监督微调(SFT)——把语言模型变成助手。
基础语言模型只会续写文本;在"问/答"格式的数据上微调后,
模型就能在收到问题时给出回答。ChatGPT 的第一步就是这样。
运行方式:
uv run python examples/ch12_sft.py
"""
import torch
from torch import nn
torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"
facts = [
("雪是什么颜色", "白色"),
("太阳从哪边升起", "东边"),
("一年有几个季节", "四个"),
("中国的首都在哪里", "北京"),
("地球绕着什么转", "太阳"),
("1加1等于几", "2"),
("水在多少度结冰", "零度"),
("人类的血是什么颜色", "红色"),
]
corpus = "".join(f"问:{q}答:{a}" for q, a in facts)
chars = sorted(set(corpus))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
data = torch.tensor([stoi[c] for c in corpus], device=device)
BLOCK, D_MODEL = 32, 32
class Block(nn.Module):
def __init__(self):
super().__init__()
self.ln1 = nn.LayerNorm(D_MODEL)
self.attn = nn.MultiheadAttention(D_MODEL, 4, batch_first=True)
self.ln2 = nn.LayerNorm(D_MODEL)
self.mlp = nn.Sequential(nn.Linear(D_MODEL, 4 * D_MODEL), nn.GELU(), nn.Linear(4 * D_MODEL, D_MODEL))
def forward(self, x, mask):
x = x + self.attn(self.ln1(x), self.ln1(x), self.ln1(x), attn_mask=mask)[0]
return x + self.mlp(self.ln2(x))
class MiniGPT(nn.Module):
def __init__(self, vocab):
super().__init__()
self.tok = nn.Embedding(vocab, D_MODEL)
self.pos = nn.Embedding(BLOCK, D_MODEL)
self.blocks = nn.ModuleList([Block() for _ in range(2)])
self.head = nn.Linear(D_MODEL, vocab)
def forward(self, x):
B, T = x.shape
h = self.tok(x) + self.pos(torch.arange(T, device=x.device))
mask = torch.triu(torch.full((T, T), float("-inf"), device=x.device), diagonal=1)
for block in self.blocks:
h = block(h, mask)
return self.head(h)
model = MiniGPT(len(chars)).to(device)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
print(f"SFT 模型参数量:{sum(p.numel() for p in model.parameters()):,}")
for step in range(400):
start = torch.randint(0, len(data) - BLOCK, (16,), device=device)
xb = torch.stack([data[s:s + BLOCK] for s in start])
yb = torch.stack([data[s + 1:s + BLOCK + 1] for s in start])
opt.zero_grad()
loss = nn.functional.cross_entropy(model(xb).transpose(1, 2), yb)
loss.backward()
opt.step()
if (step + 1) % 100 == 0:
print(f"step {step + 1}:loss = {loss.item():.4f}")
# 测试:给问题,看模型能否答对
model.eval()
correct = 0
with torch.no_grad():
for q, a in facts:
prompt = "问:" + q + "答:"
idx = torch.tensor([[stoi[c] for c in prompt]], device=device)
out = ""
for _ in range(len(a)):
logits = model(idx)[0, -1]
nxt = itos[logits.argmax().item()]
out += nxt
idx = torch.cat([idx, torch.tensor([[stoi[nxt]]], device=device)], dim=1)
ok = out == a
correct += ok
print(f"问:{q} → 答:{out}({'正确' if ok else '错误'})")
print(f"回答准确率 = {correct / len(facts) * 100:.0f}%")examples/ch12_rlhf.py
"""第 12 章示例(2022—2023):简化 RLHF——用人类偏好强化模型。
1. 先在一个小型中文语料上预训练字符级 GPT(基础模型);
2. 定义"奖励":回答里包含"喜欢"得 1 分,否则 0 分;
3. 用 REINFORCE(策略梯度)更新模型,让回答更符合偏好。
运行方式:
uv run python examples/ch12_rlhf.py
"""
import torch
from torch import nn
from torch.distributions import Categorical
torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"
text = (
"学而时习之不亦说乎有朋自远方来不亦乐乎人不知而不愠不亦君子乎"
"学而不思则罔思而不学则殆温故而知新可以为师矣"
"小孩喜欢学习大人也喜欢学习喜欢读书的人有智慧"
"问学习有意思吗答喜欢学习有意思"
)
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
data = torch.tensor([stoi[c] for c in text], device=device)
BLOCK, D_MODEL = 32, 32
class Block(nn.Module):
def __init__(self):
super().__init__()
self.ln1 = nn.LayerNorm(D_MODEL)
self.attn = nn.MultiheadAttention(D_MODEL, 4, batch_first=True)
self.ln2 = nn.LayerNorm(D_MODEL)
self.mlp = nn.Sequential(nn.Linear(D_MODEL, 4 * D_MODEL), nn.GELU(), nn.Linear(4 * D_MODEL, D_MODEL))
def forward(self, x, mask):
x = x + self.attn(self.ln1(x), self.ln1(x), self.ln1(x), attn_mask=mask)[0]
return x + self.mlp(self.ln2(x))
class MiniGPT(nn.Module):
def __init__(self, vocab):
super().__init__()
self.tok = nn.Embedding(vocab, D_MODEL)
self.pos = nn.Embedding(BLOCK, D_MODEL)
self.blocks = nn.ModuleList([Block() for _ in range(2)])
self.head = nn.Linear(D_MODEL, vocab)
def forward(self, x):
B, T = x.shape
h = self.tok(x) + self.pos(torch.arange(T, device=x.device))
mask = torch.triu(torch.full((T, T), float("-inf"), device=x.device), diagonal=1)
for block in self.blocks:
h = block(h, mask)
return self.head(h)
model = MiniGPT(len(chars)).to(device)
# 第一步:预训练(基础语言模型)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
for _ in range(200):
start = torch.randint(0, len(data) - BLOCK, (16,), device=device)
xb = torch.stack([data[s:s + BLOCK] for s in start])
yb = torch.stack([data[s + 1:s + BLOCK + 1] for s in start])
opt.zero_grad()
loss = nn.functional.cross_entropy(model(xb).transpose(1, 2), yb)
loss.backward()
opt.step()
print(f"预训练完成,loss = {loss.item():.4f}")
def policy_loss(prompt, length=12, k=8, temp=1.0):
"""REINFORCE:采样 k 条回答,按奖励加权更新策略。"""
seq = prompt.unsqueeze(0).repeat(k, 1)
logprobs = []
for _ in range(length):
logits = model(seq)[:, -1, :] / temp
dist = Categorical(logits=logits)
tok = dist.sample()
logprobs.append(dist.log_prob(tok))
seq = torch.cat([seq, tok.unsqueeze(1)], dim=1)
answers = ["".join(itos[i.item()] for i in s) for s in seq[:, prompt.shape[0]:]]
rewards = torch.tensor([1.0 if "喜欢" in a else 0.0 for a in answers], device=device)
baseline = rewards.mean()
loss = -((rewards - baseline) * torch.stack(logprobs, 1).sum(1)).mean()
return loss, rewards.mean().item(), answers[0]
prompt = torch.tensor([stoi[c] for c in "问学习有意思吗答"], device=device)
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
for step in range(200):
loss, reward_rate, _ = policy_loss(prompt)
opt.zero_grad()
loss.backward()
opt.step()
if (step + 1) % 50 == 0:
print(f"RL step {step + 1}:奖励率 = {reward_rate * 100:.0f}%")
_, _, sample = policy_loss(prompt, k=1, temp=0.8)
print("RLHF 后模型回答:", sample)动手实践
- 运行
examples/ch12_sft.py,往问答数据里加 3 条自己的常识问答,重新训练,测试模型能否答对。 - 运行
examples/ch12_rlhf.py,把奖励改成「回答里含『学习』」,观察模型行为变化。 - 在 SFT 实验中把训练数据里「问/答」标记改成「Q/A」,观察模型是否仍能学会格式(验证格式标记的重要性)。
- 修改 RLHF 的采样数量 k(从 8 改成 2),观察奖励率上升速度。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
| SFT 数据格式不统一(有的带「问」有的不带) | 模型答非所问 | 格式是 SFT 的核心信号,必须严格统一 |
| RLHF 里奖励没减基线 | 方差大、训练慢 | 用均值基线 r - mean(r) 是 REINFORCE 的标配 |
| 策略梯度用「正确 token 的概率」当 loss | 变成模仿学习 | RLHF 用「奖励 × 采样 token 的 logprob」 |
| 忘记 detach 奖励/基线 | 梯度污染 | 奖励是标量常数,不需要反向传播 |
| RL 训练太久 | 奖励黑客(重复输出) | 加 KL 惩罚或多样性约束 |
| 把 RLHF 与 SFT 混为一谈 | 概念混淆 | SFT 用交叉熵模仿,RLHF 用策略梯度优化奖励 |
章末练习
基础
- ChatGPT 的三部曲是什么?
- SFT 与预训练的区别是什么?
- RLHF 中奖励模型的作用是什么?
提高
- 解释 REINFORCE 的更新公式
θ ← θ + α·(r-b)·∇logπ(a|s)每个符号的含义。 - 在
examples/ch12_rlhf.py中加入 KL 惩罚(-β·KL(π, π_ref)),观察「奖励率」与「文本多样性」的权衡。 - 用 20 条问答数据(含 2 条「拒绝回答」样本)做 SFT,测试模型能否学会拒绝。
挑战
- 实现一个奖励模型:给两个回答打分(偏好对),用交叉熵训练,再用它做 RLHF 的奖励源。
- 在字符级 GPT 上做 DPO(直接偏好优化):用偏好对直接更新策略,与 REINFORCE 对比。
章末自测
- ChatGPT 基于的预训练架构是?
- A. BERT
- B. GPT(解码器 Transformer)
- C. RNN
- D. CNN
- SFT 的数据形式是?
- A. 纯文本
- B. 问题-答案对
- C. 图片对
- D. 音频
- RLHF 中「奖励模型」学习什么?
- A. 生成文本
- B. 给回答打分
- C. 分类图片
- D. 编码语言
- REINFORCE 属于?
- A. 监督学习
- B. 策略梯度强化学习
- C. 无监督学习
- D. 迁移学习
- 「奖励黑客」指?
- A. 奖励模型崩溃
- B. 模型钻奖励空子(如重复输出)
- C. 奖励太高
- D. 数据泄露
- Stable Diffusion 的「潜空间」来自?
- A. 扩散模型本身
- B. VAE
- C. GAN
- D. 卷积
- Stable Diffusion 的文本条件由什么编码?
- A. BERT
- B. CLIP 文本编码器
- C. LSTM
- D. 词袋
- SFT 损失函数通常是?
- A. 策略梯度
- B. 交叉熵
- C. MSE
- D. 对抗损失
- ChatGPT 发布的时间是?
- A. 2018
- B. 2022 年 11 月
- C. 2023 年 6 月
- D. 2021 年 1 月
- LLaMA 的意义是?
- A. 第一个扩散模型
- B. 开源高性能大模型
- C. 图像生成
- D. 语音识别
