qinghuan
← 监督微调与偏好对齐 / 全部笔记

单元 33 / 监督微调与偏好对齐

P1 · 重点提升L3 · 深入

偏好数据与奖励模型:奖励究竟在测什么

比较同一提示下更受偏好与较不受偏好的回答,建立成对概率和奖励损失,再训练一个能逐项核对的线性奖励头。

本篇目录 · 按需要跳转

读完这篇,你能做什么

计算极端分数下的稳定偏好损失;在 CPU 训练中观察损失下降、四组训练偏好排序正确,并说明这不能证明在新数据上也有效。

学习理由、准备要求与知识点

为什么学它

奖励模型只是在近似标注中的偏好。要看清哪些分数关系能由数据确定、标注覆盖了什么,不能把模型高分直接当成真实质量。

先读这些内容

知识点清单

  • 偏好对与同提示比较
  • Bradley-Terry概率
  • 成对交叉熵
  • 分数平移不变性
  • 标注一致性与平局
  • 提示级数据划分
  • 奖励过优化
  • 独立人工审计

运行前先确认论文机制按文中版本说明;Java 17 完整算法,Python 3.14 与 PyTorch 2.9.1+cpu 的小型 CPU 实验。未执行 GPU、多机或大规模模型训练。

01先弄懂它是什么

想训练模型判断“哪份回答更好”,首先要弄清这个“好”是按什么标准。奖励模型把提示 x 和回答 y 转成一个分数 r(x,y),但分数本身没有“正确率百分比”的单位。例如标注者在同题的 A、B 两份回答里更喜欢 A,我们知道的是 A 优于 B,并不知道 A 就该得九十分。因此要一起保存题目、两份原始回答、比较标准和标注结果,才能解释分数来自哪里。

常见方法用两份回答的分数差来预测偏好:P(A优于B|x)=sigmoid(rA-rB),其中 sigmoid(z)=1/(1+exp(-z))。一批有 B 组比较时,两组分数的形状都是 [B],相减后也一样。分数差为零,预测偏好概率就是二分之一;差为二时约为 0.881。这个概率说的是模型认为哪份更受偏好,不能直接证明回答内容真实或没有风险。

偏好来自具体的人、任务和准则。有人偏爱简短,有人需要完整推导;把这些差异混在一起并丢掉来源,模型可能学习到平均风格而非业务目标。训练前先规定事实、帮助程度和格式等评价维度,允许平局、无法判断与争议复核。严格二选一损失只是其中一种数据约定,不能把不确定标签伪造成强偏好。

文中这几个词,先认一认

偏好对
同一个提示或条件下的两份候选回答,以及哪份更好的比较标签。
奖励分数
模型学出来的一个数,用来比较回答的相对偏好;数值本身没有固定的百分制含义。
可辨识性
现有的观察或标签,能不能唯一确定想估计的参数或分数尺度。例如只知道谁更好,未必能确定绝对分数。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

对明确偏好A的样本,负对数似然为L=-log(sigmoid(d))=softplus(-d),d=rA-rB。最小化损失会增加d;对d的导数是sigmoid(d)-1,负值会推动优选分数上升、拒选分数下降。无需先计算一个可能下溢为零的概率再取对数,稳定写法是max(-d,0)+log1p(exp(-abs(d)))。当d=-1000时损失约1000而非无穷大。

如果给两份回答的分数同时加上 c,它们的差值和成对偏好概率都不变。所以只靠“谁更好”的比较,确定不了绝对零点,加一个公共偏置也不会让模型更会区分回答。把分数差拉得很大,虽可能继续压低训练损失,却可能让新类型数据上的判断更差。评估时应看保留提示上的排序、置信度和错误类型,必要时加入正则化与独立校准。

CPU实验用两个数表示候选特征,线性层把[B,2]映射成[B,1],再压成[B]计算差值。四组人工构造的偏好是可分的,实验只要求训练损失从log(2)下降并全部排对,不声称学会评价自然语言。真实数据应按提示或来源家族划分,同一提示的多个回答不能分散到训练与测试,否则评估很可能只是记住题目。

