如果你准备入门强化学习大概已经不止一次听过 RLHF 这个名字。但真正动手学的时候你会发现最劝退的不是“人类反馈”怎么采集也不是“奖励模型”怎么训练而是第三阶段那套强化学习更新为什么突然就冒出 PPO、GRPO、ReMax 这些让人眼花缭乱的算法。我的判断很明确RLHF 最值得先学透的不是 PPO 的工程细节而是它的数学骨架——REINFORCE 算法。PPO 做的事情本质上是在 REINFORCE 的基础上加裁剪、加优势估计、加 KL 约束GRPO 和 ReMax 这类最近在开源大模型对齐中重新走红的方案核心也还是“采样一句回复 → 算奖励 → 更新策略对数概率”。把 REINFORCE 的数学推导吃透再看任何策略梯度类算法你看到的都是同一个骨架。这一讲H09会从 RLHF 的整体流程讲起把 REINFORCE 的目标函数、梯度推导、baseline 和 reward-to-go 一步步拆开再给出两个可以直接运行的代码示例一个是最小化的 CartPole 强化学习训练脚本一个是 RLHF 风格的 REINFORCE 更新逻辑。读完你能回答三个问题REINFORCE 更新式子里每一项从哪来为什么可以减去一个 baseline 而不引入偏差RLHF 里的 KL 惩罚为什么能和奖励合并成一个标量。1. 基础概念RLHF 为什么需要强化学习先建立一个共同语境。RLHFReinforcement Learning from Human Feedback通常包含三个阶段SFT有监督微调用人工标注的高质量问答数据微调预训练模型让模型学会“像人一样开口说话”。奖励模型Reward ModelRM训练收集人类对同一问题的多个回答的偏好排序训练一个打分模型模拟“人类觉得哪个回答更好”。强化学习阶段把奖励模型当成环境给出的奖励信号用策略梯度算法更新大模型让模型生成的回答越来越符合人类偏好。很多人不理解SFT 阶段模型已经学会回答了为什么还要再做一轮强化学习关键区别在于SFT 是“模仿”强化学习是“优化”。SFT 的损失函数是交叉熵它只能让模型输出的概率分布逼近标注数据但没有办法直接优化“回答是否更有帮助、更安全、更受人类喜欢”这类不可微的目标。人类偏好本质上是一个函数它可能非常复杂甚至没法写出解析表达式。强化学习的思路是把人类偏好建模成一个奖励模型用奖励值作为标量反馈通过策略梯度去最大化期望奖励。用一句话概括这三个阶段的分工SFT 让模型学会生成RM 让模型知道好坏RL 让模型朝着好的方向持续更新。第三阶段最常用的优化器是 PPO但 PPO 的数学起点就是本文要讲的 REINFORCE。2. REINFORCE 算法的数学推导REINFORCE 是策略梯度Policy Gradient家族中最基础的算法由 Williams 在 1992 年提出。它的目标很直接调整策略网络的参数让“一条完整轨迹的累计回报”的期望最大化。2.1 目标函数定义一条轨迹trajectory为从初始状态到终止状态的一串状态、动作、奖励[ \tau (s_0, a_0, r_0, s_1, a_1, r_1, \dots, s_T) ]轨迹出现的概率由策略和环境共同决定[ P(\tau|\theta) p(s_0) \prod_{t0}^{T} \pi_\theta(a_t|s_t) p(s_{t1}|s_t, a_t) ]其中 (\pi_\theta(a_t|s_t)) 是策略网络输出的动作概率(p(s_{t1}|s_t, a_t)) 是环境转移概率。注意环境转移概率我们不知道也不需要知道这正是 REINFORCE 最巧妙的地方。定义轨迹回报为累计折扣回报[ R(\tau) \sum_{t0}^{T} \gamma^t r_t ]强化学习的目标就是最大化期望回报[ J(\theta) \mathbb{E}{\tau \sim \pi\theta}[R(\tau)] \sum_{\tau} P(\tau|\theta) R(\tau) ]2.2 梯度推导log 导数技巧对 (J(\theta)) 求梯度[ \nabla_\theta J(\theta) \sum_{\tau} R(\tau) \nabla_\theta P(\tau|\theta) ]这里不能直接展开 (\nabla_\theta P(\tau|\theta))因为环境转移概率 (p(s_{t1}|s_t, a_t)) 是未知的。但我们可以用 log 导数技巧[ \nabla_\theta P(\tau|\theta) P(\tau|\theta) \nabla_\theta \log P(\tau|\theta) ]代入后得到[ \nabla_\theta J(\theta) \sum_{\tau} P(\tau|\theta) R(\tau) \nabla_\theta \log P(\tau|\theta) ]把求和写成期望形式[ \nabla_\theta J(\theta) \mathbb{E}{\tau \sim \pi\theta}\left[ R(\tau) \nabla_\theta \log P(\tau|\theta) \right] ]现在展开 (\log P(\tau|\theta))[ \log P(\tau|\theta) \log p(s_0) \sum_{t0}^{T} \log \pi_\theta(a_t|s_t) \sum_{t0}^{T} \log p(s_{t1}|s_t, a_t) ]第一项和第三项与 (\theta) 无关求梯度后直接消失。于是[ \nabla_\theta \log P(\tau|\theta) \sum_{t0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t) ]最终得到 REINFORCE 的核心公式[ \nabla_\theta J(\theta) \mathbb{E}{\tau \sim \pi\theta}\left[ R(\tau) \sum_{t0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t) \right] ]这个式子就是整个策略梯度家族的基石。它的含义是如果一个动作序列最终带来了较高的回报就提高这条轨迹上所有动作的概率反过来如果回报较低就降低这些动作的概率。2.3 蒙特卡洛估计上面的期望无法精确计算因为我们需要对所有可能轨迹求和。实际中使用蒙特卡洛采样跑 (N) 条完整轨迹用样本均值近似[ \nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i1}^{N} R(\tau^{(i)}) \sum_{t0}^{T} \nabla_\theta \log \pi_\theta(a_t^{(i)}|s_t^{(i)}) ]这就是 REINFORCE 的更新方式完整跑完一条轨迹拿到总回报再统一更新一次策略。它是典型的回合制episodic算法不适用于无限长任务和在线单步更新。2.4 方差问题与 baseline直接使用上面的公式训练会极其不稳定。原因是蒙特卡洛采样的方差很大同一策略在不同轨迹上可能拿到差别很大的回报。一个经典改进是引入 baseline (b(s_t))它不依赖于当前动作[ \nabla_\theta J(\theta) \mathbb{E}{\tau}\left[ \sum{t0}^{T} \left(R(\tau) - b(s_t)\right) \nabla_\theta \log \pi_\theta(a_t|s_t) \right] ]为什么减去 baseline 不会引入偏差因为[ \mathbb{E}{a_t \sim \pi\theta}\left[ b(s_t) \nabla_\theta \log \pi_\theta(a_t|s_t) \right] b(s_t) \sum_{a_t} \pi_\theta(a_t|s_t) \nabla_\theta \log \pi_\theta(a_t|s_t) ]而[ \sum_{a_t} \pi_\theta(a_t|s_t) \nabla_\theta \log \pi_\theta(a_t|s_t) \sum_{a_t} \nabla_\theta \pi_\theta(a_t|s_t) \nabla_\theta \sum_{a_t} \pi_\theta(a_t|s_t) \nabla_\theta 1 0 ]所以再减去一个与动作无关的 baseline期望不变方差却可以显著下降。实践中常用状态价值函数 (V(s_t)) 作为 baseline或者像本文的 CartPole 示例一样直接用该回合回报的均值做简单 baseline。2.5 Reward-to-go更精细的信用分配另一个关键改进是 reward-to-go未来回报。直觉是某个动作只影响它之后获得的回报不应该为它发生之前的奖励负责。因此可以把轨迹总回报 (R(\tau)) 替换为从时刻 (t) 开始的累计折扣回报[ G_t \sum_{kt}^{T} \gamma^{k-t} r_k ]更新式变为[ \theta \leftarrow \theta \alpha \sum_{t0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t) (G_t - b(s_t)) ]这里的 ((G_t - b(s_t))) 就是“优势”的雏形。PPO 中复杂的 GAE 估计本质上就是对这个式子做更精细的方差-偏差权衡。3. RLHF 的优化目标奖励模型与 KL 约束现在回到 RLHF。强化学习阶段的输入是一批提示词 (x)策略 (\pi_\theta) 生成回答 (y)奖励模型 (r_\phi(x, y)) 给出一个分数。目标函数可以写成[ \max_\theta ; \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(\cdot|x)}\left[ r_\phi(x, y) \right] - \beta ; \mathbb{E}{x}\left[ \mathrm{KL}\left(\pi\theta(\cdot|x) | \pi_{\mathrm{ref}}(\cdot|x)\right) \right] ]其中 (\pi_{\mathrm{ref}}) 是参考策略通常是 SFT 阶段得到的模型。KL 项的作用是防止策略为了刷奖励而彻底偏离原始模型导致输出语无伦次或者模式坍缩。3.1 奖励模型的训练目标奖励模型通常用 Bradley-Terry 模型建模。对同一个提示词 (x)人类标注者认为回答 (y_w) 优于 (y_l)那么奖励模型应该给 (y_w) 打更高的分[ \mathcal{L}{\mathrm{RM}} -\mathbb{E}{(x, y_w, y_l)}\left[ \log \sigma\left(r_\phi(x, y_w) - r_\phi(x, y_l)\right) \right] ](\sigma) 是 sigmoid 函数。这个损失的含义很直观让被偏好的回答得分尽量比被拒绝的回答高。3.2 REINFORCE 视角下 RLHF 的梯度把 RLHF 目标函数套进 REINFORCE 的框架。对期望奖励项使用 log 导数技巧对 KL 项使用采样估计可以得到一个非常干净的梯度形式[ \nabla_\theta J(\theta) \mathbb{E}{x, y \sim \pi\theta}\left[ \left( r_\phi(x, y) - b - \beta \log \frac{\pi_\theta(y|x)}{\pi_{\mathrm{ref}}(y|x)} \right) \nabla_\theta \log \pi_\theta(y|x) \right] ]其中的关键推导在于 KL 项。对 KL 散度求梯度[ \nabla_\theta \mathrm{KL}(\pi_\theta | \pi_{\mathrm{ref}}) \mathbb{E}{y \sim \pi\theta}\left[ \log \frac{\pi_\theta(y|x)}{\pi_{\mathrm{ref}}(y|x)} \cdot \nabla_\theta \log \pi_\theta(y|x) \right] ]所以 KL 项的梯度贡献恰好就是让 (\log (\pi_\theta / \pi_{\mathrm{ref}})) 乘在策略对数概率的梯度上。合到一起RLHF 的 REINFORCE 更新就变成一个很简洁的形式把“奖励减去 baseline 再减去 KL 惩罚”当成一个标量乘以采样序列的对数概率的梯度。这也解释了为什么很多现代 RLHF 实现里KL 项不是单独算一个 loss而是直接加在奖励上[ \text{reward_total} r_\phi(x, y) - \beta \cdot \mathrm{kl} ]因为从策略梯度的角度看二者本来就该合并成一个系数再乘到 (\nabla_\theta \log \pi_\theta(y|x)) 上。3.3 从 REINFORCE 到 PPO、GRPO、ReMax既然 REINFORCE 这么简洁为什么 RLHF 早期还要用 PPO因为 REINFORCE 有两个严重问题方差大、更新步长不受控。算法baseline核心思想典型应用Vanilla REINFORCE常数或回合均值完整轨迹回报更新教学原型、小规模任务PPOGAE 价值网络clip 限制更新步长稳定策略更新经典 RLHFInstructGPT 等ReMax贪心解码奖励单样本去基线省掉价值网络大模型对齐实验GRPO组内平均奖励一组采样互作基线省掉价值网络DeepSeekMath、R1 类模型判断点PPO 的优势是稳定代价是要维护一个价值网络critic训练成本和实现复杂度都更高。最近 ReMax、GRPO 重新受到关注本质上是因为大模型生成的“一条完整回复”天然适合 REINFORCE 式更新——采样一次生成整句就是一个轨迹奖励模型给出一个整体分数不需要逐步奖励。理解了 REINFORCE这些算法对你来说就只是 baseline 的换法不同。4. 环境准备与最小 REINFORCE 实现先说本文代码的运行环境。示例使用 Python 和 PyTorch版本不需要严格卡死只要满足基本版本要求即可Python 3.9 或更高版本PyTorch 2.xgymnasiumOpenAI Gym 的新维护版本安装命令pip install torch gymnasium numpy如果你只跑 CartPole不需要 GPU。整个训练过程在 CPU 上几分钟内就能完成。本文的第一个示例在 CartPole 环境上实现最原始的 REINFORCE。CartPole 的任务是控制一根杆子保持平衡状态是 4 维向量位置、速度、角度、角速度动作只有两个向左、向右。这是一个标准的“一个回合几十步”的任务非常适合观察 REINFORCE 的行为。4.1 完整代码文件路径train_reinforce_cartpole.pyimport torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import gymnasium as gym import numpy as np torch.manual_seed(0) class PolicyNet(nn.Module): 两层 MLP 策略网络输出每个动作的概率 def __init__(self, obs_dim, hidden_dim, n_action): super().__init__() self.fc1 nn.Linear(obs_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, n_action) def forward(self, obs): x F.relu(self.fc1(obs)) logits self.fc2(x) return logits def act(self, obs): logits self.forward(obs) probs F.softmax(logits, dim-1) dist torch.distributions.Categorical(probs) action dist.sample() return action.item(), dist.log_prob(action) def reinforce(env, policy, optimizer, episodes500, gamma0.99, use_baselineTrue): reward_history [] for ep in range(episodes): obs, _ env.reset() log_probs [] rewards [] done False # 1. 采样一条完整轨迹 while not done: obs_t torch.as_tensor(obs, dtypetorch.float32) action, log_prob policy.act(obs_t) obs, reward, terminated, truncated, _ env.step(action) done terminated or truncated log_probs.append(log_prob) rewards.append(reward) # 2. 计算 reward-to-go returns [] G 0.0 for r in reversed(rewards): G r gamma * G returns.insert(0, G) returns torch.tensor(returns, dtypetorch.float32) # 3. baseline用整回合回报均值近似状态无关基线 if use_baseline: returns returns - returns.mean() # 4. 策略梯度损失-(G - b) * log_prob policy_loss [] for log_prob, G in zip(log_probs, returns): policy_loss.append(-log_prob * G) loss torch.stack(policy_loss).sum() optimizer.zero_grad() loss.backward() optimizer.step() reward_history.append(sum(rewards)) if (ep 1) % 50 0: avg np.mean(reward_history[-50:]) print(fEpisode {ep 1}, avg reward (last 50): {avg:.1f}) return reward_history if __name__ __main__: env gym.make(CartPole-v1) policy PolicyNet(obs_dim4, hidden_dim64, n_action2) optimizer optim.Adam(policy.parameters(), lr3e-3) history reinforce(env, policy, optimizer, episodes500) env.close()4.2 代码关键逻辑解释这段代码体现了 REINFORCE 的四个核心步骤采样轨迹用当前策略跑完一整局保存每一步的log_prob和reward。这里的log_prob是带梯度计算图的张量是后续反向传播的入口。计算 reward-to-go从回合末尾往回递推G r gamma * G每条轨迹上的每个动作都拿到自己未来回报的估计。减去 baseline用整回合奖励的均值作为 baseline把回报中心化降低方差。更新策略loss -sum(log_prob * (G - b))。梯度下降最小化这个 loss等价于梯度上升最大化期望回报。容易踩坑的地方是returns不能参与梯度计算它只是作为“标量系数”乘在log_prob上。上面的代码里returns是从 Python list 转换来的普通张量requires_grad为 False天然满足这个条件。运行命令python train_reinforce_cartpole.py5. RLHF 风格的 REINFORCE 更新实现CartPole 示例让我们理解了策略梯度本身。现在把同样思想迁移到 RLHF提示词是“起点”模型生成的完整回复是“一条轨迹”奖励模型的打分是“回报”。区别在于生成过程是自回归的每一步输出一个 token策略梯度的对数概率要对整条回复的所有 token 求和。5.1 奖励模型训练示例先给出奖励模型训练的简化代码。假设你已经有一个可以输出隐藏层特征的文本模型比如微调过的 transformer奖励模型只是在它上面加一个线性打分头文件路径train_reward_model.pyimport torch import torch.nn as nn class RewardModel(nn.Module): 在基础模型上叠加一个奖励打分头输出标量分数 def __init__(self, base_model, hidden_dim): super().__init__() self.base_model base_model # 输入 input_ids输出序列特征 self.reward_head nn.Linear(hidden_dim, 1) def forward(self, input_ids, attention_maskNone): features self.base_model(input_ids, attention_maskattention_mask) # 取最后一个非 padding token 的特征作为整句表示 last_hidden features.last_hidden_state seq_len last_hidden.shape[1] index attention_mask.sum(dim1) - 1 if attention_mask is not None else seq_len - 1 pooled last_hidden[torch.arange(last_hidden.shape[0]), index] reward self.reward_head(pooled) return reward.squeeze(-1) def bradley_terry_loss(reward_w, reward_l): 被偏好回答得分 reward_w 应高于被拒绝回答 reward_l return -torch.log(torch.sigmoid(reward_w - reward_l)).mean()这个损失函数对应第 3 节中的 Bradley-Terry 目标。实际训练中你需要构造形如(prompt, chosen_response, rejected_response)的数据集同一个提示词下成对比较。5.2 RLHF 阶段REINFORCE 策略更新下面是 RLHF 第三阶段的核心更新逻辑。为了让代码可读generate和compute_sequence_logprob用伪代码表示实际接入时替换成你所用框架的生成接口和 logprob 计算接口即可。重点看更新式的结构文件路径rlhf_reinforce_update.pyimport torch def reinforce_rlhf_update(policy, ref_policy, reward_model, tokenizer, prompt, optimizer, device, beta0.05, baseline0.0, max_new_tokens128): 一次 REINFORCE 风格的 RLHF 更新。 policy: 待训练的策略模型 ref_policy: 参考模型SFT 模型参数冻结 reward_model: 已训练好的奖励模型参数冻结 inputs tokenizer(prompt, return_tensorspt).to(device) # 1. 从当前策略采样一条完整回复 with torch.no_grad(): output_ids policy.generate( inputs.input_ids, max_new_tokensmax_new_tokens, do_sampleTrue, top_p0.9, pad_token_idtokenizer.pad_token_id, ) # 2. 计算采样序列在当前策略下的对数概率需要梯度 log_prob policy.compute_sequence_logprob(output_ids) # log_prob 形状[batch, seq_len] 或 [batch]取决于实现 # 3. 奖励模型打分 with torch.no_grad(): reward reward_model(output_ids, attention_mask(output_ids ! tokenizer.pad_token_id)).item() # 4. 计算 KL 惩罚注意 ref_log_prob 和 log_prob.detach() 都作为常数处理 with torch.no_grad(): ref_log_prob ref_policy.compute_sequence_logprob(output_ids) kl (log_prob.detach() - ref_log_prob).item() # 5. REINFORCE 更新 # 梯度只流经 log_prob(reward - baseline - beta * kl) 是标量系数 rlhf_reward reward - baseline - beta * kl loss -rlhf_reward * log_prob optimizer.zero_grad() loss.backward() optimizer.step() return reward, kl5.3 为什么 KL 项要这样实现这是 RLHF 的 REINFORCE 实现里最容易出错的地方。根据第 3 节的推导正确的梯度形式是[ \left( r_\phi(x, y) - b - \beta \log \frac{\pi_\theta(y|x)}{\pi_{\mathrm{ref}}(y|x)} \right) \nabla_\theta \log \pi_\theta(y|x) ]也就是说KL 项应该作为“奖励系数”的一部分而不是单独再算一个 loss 做反向传播。如果写成loss -(reward - baseline) * log_prob beta * kl就错了。因为这样对 (kl) 求梯度会得到 (\beta \nabla_\theta \log \pi_\theta(y|x))而正确梯度应该是 (\beta \cdot kl \cdot \nabla_\theta \log \pi_\theta(y|x))。两者相差一个 (kl) 因子。正确做法是kl用log_prob.detach()和ref_log_prob计算把 KL 值当作常数标量loss中唯一需要梯度的量是log_prob。这也正是 GRPO、ReMax 等实现的共同点奖励是标量梯度只沿着采样序列的对数概率传播。6. 运行结果与效果验证6.1 CartPole 训练结果运行train_reinforce_cartpole.py正常情况下你会看到类似输出Episode 50, avg reward (last 50): 22.4 Episode 100, avg reward (last 50): 46.8 Episode 150, avg reward (last 50): 89.5 Episode 200, avg reward (last 50): 148.2 Episode 250, avg reward (last 50): 196.0 Episode 300, avg reward (last 50): 230.4 ...CartPole-v1 的单回合上限是 500 分。不同随机种子下曲线会有波动但整体趋势应该是前 50 到 100 回合奖励在 20 到 60 之间徘徊之后逐步抬升。如果训练了 400 回合仍然稳定在 30 分以下优先检查三点学习率是否合适。3e-3用 Adam 通常可行调大容易震荡调小训练变慢。baseline 是否生效。可以尝试把use_baselineFalse对比一下方差差异。gamma是否合理。CartPole 的 reward 是每一步 1gamma0.99意味着模型会关注长期平衡而不是只盯着当前步。6.2 如何验证 RLHF 更新是否正常RLHF 的训练不像 CartPole 有明确的分数上限需要监控三个指标奖励模型得分应该整体上升但不会无限上升因为 KL 约束会拉住它。KL 散度逐渐增大说明策略开始偏离 SFT 模型如果 KL 增长过快说明beta太小。生成的文本质量这是最重要的验证。每个 checkpoint 保存后用固定几个提示词做人工对比看生成结果是否变得更有帮助、同时没有出现重复和语无伦次。一个简单的实验建议先用 10 到 20 条提示词跑 100 到 200 步更新保存 checkpoint对比更新前后的生成效果。如果奖励上升但文本质量明显变差几乎可以肯定是 KL 系数不够或者奖励模型过拟合。7. 常见问题与排查思路问题现象可能原因排查方式解决方案CartPole 训练很久 reward 不上升学习率不合适baseline 未生效打印每个 episode 的回报和 loss调整学习率开启 baseline 或使用 reward-to-go策略过早收敛到单一动作探索不足熵过低打印动作分布的熵调低学习率加入熵正则项loss 出现 NaN梯度爆炸检查梯度范数加梯度裁剪clip_grad_norm_降低学习率RLHF 阶段奖励上升但文本变差KL 系数过小策略偏离 SFT 模型太远观察 KL 散度曲线增大beta必要时回退 checkpoint奖励模型无法区分好坏训练数据太少或过拟合在验证集上计算准确率增加偏好标注数据保留验证集Early Stopping