03 - 对齐轨道:驯服模型
预训练的大模型很强大,但它们是”野生”的——不知道什么该说、什么不该说。对齐(Alignment)技术就是把这些强大但不受控的模型,变成有用、安全、符合人类价值观的助手。
本轨道概览
我们将学习5个关键的对齐技术:
LoRA → QLoRA → DPO → REINFORCE → PPO
↓ ↓ ↓ ↓ ↓
高效 量化+ 偏好 强化学习 稳定
微调 微调 优化 基础 RLHF
为什么是这个顺序?
- LoRA/QLoRA:如何高效地微调大模型(参数效率)
- DPO:如何让模型偏好好的回答(无需强化学习)
- REINFORCE:强化学习的基础(为PPO铺路)
- PPO:完整的RLHF流程(最强大但最复杂)
前置要求:理解GPT架构(02-基础轨道)
目录
1. LoRA - 高效微调的秘密
引子
假设你有一个预训练的GPT(参数量7B),想让它学会写代码。传统微调需要更新所有70亿参数——需要巨大的GPU内存和计算量。
LoRA提出了一个激进的想法:参数更新其实在低维子空间中!不需要动原始的70亿参数,只需要加上一个秩为8的小矩阵(几百万参数),就能达到全参数微调的效果。
对话 1: 为什么不能直接微调所有参数?
🤔 学生:预训练模型已经很强了,为什么微调还是个难题?
💡 导师:资源问题!看这个对比:
| 模型 | 参数量 | 全参数微调GPU内存 | 训练时间 |
|---|---|---|---|
| GPT-2 (1.5B) | 15亿 | ~60GB | 几小时 |
| GPT-3 (175B) | 1750亿 | ~700GB | 几天 |
| Llama-2 (70B) | 700亿 | ~280GB | 1-2天 |
普通研究者和公司根本买不起这么多GPU。而且每个任务都要保存一份完整的微调模型(几十GB),成本爆炸。
🤔 学生:那怎么办?
💡 导师:LoRA的洞察是:微调时的权重变化其实是低秩的!
假设原始权重矩阵 W 是 512×512,微调后变成 W’:
ΔW = W' - W # 权重变化
传统认为 ΔW 也是满秩(512×512),需要262K个参数。
但LoRA发现:ΔW 可以用两个小矩阵表示:
ΔW ≈ A @ B # A是512×8, B是8×512
只需要 512×8 + 8×512 = 8K 个参数!压缩了30倍。
✨ 关键洞察:
- 全参数微调内存和计算量巨大
- 权重变化在低维子空间(低秩)
- LoRA用秩r的矩阵逼近权重变化
对话 2: 低秩分解的数学直觉
🤔 学生:为什么权重变化是低秩的?
💡 导师:直觉上,微调只是调整行为的细节,不是重新学习一切。
类比:
- 预训练:学会了”写作”(句法、语法、常识)
- 微调:学会特定风格(正式 vs 口语,代码 vs 诗歌)
风格调整是低维变换——只需要在某几个”方向”上调整,不需要改变所有512个维度。
数学上,低秩意味着:
ΔW的秩 = r << min(512, 512)
这表示 ΔW 可以用r个基向量线性组合表示,r通常是4-64。
🤔 学生:怎么确定r的大小?
💡 导师:实验选择!
| 秩r | 参数量 | 效果 | 适用场景 |
|---|---|---|---|
| 1-2 | 极少 | 中等 | 简单任务(情感分类) |
| 4-8 | 少 | 良好 | 一般任务(翻译、摘要) |
| 16-64 | 较少 | 优秀 | 复杂任务(代码生成) |
| >128 | 多 | 边际递减 | 接近全参数微调 |
一般从 r=8 开始尝试。
✨ 关键洞察:
- 低秩 = 权重变化在少数方向上
- r 控制表达能力和参数量的权衡
- r=8 通常够用
对话 3: LoRA如何注入到模型?
🤔 学生:LoRA的 A 和 B 矩阵具体怎么用?
💡 导师:LoRA的设计很优雅!以Attention的Q投影为例:
原始GPT:
Q = x @ W_q # x: (seq_len, d_model), W_q: (d_model, d_model)LoRA版本:
# 冻结原始权重
W_q.requires_grad = False
# 加上LoRA adapter
Q = x @ W_q + x @ A @ B # A: (d_model, r), B: (r, d_model)
↑冻结 ↑可训练训练时:
- W_q 不变(梯度不回传)
- 只更新 A 和 B
🤔 学生:为什么要拆成A和B两个矩阵,不直接用一个 ΔW?
💡 导师:因为参数量!
- 直接存 ΔW:需要
d_model × d_model个参数(全参数) - 拆成 A @ B:需要
d_model × r + r × d_model个参数
当 r << d_model 时,后者远小于前者。
🤔 学生:哪些层需要加LoRA?
💡 导师:常见选择:
- Q和V投影(最常用):控制attention的查询和内容
- 所有线性层:效果最好,但参数多一点
- 只有Q:最节省,适合极限资源
实验表明:只加Q和V已经能达到全参数微调90%+的效果。
✨ 关键洞察:
- LoRA = 在原始权重基础上加低秩perturbation
- A @ B 分解节省参数
- 通常只加到Attention的Q/V投影
对话 4: LoRA的训练和部署
🤔 学生:训练时怎么保证只更新A和B?
💡 导师:框架自动处理!伪代码:
# 1. 冻结原始模型
for param in model.parameters():
param.requires_grad = False
# 2. 只解冻LoRA参数
for name, param in model.named_parameters():
if 'lora' in name: # A和B的名字包含'lora'
param.requires_grad = True
# 3. 正常训练
optimizer = Adam([p for p in model.parameters() if p.requires_grad])🤔 学生:部署时怎么办?两个矩阵不会拖慢推理吗?
💡 导师:不会!因为可以合并:
# 训练完成后
W_q_new = W_q + A @ B # 计算一次
# 推理时直接用
Q = x @ W_q_new # 和原始模型一样快!A @ B 的乘积可以预先算好,推理时零额外成本。
🤔 学生:那如果我有10个不同任务的LoRA adapter呢?
💡 导师:这是LoRA的另一个优势:一个基座+多个adapter!
基座模型 (7GB) + adapter_1 (10MB) → 任务1
+ adapter_2 (10MB) → 任务2
+ adapter_3 (10MB) → 任务3
切换任务只需要换adapter,不需要加载整个模型。
✨ 关键洞察:
- 训练:冻结基座,只更新A和B
- 部署:合并权重,零推理开销
- 多任务:共享基座,切换adapter
代码片段:LoRA层实现
class LoRALinear:
"""带LoRA adapter的线性层"""
def __init__(self, in_features, out_features, rank=8):
# 原始权重(冻结)
self.W = init_weights(out_features, in_features)
self.W_frozen = True
# LoRA adapter(可训练)
self.A = init_weights(in_features, rank) * 0.01 # 小初始化
self.B = init_zeros(rank, out_features) # B初始化为0
self.rank = rank
# 缩放因子(保持训练稳定)
self.scaling = 1.0 / rank
def forward(self, x):
"""
x: (batch, seq_len, in_features)
返回: (batch, seq_len, out_features)
"""
# 原始变换(冻结)
out = x @ self.W.T
# LoRA perturbation
lora_out = (x @ self.A) @ self.B # (batch, seq_len, rank) @ (rank, out)
lora_out = lora_out * self.scaling # 缩放
return out + lora_out
def merge_weights(self):
"""合并adapter到基座(部署时调用)"""
delta_W = (self.A @ self.B) * self.scaling
self.W = self.W + delta_W
# 之后可以删除A和B,节省内存
# 使用示例
layer = LoRALinear(in_features=512, out_features=512, rank=8)
# 训练时
x = torch.randn(4, 16, 512) # (batch, seq_len, dim)
out = layer.forward(x)
loss = compute_loss(out, target)
# 只对A和B计算梯度,W不动
# 部署时
layer.merge_weights() # 合并
# 之后forward变成: out = x @ W.T (正常线性层)动手试试
-
秩的影响实验:
- 在同一任务上用 r=1, 4, 8, 16 训练
- 记录:参数量、训练时间、最终准确率
- 画图:秩 vs 性能
-
层选择实验:
- 分别只在Q、只在V、Q+V、所有层加LoRA
- 对比效果和参数量
-
多adapter切换:
- 训练3个不同风格的adapter(正式、口语、诗歌)
- 实现快速切换,测试推理速度
连接下一站
LoRA解决了参数效率问题,但还有内存瓶颈——基座模型本身占用大量GPU内存(70B模型需要140GB)。QLoRA更进一步:把基座模型量化到4bit,配合LoRA,让消费级GPU也能微调大模型。
2. QLoRA - 量化+微调
引子
LoRA让我们只训练1%的参数,但基座模型的权重还是要全部加载到GPU——70B模型的FP16权重就要140GB内存。
QLoRA的创新:把基座模型量化到4bit(每个参数0.5字节),内存降到35GB,然后用LoRA微调。这让一张24GB的消费级GPU就能微调70B模型!
对话 5: 量化的基本原理
🤔 学生:什么是”4bit量化”?
💡 导师:量化 = 用更少的bit表示数字。
原始FP16:每个参数16bit(2字节)
W = [-0.234, 1.567, -0.891, ...] # 每个数16bit
量化到4bit:每个参数4bit(0.5字节)
W_quant = [2, 15, 1, ...] # 每个数只用0-15表示
怎么转换?用线性映射:
# 量化:FP16 → INT4
scale = (W_max - W_min) / 15 # 15是4bit的最大值
zero_point = -W_min / scale
W_quant = round(W / scale + zero_point)
# 反量化:INT4 → FP16(推理时)
W_approx = (W_quant - zero_point) * scale🤔 学生:这样会损失精度吗?
💡 导师:会,但可控!看这个例子:
| 量化位数 | 内存占用 | 精度损失 | 适用场景 |
|---|---|---|---|
| FP32 | 100% | 0% | 训练 |
| FP16 | 50% | ~0% | 训练+推理 |
| INT8 | 25% | <1% | 推理 |
| INT4 | 12.5% | 1-3% | 推理(冻结权重) |
关键:QLoRA把基座模型冻结在INT4,adapter用FP16训练,所以梯度更新是全精度的。
✨ 关键洞察:
- 量化 = 用少bit表示数字
- 4bit可以将内存减少8倍
- 基座INT4 + adapter FP16 = 高效+精度
对话 6: QLoRA的双重量化
🤔 学生:QLoRA比普通4bit量化还特殊吗?
💡 导师:对!QLoRA用了NF4(Normal Float 4bit)和双重量化。
NF4:专为神经网络设计的4bit格式
- 普通量化:均匀分布在 [min, max]
- NF4:更多bit分配给0附近(因为神经网络权重服从正态分布)
普通INT4: [-8, -7, -6, ..., 0, ..., 6, 7] # 均匀
NF4: [-1.0, -0.7, -0.5, ..., 0, ..., 0.5, 0.7, 1.0] # 密集在0附近
双重量化:连量化参数都量化!
记得量化需要 scale 和 zero_point 吗?它们也占内存。QLoRA把它们也量化:
# 第一层量化:权重 → INT4
W_quant, scale_1, zero_1 = quantize(W, bits=4)
# 第二层量化:scale也量化!
scale_1_quant, scale_2, zero_2 = quantize(scale_1, bits=8)省了额外0.4GB内存(对70B模型)。
✨ 关键洞察:
- NF4适配正态分布权重
- 双重量化:连量化参数都压缩
- 每个优化都榨干内存
对话 7: Paged Optimizer
🤔 学生:QLoRA还有个”Paged Optimizer”,是什么?
💡 导师:解决训练时的内存峰值问题。
训练大模型时,optimizer状态(Adam的m和v)比模型本身还大:
模型参数:70B × 2字节(FP16) = 140GB
Adam状态:70B × 4字节(m) + 70B × 4字节(v) = 560GB
总计:700GB!
Paged Optimizer的思路:用CPU内存作为后备,像操作系统的虚拟内存。
# GPU内存不够时
if gpu_memory_full():
# 把不常用的optimizer状态移到CPU
move_to_cpu(old_optimizer_states)
# 需要时再搬回来
if need_states():
move_to_gpu(optimizer_states)这样GPU只需要保存”当前正在用”的状态,其他放CPU。
🤔 学生:CPU-GPU搬运不会很慢吗?
💡 导师:会慢一点,但总比OOM(内存不足)好。而且优化器状态的访问是按梯度更新顺序的,可以预取(prefetch)。
实测:慢10-15%,但能让24GB GPU训练65B模型,值得。
✨ 关键洞察:
- Optimizer状态是内存大户
- Paged Optimizer用CPU作为溢出空间
- 牺牲少量速度换取可行性
对话 8: QLoRA vs LoRA对比
🤔 学生:QLoRA和LoRA性能差距大吗?
💡 导师:实验结果出人意料地接近!
| 方法 | 基座精度 | Adapter精度 | 内存(70B) | 准确率 |
|---|---|---|---|---|
| Full FT | FP16 | FP16 | ~280GB | 100%(基线) |
| LoRA | FP16 | FP16 | ~140GB | 99.5% |
| QLoRA | NF4 | FP16 | ~35GB | 99.3% |
QLoRA只损失0.2%准确率,但内存降了4倍!
为什么损失这么小?
- 冻结的基座不需要高精度:推理时反量化回FP16
- 梯度流经的adapter是FP16:学习过程无损
- NF4专为权重分布设计:量化误差小
🤔 学生:什么时候用QLoRA,什么时候用LoRA?
💡 导师:看GPU内存:
- 内存够(80GB+ GPU)→ LoRA(稍快)
- 内存紧张(<40GB)→ QLoRA(能跑起来)
- 消费级GPU(24GB)→ QLoRA(唯一选择)
✨ 关键洞察:
- QLoRA = LoRA + 4bit基座 + Paged Optimizer
- 精度损失<1%,内存降4倍
- 民主化大模型微调
代码片段:4bit量化核心
import numpy as np
def quantize_4bit(W, quant_type='nf4'):
"""
W: FP16权重矩阵
返回: 量化后的INT4 + 量化参数
"""
W_flat = W.flatten()
if quant_type == 'uniform':
# 普通线性量化
W_min, W_max = W_flat.min(), W_flat.max()
scale = (W_max - W_min) / 15 # 4bit: 0-15
zero_point = -W_min / scale
W_quant = np.clip(np.round(W_flat / scale + zero_point), 0, 15).astype(np.uint8)
elif quant_type == 'nf4':
# NF4: 专为正态分布设计的非均匀量化
# 预定义的16个量化级别(优化for正态分布)
nf4_levels = np.array([
-1.0, -0.6962, -0.5251, -0.3949, -0.2844, -0.1848,
-0.0911, 0.0, 0.0911, 0.1848, 0.2844, 0.3949,
0.5251, 0.6962, 0.8614, 1.0
])
# 归一化到[-1, 1]
absmax = np.abs(W_flat).max()
W_norm = W_flat / (absmax + 1e-8)
# 找最近的量化级别
W_quant = np.argmin(np.abs(W_norm[:, None] - nf4_levels), axis=1).astype(np.uint8)
scale = absmax
zero_point = 0
return W_quant.reshape(W.shape), scale, zero_point
def dequantize_4bit(W_quant, scale, zero_point, quant_type='nf4'):
"""反量化:INT4 → FP16"""
if quant_type == 'uniform':
W_fp16 = (W_quant.astype(np.float32) - zero_point) * scale
elif quant_type == 'nf4':
nf4_levels = np.array([-1.0, -0.6962, ..., 1.0]) # 同上
W_norm = nf4_levels[W_quant]
W_fp16 = W_norm * scale
return W_fp16
# 使用示例
W_original = np.random.randn(512, 512).astype(np.float16)
# 量化
W_quant, scale, zero = quantize_4bit(W_original, 'nf4')
print(f"原始: {W_original.nbytes / 1024:.1f}KB")
print(f"量化: {W_quant.nbytes / 1024:.1f}KB") # 应该是1/8
# 反量化
W_reconstructed = dequantize_4bit(W_quant, scale, zero, 'nf4')
# 计算误差
error = np.abs(W_original - W_reconstructed).mean()
print(f"平均误差: {error:.4f}") # 通常 < 0.01动手试试
-
量化误差分析:
- 用不同bit数(8/6/4/2)量化同一个权重矩阵
- 可视化:量化前后的分布直方图
- 计算:每种bit数的RMSE
-
NF4 vs Uniform对比:
- 生成服从正态分布的权重
- 分别用NF4和uniform量化
- 对比:哪个误差更小?
-
内存占用实测:
- 加载一个小模型(如GPT-2 small)
- 测量:FP16/INT8/INT4的实际内存占用
- 验证:是否真的减少了8倍?
连接下一站
LoRA/QLoRA解决了”如何高效微调”的问题。但微调的目标是什么?如何让模型偏好好的回答、避免有害回答?这就需要对齐技术。
**DPO(Direct Preference Optimization)**是最新的对齐方法——不需要复杂的强化学习,直接用偏好对数据训练。
3. DPO - 偏好优化
引子
想象你要训练一个客服AI。用户问:“怎么取消订单?”
回答A(好):“请登录账户,进入订单页面,点击取消按钮。“
回答B(差):“我不知道。”
传统RLHF需要:
- 训练一个奖励模型(判断A比B好)
- 用强化学习优化策略(让模型多生成A类回答)
DPO说:不需要奖励模型和RL,直接用(A好, B差)这样的偏好对训练!
对话 9: 偏好学习的本质
🤔 学生:什么是”偏好”数据?
💡 导师:偏好数据 = 同一个问题的两个回答,标注哪个更好。
preference_data = [
{
"prompt": "解释量子计算",
"chosen": "量子计算利用量子叠加和纠缠...", # 好回答
"rejected": "量子计算就是很快的计算。" # 差回答
},
...
]为什么不用绝对评分(如1-5分)?
因为人类不擅长绝对评分,但很擅长比较:
- “这个回答值几分?” → 难回答
- “A和B哪个更好?” → 很容易
🤔 学生:偏好数据怎么获得?
💡 导师:三种方式:
- 人工标注:让标注员对比两个回答
- AI辅助:用GPT-4评判哪个更好
- 隐式收集:用户点击/点赞=偏好信号
✨ 关键洞察:
- 偏好数据 = (prompt, chosen, rejected) 三元组
- 比绝对评分更容易标注
- 是RLHF的替代方案
对话 10: DPO的核心公式
🤔 学生:DPO如何从偏好数据学习?
💡 导师:DPO的损失函数非常优雅:
loss = -log(sigmoid(β * (log_ratio_chosen - log_ratio_rejected)))看起来复杂,但拆解一下:
# 1. 计算chosen和rejected的log概率
log_pi_chosen = model.log_prob(chosen_response)
log_pi_rejected = model.log_prob(rejected_response)
# 2. 计算相对参考模型的ratio
log_ratio_chosen = log_pi_chosen - log_pi_ref(chosen_response)
log_ratio_rejected = log_pi_rejected - log_pi_ref(rejected_response)
# 3. DPO loss
diff = β * (log_ratio_chosen - log_ratio_rejected)
loss = -log_sigmoid(diff)直觉:
- 如果 chosen 的概率 > rejected → diff > 0 → loss 小(好!)
- 如果 chosen 的概率 < rejected → diff < 0 → loss 大(惩罚)
🤔 学生:β是什么?
💡 导师:β控制对齐强度:
- β小(0.01)→ 轻微偏好,模型变化不大
- β大(1.0)→ 强烈偏好,模型激进调整
类比:β是”老师的严格程度”。太严格(β大)可能导致模型只会说一种答案(mode collapse)。
✨ 关键洞察:
- DPO loss = 让chosen概率上升,rejected下降
- β控制偏好强度
- 只需要一个模型(不需要奖励模型)
对话 11: 为什么需要参考模型?
🤔 学生:公式里的 log_pi_ref 是什么?
💡 导师:参考模型 = 训练前的模型副本(冻结)。
为什么需要它?
防止模式崩溃!如果只优化”chosen概率高”,模型可能学会:
- 只生成一种”万能回答”
- 或者生成极短的回答(概率高但没用)
参考模型提供”锚点”:
log_ratio = log(p_new / p_ref)这个ratio告诉我们”新模型相比原始模型变化了多少”。如果变化太大,loss会惩罚。
效果:
- 模型在保持原有能力(p_ref)的基础上
- 微调偏好(p_new 向 chosen靠拢)
🤔 学生:这不就是正则化吗?
💡 导师:完全正确!DPO隐式地包含了KL散度正则:
min KL(p_new || p_ref) subject to chosen > rejected
保持和参考模型接近,防止过度优化。
✨ 关键洞察:
- 参考模型 = 训练前的原始模型
- 防止模型偏离太远
- 相当于KL正则化
对话 12: DPO vs RLHF
🤔 学生:DPO比RLHF好在哪?
💡 导师:看这个对比:
| 维度 | RLHF (PPO) | DPO |
|---|---|---|
| 模型数量 | 3个(policy, value, reward) | 1个 |
| 训练复杂度 | 高(RL不稳定) | 低(监督学习) |
| 数据需求 | 奖励标注 | 偏好对 |
| 超参数 | 10+ | 2-3 |
| 训练速度 | 慢 | 快 |
| 效果 | 优秀 | 接近RLHF |
DPO的优势:
- 简单:就是个分类任务(chosen vs rejected)
- 稳定:没有RL的不稳定性
- 高效:单模型,训练快
RLHF的优势:
- 灵活:可以优化任意奖励函数
- 在线学习:可以边生成边优化
🤔 学生:什么时候用哪个?
💡 导师:
- 有偏好对数据 + 追求简单 → DPO
- 需要复杂奖励函数 → RLHF
- 资源有限 → DPO(省内存)
实际上,大部分场景DPO够用了。Llama-2-Chat、Zephyr都用DPO对齐。
✨ 关键洞察:
- DPO = 简化版RLHF
- 牺牲了灵活性,换来简单和稳定
- 适合90%的对齐场景
代码片段:DPO训练循环
def dpo_loss(model, ref_model, prompt, chosen, rejected, beta=0.1):
"""
DPO损失计算
"""
# 1. 计算chosen的log概率
logits_chosen = model(prompt + chosen)
logprob_chosen = log_prob(logits_chosen, chosen)
# 参考模型的chosen概率(冻结,不计算梯度)
with torch.no_grad():
logits_ref_chosen = ref_model(prompt + chosen)
logprob_ref_chosen = log_prob(logits_ref_chosen, chosen)
# 2. 计算rejected的log概率
logprob_rejected = log_prob(model(prompt + rejected), rejected)
with torch.no_grad():
logprob_ref_rejected = log_prob(ref_model(prompt + rejected), rejected)
# 3. 计算ratio
log_ratio_chosen = logprob_chosen - logprob_ref_chosen
log_ratio_rejected = logprob_rejected - logprob_ref_rejected
# 4. DPO loss
diff = beta * (log_ratio_chosen - log_ratio_rejected)
loss = -torch.log(torch.sigmoid(diff)).mean()
return loss
# 训练循环
model = load_pretrained_model()
ref_model = copy.deepcopy(model) # 参考模型
ref_model.eval() # 冻结
optimizer = Adam(model.parameters(), lr=1e-5)
for batch in preference_dataloader:
prompt, chosen, rejected = batch
loss = dpo_loss(model, ref_model, prompt, chosen, rejected, beta=0.1)
loss.backward()
optimizer.step()
optimizer.zero_grad()
if step % 100 == 0:
print(f"Step {step}, Loss: {loss.item():.4f}")动手试试
-
偏好数据构造:
- 用GPT生成同一问题的好/坏回答
- 构造100条偏好对
- 可视化:chosen和rejected的长度/情感差异
-
β参数实验:
- 用 β=0.01, 0.1, 0.5, 1.0 训练
- 观察:模型生成的多样性和质量
- β过大会不会mode collapse?
-
DPO vs 监督微调:
- 同一数据集,分别用DPO和SFT(只用chosen)
- 对比:偏好准确率和生成质量
连接下一站
DPO简化了对齐,但如果你需要更灵活的控制——比如优化”有用性+无害性+诚实性”的组合奖励,就需要强化学习。
REINFORCE是策略梯度的基础算法,理解它是理解PPO的前提。
🎯 算法四: REINFORCE - 策略梯度基础
什么是REINFORCE?
REINFORCE (Williams, 1992) 是最基础的策略梯度算法——它让模型”试错”,通过奖励信号学习什么是好行为。
核心思想:
- 让模型生成轨迹(trajectory)
- 收到奖励R(高奖励=好行为,低奖励=差行为)
- 调整策略:增大高奖励行为的概率,减小低奖励行为的概率
公式:
∇J(θ) = E_τ [ R(τ) · ∇ log π_θ(a_t | s_t) ]
翻译:期望奖励的梯度 = 奖励 × 对数概率梯度
对话13: 强化学习vs监督学习有什么本质区别?
🤔 学生: 强化学习和监督学习都是在学习,区别在哪里?
💡 导师: 想象你在教小孩:
监督学习: “这是猫,这是狗,记住!” (直接给正确答案) 强化学习: “去抓玩具,抓到了给你糖,抓不到什么都没有” (只给奖励信号)
核心区别:
| 维度 | 监督学习 | 强化学习 |
|---|---|---|
| 输入 | (x, y)标注对 | (状态, 动作, 奖励) |
| 学习目标 | 最小化预测误差 | 最大化累积奖励 |
| 反馈 | 每步都有正确答案 | 只有奖励(可能延迟) |
| 探索 | 不需要 | 必须探索好/坏行为 |
在LLM对齐中:
- DPO: “这个回答好,那个回答差” (监督学习,需要标注对)
- REINFORCE: “生成回答,看奖励模型打分,高分的多生成” (强化学习,只需奖励)
✨ 关键洞察: 强化学习适合奖励可计算但正确答案难枚举的任务——比如”回答要有用+无害+诚实”,这是一个复合奖励,很难提前标注”最佳回答”。
对话14: log概率梯度为什么能表示”让好行为更可能”?
🤔 学生:
公式里的 R · ∇ log π(a|s) 是什么意思?为什么乘以奖励就能强化行为?
💡 导师: 拆开看这个魔法:
1. π(a|s) 是什么?
- 策略(policy):在状态s下选择动作a的概率
- 例子:在写作任务中,s=“The cat”,a=“sat”,π(sat|The cat)=0.3
2. log π(a|s) 为什么有用?
- 概率范围[0,1]难优化(容易数值下溢)
- log映射到(-∞, 0],梯度更稳定
- 性质:∇ log π = (1/π) · ∇π (链式法则)
3. 为什么乘以R? 假设采样到动作a,奖励R=+10(很好):
Loss = -R · log π(a|s) = -10 · log π(a|s)
∇Loss = -10 · ∇ log π(a|s)
梯度下降会增大 π(a|s),因为:
- Loss越小越好 → 要让 -10·log π 小 → π接近1
- 结果:高奖励行为的概率被提升
如果R=-5(差行为):
- Loss = +5 · log π(a|s)
- 梯度下降会减小 π(a|s)
数学证明(策略梯度定理):
∇_θ E[R] = E[ R · ∇_θ log π_θ(a|s) ]
证明跳过(见Williams 1992论文),但直觉是:
- ∇ log π 指向”让a更可能”的方向
- 乘以R后,高奖励动作被推向这个方向,低奖励被拉离
✨ 关键洞察: log概率梯度是个”方向指示器”,奖励是个”力度旋钮”——两者结合实现”试错学习”。
对话15: 为什么REINFORCE方差特别高?
🤔 学生: 听说REINFORCE训练不稳定,方差高,为什么?
💡 导师: 想象你在玩游戏,每次都随机到不同结果:
例子:生成名字任务
- 第1次生成”alice”,奖励R=5.0
- 第2次生成”bob”,奖励R=0.1
- 第3次生成”zxy”,奖励R=-2.0
问题:
-
蒙特卡洛估计:每条轨迹只采样一次
- R=5.0那次可能是运气好(采样噪声)
- R=-2.0那次可能只是这个采样差
- 梯度估计 = 单次奖励 × ∇log π,噪声直接传播
-
信用分配(credit assignment):
- “alice”奖励高,是因为’a’好?还是’lice’好?
- REINFORCE用整条轨迹奖励R加权所有步的log π
- 结果:好坏行为都被同等对待
数学上:
Var[R · ∇log π] = E[R²] · Var[∇log π] + Var[R] · E[∇log π]²
- Var[R]很大(轨迹奖励波动)
- Var[∇log π]也大(策略不确定性)
- 两个方差相乘 → 爆炸
解决方案预告:
- Baseline减法(下个对话):R - b,减去期望奖励
- 批量平均:多条轨迹求平均,方差降低√N
- 价值函数(PPO):用神经网络估计期望奖励,替代单次R
✨ 关键洞察: REINFORCE是”纯蒙特卡洛”——完全靠采样,没有利用任何结构(如动态规划)。高方差是其简单性的代价。
对话16: Baseline减法如何降低方差?
🤔 学生:
公式里看到 (R - b) · ∇log π,这个b是什么?为什么减去它方差就小了?
💡 导师: Baseline b 是期望奖励的估计——告诉你”平均水平是多少”。
没有baseline的问题: 假设所有奖励都是正数:
- 轨迹1:R=3.0 → 所有动作都被强化(增大概率)
- 轨迹2:R=2.8 → 所有动作也被强化
- 但2.8其实比平均差!应该减弱,不是强化
加上baseline后: 设 b=2.9(期望奖励)
- 轨迹1:R-b=3.0-2.9=+0.1 → 微弱强化(确实比平均好)
- 轨迹2:R-b=2.8-2.9=-0.1 → 微弱减弱(比平均差)
为什么不改变期望梯度? 数学性质(score function identity):
E[ ∇log π ] = 0 (期望梯度和为零)
E[ (R-b) · ∇log π ] = E[ R · ∇log π ] - b · E[∇log π]
= E[ R · ∇log π ] (第二项为0)
证明:∇log π 的期望在所有动作上积分为0(因为∇∫π=∫∇π,而∫π=1是常数)
方差为什么降低? 直觉:R-b把奖励”中心化”到0附近
- 原始R: [0, 10],均值5,方差大
- R-b: [-5, +5],均值0,方差小(因为去掉了均值的偏移)
数学上:
Var[R · ∇log π] 大
Var[(R-b) · ∇log π] 小 (当b=E[R]时方差最小)
实践中的baseline:
- 移动平均:b = 0.9·b_old + 0.1·R_new (指数移动平均)
- 价值网络(PPO):b = V(s),神经网络预测期望回报
✨ 关键洞察: Baseline是个”锚点”——不改变梯度方向(期望),但减少噪声(方差)。类似标准化:x → (x-μ)/σ,减去均值不改变相对关系。
对话17: REINFORCE在实际RLHF中够用吗?
🤔 学生: 听起来加了baseline的REINFORCE就很好了,为什么OpenAI用PPO而不是REINFORCE?
💡 导师: REINFORCE是”可行但不实用”——理论正确,工程上太脆弱。
REINFORCE的三大缺陷:
1. 样本效率低
- 每条轨迹只用一次(on-policy)
- 策略稍微更新,旧轨迹就不能再用(分布不匹配)
- LLM生成一条回答很贵(几秒),浪费不起
2. 梯度爆炸/消失
- 单步更新可能让策略剧变:π_new(a|s) / π_old(a|s) = 100x
- 下一次采样完全不同,训练震荡/崩溃
- Baseline只减少方差,不约束步长
3. 奖励稀疏问题
- 如果大部分R≈0,只有少数R很大,梯度信号噪声极大
- 例子:安全性奖励,99%回答R=0(安全),1%是R=-100(有害)
- REINFORCE会被那1%支配,不稳定
PPO的改进:
- 重要性采样:旧轨迹可以复用多次(sample efficiency)
- Clip约束:限制策略更新幅度(stability)
- 价值网络:更准确的baseline(lower variance)
- GAE(Generalized Advantage Estimation):平衡bias/variance
真实数据:
- OpenAI InstructGPT:PPO稳定训练,REINFORCE崩溃
- DeepMind Sparrow:用PPO + 多个奖励模型
✨ 关键洞察: REINFORCE是”教科书算法”——理解策略梯度的最小实现,但工业界需要PPO的稳定性。类似SGD vs Adam:SGD理论干净,Adam工程实用。
代码片段: REINFORCE核心循环
# === REINFORCE with Baseline ===
def reinforce_update(policy, optimizer, baseline):
"""
一次REINFORCE更新
policy: 语言模型策略 π_θ(a|s)
optimizer: Adam优化器
baseline: 移动平均奖励
"""
# 1. 生成轨迹(batch)
trajectories = []
rewards = []
log_probs_list = []
for _ in range(batch_size):
# 生成一条回答
prompt = sample_prompt()
tokens, log_probs = policy.generate(prompt) # 采样 + 记录log π
# 计算奖励(调用奖励模型)
reward = reward_model.score(prompt, tokens)
trajectories.append(tokens)
rewards.append(reward)
log_probs_list.append(log_probs)
# 2. 更新baseline(指数移动平均)
avg_reward = sum(rewards) / len(rewards)
baseline.update(avg_reward)
# 3. 计算损失
loss = 0
for log_probs, reward in zip(log_probs_list, rewards):
# Advantage = R - baseline
advantage = reward - baseline.value
# REINFORCE损失: -advantage · Σ log π(a_t|s_t)
total_log_prob = sum(log_probs)
loss += -advantage * total_log_prob
loss = loss / batch_size
# 4. 反向传播 + 梯度下降
optimizer.zero_grad()
loss.backward()
# 梯度裁剪(防止爆炸)
torch.nn.utils.clip_grad_norm_(policy.parameters(), max_norm=1.0)
optimizer.step()
return loss.item(), avg_reward
# === 训练循环 ===
baseline = ExponentialMovingAverage(alpha=0.1)
for step in range(num_steps):
loss, avg_r = reinforce_update(policy, optimizer, baseline)
if step % 100 == 0:
print(f"Step {step}: Loss={loss:.4f}, Reward={avg_r:.2f}, Baseline={baseline.value:.2f}")关键细节:
- log_probs记录: 生成时存每步的log π(a_t|s_t)
- Advantage: R - baseline,而不是原始R
- 梯度裁剪:
clip_grad_norm,防止单个高奖励轨迹导致的梯度爆炸 - 批量平均: 多条轨迹求平均,降低方差
🚀 算法五: PPO - 近端策略优化
什么是PPO?
PPO (Proximal Policy Optimization, Schulman et al. 2017) 是当前RLHF的工业标准——ChatGPT、Claude、Llama都用它对齐模型。
核心改进:在REINFORCE基础上加了”近端约束”——限制每次更新的步长,防止策略崩溃。
公式:
L_CLIP = E[ min(r_t · A_t, clip(r_t, 1-ε, 1+ε) · A_t) ]
其中 r_t = π_new(a|s) / π_old(a|s) (重要性采样比率)
A_t = R - V(s) (优势函数)
翻译:目标函数 = 在”原始目标”和”裁剪目标”之间取小者
对话18: REINFORCE的步长失控问题具体是什么?
🤔 学生: 前面说REINFORCE会”策略剧变”,能举个具体例子吗?
💡 导师: 想象你在训练模型写诗:
初始策略:
Prompt: "写一首关于春天的诗"
生成: "春风拂面花开艳" π_old(这句话) = 0.01
奖励: R = 8.0 (挺好)
REINFORCE更新后:
Loss = -8.0 · log(0.01) = -8.0 · (-4.6) = +36.8
梯度很大 → 参数剧变
新策略: π_new(这句话) = 0.85 (暴涨85倍!)
问题来了: 下一次采样时,模型疯狂生成这句话的变体:
"春风拂面花开艳"
"春风拂面花开好"
"春风拂面花真艳"
...
为什么坏?
- Mode collapse:策略坍缩到单一输出
- 过拟合:那句话可能是偶然高分,不代表真的好
- 探索停止:不再尝试其他风格,卡在局部最优
数学上:
重要性采样比 r = π_new / π_old = 0.85 / 0.01 = 85
- r >> 1 意味着新策略把概率提升了85倍
- 单次更新太激进,策略分布完全变了
✨ 关键洞察: 强化学习的核心矛盾:利用(exploit)高奖励行为 vs 探索(explore)新行为。REINFORCE倾向过度利用,丧失探索。
对话19: PPO的Clip约束怎么解决步长问题?
🤔 学生:
公式里的 clip(r, 1-ε, 1+ε) 是什么意思?怎么限制步长?
💡 导师: Clip函数:把比率r夹到 [1-ε, 1+ε] 范围内
clip(r, 1-ε, 1+ε) = max(1-ε, min(r, 1+ε))
例如 ε=0.2:
- r=0.5 → clip到0.8 (不让降太多)
- r=1.1 → 保持1.1 (在范围内)
- r=3.0 → clip到1.2 (不让涨太多)
PPO目标函数拆解:
L = min( r·A, clip(r,1-ε,1+ε)·A )
情况1: 优势A > 0 (好行为,要强化)
- r·A = 1.5 · 2.0 = 3.0 (原始目标:大力强化)
- clip(1.5, 0.8, 1.2)·A = 1.2 · 2.0 = 2.4 (裁剪目标:温和强化)
- min(3.0, 2.4) = 2.4 ← 选择保守的
情况2: 优势A < 0 (坏行为,要减弱)
- r·A = 0.5 · (-2.0) = -1.0 (原始:大力减弱)
- clip(0.5, 0.8, 1.2)·A = 0.8 · (-2.0) = -1.6 (裁剪:更大力减弱)
- min(-1.0, -1.6) = -1.6 ← 选择保守的(因为A<0,小=更保守)
图示(A>0时):
目标函数
│ ╱
│ ╱ 原始r·A (斜率=A)
│ ╱
│ ╱________ 裁剪后(斜率=0,在r>1+ε时)
│ ╱
└─────────→ 比率r
0 1 1+ε
当r超过1+ε后,目标函数不再增长 → 梯度为0 → 停止强化
为什么取min?
- 保守原则:宁可更新不够,不要更新过头
- 防止”lucky sample”(偶然高奖励)导致的策略崩溃
✨ 关键洞察: Clip是个”刹车”——策略想大步前进时,强制减速。ε=0.2意味着”单次更新最多让概率变化20%“。
对话20: PPO为什么需要价值网络V(s)?
🤔 学生: 优势函数 A = R - V(s),这个V(s)和前面的baseline有什么区别?
💡 导师: Baseline(REINFORCE中):
- 简单标量,全局共享
- 更新: b ← 0.9·b + 0.1·R (指数移动平均)
- 含义: “所有状态的平均期望奖励”
价值网络V(s) (PPO中):
- 神经网络,输入状态s,输出标量
- 更新: 最小化MSE: (V(s) - R)²
- 含义: “状态s的期望奖励”
为什么需要V(s)?
问题: Baseline是全局常数,不考虑状态差异
- 状态1: “写诗的开头”,期望奖励 E[R|s1]=3.0
- 状态2: “写诗已写一半”,期望奖励 E[R|s2]=5.0
- 全局baseline b=4.0 (两者平均)
结果:
- s1得到R=4.0 → A=4.0-4.0=0,没信号!(实际比s1期望高)
- s2得到R=4.0 → A=4.0-4.0=0,没信号!(实际比s2期望低)
用V(s)后:
- s1: A = 4.0 - V(s1) = 4.0 - 3.0 = +1.0 (确实比这个状态期望好)
- s2: A = 4.0 - V(s2) = 4.0 - 5.0 = -1.0 (比这个状态期望差)
价值网络架构(在LLM RLHF中):
class ValueHead(nn.Module):
def __init__(self, transformer):
self.transformer = transformer # 共享策略的主干
self.v_head = nn.Linear(hidden_size, 1) # 额外的价值头
def forward(self, input_ids):
hidden = self.transformer(input_ids) # [batch, seq_len, hidden]
values = self.v_head(hidden) # [batch, seq_len, 1]
return values[:, -1, 0] # 取最后一个token的价值训练V(s):
# 收集轨迹后
for state, actual_return in zip(states, returns):
v_pred = value_network(state)
v_loss = (v_pred - actual_return) ** 2
v_loss.backward()✨ 关键洞察: 价值网络是个”状态感知的baseline”——把全局常数升级为条件期望,梯度信号更精准。代价:要训练额外网络,增加计算量。
对话21: 完整RLHF流程是怎样的?
🤔 学生: 从预训练模型到ChatGPT,中间具体经历了什么?
💡 导师: 完整RLHF三阶段:
阶段1: 监督微调(SFT)
- 数据: 高质量人类演示对话 (prompt, ideal_response)
- 方法: 标准语言建模损失
- 目的: 让模型学会对话格式,基本遵循指令
- 输出: SFT模型(例如GPT-3.5-SFT)
阶段2: 奖励模型训练(RM)
- 数据: 偏好对 (prompt, response_A, response_B, label_human_prefers)
- 方法: 排序损失(类似DPO,但不直接优化策略)
- 架构: 复用SFT模型主干 + 标量head
- 输出: 奖励模型RM(prompt, response) → 分数
阶段3: PPO优化
- 策略网络: SFT模型(会被更新)
- 价值网络: 随机初始化,输出V(s)
- 参考网络: SFT模型快照(冻结,用于KL惩罚)
- 奖励模型: 阶段2训练的RM(冻结)
训练循环:
for step in range(ppo_steps):
# 1. 生成回答(从策略网络)
prompts = sample_prompts()
responses, log_probs_old = policy.generate(prompts)
# 2. 计算奖励
rewards = reward_model(prompts, responses)
# 3. 计算KL惩罚(相对参考网络)
log_probs_ref = reference_model.log_prob(prompts, responses)
kl_penalty = (log_probs_old - log_probs_ref)
shaped_rewards = rewards - kl_coef * kl_penalty
# 4. 价值网络预测baseline
values = value_network(prompts)
advantages = shaped_rewards - values
# 5. PPO更新策略网络
for _ in range(ppo_epochs): # 多轮复用数据
log_probs_new = policy.log_prob(prompts, responses)
ratio = torch.exp(log_probs_new - log_probs_old)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1-eps, 1+eps) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
policy_loss.backward()
optimizer_policy.step()
# 6. 更新价值网络
v_loss = (value_network(prompts) - shaped_rewards) ** 2
v_loss.mean().backward()
optimizer_value.step()四个模型的角色:
| 模型 | 更新? | 用途 |
|---|---|---|
| 策略网络 | ✅ 是 | 生成回答 |
| 价值网络 | ✅ 是 | 预测期望奖励(baseline) |
| 参考网络 | ❌ 否 | KL惩罚锚点(防止崩溃) |
| 奖励模型 | ❌ 否 | 评分回答质量 |
✨ 关键洞察: RLHF是”四模型协作”——策略学习生成,价值学习评估,参考保持稳定,奖励提供信号。PPO把它们编织成稳定的训练循环。
对话22: PPO vs DPO,实际中如何选择?
🤔 学生: DPO不是更简单吗?为什么OpenAI还用PPO?
💡 导师: PPO优势:
-
灵活的奖励函数
- PPO: 任意奖励,可组合(有用性0.6 + 无害性0.3 + 诚实性0.1)
- DPO: 只能用成对偏好,无法直接优化组合目标
-
在线探索
- PPO: 策略生成新数据 → 奖励模型打分 → 发现新的好行为
- DPO: 离线,只能在现有偏好对上学习
-
长序列任务
- PPO: 逐步奖励(每个token都有V(s)),信用分配更精确
- DPO: 整体偏好,难处理”前半段好,后半段差”的情况
DPO优势:
-
简单
- 无需4个模型,只训练策略
- 不需要调PPO的超参(ε, KL系数, value_lr…)
-
稳定
- 无强化学习的探索噪声
- 收敛更快(几千步 vs PPO几万步)
-
数据效率
- 每条偏好对都充分利用
- PPO需要大量on-policy采样(expensive)
实际选择:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 通用对话(ChatGPT) | PPO | 需要组合奖励,在线探索 |
| 特定任务微调(摘要) | DPO | 偏好清晰,数据固定 |
| 安全对齐 | PPO | 需要红队测试 + 即时奖励 |
| 资源受限 | DPO | 单模型,训练快 |
| 研究原型 | DPO | 快速迭代 |
最新趋势(2024-2026):
- GRPO (Group Relative Policy Optimization): PPO的简化版,减少超参
- Offline PPO: 在固定数据集上跑PPO,结合两者优点
- DPO变体: RDPO, IPO, cDPO,改进DPO的理论缺陷
✨ 关键洞察: PPO是”瑞士军刀”(万能但复杂),DPO是”剪刀”(专精但简单)。工业界倾向PPO因为需要应对复杂多变的对齐需求。
代码片段: PPO核心更新
def ppo_update(policy, value_net, ref_policy, optimizer_p, optimizer_v,
prompts, responses, old_log_probs, rewards, epsilon=0.2):
"""
一次PPO更新(简化版)
Args:
policy: 当前策略网络(会更新)
value_net: 价值网络
ref_policy: 参考策略(冻结)
old_log_probs: 生成时的log π_old(a|s)
rewards: 奖励模型打分
"""
# 1. 计算KL惩罚
with torch.no_grad():
ref_log_probs = ref_policy.log_prob(prompts, responses)
kl_div = old_log_probs - ref_log_probs
shaped_rewards = rewards - kl_coef * kl_div
# 2. 价值网络预测 + 优势计算
values = value_net(prompts)
advantages = shaped_rewards - values
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) # 标准化
# 3. PPO epoch (复用数据多次)
for _ in range(ppo_epochs):
# 重新计算log probs(梯度图)
new_log_probs = policy.log_prob(prompts, responses)
# 重要性采样比率
ratio = torch.exp(new_log_probs - old_log_probs)
# Clipped surrogate loss
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1.0 - epsilon, 1.0 + epsilon) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
# 熵正则(鼓励探索)
entropy = policy.entropy(prompts)
entropy_bonus = 0.01 * entropy.mean()
total_loss = policy_loss - entropy_bonus
# 策略网络更新
optimizer_p.zero_grad()
total_loss.backward()
torch.nn.utils.clip_grad_norm_(policy.parameters(), 1.0)
optimizer_p.step()
# 4. 价值网络更新(MSE loss)
for _ in range(value_epochs):
v_pred = value_net(prompts)
v_loss = F.mse_loss(v_pred, shaped_rewards)
optimizer_v.zero_grad()
v_loss.backward()
optimizer_v.step()
# 返回监控指标
metrics = {
'policy_loss': policy_loss.item(),
'value_loss': v_loss.item(),
'mean_reward': rewards.mean().item(),
'mean_kl': kl_div.mean().item(),
'mean_advantage': advantages.mean().item()
}
return metrics关键细节:
- Advantage标准化:
(A - μ) / σ,稳定训练 - 多轮epoch: PPO_epochs=4,复用每批数据
- 熵正则: 防止策略过早收敛(探索vs利用)
- 梯度裁剪: 必须!防止离群奖励导致的梯度爆炸
📊 五大对齐算法对比
| 算法 | 训练数据 | 模型数量 | 稳定性 | 灵活性 | 适用场景 |
|---|---|---|---|---|---|
| 监督微调(SFT) | (x,y)标注对 | 1 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 有明确正确答案 |
| LoRA | 同SFT | 1 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 参数高效微调 |
| QLoRA | 同SFT | 1 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 极低资源微调 |
| DPO | 偏好对(x,y+,y-) | 1(+ref) | ⭐⭐⭐⭐ | ⭐⭐⭐ | 偏好对齐,离线 |
| REINFORCE | 奖励函数 | 1 | ⭐⭐ | ⭐⭐⭐⭐ | 原型/教学 |
| PPO | 奖励函数 | 4(策略+价值+参考+RM) | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 复杂对齐,工业界 |
🎯 本章总结
你已经掌握了驯服大模型的完整工具箱:
- LoRA/QLoRA: 参数高效微调,用1%参数适配新任务
- DPO: 直接从偏好学习,无需奖励模型
- REINFORCE: 策略梯度基础,理解”试错学习”
- PPO: RLHF工业标准,稳定优化复杂奖励
核心理念:
- 微调是让模型”适应”,对齐是让模型”听话”
- 没有万能方法,只有合适的权衡
- 实践中常组合使用:SFT → DPO → PPO
下一站: 04-推理优化轨道 - 学习如何让训练好的模型跑得更快!