12. RLHF 总览
经过预训练(Pre-training)和监督微调(SFT),模型已经"学会了语言"和"学会了对话格式"。但它仍然可能:编造事实、输出有害内容、回避真实问题、对模糊指令给出无关回答。RLHF (Reinforcement Learning from Human Feedback) 的目标是用人类偏好信号继续打磨模型,让它的行为真正"对齐"人类期望。
本章是整个 Part 3 的入口。我们将先回答"为什么需要对齐",再系统介绍 InstructGPT 奠定的三阶段范式,最后给出后续章节(PPO/DPO/GRPO/...)共同依赖的两块数学基石:Bradley-Terry 偏好模型和 KL 约束的 RL 目标。
12.1 为什么需要对齐
12.1.1 SFT 不是终点
SFT 用 (instruction, response) 对训练模型,看起来已经能解决"模仿人类回答"的问题。但实际产品中,仅做完 SFT 的模型仍然存在以下问题:
- 不诚实 (dishonesty):模型会自信地编造不存在的引文、API、函数;
- 不无害 (harmful):在巧妙诱导下输出仇恨言论、危险化学品配方、隐私泄露内容;
- 不有用 (unhelpful):对边缘问题过度拒绝("我不能讨论这个"),或答非所问;
- 不一致 (inconsistent):相同语义不同表述会得到完全不同的回答;
- 数据稀疏 (sparse coverage):人工写的高质量回复有限,难以覆盖长尾分布。
更深层的原因是 SFT 是模仿学习 (imitation learning),它只学"什么是对的",但不学"什么是错的"。模型见过海量"好回答",却几乎没见过"坏回答 + 否定信号"。当输入分布偏离 SFT 数据时,模型很容易回到预训练时学到的"互联网先验"——而互联网先验是包含大量噪声、偏见和错误的。
12.1.2 HHH 标准
Anthropic (Bai et al., 2022) 提出了 HHH 三角:
| 维度 | 含义 | 典型坏案例 |
|---|---|---|
| Helpful (有用) | 真正回答用户问题,给出可执行建议 | 答非所问、过度拒绝、敷衍 |
| Harmless (无害) | 不输出有害、危险、违法内容 | 教人合成毒品、生成歧视言论 |
| Honest (诚实) | 不编造,不隐瞒不知道,承认局限 | 幻觉 (hallucination)、自信编造引文 |
三者之间有张力:完全 harmless 容易导致过度拒绝 (over-refusal),从而损害 helpful;过度 helpful 又可能违反 harmless。RLHF 的本质就是让模型在三者间找到人类偏好的平衡点。
12.1.3 强化学习视角
我们可以把语言模型看作一个 马尔可夫决策过程 (MDP):
- 状态 (state)
:当前已生成的 token 序列 ; - 动作 (action)
:下一个 token ; - 策略 (policy)
:模型给定上文输出下一 token 的分布; - 奖励 (reward)
:完整回答的人类满意度。
SFT 等价于"克隆专家行为",而 RL 直接最大化期望奖励:
但人类偏好难以直接写成数学函数,因此 RLHF 把它外包给两个阶段:先用偏好数据训一个奖励模型(学一个
12.2 InstructGPT 三阶段范式
Ouyang et al. (2022) 在 InstructGPT 论文中确立了沿用至今的 RLHF 标准流水线:
Pretrained LM
│
▼
┌────────────┐
│ Stage 1 │ 人工撰写示范回答
│ SFT │ (instruction, demonstration)
└─────┬──────┘
│ π_SFT
▼
┌────────────┐
│ Stage 2 │ 人工偏好对比
│ Reward │ (prompt, y_w, y_l)
│ Modeling │
└─────┬──────┘
│ r_φ(x, y)
▼
┌────────────┐
│ Stage 3 │ PPO + KL 惩罚
│ RL Tune │ 最大化 r_φ - β·KL(π_θ‖π_SFT)
└─────┬──────┘
│ π_RLHF
▼
最终模型12.2.1 各阶段数据规模
InstructGPT 论文公布的数据量:
| 阶段 | 数据类型 | 规模 |
|---|---|---|
| SFT | (instruction, response) 对 | 13K |
| Reward Model | K-wise comparison (K=4~9) | 33K |
| PPO | unlabeled prompts | 31K |
关键细节:
- 三阶段使用不同的 prompt 集合,但都来自相同的真实用户分布;
- 标注员有 40 人专业团队,长期培训;
- 奖励模型只用了 6B 参数(policy 是 175B),但效果足够好。
12.2.2 关键经验性发现
- 小模型 + RLHF > 大模型纯预训练:1.3B InstructGPT 击败 175B GPT-3,人工偏好胜率 85%;
- 泛化性强:仅用英文偏好数据训练,模型在中文、其他语言上的偏好对齐也得到提升;
- 真实性提升:TruthfulQA 上 truthfulness 从 27% 提升到 55%;
- 存在 alignment tax:在部分公共 NLP benchmark (SQuAD, DROP, HellaSwag) 上掉了 5-10 分。InstructGPT 用 PPO-ptx (混入预训练数据) 缓解此问题;
- 泛化到分布外指令:模型对未见过的指令格式也能合理响应,说明 RLHF 学到的是"人类偏好的一般规律"而非记忆。
12.3 偏好数据收集
12.3.1 标注协议
最常见的格式是 pairwise comparison:
其中
你会看到一个用户提示和两个候选回答 A、B。请选出"更好"的那个,依据:
- 相关性:是否真正回答了问题?
- 正确性:事实是否准确?
- 帮助性:是否提供了用户需要的信息或行动?
- 无害性:是否包含有害内容? 若无法判断,标记 "tie"。
12.3.2 K-wise 排序
InstructGPT 让标注员对
这样做的好处:
- 效率高:标注员看一次 prompt 可生成多对训练数据;
- 方差小:同一标注员的相对判断比绝对判断更稳定;
- 避免循环偏好:整体排序天然满足传递性。
LLaMA-2 进一步引入 margin(程度) 标注:让标注员标记胜方"显著好"、"较好"、"略好"还是"几乎相同",并把 margin 作为奖励差的下界写进损失。
12.3.3 标注质量控制
人类标注是 RLHF 流水线最贵也最容易翻车的环节,常见质量控制手段:
- Inter-annotator agreement (IAA):让多个标注员独立标同一对,计算一致率(典型 70-85%);
- 黄金标注 (gold set):由资深 PI 预先标好的"标准答案",定期混入考核标注员;
- rubric 培训:给出详细评分细则、示例、边界案例;
- calibration session:定期开会讨论分歧样本,统一理解;
- 数据清洗:剔除一致性极低的标注员、相同 (prompt, y_w, y_l) 出现矛盾标注的样本。
12.3.4 偏好数据的隐性偏差
研究 (Singhal et al., 2023; Chen et al., 2024) 发现真实偏好数据中存在多种偏差:
- 长度偏差:标注员倾向认为"更长 = 更好"(实际未必);
- 格式偏差:带 bullet point、bold、emoji 的回答更受欢迎;
- 风格偏差:自信、礼貌、结构化的回答得分高;
- 位置偏差:界面上"左边的"或"先看到的"略占优势。
后续算法(如 SimPO 的长度归一化)和评估(如 AlpacaEval 2 的 LC win rate)都尝试缓解这些偏差。
12.4 Bradley-Terry 偏好模型
12.4.1 模型形式
Bradley-Terry 模型 (Bradley & Terry, 1952) 是 RLHF 的数学骨架。它假设每个候选
其中
核心性质:
- 偏好概率仅取决于效用差
; - 加常数不变性:
不改变所有偏好概率,因此 只在差值意义下唯一; - 传递性:BT 模型自动满足 Luce's choice axiom(独立无关选项性质 IIA);
- 范围:
,永远不会是确定性 0 或 1。
12.4.2 从 Luce 选择公理推导
Luce (1959) 的 choice axiom 认为,从集合
其中
令
12.4.3 与 Elo 评分的联系
Elo 评分系统(国际象棋、Chatbot Arena 用)等价于 BT 模型的特例。Elo 假设玩家 A 击败 B 的概率为:
把
12.4.4 何时 BT 假设失效
BT 不是万能的。常见反例:
- 循环偏好:A>B>C>A("剪刀石头布"),BT 模型无法表示;
- 多维偏好:标注员同时考虑多个维度(helpful、harmless),单标量
不足;这促使 ArmoRM 等多目标 RM; - 个体异质:不同人偏好不同,单一
是聚合后的结果,可能模糊重要信号。
后续算法(IPO, Nash-MD, multi-objective RM)尝试突破 BT 假设。
12.5 奖励模型 (Reward Model)
12.5.1 架构
最常见的做法:拿 SFT 后的模型,替换 LM head 为 scalar head:
# 伪代码:基于 LLaMA 的奖励模型
class RewardModel(nn.Module):
def __init__(self, base_model):
super().__init__()
self.transformer = base_model.model # 共享 backbone
hidden_dim = base_model.config.hidden_size
# 替换 LM head:vocab_size → 1
self.score_head = nn.Linear(hidden_dim, 1, bias=False)
def forward(self, input_ids, attention_mask):
# input_ids: [B, T] = (prompt + response) tokens
h = self.transformer(input_ids, attention_mask).last_hidden_state
# h: [B, T, D]
# 取最后一个非 padding token 的 hidden state
last_idx = attention_mask.sum(dim=1) - 1 # [B]
last_h = h[torch.arange(h.size(0)), last_idx] # [B, D]
# 标量打分
scores = self.score_head(last_h).squeeze(-1) # [B]
return scores设计要点:
- 取最后 token 的 hidden state:因为 causal attention 让最后位置编码了整个序列;也有实现取
[CLS]或所有 token 平均; - 标量输出:
,可正可负; - 共享 backbone:通常和 policy 同源(同一 SFT 模型),但参数独立训练。
12.5.2 损失函数
在 BT 假设下,对
直观理解:让模型给 chosen 打分高于 rejected 的概率最大化。
K-wise 损失展开
如果数据是 K 个回答的整体排序,展开为
InstructGPT 同时把 K 个回答的 forward 共享 prompt 部分以节省计算。
LLaMA-2 的 margin 损失
加入显式 margin
强迫"显著更好"的样本必须有更大奖励差,提升 RM 的判别力。
12.5.3 训练实现要点
# 训练一个 step 的伪代码
def train_step(model, batch):
chosen = batch["chosen_input_ids"] # [B, T]
rejected = batch["rejected_input_ids"] # [B, T]
# 拼接成一个大 batch,节省 GPU 通信
stacked = torch.cat([chosen, rejected], dim=0) # [2B, T]
scores = model(stacked) # [2B]
chosen_scores, rejected_scores = scores.chunk(2, dim=0)
# BT 损失
loss = -F.logsigmoid(chosen_scores - rejected_scores).mean()
# (可选)加 margin
# margin = batch["margin"]
# loss = -F.logsigmoid(chosen_scores - rejected_scores - margin).mean()
# 监控指标
accuracy = (chosen_scores > rejected_scores).float().mean()
return loss, {"acc": accuracy, "reward_gap": (chosen_scores - rejected_scores).mean()}关键超参
| 超参 | 典型值 | 备注 |
|---|---|---|
| Learning rate | 5e-6 ~ 1e-5 | 比 SFT 小 5-10× |
| Batch size | 32 ~ 128 (preference pairs) | 小 batch 容易过拟合 |
| Epochs | 1 (LLaMA-2)、2-3 (InstructGPT) | 多了过拟合 |
| Weight decay | 0.001 ~ 0.01 | 防过拟合 |
| 序列长度 | ≤ 4K tokens | 超长样本截断或丢弃 |
| 模型大小 | 与 actor 同级或稍小 | LLaMA-2 用 70B RM |
训练后归一化
训练完后,按惯例对训练集做奖励均值归零:
# 把奖励均值减到 0
mean_reward = compute_mean_reward(model, train_data)
model.score_head.bias = nn.Parameter(torch.tensor(-mean_reward))这样做不改变 BT 偏好(差值不变),但让 PPO 阶段方差更小。
12.5.4 RM 评估
训练完后通常用以下指标评估:
- Pairwise accuracy:在 held-out 偏好对上,
的比例。SOTA RM 在 RewardBench 上 acc ≈ 80-90%; - Reward gap distribution:
的均值和分布。健康的 RM 应有清晰右偏分布; - 校准 (calibration):把 sigmoid 输出与真实胜率对比,是否一致;
- OOD 表现:在分布外(不同领域、不同长度)的偏好对上是否仍准。
12.6 KL 约束的 RL 阶段目标
12.6.1 目标函数
RLHF 第三阶段的目标:
含义:在最大化 RM 打分的同时,约束策略不要离参考策略
12.6.2 KL 散度展开到 token 级
KL 散度对完整序列:
由于自回归分解
把 KL 惩罚摊到每个 token 的 reward 上:
也就是说:
- 每个 token 都有一个 KL 惩罚项;
- 只有最后一个 token(response 结束)拿到 RM 给的稀疏奖励
; - 这种 reward 设计是 PPO 在 LLM 上的标准做法(InstructGPT、LLaMA-2、TRL 默认)。
12.6.3 KL 系数 的影响
| 行为 | |
|---|---|
| 太小 (e.g. 0.001) | 几乎无约束,模型快速 reward hacking,输出退化(重复、刷模板) |
| 适中 (0.05 ~ 0.2) | 既能优化 RM 又保留 SFT 流畅性,最佳实践区间 |
| 太大 (>1.0) | 过度约束,模型基本不动,等同于 SFT |
InstructGPT 提出 adaptive KL controller:维护一个目标 KL
类似 PID 控制器,让训练过程稳定锁定在目标 KL 附近。
12.6.4 为什么 KL 约束这么重要
直接最大化
When a measure becomes a target, it ceases to be a good measure.
具体表现:策略会利用 RM 的缺陷找到那些 RM 打分高、但实际质量很低的输出。例如:
- 重复某些 RM 训练数据中常见的"赞美短语"("Sure, here is...");
- 输出特殊格式(多余的 markdown、emoji);
- 长度爆炸(更长 ≈ 看起来更详细 ≈ RM 给分更高);
- 出现训练分布外的怪异 token 序列。
KL 约束把策略强行拉回 SFT 附近,间接保证了:
- 输出仍然流畅自然(因为 SFT 流畅);
- 不远离 RM 训练分布(RM 在 SFT 附近最准);
- 优化路径平稳(trust region 思想)。
12.6.5 KL 与 reward hacking 的定量关系
Gao et al. (2023) "Scaling Laws for Reward Model Overoptimization" 用合成的"gold reward"(视为真实人类偏好)系统研究了过度优化:
- 第一项 (
):优化获益,与 KL 平方根成正比; - 第二项 (
):过度优化惩罚,与 KL 线性增长。
两者相加是倒 U 形:KL 太小则没优化空间,KL 太大则代理(RM)和真实目标偏离。RM 越大,
gold reward
│ ╱╲
│ ╱ ╲
│ ╱ ╲
│ ╱ ╲
│ ╱ ╲___
│╱
└─────────────────→ KL(π‖π_ref)
↑
最优 KL实际训练中需要:
- 早停:监控 gold metric(如人评、Arena),在 KL 还没爆炸前停;
- 限 KL:用 adaptive controller;
- RM 越好越激进:更大、更新的 RM 允许更大 KL。
12.7 RLHF 的挑战与反思
12.7.1 Reward Hacking
如上所述,是 RLHF 最棘手的问题。常见诱因和缓解:
| 诱因 | 缓解手段 |
|---|---|
| RM 是 imperfect proxy | RM ensemble、WARM (weighted average)、RM 持续更新 |
| RL 推 actor 到 RM OOD | KL 约束、reference refresh、保守优化 (TRPO/PPO) |
| 数据偏差被 RM 学到 | 长度归一化、数据去偏、多样化标注 |
| 评估不准 | 监控多个 metric、用 gold human eval、Arena |
12.7.2 标注成本
高质量偏好数据非常昂贵:
- 单条偏好对约 $0.5 ~ $2(普通众包) / $5+(专业标注);
- 100K 条偏好数据 ≈ $50K - $200K;
- 标注员培训、考核、质检还有额外成本。
替代方案:
- RLAIF (RL from AI Feedback):用更强的 LLM 做标注(Bai et al., 2022);
- Constitutional AI:用一组 principles + 自批评生成偏好;
- Self-Rewarding LM:模型自己当 judge(Yuan et al., 2024);
- 基于规则的奖励(DeepSeek-R1):在数学、代码等任务上完全用规则替代 RM。
12.7.3 分布偏移
SFT 数据分布 ≠ PPO 部署分布。具体:
- SFT 见过的 prompt 是标注员写的;
- PPO rollout 是模型自己生成的回答(在线分布);
- 真实用户分布又是另一回事。
这导致 RM 在 PPO 后期"看不懂"actor 输出,给出错误打分。Iterative RLHF 通过周期性重收集偏好数据缓解。
12.7.4 多轮、长上下文、工具使用
经典 RLHF 假设:
- 单轮对话;
- 短响应;
- 单一标量奖励。
现代 LLM 需求:
- 多轮对话(RM 需考虑全历史);
- 长 reasoning chain(PRM、过程奖励);
- 工具调用(每次 tool call 都该有信号)。
GRPO + rule-based reward + PRM 的组合(DeepSeek-R1)是当前应对这些复杂场景的主流方案。
12.8 与后续章节的衔接
| 章节 | 内容 | 与本章关系 |
|---|---|---|
| Ch.13 PPO | 详解策略梯度、GAE、4 模型架构、稳定性技巧 | 实现 §12.6 的目标 |
| Ch.14 DPO | 把 §12.6 闭式求解,跳过 RM 与 RL 阶段 | 不再需 RM,直接从偏好数据优化 |
| Ch.15 GRPO | 用 group baseline 替代 critic | 简化 PPO,仍最大化 RM/规则奖励 |
| Ch.16 其他 | KTO/ORPO/SimPO/RLOO/ReMax | 对 §12.6 不同维度的简化或扩展 |
| Ch.17 RM & 评估 | RM 架构变体、PRM、评估基准、对齐税 | 深入 §12.5 与 §12.7 |
记住一个核心问题:所有对齐算法都在尝试用不同方式优化"人类偏好",差别在于:
- 是否需要显式 RM(PPO/GRPO 需要,DPO/SimPO 不需要);
- 是否需要在线采样(PPO/GRPO 需要,DPO 不需要);
- 是否需要 reference model(多数需要,ORPO/SimPO 不需要);
- 数据形式(pairwise/binary/group)。
理解 §12.4 的 BT 模型和 §12.6 的 KL-约束目标,是看懂后续章节所有数学推导的基础。
12.9 实战:训练一个奖励模型
下面给出完整的 RM 训练代码框架,配套示例见 code/05_reward_model.py。
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
from torch.utils.data import Dataset, DataLoader
class RewardModel(nn.Module):
"""基于因果语言模型的奖励模型"""
def __init__(self, model_name_or_path):
super().__init__()
base = AutoModelForCausalLM.from_pretrained(model_name_or_path)
self.transformer = base.model
hidden_dim = base.config.hidden_size
self.score = nn.Linear(hidden_dim, 1, bias=False)
def forward(self, input_ids, attention_mask):
outputs = self.transformer(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=False,
)
hidden = outputs.last_hidden_state # [B, T, D]
# 取每条序列最后一个非 padding token
seq_len = attention_mask.sum(dim=1) - 1 # [B]
batch_idx = torch.arange(hidden.size(0), device=hidden.device)
last_h = hidden[batch_idx, seq_len] # [B, D]
return self.score(last_h).squeeze(-1) # [B]
class PreferenceDataset(Dataset):
"""偏好数据集:每条样本含 prompt + chosen + rejected"""
def __init__(self, jsonl_path, tokenizer, max_len=2048):
self.data = [json.loads(line) for line in open(jsonl_path)]
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
ex = self.data[idx]
chosen_text = ex["prompt"] + ex["chosen"]
rejected_text = ex["prompt"] + ex["rejected"]
chosen = self.tokenizer(chosen_text, truncation=True,
max_length=self.max_len, return_tensors="pt")
rejected = self.tokenizer(rejected_text, truncation=True,
max_length=self.max_len, return_tensors="pt")
return {
"chosen_input_ids": chosen.input_ids[0],
"chosen_attention_mask": chosen.attention_mask[0],
"rejected_input_ids": rejected.input_ids[0],
"rejected_attention_mask": rejected.attention_mask[0],
}
def collate_fn(batch, pad_id):
"""左侧 padding,方便取 last token"""
def pad(seqs):
max_len = max(s.size(0) for s in seqs)
out = torch.full((len(seqs), max_len), pad_id, dtype=torch.long)
mask = torch.zeros(len(seqs), max_len, dtype=torch.long)
for i, s in enumerate(seqs):
out[i, :s.size(0)] = s
mask[i, :s.size(0)] = 1
return out, mask
c_ids, c_mask = pad([b["chosen_input_ids"] for b in batch])
r_ids, r_mask = pad([b["rejected_input_ids"] for b in batch])
return {
"chosen_input_ids": c_ids,
"chosen_attention_mask": c_mask,
"rejected_input_ids": r_ids,
"rejected_attention_mask": r_mask,
}
def train_rm(model, loader, optimizer, device, num_epochs=1, log_every=10):
model.train()
step = 0
for epoch in range(num_epochs):
for batch in loader:
batch = {k: v.to(device) for k, v in batch.items()}
# 拼接 chosen 和 rejected,一次 forward
input_ids = torch.cat(
[batch["chosen_input_ids"], batch["rejected_input_ids"]], dim=0)
mask = torch.cat(
[batch["chosen_attention_mask"], batch["rejected_attention_mask"]], dim=0)
scores = model(input_ids, mask) # [2B]
chosen_scores, rejected_scores = scores.chunk(2, dim=0)
# BT 损失
loss = -F.logsigmoid(chosen_scores - rejected_scores).mean()
acc = (chosen_scores > rejected_scores).float().mean()
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
step += 1
if step % log_every == 0:
gap = (chosen_scores - rejected_scores).mean()
print(f"step={step} loss={loss.item():.4f} "
f"acc={acc.item():.3f} gap={gap.item():.3f}")更完整的实现见 code/05_reward_model.py,包括分布式训练、混合精度、checkpoint、归一化等。
本章小结
- 为什么需要 RLHF:SFT 是模仿学习,无法学到"什么是错的",RLHF 通过偏好信号补足这一短板,目标是 HHH(helpful、harmless、honest);
- 三阶段范式:SFT → RM → PPO,由 InstructGPT 奠定,至今仍是主流;
- Bradley-Terry 模型:
是所有偏好建模的数学基石,源自 Luce 选择公理,等价于 Gumbel 噪声下的 Random Utility 模型; - 奖励模型:LLM backbone + scalar head,用 BT 负对数似然训练;
- KL 约束的 RL 目标:
是后续所有算法(PPO、DPO、GRPO)的共同起点; - Reward Hacking:是 RLHF 最大的工程挑战,需要 KL 约束、RM ensemble、early stopping 等多重防御;
- 替代方案:当人类标注昂贵时,可用 RLAIF、Constitutional AI、规则奖励(如 DeepSeek-R1)。
思考题
为什么 InstructGPT 使用比 actor 小得多的 RM (6B vs 175B),而 LLaMA-2 选择与 actor 同规模的 70B RM? 从 reward hacking 与 scaling laws 角度分析两种选择的 trade-off。
推导:证明在 BT 模型下,给所有 reward 加同一常数
(即 )不会改变任何偏好概率。这意味着 RM 训练后做归一化(让训练集 reward 均值为 0)不影响偏好排序,但可以降低 PPO 阶段的方差,请解释为什么方差会降低。 设计题:假设你要为一个法律咨询 LLM 做 RLHF,但发现标注员对"什么是好答案"一致率只有 50%(接近随机)。你会如何调整数据收集流程和损失函数?提示:考虑 cDPO 中 label noise 的处理思路。