先跑通一个最小例子

保存为experiment.py,以已安装PyTorch 2.9.1+cpu的解释器执行 python experiment.py。实验只训练二维线性奖励头;不下载模型或数据,也不执行GPU训练。

experiment.py · python
import torch
import torch.nn.functional as F

torch.manual_seed(7)
torch.set_num_threads(1)
chosen = torch.tensor([[2., 0.], [3., 1.], [1., 0.], [2., 1.]], dtype=torch.float64)
rejected = torch.tensor([[1., 1.], [1., 2.], [0., 2.], [0., 3.]], dtype=torch.float64)
head = torch.nn.Linear(2, 1, bias=False, dtype=torch.float64)
with torch.no_grad():
    head.weight.zero_()  # 从相同分数开始,初始损失应为 log(2)。
optimizer = torch.optim.SGD(head.parameters(), lr=0.1)
def objective():
    difference = head(chosen).squeeze(-1) - head(rejected).squeeze(-1)
    return F.softplus(-difference).mean()
initial = objective().item()
assert abs(initial - 0.6931471805599453) < 1e-12
for _ in range(120):
    optimizer.zero_grad()
    loss = objective()
    loss.backward()
    optimizer.step()  # 真实更新一个小型线性奖励头,不是语言模型训练。
final = objective().item()
assert final < 0.1 and final < initial
assert torch.all(head(chosen) > head(rejected))
scores = torch.tensor([[-1000., 1000.], [2., 1.]], dtype=torch.float64)
losses = F.softplus(-(scores[:, 0] - scores[:, 1]))
assert torch.isfinite(losses).all()  # 极端分数不会令指数计算溢出。
shifted = scores + 17
assert torch.allclose(losses, F.softplus(-(shifted[:, 0] - shifted[:, 1])))
assert torch.allclose(F.softplus(torch.zeros(2)), torch.full((2,), 0.6931471805599453))
print(f"initial={initial:.6f} final={final:.6f} ranking=4/4")
print("PASS 6 checks; CPU linear reward head only")

核对运行结果初始损失约0.693147,最终小于0.1,ranking=4/4,并输出PASS 6 checks;训练集拟合不等于泛化。

正在加载流程图…

同一提示的两个分数先形成差值,再优化偏好概率;保留提示评价与训练排序分开。
查看流程图源码(Mermaid)
flowchart TD
  N0["确定比较准则与平局策略"]
  N1["按提示家族划分偏好对"]
  N2["输出优选与拒选分数"]
  N3["以稳定成对损失更新"]
  N4["在保留提示与审计样本评价"]
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N3 --> N4

按顺序看,每一步做了什么

  1. 确定比较准则与平局策略
  2. 按提示家族划分偏好对
  3. 输出优选与拒选分数
  4. 以稳定成对损失更新
  5. 在保留提示与审计样本评价

03对照实现代码

成对奖励目标与稳定LogSigmoid入口 (新标签页)↗

PyTorch v2.9.1;Christiano等2017论文v4 · torch.nn.functional.logsigmoid / softplus;论文偏好概率与交叉熵

原创成对损失片段,调用固定版本PyTorch真实算子;不是原样框架源码,不包含语言模型与人工标注平台。

核心机制代码 · python
import torch.nn.functional as F
def reward_pair_loss(chosen_score, rejected_score):
    delta = chosen_score - rejected_score  # 同一提示的一对分数,形状均为[B]。
    per_pair = -F.logsigmoid(delta)  # 稳定计算负对数概率,不先做sigmoid再log。
    return per_pair.mean()  # 此处每对等权;标注置信度加权需另定契约。

真实functional.py把logsigmoid绑定到torch._C._nn.log_sigmoid,Python层提供接口与文档,底层数值实现并不是这三行教学函数。教学函数调用真正的PyTorch算子构造成对损失,自动微分再沿分数回传到奖励网络。不要把Java中的标量稳定公式称为PyTorch训练内核。

