人类反馈与偏好优化
同一个问题可以有几种都说得通的回答:
问题:只回答颜色。小林选了红色杯子。
A:红
B:红色。
C:蓝
A 和 B 都提到了正确颜色,但 A 更贴近“只回答颜色”的要求。C 则内容错误。让人写出一份完美答案,有时很费力;把两份回答摆在一起,请人比较哪份更好,往往更容易组织。
这一章研究怎样使用这样的比较。我们会先把偏好变成一个奖励预测,再用策略更新提高奖励;还会看到一种直接使用偏好对的方法 DPO。最后回到一个朴素问题:更讨评分器喜欢,是否真的等于更好?
1. 一条偏好记录里有什么
最基本的偏好样本包含三个部分:同一个提示 、较受偏好的回答 、另一份回答 。下标 、 分别表示这次比较里的胜者与败者。
“这次比较”很重要。人可能在准确性、简洁性、完整性和语气之间做不同权衡。没有说明比较规则,标注分歧未必意味着谁粗心。
真实数据通常还需要保留候选生成方式、评分标准、标注来源与分歧情况。呈现顺序、回答长度和醒目的措辞都可能影响判断;同一来源的近重复提示也不该随意拆进训练与测试两边。
本章只用一个提示、三个候选和十个模拟偏好标签:八次选择 A,二次选择 B,C 没有参与这些比较。它刻意保留了分歧和未覆盖的候选,方便观察训练目标究竟约束了什么。
2. 奖励模型:把“更喜欢”变成分数差
奖励模型给提示与回答一个分数:
是奖励模型参数。分数本身可以是任意实数,通常不能直接解释成正确概率。
一种常用比较模型把分数差通过 Sigmoid 变成偏好概率:
其中 。两者分数相等,比较概率为 0.5;胜者得分越高,预测偏好概率越大。
相应的训练损失为:
再对偏好样本平均。给两个分数同时加同一个常数,分数差不变,因此偏好记录并不决定奖励的绝对零点。
在八比二的模拟标签中,最佳比较概率接近 0.8,所需分数差为:
这没有告诉我们 C 应该得多少分。没有比较过的回答区域,需要更多数据、合理的泛化或额外检查,不能靠这个式子自动补齐。
3. RLHF:用反馈学奖励,再用奖励调整策略
一种典型路线是:先做 SFT,让模型具备基本回答行为;再收集同一提示下的回答比较,训练奖励模型;最后让策略生成回答,用预测奖励指导更新。
指令跟随与人类反馈研究 展示了这类流程,通常称为 RLHF,即从人类反馈中进行强化学习。具体系统可以有不同的数据和训练安排,不能把某一套流程当成唯一实现。
如果只最大化奖励模型分数,策略可能迅速远离原来的语言行为。常见做法是保留一个冻结的参考策略 ,给偏离它的行为加惩罚:
为简洁省略了对提示的平均。 控制参考约束的强弱。若候选回答可以枚举,这里的 KL 是:
它衡量两个分布的差异。语言模型通常不能枚举所有完整回答,会使用相应的词元计算或采样估计。KL 约束可以限制漂移,但不能保证奖励正确,也不能保证所有能力都被保留。
4. PPO:同一批尝试,不要一次改得太猛
PPO 是一种常用的策略优化方法。它先用当前策略采样一批轨迹,把这份采样策略暂时记为 ,再对这批轨迹进行若干轮更新。
某个已采样动作的概率比是:
如果 ,表示这个动作在新策略下的概率是采样时的 1.3 倍。它不是概率本身,所以可以大于 1。
一种常见的 PPO 裁剪收益写成:
是优势估计, 是裁剪宽度。优化时提高平均的 ,再配合价值学习、KL 项等相应设计。
若优势为正,把动作概率提高一些是好方向,但比例超过 后,裁剪项不再给额外收益。若优势为负,则需要注意不等号随符号改变;并不是把所有更新都机械地截到同一个区间。
图 1:,优势分别为 1 和 。这里画的是单个样本的目标函数,不是对整张策略分布的硬约束。
裁剪不会保证每个动作最终概率比都处在区间内,也不能保证 KL 一定小于某个阈值。它是优化目标的一部分,仍需要检查实际更新幅度与训练过程。PPO 原始论文 给出了这种代理目标及其实验。
两个旧模型,别混在一起
| 名称 | 在做什么 | 什么时候更新 |
|---|---|---|
| 记录本批轨迹来自哪个策略,用于概率比 | 每次重新采样时换成新的快照 | |
| 作为整个后训练过程的参考分布 | 本例始终冻结 |
当前策略、旧采样策略和参考策略,可能一开始参数相同,之后职责却不同。把它们混成一个变量,会改变算法。
5. DPO:直接学习偏好对的相对变化
DPO 不先显式训练一个奖励模型再运行同样的在线奖励优化循环,而是把偏好对与参考策略直接放进损失。
先定义一个相对参考的回答分数:
然后对一对回答计算:
直观上,它希望胜者相对参考策略得到的提升,比败者更大。参考项不能在微调时跟着一起更新,否则比较的基准就变了。
对于真正的自回归语言模型,一份回答的对数概率是回答各词元条件对数概率之和,通常包含约定的终止符:
提示部分提供条件,不作为这份回答的生成概率累加进去。直接把“序列总对数概率”换成“每词元平均”会改变目标,不能当作无影响的代码整理。
DPO 原始研究 从带 KL 约束的奖励优化与偏好概率模型之间的联系得到这个形式。它有自己的假设,包括偏好建模方式、参考分布及数据覆盖。更简单的训练流程不意味着偏好数据的偏差消失了。
也不是学习率。它改变偏好损失中的相对分数尺度;固定偏好数据下,选择不同值会改变需要多大策略概率变化才能拟合这些偏好。
6. 动手:三个候选上的奖励、PPO 与 DPO
preference_optimization.py 把每份完整候选回答看作一个动作,直接训练三个分数。这样可以枚举概率、计算精确 KL,并检查优化结果。
它不是完整的语言模型 RLHF:没有大规模文本生成,没有逐词元价值网络,也没有人类标注平台。PPO 部分是单步环境,已知三个奖励后,用旧策略的精确期望奖励作基线;每轮抽 64 个动作,重复更新 4 次,共 100 轮。
运行:
uv run --python 3.12 --with-requirements requirements.txt python src/examples/preference_optimization.py
同一提示的三个候选:红 | 红色。 | 蓝
模拟偏好:简短正确答案赢 8 次,完整正确答案赢 2 次;错误答案未参与比较。
参考策略:0.200, 0.600, 0.200
DPO:0.444, 0.333, 0.222
单步 PPO:0.449, 0.330, 0.220
拟合奖励的解析最优:0.444, 0.333, 0.222
奖励差=1.386;拟合的简短偏好概率=0.800
ratio=0.7,1.0,1.3;正优势的裁剪收益:0.7, 1.0, 1.2
ratio=0.7,1.0,1.3;负优势的裁剪收益:-0.8, -1.0, -1.3
另一个假设:代理奖励高估错误答案。
beta=2.0:代理奖励=1.761;真实正确概率=0.577
beta=1.0:代理奖励=2.317;真实正确概率=0.319
beta=0.2:代理奖励=3.000;真实正确概率=0.000
前半段同时拟合奖励与 DPO,二者的参数独立,没有互相传递梯度。随后用拟合奖励做单步 PPO。
这里还可以求出奖励目标的解析最优分布:
正比号表示最后还要除以所有候选权重之和。我们取 。这个结果给数值优化提供了一个独立检查,而不是单凭“损失降了”就认定实现正确。
图 2:候选 C 从未参与偏好比较。相对提高 A 对 B 的偏好,并不自动保证 C 的概率下降。
看到 0.222,先别跳过它
DPO 后,“红”的概率从 0.2 提高到约 0.444,达到了这批偏好想表达的方向。但错误答案“蓝”的概率从 0.2 变成约 0.222,并没有下降。
这不是说 DPO 必然增加错误答案,而是说明:数据没有约束的区域,不会因为偏好损失下降就自动变好。 整张分布需要归一化,调整两个候选也会影响第三个的概率。实际语言模型的未覆盖区域远比这个例子大。
展开完整代码:奖励拟合、DPO、单步 PPO 与反例
"""第 29 章:三候选回答上的奖励拟合、DPO 与单步 PPO;不是完整 LLM RLHF。"""
import torch
from torch import nn
from torch.nn import functional as F
REFERENCE = torch.tensor([.2, .6, .2], dtype=torch.float64)
RESPONSES = ["红", "红色。", "蓝"]
def preference_pairs():
# 同一个提示:两份正确答案比较,8 个模拟标注选简短,2 个选完整句。
winners = torch.tensor([0]*8+[1]*2)
losers = 1-winners
return winners, losers
def dpo_loss(logits, reference, winners, losers, beta=1):
log_probs, log_ref = logits.log_softmax(-1), reference.log()
margin = beta*((log_probs[winners]-log_ref[winners])
-(log_probs[losers]-log_ref[losers]))
return -F.logsigmoid(margin).mean()
def clipped_gain(ratio, advantage, epsilon=.2):
return torch.minimum(ratio*advantage, ratio.clamp(1-epsilon, 1+epsilon)*advantage)
def fit_reward_and_dpo():
winners, losers = preference_pairs()
rewards = nn.Parameter(torch.zeros(3, dtype=torch.float64))
logits = nn.Parameter(REFERENCE.log().clone())
optimizer = torch.optim.SGD([rewards, logits], lr=.3)
for _ in range(300):
reward_loss = -F.logsigmoid(rewards[winners]-rewards[losers]).mean()
loss = reward_loss+dpo_loss(logits, REFERENCE, winners, losers)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
return rewards.detach(), logits.detach()
def ppo_bandit(rewards, rounds=100):
logits = nn.Parameter(REFERENCE.log().clone())
optimizer = torch.optim.SGD([logits], lr=.2)
generator = torch.Generator().manual_seed(2026)
for _ in range(rounds):
# old 随每轮采样更新;REFERENCE 在整个实验中固定。
old_probs = logits.detach().softmax(-1)
actions = torch.multinomial(old_probs, 64, replacement=True, generator=generator)
old_log_probs = old_probs[actions].log()
advantages = (rewards[actions]-(old_probs*rewards).sum()).detach()
for _ in range(4):
log_probs = logits.log_softmax(-1)
ratio = (log_probs[actions]-old_log_probs).exp()
gain = clipped_gain(ratio, advantages).mean()
# 三个候选全部可枚举,所以用精确 KL,不使用采样近似。
kl = (log_probs.exp()*(log_probs-REFERENCE.log())).sum()
loss = -gain+kl
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
return logits.detach()
def overoptimization():
# 独立假设:第三个错误答案被一个有缺陷的代理奖励高估。
proxy = torch.tensor([1., .8, 3.], dtype=torch.float64)
utility = torch.tensor([1., 1., 0.], dtype=torch.float64)
rows = []
for beta in [2., 1., .2]:
probabilities = (REFERENCE.log()+proxy/beta).softmax(-1)
rows.append((beta, float(probabilities @ proxy), float(probabilities @ utility)))
return rows
def main():
torch.set_num_threads(1)
rewards, dpo = fit_reward_and_dpo()
ppo = ppo_bandit(rewards)
print("同一提示的三个候选:" + " | ".join(RESPONSES))
print("模拟偏好:简短正确答案赢 8 次,完整正确答案赢 2 次;错误答案未参与比较。")
for name, probabilities in [("参考策略", REFERENCE), ("DPO", dpo.softmax(-1)),
("单步 PPO", ppo.softmax(-1)),
("拟合奖励的解析最优", (REFERENCE.log()+rewards).softmax(-1))]:
print(name + ":" + ", ".join(f"{value:.3f}" for value in probabilities))
print(f"奖励差={rewards[0]-rewards[1]:.3f};"
f"拟合的简短偏好概率={torch.sigmoid(rewards[0]-rewards[1]):.3f}")
ratios = torch.tensor([.7, 1., 1.3], dtype=torch.float64)
print("ratio=0.7,1.0,1.3;正优势的裁剪收益:" +
", ".join(f"{value:.1f}" for value in clipped_gain(ratios, torch.ones(3))))
print("ratio=0.7,1.0,1.3;负优势的裁剪收益:" +
", ".join(f"{value:.1f}" for value in clipped_gain(ratios, -torch.ones(3))))
print("另一个假设:代理奖励高估错误答案。")
for beta, proxy, utility in overoptimization():
print(f"beta={beta:.1f}:代理奖励={proxy:.3f};真实正确概率={utility:.3f}")
if __name__ == "__main__":
main()
7. 奖励越高,可能离目标越远
输出的后半段是一个独立的假设反例。我们故意给代理奖励设置:
“红”得 1 分,“红色。”得 0.8 分,错误的“蓝”却得 3 分。
真实任务只关心颜色是否正确,前两个候选正确,第三个错误。随后直接计算不同 KL 系数下的最优策略。随着约束减弱,策略越来越喜欢那个被高估的错误回答:代理奖励接近 3,正确概率却接近 0。
图 3:这是人为指定奖励误差的演示,不是前半段拟合奖励的结果。用来说明代理目标与真实目标可以朝不同方向变化。
这类问题常被讨论为奖励过度优化:优化器在越来越有效地追逐一个并不完整的代理目标。Gao 等人的研究 对奖励模型过度优化进行了系统实验。
实际偏差可能来自偏好数据覆盖不足、奖励模型把篇幅或口气当作质量线索、标注标准含糊,以及策略逐渐生成奖励模型没见过的回答。增加训练轮数可以把这些偏差利用得更充分,而不是自动修复它们。
因此,后训练评估需要与训练奖励保持一定独立性:检查真实任务正确性、要求遵循情况、保留能力和具体失败样本;必要时补充新的比较数据。仅看训练奖励或偏好分类准确率,不足以证明最终助手更可靠。
8. 停下来试一试
- 给两份回答的奖励都加 100,比较概率会改变吗?
- 十次比较中八次偏好 A、两次偏好 B,为什么拟合概率应接近 0.8,而不是 1?
- PPO 的概率比为何可能大于 1?
- 、、 时,裁剪收益为什么是 ?
- 与 分别何时变化?
- DPO 中直接平均回答词元对数概率,会不会得到同一个目标?
- C 没参与比较,为什么仍要检查它的概率?
- 为本例补充涉及 C 的偏好记录,再比较三种分布。你会把哪些结果作为任务改善的证据?
参考答案与提示
- 不会,分数差没有变化。
- 标签存在分歧,始终预测 A 胜出会给选择 B 的样本很大损失。
- 它是新旧概率之比,例如 ,不是一个独立概率。
- 两项分别为 与 ,取较小者;负优势改变了比较方向。
- old 随新一轮采样更新,随后在本批更新中固定;ref 在本章整个后训练中固定。
- 不会。长度归一化会改变不同回答的相对尺度,应视为目标修改。
- 我们最终使用整张回答分布,而不是只使用已经比较过的那两个选项。
- 预先规定正确候选优于 C,报告新增标签、偏好拟合与全部候选概率,并检查真实正确概率是否提高。只展示偏好训练损失不够。
9. 继续阅读
- Ouyang 等,Training Language Models to Follow Instructions with Human Feedback:示范、偏好数据与奖励优化的连接。
- Schulman 等,PPO:概率比与裁剪代理目标。
- Rafailov 等,DPO:直接偏好损失的推导与实验。
- Gao 等,Scaling Laws for Reward Model Overoptimization:奖励改善与真实表现之间的分离。
如果任务答案可以用程序检查,能否直接把检查结果变成奖励,少依赖一个预测偏好的模型?下一章从这个问题出发。