今天讲对齐(Alignment)背后的数学:当一个目标无法写成代码("有帮助""无害""诚实"没有单元测试),我们怎么用数学把人类偏好变成模型能优化的损失函数。四张卡片是一条逻辑链:① RLHF 整体框架 → ② 核心组件「奖励模型」怎么从偏好学分数 → ③ 把昂贵的人换成 AI(Constitutional AI)→ ④ 用代数把整个框架塌缩成一个分类 loss(DPO)。
RLHF 是"无法写测试时的 CI 流水线"。后端世界里你能为「金额计算」写断言,但你无法为「这段回答够不够礼貌」写 assert。RLHF 的做法是:先把大量人类的"这个比那个好"蒸馏成一个会打分的模型(奖励模型),再让主模型像通过 CI 一样不断优化去取悦这个打分器——把"无法形式化的需求"变成"可微分的目标函数"。
预训练后的模型只会"续写最可能的下一个 token",它没有动机变得有用或无害——这是目标错位:训练目标(语言建模)≠ 我们真正想要的(有帮助的助手)。InstructGPT(Ouyang et al. 2022)确立了经典三阶段:
第三步要最大化的目标(核心公式):
逐项拆解:x 是输入提示,y 是模型生成的回答;r(x,y) 是奖励模型给这条回答打的分;πθ 是正在训练的策略,πref 是冻结的 SFT 模型。第一项"奖励最大化"很直白——生成奖励模型喜欢的回答。关键是第二项 KL 惩罚:KL 散度衡量新策略相对参考模型偏移了多少,β 是它的权重。
为什么必须有这根"拴绳"?因为奖励模型只是个有限的近似,存在盲点。没有 KL 约束,优化器会去钻奖励模型的空子(reward hacking)——找到一些人类看着是乱码、但奖励模型莫名给高分的输出,模型迅速"塌缩"到这些退化解。KL 惩罚像分布式系统里的限流器 / 熔断器:允许探索,但不准跑太远偏离已知良好的 π_ref。β 大 = 短拴绳(保守、贴近 SFT),β 小 = 长拴绳(敢探索、但易被 reward hacking)。
import torch # RLHF 第三步:实际优化信号 = 奖励 − β·KL(这是公式的逐 token 实现) def rlhf_signal(policy_logp, ref_logp, reward, beta=0.1): # policy_logp / ref_logp:当前策略 / 参考模型对生成 token 的 log 概率 # reward:奖励模型对整条回答打的标量分 kl = policy_logp - ref_logp # KL 散度的单样本估计 return reward - beta * kl # ← 拴绳:奖励减去"跑偏惩罚" p = torch.tensor([-0.2, -1.1, -0.5]) # 策略对各 token 更自信 r = torch.tensor(2.0) print(rlhf_signal(p, torch.tensor([-0.3, -0.9, -0.6]), r)) # β 调大 → KL 项压制奖励 → 输出更贴近 SFT,更稳但更保守
奖励模型就是给回答算"ELO 棋力分"。象棋选手没人能直接打出绝对实力分,但通过大量"A 赢 B"的对局,ELO 系统能反推出每人一个标量等级分。奖励模型一模一样:人类说不准"这答案值 7.3 分",但能可靠判断"A 比 B 好"。于是我们只收集成对比较,再拟合一个标量分数,让它和所有比较结果一致——背后是同一套数学(Bradley-Terry / 逻辑回归)。
痛点:RLHF 第三步要一个能实时、自动给任意回答打分的函数,但人类标注又慢又贵又主观。解法分两步:(1) 收集偏好数据——同一个提示生成两条回答,人选出更好的那条,得到三元组 (x, yw, yl),w=winner(被选中)、l=loser(被拒绝);(2) 用这些比较训练一个标量打分模型 r。
Bradley-Terry 模型把"偏好"翻译成概率——"yw 胜过 yl"的概率是两者分差经过 sigmoid:
σ 是 sigmoid 函数(把任意实数压到 0~1 当概率)。直觉:两条回答分差越大,人选高分那条的概率越接近 1;分数相等时概率正好 0.5(完全猜不准)。把它写成最大似然,就是奖励模型的训练损失:
这个 loss 只看分数之差,不看绝对值——所以奖励的绝对刻度是没有意义的(整体加 100 分,loss 不变),和 ELO 只关心相对强弱完全一致。这套从人类偏好学奖励的思路最早来自 Christiano et al. 2017。
import torch import torch.nn.functional as F # 奖励模型的 Bradley-Terry 损失:只学"相对差",绝对刻度无意义 def bt_loss(r_chosen, r_rejected): # r_*:奖励模型给"被选中"/"被拒绝"回答打的标量分 # −log σ(r_w − r_l):分差越大、方向越对,loss 越小 return -F.logsigmoid(r_chosen - r_rejected).mean() r_w = torch.tensor([2.1, 0.5]) # 第二对:模型给被选中的反而打低分 r_l = torch.tensor([1.0, 0.8]) print(bt_loss(r_w, r_l)) # 第二对方向错 → 贡献更大的 loss,推动修正
Constitutional AI 是"把人工 code review 换成跑 linter"。RLHF 里每条偏好都要人来标,像每个 PR 都要资深工程师手动审——又慢又贵又不一致。CAI 的做法是:写一份明文规则("宪法"),让模型拿这份规则审查并改写自己的输出,再用模型自己的偏好判断代替人类标注。用一份可读、可审计、可版本管理的原则文档,换掉成千上万次人工标注。
痛点:RLHF 的人类偏好标注是对齐的成本瓶颈与一致性瓶颈——尤其"无害性"标注要让人反复阅读有害内容,既贵又伤人。Anthropic 的 Constitutional AI(Bai et al. 2022)用两阶段把人换成 AI:
核心机制叫 RLAIF(Reinforcement Learning from AI Feedback):流程和 RLHF 一模一样(还是奖励模型 + KL 拴绳 + RL),唯一区别是偏好标签由模型依据宪法生成,而非人类。"宪法"是一组自然语言原则(如"选择更无害、更少说教的回答")。这之所以能 work,依赖一个关键假设:判断哪个回答更好,比从零生成一个好回答更容易——模型有能力当合格的裁判,即使它做选手时会犯错。
from anthropic import Anthropic client = Anthropic() # 需要 ANTHROPIC_API_KEY principle = "回答不得提供可造成伤害的操作细节,应礼貌说明拒绝理由。" draft = "<模型对某敏感问题的初版回答>" # CAI 阶段 1:让模型按"宪法"自我批判 + 改写(无需人工标注有害样本) revision = client.messages.create( model="claude-opus-4-8", max_tokens=512, messages=[{"role": "user", "content": f"原则:{principle}\n初版回答:{draft}\n" "请先指出它违反原则之处,再给出符合原则的改写版本。"}] ).content[0].text print(revision) # 用这些"自我改写"样本做 SFT → 再用 AI 偏好做 RLAIF
DPO 是"用代数化简干掉一整个微服务"。RLHF 是个三件套架构:奖励模型服务 + 采样循环 + PPO 训练,组件多、互相耦合、调起来像调分布式系统一样脆。DPO 发现了一个数学恒等式,证明"奖励模型"其实可以用"策略本身相对参考模型的对数概率比"来表示——于是把奖励模型这个中间组件整个消掉,只剩一个普通的分类损失。就像你发现一个 RPC 调用其实能被代数化简成本地函数调用。
痛点:RLHF 工程上很难——要同时维护 4 个模型(策略、参考、奖励、价值),PPO 对超参敏感、易崩、采样慢。DPO(Rafailov et al. 2023)的洞见从卡片 1 那个带 KL 的目标出发:那个目标其实有闭式最优解。最优策略长这样:
Z 是归一化配分函数(保证概率和为 1,依赖 x)。这步反过来解出奖励:
关键魔法来了:把这个 r 代回卡片 2 的 Bradley-Terry loss,因为 loss 只含奖励之差 r(yw)−r(yl),而 β·log Z 对同一个 x 完全相同,相减直接抵消——那个最棘手、最难算的归一化项凭空消失了。剩下的就是直接作用在策略上的损失:
对比卡片 2 的奖励模型 loss:结构一模一样的 −log σ(差),只是把"奖励差"换成了"策略相对参考模型的对数概率比之差"。这就是论文标题的含义——语言模型本身就是一个隐式的奖励模型。KL 拴绳没消失,它被 β 和 π_ref 偷偷编码进了 loss 里。
import torch import torch.nn.functional as F # DPO loss:奖励 = β·log(π/π_ref),配分函数 Z 在相减中自动消掉 def dpo_loss(pi_w, pi_l, ref_w, ref_l, beta=0.1): # *_w / *_l:策略(pi)/参考(ref)对 chosen/rejected 回答的序列 log 概率 logits = beta * ((pi_w - ref_w) - (pi_l - ref_l)) return -F.logsigmoid(logits).mean() # 和卡片 2 同一个 σ! # 策略相对参考,提升了 chosen、压低了 rejected → logits>0 → loss 小 print(dpo_loss(torch.tensor([-1.0]), torch.tensor([-2.0]), torch.tensor([-1.2]), torch.tensor([-1.5]))) # 实战中直接用 HuggingFace TRL 的 DPOTrainer,无需手写