如果你在 2023 年上半年刷过 GitHub Trending大概率见过一个叫higgsfield/RLHF的仓库。它没有花哨的 README 动画也没有大厂背书却在很短的时间里成了无数人学习 RLHF基于人类反馈的强化学习的第一个完整开源实现。我至今记得第一次把这个仓库跑通时的感受原来令人头大的人类偏好 → 奖励模型 → 强化学习这条链路真的可以用不到一千行代码从零搭起来。这篇博文就围绕 higgsfield 这个项目来写我的目的是把它拆开、揉碎讲清楚仓库里的核心链路、关键代码逻辑、以及我当年复现时踩过的各种坑。适合以下人群已经会使用 transformers但对强化学习了解不深的人想知道 RLHF 的论文到代码到底怎么落地的人以及想评估 RLHF 训练成本、打算自己做微调实验的开发者。看完之后你能获得的不只是会跑脚本而是一套可以迁移到你自己的任务上的方法论。1. 一个 GitHub 仓库为什么能成为 RLHF 的教科书1.1 它解决的痛点是有概念没实现CLAUDE 2023 年那阵RLHF 在社交媒体和论文里已经非常热但真实情况是大多数人只知道PPO 微调这几个字不知道具体的损失函数怎么写、奖励模型怎么训练、数据从哪来、生成阶段要不要梯度。higgsfield/RLHF恰好把这个缺口补上了。它的定位不是生产级框架而是可读性极强的教学实现。仓库基于 PyTorch 和 Hugging Face 的 transformers用文本摘要这个具体任务把 RLHF 的每一个环节都落地了。从奖励模型的输入输出到 PPO 里的概率比计算再到 KL 散度如何钳制策略偏移都能在代码里找到对应位置。1.2 为什么是摘要任务选择摘要任务不是偶然。OpenAI 研究 RLHF 的原始论文之一就是用偏好数据微调摘要模型后来一系列工作也都沿用了这个设定。摘要任务有两个优势第一人类标注者可以快速比较两段摘要的好坏偏好标注成本低第二摘要质量可以通过 ROUGE 这类自动化指标做粗略校验方便我们判断奖励模型学到的打分是否合理。这个选择对学习者也极其友好。摘要任务不需要复杂的多轮对话模板不需要考虑系统提示词一条文章 摘要就能作为一个训练样本。最高兴的一点是单条样本的处理逻辑简单意味着如果你想把仓库里的方法迁移到自己的任务上改动成本非常低。1.3 仓库的三阶段布局仓库把整个 RLHF 流程拆成了三个清晰的部分这个拆法后来成了很多教学项目的模板阶段输入输出核心损失/目标SFT监督微调文章-摘要对能做摘要的基座模型交叉熵损失RM奖励模型文章多个摘要给摘要打分的模型偏好排序损失PPO策略优化文章策略模型生成的摘要PPO 目标 KL 惩罚我会按照这个顺序在后面每一节里把每一阶段的关键实现细节、以及为什么这样设计讲清楚。2. 从人类偏好到模型参数更新的最小闭环2.1 SFT先让模型学会像做任务的样子很多初学者容易犯一个错误直接从预训练模型开始做 RLHF。这是不现实的。基座模型虽然能生成文字但它的生成分布和回答特定任务差得太远哪怕有奖励模型的引导你也很难在一个完全没学过摘要的模型上凭空做出高质量结果。higgsfield 的做法是先做标准的监督微调。用 CNN/DailyMail 这类新闻摘要数据集把文章和参考摘要拼成文本序列然后用常见的语言模型交叉熵损失训练。这个阶段非常朴素本质上就是一次普通的 sequence-to-sequence 微调。学习率建议在 5e-5 左右训练 1 到 3 个 epoch不要贪多。SFT 模型过拟合会压缩后续 PPO 的探索空间。这里我想提醒一个很容易忽略的点SFT 模型不仅仅是一个能用的基座它还是 PPO 阶段参考模型的权重来源。后面计算 KL 散度时我们需要一个初始策略作为基准而 SFT 模型就是这个基准。所以这个模型单独存一份不要覆盖后面用得上。2.2 RM 奖励模型把人类的更好变成可优化的标量奖励模型的直觉很简单我们想知道一段摘要到底好不好。但人工打分不能实时接入训练循环所以训练一个模型去预测人类偏好。仓库沿用了 OpenAI 摘要论文里的思路给定一篇文章和两段摘要人类标注者会选择更好的一段奖励模型要学习输出一个标量分数让被选中的摘要得分高于被放弃的摘要。训练损失通常用 pairwise margin loss 或 logistic 式的偏好排序损失基本思想是正样本的分数至少要比负样本高出一个 margin。奖励模型通常在 SFT 模型的基础上加一个回归头把最后一层 hidden state 映射成一个标量。这里的陷阱是标注数据的长尾分布很重人类对摘要质量的偏好往往不是线性的。实操中可以在训练奖励模型时对分数做归一化并把 margin 作为一个超参数常见取值在 1.0 附近。2.3 PPO让策略模型读懂奖励模型的反馈有了奖励模型就有了对策略模型输出打分的工具。PPO 阶段的完整闭环是策略模型读入新闻文章生成一段摘要奖励模型给这段摘要打分用这个分数减去策略模型与参考模型之间的 KL 散度得到最终奖励用 PPO 目标函数更新策略模型。为什么要减 KL 散度因为奖励模型并不是完美的。如果只让策略模型最大化奖励模型给的分模型很快就会钻空子生成人类看起来不通顺、但有某种统计规律能骗过高分的文本。KL 惩罚的作用是让策略更新不要偏离 SFT 参考模型太远——毕竟 SFT 模型至少还是说人话的。这一步在代码里需要特别注意策略模型在生成摘要时要同时记录当前策略的对数概率logp和参考模型的对数概率这两个值在后续 PPO 更新里都要用到。很多第一次看代码的人会忽略这两个 logp 的保存位置导致后面训练直接报 shape 不匹配。3. PPO 实现细节最容易看走眼的五个技术节点3.1 logp 怎么算对 response token 求和不是对 prompt 求和语言模型里一段文本的对数概率是序列中每个 token 对数概率的累加。但 RLHF 的 PPO 里我们真正关心的是模型生成的摘要这一段的概率而不是 prompt 部分的概率。higgsfield 的代码里这一块很简单生成完成后把 prompt 长度截掉只保留 response 部分的 logits然后计算交叉熵损失得到逐 token 的 logp最后求和。我见过不少人在这一步踩坑用全序列的 logp更新时把 prompt 也纳入优化目标导致模型开始改动 prompt训练指标看起来很好生成的摘要却一塌糊涂。正确处理方式是只对 response 部分做 masked sum。伪代码如下def compute_logp(model, input_ids, response_start_idx): outputs model(input_ids, labelsinput_ids) logits outputs.logits[:, :-1, :] # 预测下一个 token token_logp log_softmax(logits, dim-1) response_logp token_logp.gather( dim-1, indexinput_ids[:, 1:].unsqueeze(-1) ).squeeze(-1) # 只保留 response 部分mask 掉 prompt response_logp response_logp * response_mask(response_start_idx) return response_logp.sum(dim-1)3.2 奖励和 KL 散度的组合时机在 higgsfield 的实现里KL 惩罚用的是采样式估计。每次用当前策略生成文本后我们用同一个输入重新跑一次参考模型拿到参考模型在每个生成 token 上的对数概率。然后 KL 近似为当前策略 logp 减去参考模型 logp 的期望值。这个近似很好用因为它不需要分布解析解只需要两次前向计算。但注意参考模型的前向计算不需要梯度所以要放在torch.no_grad()下。我记得踩过一次很蠢的坑就是参考模型的 logits 没有 detachKL 直接回传到了参考模型训练时显存爆炸一度以为是 batch size 太大了。最终奖励的计算方式是final_reward reward_score - beta * kl_estimate其中 beta 是 KL 惩罚系数仓库里给过 0.02 这类经验值。我建议做实验时先把 beta 设大一点看看 KL 曲线是否平稳再逐步降下来。没有这个约束的 PPO几乎必然发散。3.3 优势估计简化版也可以很稳OpenAI 的原始论文用的是 Generalized Advantage EstimationGAE但 higgsfield 做摘要任务时因为每个回合就生成一段摘要轨迹非常短所以一个常见的简化做法是直接用整段回报的均值作为优势。你会在代码里看到一个更轻量的实现advantage final_reward - value其中 value 来自一个价值网络。这里价值网络的输出至关重要。价值网络通常是在 SFT 模型的 hidden state 上加一个线性层预测当前状态下的期望回报。训练 value 网络时label 是实际得到的 final_reward损失函数是 MSE。这个 value 网络并不参与生成只参与 PPO 的优势计算。很多复现者把它当成配角结果优势估计方差巨大训练根本走不稳。3.4 PPO clip保护策略不被一步推太远PPO 的核心更新公式里最重要的是 importance ratioratio exp(current_logp - old_logp)然后这个 ratio 会乘上优势再和 clip 后的版本取最小值。higgsfield 的代码里clip 范围和常见实现一致通常取 0.2ratio torch.exp(current_logp - old_logp).sum(-1) loss_policy -torch.min( ratio * advantage, torch.clamp(ratio, 1.0 - clip_eps, 1.0 clip_eps) * advantage ).mean()这个小式子有两层意思。第一它让策略更新倾向于提高优势为正的 token 的概率第二当 ratio 偏离 1 太远时更新被截断防止一次 step 就把模型毁掉。再加上 KL 惩罚的双重保护策略会在一个安全区域内缓慢移动。我把这个设计理解成系好安全带开车你可以踩油门但不能直接起飞。3.5 训练稳定性从 fp32 开始奖励要归一化最后想强调的代码细节是数值稳定性。最开始复现时我直接开了 AMP 的 fp16结果 reward 曲线一路乱跳。后来发现这个项目的损失计算里KL 项和 value loss 都容易在 fp16 下发生数值溢出。我的建议是先把整个流程在 fp32 下跑通再考虑混合精度优化。另外奖励模型的原始打分可能在一个很大的值域里波动比如 0 到 10或者更大的绝对值直接喂给 PPO 会让 ratio 和 advantage 的尺度失衡。实操里要对 reward 做标准化最常用的办法是减均值除标准差也就是 running normalization。这一步看着不起眼却往往决定了 PPO 是收敛还是发散。4. 我按 higgsfield 项目复现时踩过的真实坑4.1 数据集获取与本地缓存摘要任务的标准数据集来自 Hugging Face 的openai/summarize_from_feedback但这个数据集的加载有时候会卡在下载环节。我自己遇到最多的问题是网络不稳定导致数据集拉取一半就中断。一个很实用的替代方案是先用 CNN/DailyMail 的摘要子集来做 SFT奖励模型阶段再临时构造一部分对比样本。我发现对于学习目的奖励模型不需要做得非常准只要有稳定的梯度信号就行。如果实在连不上可以把数据集提前下载到本地然后用datasets.load_from_disk加载避免每次运行都重新下载。4.2 显存不够时的基本策略很多人一上来就想用 gpt2-large 甚至更大的模型。我个人的建议是第一遍用最小的gpt2跑通链路后再逐步换大。记得我在第一阶段只用了约 12GB 显存的中等显卡搭配 gradient checkpointing 和 4 的 accumulation steps也能稳定跑完整个 PPO 流程。一个特别有用的技巧是生成阶段的 batch size 可以比训练阶段小。因为生成阶段需要保存 logp、reward、value 等一堆中间量显存占用反而更高。把生成和更新拆成两个不同 batch size 的 step能明显降低显存峰值。4.3 reward 曲线疯涨又崩掉训练过程中最典型的现象是reward 先快速上升然后突然直线下降。我调了两周总结出两个主要原因。第一个原因是 KL 惩罚系数太小。模型发现了奖励模型的漏洞开始疯狂生成高分但语法混乱的摘要KL 爆炸然后 reward 也崩了。解决方法是增大 beta并且时不时看 KL 曲线的绝对值。第二个原因是奖励模型本身过拟合。摘要任务的数据量并不大奖励模型训练几个 epoch 后开始记忆训练集打分失去泛化能力。遇到这种情况早点固定奖励模型不要边训练策略边更新奖励模型。4.4 logp 计算没有 detach 的隐蔽 bug这是一个非常隐蔽的坑。如果你在 PPO 更新时用计算图里的 logp 去算 ratio然后把 ratio 和 advantage 相乘但 advantage 是来自 reward 模型的常数还好一旦你误把 old_logp 保存在计算图里梯度会穿过旧路径回流造成不可预测的更新。最直观的表现是损失数值偶尔变得很大并且训练速度越来越慢。我排查时发现records列表里保存的不是纯 tensor 数值而是带 autograd 的计算图。修正方式是在保存日志数据时统一调用.detach().cpu().float()确保 PPO 更新前的所有中间量都不带梯度。4.5 seed 不固定结果堪比开盲盒RLHF 训练的随机性来源特别多数据加载顺序、生成时的采样随机性、梯度 dropout、多卡并行时的 order。如果不固定 seed每次实验的结果差异会大到让你怀疑代码写错了。我在所有实验脚本的入口都固定了三层 seedPython 的random.seed、NumPy 的np.random.seed、PyTorch 的torch.manual_seed。另外生成摘要时的top_p和temperature也要固定因为我发现这两个采样参数对 PPO 稳定性的影响比很多人以为的要大得多。temperature 过高会让 logp 估计方差变大过低又会让策略失去探索能力常见取 0.7 到 1.0 之间。5. higgsfield 对今天 RLHF 工具链的启示与迁移思路5.1 和现代封装库 TRL 的对比现在再回头去看 higgsfield会觉得它糙因为它什么事情都要自己手动处理。但从学习的角度看这种糙恰恰是最有价值的。Hugging Face 的 TRL 把 SFT、RM、PPO 全部封装成了 Trainer你只需要准备数据、传几个参数就能一键开跑。对比之下higgsfield 的优势是透明TRL 的优势是高效。我的建议非常明确如果你想真正理解 RLHF先 spend 一两周把一个 handcrafted 实现跑明白如果你要在生产环境里微调某个真实模型直接用 TRL 这类成熟框架别自己造轮子。做产品要的是稳定做学习要的是透明这两者对工具的需求完全不同。5.2 从仓库学到的迁移方法论higgsfield 的三阶段流程不是只属于摘要任务。现实中它完全适用于以下场景有一批生成结果可以由人工或规则打分把打分变成奖励信号再用 PPO 优化生成策略。比如推荐理由生成、电商标题改写、甚至代码注释生成都可以按这个套路来。最关键的一点是在你设计奖励函数的时候要像设计老师一样思考而不是像设计裁判一样思考。老师会提示模型走对方向而裁判只是给出一个冷冰冰的分数。KL 惩罚就是那个在耳边提醒模型别走太远的老师。如果你在你的任务里发现策略很容易崩溃多半是老师不够好而不是模型不够聪明。5.3 继续往下走的路径复现完 higgsfield 之后下一个自然的节点是去读 OpenAI 那几篇关于 RLHF 的论文你会发现论文里的公式在代码里都有了具体的形状。再往后可以尝试用更大的模型、更长的上下文、多轮对话做实验看看 KL 惩罚和奖励尺度会怎样漂移。如果你在找工作或做研究能流畅地把rlhf 的三阶段、PPO 的 clip 目标、KL 散度为什么必要讲给别人听比列出一堆工具名要有说服力得多。我见过不少候选人在简历里写熟悉 RLHF但问到 logp 怎么计算时答不上来。真正把 higgsfield 这类从零实现的代码通读一遍之后你对原理的理解深度会完全不同。最后分享一个我现在的习惯每当我要在新的任务上尝试 RLHF我都会先看一眼 higgsfield 这个仓库的目录结构问自己三个问题——我的 SFT 从哪来我的奖励模型怎么打分我的 PPO 里 KL 惩罚和 clip 哪个更重要这三个问题答清楚实验基本已经成功了一半。