如何训练奖励模型:train-llm-from-scratch的Bradley-Terry损失详解
如何训练奖励模型:train-llm-from-scratch的Bradley-Terry损失详解
【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch
train-llm-from-scratch是一个从零训练大语言模型(LLM)的开源项目,覆盖数据下载、预训练、SFT 到 RLHF 全流程。其中奖励模型(Reward Model)是 RLHF 的关键一环:它给模型回答打一个分数,分数越高代表越受人类偏好。本文将带你用Bradley-Terry 损失亲手训练一个奖励模型,并讲清每一步背后的原理。
奖励模型在整体流程中的位置
奖励模型不是孤立存在的,它处于整个后训练流水线的中心位置——上游接收 SFT 模型,下游为 PPO 提供奖励信号:
从图中可以看到:预训练基座 → SFT 指令微调之后,分三条支路:
- Reward Model(Bradley-Terry):训练出奖励模型,为 PPO 提供
reward signal; - DPO / ORPO / KTO:不依赖奖励模型的偏好对齐路线;
- GRPO / RLVR:使用可验证奖励的强化学习路线。
三条路最终都会汇入评估与聊天环节。
奖励模型长什么样:给 Transformer 加一个"打分头"
奖励模型的核心思路很简单(沿用 InstructGPT 的做法):
- 取 SFT 训练好的 Transformer 主干,扔掉语言模型头(lm_head);
- 在最上面接一个极小的标量奖励头:
Linear(n_embed → 1),把隐藏状态压成一个数; - 奖励值从最后一个真实 token的隐藏状态读取。
为什么是最后一个 token?因为注意力是因果的(causal),序列末尾的 token "看过"了整段内容,却不会注意到它右侧的填充(padding)——所以连注意力掩码都省了。
模型定义见 RewardModel:
源码中的注释写得很直白:a scalar reward head on top of a Transformer backbone(在 Transformer 主干上叠加标量奖励头)。
Bradley-Terry 损失详解:整个训练信号就一行代码
给定同一个 prompt 下的两个回答:chosen(偏好)和rejected(拒绝),奖励模型要学的事情只有一件——让 chosen 的分数高于 rejected。Bradley-Terry 模型把这变成如下损失:
L = -log σ(r_chosen - r_rejected)其中 σ 是 sigmoid 函数。直观理解:
| 情况 | r_chosen − r_rejected | 损失 L | 含义 |
|---|---|---|---|
| 分数差距很大(判对) | ≫ 0 | ≈ 0 | 几乎没有梯度,模型"满意" |
| 两个分数相同(瞎猜) | 0 | 0.693 | 随机水平,训练起点 |
| 分数判反了 | ≪ 0 | 很大 | 强梯度,逼模型纠正 |
整个损失函数只有三行(bradley_terry_loss):
def bradley_terry_loss(chosen_rewards, rejected_rewards): return -F.logsigmoid(chosen_rewards - rejected_rewards).mean()除了损失,还有两个配套指标(reward_train.py):
- preference_accuracy(偏好准确率):
r_chosen > r_rejected的样本占比,这是最值得盯的核心指标; - reward_margin(奖励间隔):平均
r_chosen − r_rejected,衡量模型是否还在有效区分好坏回答。
偏好数据从哪来:一条完整的数据管道
Bradley-Terry 训练需要{"prompt", "chosen", "rejected"}三元组。项目的数据管道把公共数据集(HH-RLHF、UltraFeedback)转换成标准 JSONL 格式:
对应脚本是 prepare_preference_data.py,它会:
- 分别从 HH-RLHF(人类标注的 helpful/harmless 偏好)和 UltraFeedback(LLM 评审的偏好对)拉取数据;
- 过滤空 prompt、chosen == rejected 的噪声样本;
- 输出训练集
preferences.jsonl和留出测试集preferences_test.jsonl(偏好准确率就在它上面测)。
批次构造逻辑在 preference_dataset.py:chosen 和 rejected 两侧各自经 chat template 渲染成 token,右侧填充(right-padding)到同长——这在因果注意力下是安全的。
实战:一条命令训练你的奖励模型
先确认已完成前序阶段(预训练 → SFT,产出sft.pt)和偏好数据,然后运行:
# 单卡 PYTHONPATH=. python scripts/train_reward.py # 双卡并行 PYTHONPATH=. torchrun --standalone --nproc_per_node=2 scripts/train_reward.py训练入口 train_reward.py 做了这些事:
- 从 SFT 检查点加载主干(
sft_ckpt),加上奖励头; - 每个 batch 把 chosen 与 rejected拼成 2B 条序列,一次前向拿到所有奖励值,再对半拆分;
- 计算 Bradley-Terry 损失 → 反向传播 → AdamW + 余弦学习率更新参数;
- 定期在测试集上评估偏好准确率并保存检查点。
关键超参在 configs/reward.json 中,默认配置一目了然:
| 参数 | 默认值 | 说明 |
|---|---|---|
batch_size | 8 | 每步 8 对偏好(实际过 16 条序列) |
lr | 1e-5 | 精细微调级别的学习率 |
max_len | 768 | 单条最大 token 长度 |
grad_clip | 1.0 | 梯度裁剪保稳定 |
想调参可以直接传命令行参数,例如--lr 1e-5 --max_len 768。
训练结果怎么看:三个数字背后的含义
训练日志会打印loss / train_acc / test_acc / margin,官方文档 docs/04_reward_model.md 给出了清晰的解读:
- loss:从 0.693(纯随机)起步,随分数差距拉大而下降;
- train_acc / test_acc:在干净的小数据上能到 1.0;在真实的、带噪声的HH-RLHF / UltraFeedback 上,0.65–0.75 才是正常水平——人类偏好本身就很嘈杂,别为 0.7 焦虑;
- margin:还在持续增长说明模型仍在有效区分好坏回答。
训练完成后,奖励模型保存为reward.pt,供下游 PPO 直接加载。
下一步:奖励模型如何被 PPO 消费
PPO 用奖励模型给每次采样(rollout)的回答打分,作为强化学习的目标信号:
在 train_ppo.py 中通过--reward_source rm即可切换为使用训练好的奖励模型(代码内部调用 load_reward_model 加载)。
📌小贴士:如果你的任务不需要通用偏好打分(比如数学题有标准答案),也可以跳过奖励模型,直接走 DPO 这条"免奖励模型"路线:
相关实现见 dpo.py 与 train_dpo.py。
总结:5 个文件看懂奖励模型训练
| 文件 | 职责 |
|---|---|
| src/post_training/reward_model.py | 奖励模型:SFT 主干 + 标量奖励头 |
| src/post_training/reward_train.py | Bradley-Terry 损失与评估指标 |
| data_loader/preference_dataset.py | 偏好数据批次构造(右侧填充) |
| scripts/train_reward.py | 训练入口:单卡 / 双卡 DDP |
| configs/reward.json | 默认超参配置 |
一句话回顾:给 SFT 模型加一个打分头 → 用 Bradley-Terry 损失在偏好对上训练 → 偏好准确率 0.65–0.75 即为合格 → 交给 PPO 当"老师"。跟着 docs/04_reward_model.md 与 POST_TRAINING.md,你就能完整复现这一经典 RLHF 环节。
【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
