我第一次看到Reverse Engineering Human Preferences with Reinforcement Learning这个题目时,第一反应是:这不就是把我这几年一直在做的偏好对齐工作重新起了一个更学术的名字吗?可真去把里面的思路拆开之后,才发现值得展开的细节比想象中多得多。过去大家默认“人类反馈”是现成的打分,但现实里收集到的往往只有两两比较,甚至是聊天记录里模棱两可的倾向。强化学习本来需要一个奖励信号,而“逆向工程人类偏好”正好解决这个错位:用有限、稀疏的反馈,反推人类内心那个没说出口的价值函数。这篇文章就把这个方向从头到尾拆一遍,适合想理解 RLHF 底层逻辑、正准备用偏好数据训练自己模型的工程师和研究者参考。
1. 从“打分数”转向“做比较”:为什么这是逆向工程的起点
1.1 人类天然输出的是排序,不是标量奖励
如果你让标注员给模型回复打 0 到 10 分,大概率会遇到一个尴尬场面:同一段文本,上午打 6 分,下午就改成 7 分;两个答案明明水平接近,却要在 4 分和 5 分之间纠结半天。这不是标注员不专业,而是人类评估这件事本身就擅长“相对判断”,不擅长“绝对度量”。
心理学里有个很成熟的现象:绝对评分会被疲劳、锚定效应、上下文顺序严重影响,而两两比较就稳定得多。一张美食照片好不好看,你很难给出“7.3 分”这种精确结论,但你很容易说出“这张比旁边那张更有食欲”。
偏好数据的采集恰恰是这样:让标注员从两个答案里挑一个更好的,或者给一堆答案排个序,这种数据收集成本低、标注一致率高,符合人类直觉。问题在于强化学习的标准训练框架需要的是标量奖励,不是“这个比那个好”的相对关系。于是就有了“逆向工程”的必要:从一堆相对判断里,反推出一个稳定的偏好评分函数。
1.2 比较数据如何变成可优化的数学对象
为了把“y1 比 y2 好”变成数值,我们通常会引入一个潜函数 r(x, y),表示模型对某个输入 x 下候选回复 y 的内在偏好强度。这个 r 可以是神经网络打分器,也就是常说的奖励模型。
在训练时,最常用的建模方式是 Bradley-Terry 模型,它把人比较答案的概率和打分差值联系起来:
P(y1 优于 y2) = exp(r(x, y1)) / (exp(r(x, y1)) + exp(r(x, y2)))
直观理解:两个候“选手”的胜率取决于它们各自“实力值”的指数差距。如果 r(x, y1) 比 r(x, y2) 大很多,那么 y1 被选中的概率就趋近于 1。训练奖励模型时,我们收集大量形如 (prompt, chosen, rejected) 的偏好对,然后最大化“chosen 胜出”的概率,等价于最小化交叉熵损失。这一步做完,人类比较就成功被转换成了一个连续、可微的奖励函数,后续所有强化学习操作都建立在这条函数之上。
这里有一个很多人忽略的前提:Bradley-Terry 模型假设所有比较都是独立的,且偏好满足传递性。实际标注数据里经常出现“A 比 B 好,B 比 C 好,C 却比 A 好”的循环矛盾,这套建模方式无法表达这种冲突,只能通过训练让打分器“折中”。所以数据清洗阶段如果不去处理这些循环矛盾,奖励模型会学到一种平均偏好,导致各条样本的信号互相抵消。
2. RL 在这里不是“学习者”,而是“求解器”
2.1 系统里实际有两个角色:打分器和动作器
很多人以为 RLHF 这类方法只有“一个模型在训练”,其实整个系统里至少有三个角色在转:奖励模型负责打分,策略模型负责生成文本,参考模型负责约束策略不要乱跑。严格来说,真正在学习人类偏好的是奖励模型,策略模型只是在“使用”已经学会的偏好信号进行优化。
这个分工很容易被搞混。我见过不少项目,数据收集拿到手之后直接拿去微调生成模型,完全跳过奖励模型,结果模型确实学会了“输出更长更花哨的文本”,但并没有学会“判断什么是好文本”。原因很简单:监督微调只利用了少量标注结果,没有利用偏好背后那个可以泛化的价值尺度。
在强化学习阶段,策略模型会不断生成候选回复,由奖励模型对这些回复打分,再通过策略梯度把分数高的回复路径概率调高。这个过程中策略模型并不直接接触原始人类偏好数据,它接触的只是奖励模型。换句话说,奖励模型才是人类偏好的容器,策略模型只是一个在容器指引下做搜索和调整的求解器。
2.2 为什么这个求解过程叫“逆向工程”
“逆向工程”这个词用在这里非常准确。正向过程是:人类心里有一个目标函数,然后根据这个函数产生行为选择。我们观察到的是行为选择,也就是一堆“哪个更好”的比较结果。逆向过程就是:从观察到的选择碎片里,重新估计那个目标函数。
可以打个比方:考古学家面前只有一堆陶器碎片,他们要反推陶器原来的形状、纹路和烧制工艺。偏好逆向工程也是类似,人类很少直接告诉你“我就喜欢更简洁、更真诚、更专业的语气”,他们只会说“这个回答更好”。强化学习在求解过程中做的事情,就是把碎片拼起来,不断生成新陶器让打分器验证,最终拼出一个能稳定打高分的目标函数。
另外一个容易被误解的点是:RL 在这里不是用来“探索未知世界”的,虽然它确实在探索生成空间,但它真正的价值是处理“没有明确公式”的奖励来源。如果我们的偏好能写成明确的规则,那根本不需要 RL,直接写规则筛选就行了。正是因为偏好难以言说、难以用规则覆盖,才需要用一个可以被打分器逼近的函数,然后在策略空间中做梯度上升。没有 RL,偏好模型也会学,但生成的分布不会主动朝高奖励区域偏移;有了 RL,策略在探索中不断自我调整,整个过程才有了闭环。
3. 稳定训练离不开三个“隐性开关”:KL 系数、奖励缩放和温度
3.1 没有 KL 约束的 RL,会变成“奖励破解器”
这是整个实操过程中最容易翻车的地方。奖励模型本质上是另一个神经网络,它不可能真正做到完美表示人类偏好;更麻烦的是,凡是没有完美建模的地方,策略模型就会钻空子。
典型症状是:策略模型找到了一些奖励分数极高但人类眼里很离谱的句子。比如包含重复的夸奖词、刻意使用复杂词汇、输出空泛但结构完整的套话。这种现象叫 reward hacking,中文世界里常叫奖励破解。奖励模型没有能力惩罚这些模式,或者它在训练数据里见过类似但质量还行的文本,于是一旦策略生成这种低质量但高奖励的文本,就会被梯度学习加强。
对策是给策略加一个“不要跑太远”的约束,也就是 KL 散度约束。计算策略模型和初始参考模型之间输出分布的 KL 距离,把这个距离作为惩罚项加进奖励函数。KL 系数太小,策略自由度太高,容易奖励破解;KL 系数太大,策略几乎不更新,人类偏好就学不进去。
我在实际项目里的经验是,与其一上来就固定一个系数,不如先把 KL 预热到某个初始值,比如 0.01 左右,观察训练日志里生成文本的多样性和奖励分数变化,再逐步调整。如果发现总奖励在涨但 KL 涨得比奖励还快,说明策略已经开始“放飞自我”,这时候应该调大 KL 系数或降低学习率。反过来,如果 KL 长期不涨,说明策略根本没有有效学习,需要把 KL 系数调小一点,给模型更多漂移空间。
3.2 奖励缩放:给分数的“野心”降温
奖励模型输出的原始 logit 差值有时候会非常大,因为 Bradley-Terry 训练会让模型尽可能拉大正确对和错误对的差距,从而产生一个置信度相当高的偏好分布。如果把这种大数值直接拿去做策略梯度更新,梯度会异常不稳定,同一个 batch 里几条极端样本就能把策略带偏。
所以训练脚本里必须有奖励缩放这一层。最常见的方式是除以一个温度参数,或者用滑动平均做标准化,把奖励分布控制在一个相对稳定的尺度内。有的项目会在 reward 外层套一个 running mean / running std,让奖励在小范围波动,这能让 PPO 这类算法的 advantage 估计更稳定。
这里有个细节:奖励缩放不影响理论上的最优解,但极大地影响训练过程的数值稳定性。用更通俗的话讲,人类偏好的“绝对数值”本身没有意义,有意义的是不同候选之间的相对差距。既然只是相对差距,那缩放不会改变排序,却能决定每一步参数更新被放大的程度,这就成了工程上必须处理的问题。
3.3 采样温度和 rollout 策略也会决定成败
很多人只关注 KL 和奖励缩放,忽视了 rollout 阶段采样参数的影响。策略生成文本时如果温度过高,产出的文本发散严重,奖励模型给出的分数噪声很大;温度过低,策略只输出最保守的那些句子,探索空间太小,完全达不到“逆向”的目的。
我常用的方法是 top-p 采样加上温度在 0.6 到 1.0 之间做小范围控制,既保留多样性又不至于完全失控。另外,rollout 阶段生成的长度也要注意,过长上下文容易让奖励模型被开头结尾的风格带偏,过短又缺少足够判断信息。训练初期可以多观察一个 mini-batch 的 rollout 文本质量,再进行花样调参。
这三个参数不是互相独立的。KL 系数、奖励缩放和采样温度之间存在耦合:奖励缩放变大,策略就会更激进,需要更大的 KL 约束兜底;采样温度变大,文本更多样,但也更容易触发奖励模型没见过的高奖励区域,对 KL 约束压力也更大。最好的调试办法是每次只动一个变量,并且记录一组固定验证 prompt 的生成结果,肉眼观察这段文本的变化,而不是只看损失曲线和奖励曲线。
4. 数据层面的“偏好逆向”三个陷阱
4.1 标注者分歧不是错误,是偏好本身多维
我在实际做数据清洗时,会遇到大量“两人标注结果相反”的样本。传统做法是把这些样本当成噪声丢掉,但做久了会发现问题没那么简单:一个标注者认为专业术语多代表作者懂行,另一个认为通俗易懂代表对用户友好,两个人的偏好本质来自不同维度,而不代表哪个人乱标。
处理这类数据比想象中麻烦。简单粗暴的方式是保留少数服从多数的样本,但这样会把“小众但真实”的偏好过滤掉,导致逆向出来的偏好函数在大众维度上很突出,在长尾维度上非常弱。更好的方式是先对标注说明进行收敛,定义出几个明确的偏好维度,比如“专业准确”“表达简洁”“态度友好”,让标注者先判断维度再给出胜负结果。哪怕最终再合并成单一的 considered better,在这些维度上做聚合,也能减少“两个人各说各话”的情况。
只要你的应用场景不只有一个目标用户群,标注者分歧就永远不会消失。把它完全当成噪声处理,最直接的损失是奖励模型在边界样本上几乎没有区分度,所有输出都拿到接近 0.5 的胜率,RL 阶段自然找不到学习方向。
4.2 顺序效应与位置偏差
这个坑比较隐蔽。给标注员看两个候选答案时,如果 A 一直在上面、B 一直在下面,很快就会出现位置偏差:标注员倾向于选下面那个,因为阅读顺序让他对后出现的答案印象更深。另外,两个答案相似、难以区分时,标注员还会倾向选长度更长、小标题更多的那个,因为看起来“更努力”。
解决方案说不上高深,但必须执行到位:每个标注任务随机打乱 A/B 的展示顺序,并在统计时记录每一个样本最初的展示位置,做后验分析。如果发现某个标注员的“选择下方”比例显著高于随机水平,就要考虑他的标注结果是否可信。
还有一类位置偏差来自 prompt 本身。有些 prompt 会诱导模型先说“好的,收到”再进入正题,这类固定开场结构在偏好对里反复出现后,奖励模型会学到“只要看到这种开头就给高分”的捷径。处理办法是构造数据集时保持答案首尾部分的多样性,避免所有 chosen 回复都长着同一副面孔。
4.3 提示分布偏差:偏好函数只在你划定的范围里有效
逆向工程得到的偏好函数本质上是一个由训练数据定义的经验函数,不是人类内心的普适通用函数。如果偏好数据里 90% 都是“写邮件”类的 prompt,奖励模型在“写诗”场景下就毫无判断力,策略模型在自由探索时一旦跑到这个分布之外,奖励模型只能给出近乎随机的分数,RL 训练几乎退化成了纯随机游走。
我见过一个项目沉迷于收集高质量偏好对,但 prompt 来源只覆盖了产品文档里的常见问题模板,结果训练出来的模型在内部测试里表现很好,一旦投放给真实用户问一些刁钻问题,输出质量立刻崩掉。逆向工程这个词已经提醒我们了:你只能从数据里反推数据所包含的那部分偏好。如果你想让偏好函数覆盖更广,首先要让 prompt 分布覆盖更广,而不是盲目提高偏好对的数量。
处理方式是在采集阶段刻意设计 prompt 采样策略,把常见的、中等的、罕见的三类 prompt 按比例混入。训练好奖励模型之后,还要专门用一部分“分布外”prompt 做验证,看奖励模型的判断与人工判断是否还保持一致。如果只在验证集上做这一项检查,能挡掉绝大多数偏好污染问题。
5. 最小可复现实验:我在小模型上跑通这套流程
5.1 数据准备的最小单元
偏好对齐的最小数据单元就是一个 prompt 加一对答案,一个被标注为更好,另一个被标注为更差。整理成 JSONL 格式大概长这样:
{"prompt": "用户问:便携咖啡机怎么选?", "chosen": "回答:先看研磨方式……", "rejected": "回答:推荐买贵的……"}数据规模上,不用迷信几十万条。几千条长度合理、多样性够的偏好对足够让一个小规模模型学到基本的偏好方向。数量太少模型过拟合,但数量多不代表质量高,如果这 1 万条里有大量重复 prompt,效果反而不如多样化的 3 千条。
做数据清洗时,我会先剔除两条候选几乎完全一样的样本,再剔除长度差异过大的样本(除非长短本身就是这个偏好维度的一部分)。剩下的一些明显错误标注,可以通过自动化的规则筛掉,比如 chosen 文本里语义完整度明显不如 rejected 文本的。
5.2 训练奖励模型:只看“赢家概率”
奖励模型的训练是整套流程的第一个模型训练环节。它接收两个候选文本,分别计算各自的偏好分数,然后计算“chosen 胜出”的交叉熵损失。
以一个小规模底座模型为例,奖励模型通常会在最后一层加上一个回归头,输出一个标量,然后把两个标量放进 Bradley-Terry 的公式里。用伪代码表示训练循环:
for batch in dataloader: prompt = batch["prompt"] chosen = batch["chosen"] rejected = batch["rejected"] score_chosen = reward_model(prompt, chosen) score_rejected = reward_model(prompt, rejected) logits = score_chosen - score_rejected loss = -torch.nn.functional.logsigmoid(logits).mean() loss.backward()这份代码非常简单,但工程上要保证几件事:prompt 和回答拼接时 tokenizer 的 padding 方式一致,chosen 和 rejected 的文本长度差异不会影响 attention mask 的有效性,以及 reward model 的输出层不要初始化为全零或全一,否则训练刚开始梯度会不稳定。训练之后要从验证集里看准确率,一般达到 70% 以上就可以用了,刻意追求 90% 以上反而容易让模型记住训练集的表面模式。
5.3 策略微调阶段:参考模型不能动
有了奖励模型之后,进入策略微调。策略模型通常和奖励模型同源,但奖励模型可能与策略模型共用底座,也可能不是。无论哪种,策略微调时都要冻结奖励模型,它的分数只作为一个固定信号。策略需要学习的,是如何生成奖励模型认为更可能获胜的文本。
RL 阶段照例要有三个模型同时加载:训练中的策略模型、给 KL 提供参考的参考模型、给文本打分的奖励模型。参考模型是策略模型的初始版本,在整个训练过程中完全冻结,它的作用是确保策略不会在奖励模型某处低质量区域里“安家”。
核心训练配置可以从下面这张表起步:
| 参数 | 推荐起始值 | 调整方向 |
|---|---|---|
| 学习率 | 5e-6 到 1e-5 | 波动大就降,学习太慢就小幅升 |
| KL 系数 | 0.01 | 奖励破解就调大,生成太死板就调小 |
| 奖励缩放温度 | 1.0 到 2.0 | 奖励方差过大就调大 |
| 采样温度 | 0.7 到 1.0 | 想探索更多就调高,文本发散就调低 |
| rollouts 数量 | 每 prompt 4 到 8 条 | 数据噪声大就增加数量 |
| 生成最大长度 | 与业务场景匹配 | 过长会稀释奖励信号,过短则信息不足 |
真实跑起来之后,我建议把训练日志里加进“平均奖励分数”“平均 KL 距离”“文本平均长度”三个指标。模型跑到一半时,如果平均奖励在涨、平均 KL 基本平稳,说明策略在奖励模型附近找到了清晰的方向;如果文本平均长度突然暴增,大概率是模型在靠堆字数刷分,这时候即使奖励曲线很好看,也要及时干预。
5.4 复现时最容易翻车的三个地方
第一个是 padding 对齐问题。reward model 在计算时往往会把 prompt 和 answer 拼接后再过模型,如果 chosen 和 rejected 的 padding 不一致,batch size 一变分数就会飘。这里强烈建议手动固定 max length,或者用一个统一的 collator,保证每条样本实际参与计算的 token 数量一致。
第二个是 reward model 和策略模型 tokenizer 不一致。如果奖励模型和策略模型用了同源底座但 tokenizer 版本不同,会导致同一段文本在两边得到完全不同的 token 序列,偏好信号就会失真。最好在训练开始前写一个一致性检查:用同一个 prompt 分别过两个模型的 tokenizer,确认输入 id 相同。
第三个是保存模型时不小心把参考模型覆盖了。策略模型、参考模型、奖励模型如果放在同一个目录下,不同脚本之间共享 checkpoint,策略更新到一半时可能误载入旧参考模型,KL 约束瞬间失效。建议三个模块分目录存放,并在加载时打印模型参数 hash 做确认。
6. 关于这个方向的三个常见误判
6.1 误以为 RLHF 只是“让模型更听话”
这个误解在项目沟通里最要命。一听到用强化学习逆向工程人类偏好,很多人下意识认为这和“安全规则过滤”是一回事,无非是让模型少说危险内容、多遵循指令。但偏好是一个远比安全规则更宽泛的概念,它包括语气风格、信息密度、逻辑顺序、文化习惯,甚至短暂的时代审美。
在项目里理解这一点很重要:如果产品团队只把偏好当成“负面清单”,你收集数据时就会偏重标注哪些回答不能出现,而忽略哪些回答让人真正愿意读下去。真正的偏好学习要同时处理“好”与“坏”的相对关系,不是单纯做内容过滤。这个定位决定了数据采集策略、标注规范甚至损失函数设计,从一开始就不能跑偏。
6.2 误以为偏好数据越多越好
有一阵子业内流行“堆数据”,大家觉得只要偏好对数量够大,奖励模型自然会更准。实际情况是:每增加一批数据,都要花额外精力检查这批数据的分布是否与已有数据重叠。如果新增数据高度重复,模型只会进一步记住重复样本的偏好尾部,对长尾泛化没有帮助。
真正高的数据增益来自新增场景,而不是新增同场景样本。所以做数据扩展时,我会先对 prompt 做聚类,统计哪些场景还没覆盖到,优先补这些场景的偏好数据。数据量增加一倍但 prompt 多样性没有变化时,奖励模型验证集准确率往往几乎不动,这就是典型的虚假增长。
6.3 误以为奖励模型准确率越高越成功
奖励模型的准确率简单、直观,很容易被当成核心指标来盯。但它只能说明模型在两两比较任务上拟合得好,不等于它在 RL 阶段能够提供稳定指引。更准确的一种评估方式,是把训练好的策略模型生成的文本拿出来,让真人重新做一轮比较,看策略文本被选中的比例是否真的高于参考模型。
这个“最终人工评估”步骤有相当多的项目会省略,只看到 reward 曲线上升就宣布训练成功。我自己的体会是,奖励分数和人类满意度之间总会存在不同程度的偏移,尤其在 KL 约束不是很大的时候,策略会越来越擅长“骗”奖励模型,而偏离人类真实偏好。定期把策略文本匿名混在参考模型文本里做人工盲评,哪怕只是几十条样本,也能提前发现奖励破解的苗头。
再说一个我反复踩过之后才记住的教训:训练结束之后,不要只看奖励模型对最终策略文本的打分,还要回到原始偏好数据里挑几十个样本,看策略对这些样本的输出是否真的和人类 judged better 的方向一致。模型给高分不等于人觉得好,这一步看起来很笨,却是整个流程里最可靠的质量守门员。