Christiano等人的原研究比较的是轨迹片段,片段分数由预测奖励累计得到,再以两个累计分数构成偏好概率。语言回答的成对评分沿用相同的比较思想,但输入结构、长度和标注任务不同。本篇用回答级标量说明目标,没有复制论文完整环境、集成模型或主动查询流程。

mean是学习目标的一部分而非无关语法。如果一个提示有十对回答、另一个只有一对,逐对平均会让前者具有更高权重。可以按提示分组平均或使用明确权重,但必须同步改变采样和评测说明;把所有配对展开后宣称每个提示贡献相同是不准确的。

数据张量不应包含人工标签之外的未来评价结果,优选和拒选的截断规则也要一致。若一个回答的关键信息被截掉,训练仍会按原标签更新,产生错误监督。沿真实调用链检查分词、掩码、序列聚合、奖励头和损失,比只确认某个函数名字存在更能发现问题。

代码先调用谁,再调用谁

  1. 构造同条件偏好对
  2. 奖励网络分别输出标量
  3. 计算分数差与logsigmoid
  4. 按约定权重归约
  5. 反向传播并更新可训练参数

04动手写一个例子

数值稳定的加权偏好损失

给定有限分数差和非负权重,计算成对负对数似然的加权平均;无正权重或总权重溢出拒绝。

输入 1
差值0,权重1
输出 1
log(2)
输入 2
差值-1000,权重1
输出 2
约1000

直接写-log(1/(1+exp(-d)))会在很负的d处溢出。先利用softplus恒等式把指数参数限制为非正,最大损失部分单独保留,就能处理正负一千的输入。该变换保持同一数学目标,没有把难样本裁掉或改变标签。

权重平均也有数值问题:先累计w×loss可能溢出,即使最后均值仍有限。代码维护已有总权重total和均值mean,新样本用w/(total+w)调整均值。更新前检查新增总权重有限,零权重样本不参与平均但仍检查输入,避免无效数值隐藏在数据中。

归纳地看,处理前i项后mean等于这些正权重项的加权损失。加入第i+1项时,新权重占比乘上“新损失减旧均值”,恰好把旧平均改成扩大集合的平均。浮点运算仍有舍入误差,所以自测使用容差;算法不提供任意实数精确算术保证。

为什么这样写是对的已处理正权重样本的total是权重和,mean是其加权损失均值。

时间 O(n)额外空间 O(1)额外空间

每对只计算常数次标量运算,不保留全部损失。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 42 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static void near(double expected,double actual) {
        if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
            throw new AssertionError(expected+" != "+actual);
    }
    static void check(boolean ok) { if(!ok) throw new AssertionError(); }
    static void bad(Runnable r) {
        try { r.run(); } catch(IllegalArgumentException e) { return; }
        throw new AssertionError("invalid input accepted");
    }
static double pairLoss(double[] margin,double[] weight) {
        if(margin==null || weight==null || margin.length!=weight.length)
            throw new IllegalArgumentException("shape");
        double total=0,mean=0;
        for(int i=0;i<margin.length;i++) {
            double d=margin[i],w=weight[i];
            if(!Double.isFinite(d)||!Double.isFinite(w)||w<0)
                throw new IllegalArgumentException("nonfinite or negative");
            if(w==0) continue; // 零权重不参与归一化,但输入仍然检查。
            double next=total+w;
            if(!Double.isFinite(next)) throw new IllegalArgumentException("weight overflow");
            double loss=Math.max(-d,0)+Math.log1p(Math.exp(-Math.abs(d)));
            // 指数的参数非正,避免直接计算 exp(1000) 溢出。
            mean += (w/next)*(loss-mean); // 在线加权均值,避免大损失乘权重。
            total=next;
        }
        if(total==0) throw new IllegalArgumentException("no positive weight");
        return mean;
    }
    public static void main(String[] args) {
near(Math.log(2),pairLoss(new double[]{0},new double[]{1}));
        near(1.1269280110429725,pairLoss(new double[]{2,-2},new double[]{1,1}));
        near(0,pairLoss(new double[]{1000},new double[]{1}));
        near(1000,pairLoss(new double[]{-1000},new double[]{1}));
        near(1000,pairLoss(new double[]{1000,-1000},new double[]{0,1}));
        bad(()->pairLoss(new double[]{},new double[]{}));
        bad(()->pairLoss(new double[]{1},new double[]{-1}));
        bad(()->pairLoss(new double[]{Double.NaN},new double[]{1}));
        System.out.println("PASS 8 cases");
    }
}

这些特殊情况也要试一试

  • 空输入或全零权重拒绝
  • 极端正负差值仍应有限
  • 负权重与NaN拒绝
  • 权重和溢出明确失败
进阶选读 · 带平局和标注者差异的偏好模型

场景不同标注者对相同候选给出冲突结果。

做法保存软标签与标注者分组,对称平局可使用两方向交叉熵;分别评估组内一致性和跨群体泛化,不靠删除全部争议样本美化结果。

复杂度每轮N对、特征维度D的线性头计算O(ND),标注与神经编码成本另外计算。

好处与代价更丰富标签减少信息损失,但引入建模假设和校准需求。

05项目里怎么用

奖励模型的保留集应覆盖真实任务类型、长度与语言分布。用总体准确率可能掩盖长回答偏好、格式捷径或少数类别失效;还要检查分数差与人工信心是否匹配。用于选择候选的模型和最终审计者应尽量独立,不能让同一错误准则同时负责优化与证明成功。

当策略反复优化某个奖励模型时,输入分布会逐步移动到它没见过的区域。模型可能学会提高奖励却降低真实质量,这称为奖励过优化。应限制更新预算,定期收集当前策略的新样本做人工审计,并保留不可由奖励分数抵消的业务约束,例如引用存在性与权限校验。

建立数据版本、标注指南和争议记录,使一次回归能够追到具体偏好变化。模型权重、训练种子、提示模板和截断配置一同保存。小实验的四对全部正确只是程序和优化方向的证据,研究报告仍需独立测试集及置信区间,不能把这个数值写成自然语言评价能力。

容易踩的坑

  • 把奖励分数当成事实正确率
  • 同提示候选泄漏到测试集
  • 强行将平局改成严格偏好
  • 忽略回答长度与截断偏差
  • 只优化奖励而不做独立审计

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
成对偏好标注者通常较易比较候选需要大量配对并处理冲突相对质量优化
绝对打分可表达多个维度标尺在标注者间容易漂移明确评分规范的任务
规则校验加偏好把硬条件与软质量分开规则覆盖不完整有客观格式或执行约束的系统

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01两个奖励都加10会怎样?

展开参考答案与推导

概率与成对损失不变,因为仅分数差进入目标。

02奖励高代表事实正确吗?

展开参考答案与推导

不代表;它表示对标注分布的相对偏好预测。

03为何不用先sigmoid再log?

展开参考答案与推导

极端差值可能导致概率下溢,logsigmoid或softplus保持稳定。

换个情况,还会处理吗

01训练排序很好、线上偏爱冗长怎么办?

展开参考答案与推导

按长度分桶审计,检查标注准则、截断和长度捷径,再加入对照候选。

02多人意见不一致是否删样本?

展开参考答案与推导

先区分标注错误与真实偏好分歧,保留来源并允许软标签或复核。

03策略奖励上升、人工评价下降怎么办?

展开参考答案与推导

暂停扩大优化,审计分布偏移与漏洞,收集当前策略样本并用独立标准复评。

写代码试一试

01实现稳定单对损失。

输入 / 条件
d=-1000
输出 / 预期
约1000

提示 避免正大数进入exp

展开参考答案与推导

按正文softplus恒等式计算并检查有限性。

02验证分数平移不变。

输入 / 条件
(3,1)与(103,101)
输出 / 预期
两者损失相等

提示 先求差

展开参考答案与推导

构造同差值,容差比较两份结果。

03加入零权重样本。

输入 / 条件
差值[1000,-1000],权重[0,1]
输出 / 预期
1000

提示 归一化只计正权重

展开参考答案与推导

跳过零权重贡献,全部为零则拒绝。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

计算极端分数下的稳定偏好损失;在 CPU 训练中观察损失下降、四组训练偏好排序正确,并说明这不能证明在新数据上也有效。

本篇重点加练

评分的可识别性与提示级隔离共同限制泛化声明,重点考查模型是否学到比较关系。

  • 分数平移不变性
  • 提示级数据划分

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    同提示下三个回答的评分为 [-1,0.5,2]。选取三对偏好标签,逐对写 Bradley–Terry 概率和损失定义。

    提交内容
    成对计算表
    怎样算完成
    • 同提示比较前提明确
    • 标签方向一致

    练到的知识点偏好对与同提示比较 · Bradley-Terry概率 · 成对交叉熵

  2. 作业 02原理问答

    设计包含平局、标注冲突和置信度的偏好记录格式;说明分数绝对零点与标注者一致性各自能否由成对数据识别。

    提交内容
    数据契约与可识别性说明
    怎样算完成
    • 不确定标签保留证据
    • 评分与事实正确性分开

    练到的知识点分数平移不变性 · 标注一致性与平局

  3. 作业 03原理问答

    只有同提示成对偏好时,分别给每个提示所有回答加一个不同常数,分析哪些观测仍能确定、哪些跨提示比较需要额外假设。

    提交内容
    可识别量与不可识别量表
    怎样算完成
    • 变换作用范围明确
    • 组内和跨组比较分开

    练到的知识点分数平移不变性

综合应用

  1. 作业 04编程实践

    用 CPU 训练一个线性偏好评分器,按提示 ID 划分数据;报告逐提示排序准确率、损失及保留集结果。

    提交内容
    脚本、切分清单与报告
    怎样算完成
    • 提示不跨集合泄漏
    • 预测和标签可逐对复核

    练到的知识点Bradley-Terry概率 · 成对交叉熵 · 提示级数据划分

  2. 作业 05排错实验

    构造答案长度与偏好标签强相关的训练集,再做相关性被打破的保留集;观察奖励模型的变化并记录失败类型。

    提交内容
    合成数据与受控实验
    怎样算完成
    • 偏差因素可控制
    • 保留集不参与调参

    练到的知识点标注一致性与平局 · 奖励过优化

  3. 作业 06面试场景

    策略训练分数持续升高,但独立人工评估不变。设计奖励过优化审计,规定抽样、盲评和异常样本归档方式。

    提交内容
    审计协议与记录表
    怎样算完成
    • 评价器与奖励模型独立
    • 抽样不只挑有利样本

    练到的知识点奖励过优化 · 独立人工审计

  4. 作业 07编程实践

    在 CPU 小评分器上为每个提示加入不同的常量分数偏移,核对组内损失及跨提示排序;评价集按未见提示划分。

    提交内容
    评分变换实验
    怎样算完成
    • 提示隔离真实执行
    • 损失不变与泛化质量分开

    练到的知识点分数平移不变性 · 提示级数据划分

  5. 作业 08设计评审

    同一提示的多种改写使用不同 ID。设计提示家族划分规则与人工复核样本,明确训练/验证/测试的来源身份。

    提交内容
    提示家族映射与审计表
    怎样算完成
    • 字面 ID 不代替独立性
    • 规则不使用保留集结果调优

    练到的知识点提示级数据划分

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    制定跨标注人偏好泛化实验:一部分标注人仅出现在保留集;比较分歧强度下的表现并声明结论针对的人群。

    提交内容
    实验设计与分析计划
    怎样算完成
    • 标注人身份隔离
    • 分歧不被一概视作噪声

    练到的知识点偏好对与同提示比较 · 标注一致性与平局 · 提示级数据划分

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    能否只依靠偏好对训练出的标量奖励,对完全不同任务做统一资源分配?构造组内排序合理但跨任务分数不可比的例子,提出新增证据需求。

    提交内容
    反例与跨任务校准需求
    怎样算完成
    • 评分等价类明确
    • 新增假设可被检验

    练到的知识点分数平移不变性 · 提示级数据划分

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