资讯动态

强化学习策略梯度图解:REINFORCE推导、实现与调参避坑

发布时间:2026/10/21 5:34:42 来源:尧图企业网站定制
策略梯度Policy Gradients大概是强化学习里最值得花时间搞明白的一块。很多同学一上来就被 REINFORCE 的推导公式劝退觉得满屏的期望符号太抽象转头去跑现成库结果连网络输出 log_prob 为什么乘上 reward 就能提升策略都没想通调参全靠撞运气。这篇内容我用图解的思路把策略梯度背后的直觉、公式里每个符号在干什么、以及实操中最容易踩的坑一次性讲清楚。适合刚接触强化学习、能跑 DQN 但还没打通策略梯度这条线的读者也适合用过策略梯度但一直没细究推导细节的从业者。1. 先搞清楚策略梯度到底在解决什么问题1.1 价值派和策略派的分野强化学习里一直有两派人。价值派Value-based的思路是先估计在某个状态下做某个动作有多好也就是 Q 值然后根据 Q 值选动作。DQN 就是典型代表它把 Q 网络拟合出来决策时直接取 argmax。这套思路在离散动作空间里很好使但在连续动作空间里就尴尬了——你总不能对无穷多个连续动作挨个取 argmax 吧策略派Policy-based换了个思路不绕弯子直接学习一个策略输入状态输出动作的概率分布离散场景或者动作分布的参数连续场景。策略梯度就是这一派的训练方法。它让智能体按照当前策略采样一批轨迹然后用采样结果去修正策略参数让好的动作更大概率出现坏的动作概率下降。我见过不少初学者问既然有了价值派为什么还要策略派最直观的答案有两个。第一个是连续控制问题比如机器人关节力矩输出动作空间是连续的没法简单枚举第二个是随机策略的表达能力很多环境里最优策略本身就是随机的比如扑克游戏里诈唬这个动作必须带概率确定性的贪心策略反而吃亏。这两点决定了策略梯度在实践中的地位。1.2 策略梯度想画的那张地图从图解的角度理解策略梯度算法其实在画一张动作概率的地形图。横轴是策略参数空间纵轴是期望回报。目标很朴素找到参数山峰的最高点。爬山最直接的办法就是算梯度沿着上升方向迈一步再算再迈直到收敛。这张图给我最重要的启发是策略梯度本质上是带噪声的爬山过程。因为期望回报没法精确计算我们只能用采样轨迹来估计梯度。采样天然有方差所以每一步的方向其实带有噪声并不精确。理解了这一点后面很多调参问题的答案就自己浮现了为什么学习率要调小一点因为噪声大步子太大会跨过山头为什么要做 reward normalization因为奖励尺度不同梯度步长会被无量纲放大导致训练震荡。你也可以把策略梯度的迭代过程想象成盲人爬山。盲人站在山坡上没法看到全局地形只能通过手杖在周围戳几个点来判断哪里在往上。每一次采样轨迹就是一次手杖试探用的是局部信息走一步看一步。这个类比虽然粗糙但能解释为什么策略梯度方法通常需要更多样本——局部噪声意味着你要多试探几次才能找到可靠的方向。2. 从图解推导看策略梯度的核心直觉2.1 那个神秘期望到底在算什么策略梯度最常被贴出来的公式长这样[ \nabla_\theta J(\theta) \mathbb{E}{\tau \sim \pi\theta}\left[ \sum_{t0}^{T} \nabla_\theta \log \pi_\theta(a_t | s_t) \cdot R_t \right] ]我第一次看到这个公式时完全懵了尤其是那个 (\log) 到底从哪冒出来的。这里我拆开讲。我们先定义目标函数 (J(\theta))它是在策略 (\pi_\theta) 下整个轨迹回报的期望。对期望求梯度问题就来了期望里包含了策略分布而分布本身就依赖参数 (\theta)不能直接把梯度塞进期望里。这时候需要一点老技巧——log-derivative trick。因为 (\nabla_\theta \pi_\theta \pi_\theta \cdot \nabla_\theta \log \pi_\theta)把 (\pi_\theta) 当作权重提出来期望的梯度就变成了带 log 项的期望。从图解视角看这个公式真正在做的操作是沿轨迹逐时刻计算当前动作概率的梯度方向用整条轨迹的回报作为权重对梯度方向做加权平均。回报高的轨迹其动作梯度会被放大策略会朝让这些动作更容易出现的参数方向挪动回报低的轨迹权重小对参数更新影响弱。换句话说策略梯度的核心不是教智能体做对的动作而是给经历过的动作按回报高低打分然后调概率。2.2 为什么一定要出现 log我见过有的博客直接把 log 当成纯粹的数学技巧一笔带过但我在实践中觉得它对数值稳定性也有影响。(\log \pi_\theta(a|s)) 的梯度其实等价于在状态 (s) 下让动作 (a) 概率上升最快的方向具体形式因策略分布的参数化方式而异。拿最常见的 categorical 策略举例子网络最后一层输出离散动作的 logits经过 softmax 转成概率。此时 (\log \pi_\theta(a|s)) 对 logits 的梯度就是 one-hot 编码减去该动作的概率。这个形式非常优雅如果某个动作概率是 0.2它的 log 梯度方向就是让这个动作的 logit 上升 0.8、让其他动作 logit 下降 0.2从而整体把概率质量往这个动作挤。如果动作概率是 0.8那梯度方向就反过来把概率拉低。如果是高斯策略连续动作网络输出均值 (\mu) 和标准差 (\sigma)(\log \pi_\theta(a|s)) 对均值的梯度就是 ((a-\mu)/\sigma^2)。这也很符合直觉实际采出的动作越偏离均值梯度方向就越强地把均值往实际动作方向拉。注意这里有个隐含的坑(\sigma) 出现在分母上如果网络预测的 (\sigma) 特别小梯度会被异常放大后面我会详细讲。2.3 基线Baseline为什么不是可选项REINFORCE 类的算法里几乎一定会引入基线 (b(s))把公式改写成[ \nabla_\theta J(\theta) \mathbb{E}\left[ \sum_{t0}^{T} \nabla_\theta \log \pi_\theta(a_t | s_t) \cdot (R_t - b(s_t)) \right] ]很多教程会说减基线是为了降低方差然后就没下文了。我来说得更形象一点设想一个场景所有轨迹的回报都在 80-100 分之间只是有的动作导致 80 分有的导致 100 分。如果直接用原始回报所有动作的权重都为正且接近梯度更新会把所有经历过的动作概率一起推高等于一荣俱荣分辨不出哪些动作真正值得学。减去基线就是改变零点。如果基线设在 90 分那么 80 分的轨迹权重变成负的它的动作概率会被压低100 分的轨迹权重为正概率被抬高。这样梯度才真正战胜了运气因素——环境全局给的分数高低被你剔除了你关心的是这个动作相对平均水平到底好还是坏。理论上最优基线是该状态下期望回报的某个估计实际中最便宜的做法就是用一个和状态相关的价值网络 (V(s)) 做基线或者退而求其次用一个跨轨迹的常数基线。我个人的经验是基线带来的方差缩减效果非常明显没有基线的训练跑起来像过山车。你用同一个随机种子跑有基线和没基线的 REINFORCE损失曲线能差出一整个量级的平滑度。3. 手写一个最小 Policy Gradient 实现3.1 网络结构怎么搭我以 PyTorch 风格的伪代码来写一个最小实现对应经典 CartPole 环境。策略网络就是一个两层的 MLP输入是状态向量输出是左右两个动作的 logits。import torch import torch.nn as nn class PolicyNet(nn.Module): def __init__(self, obs_dim, hidden_dim, n_actions): super().__init__() self.fc nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, n_actions) ) def forward(self, obs): logits self.fc(obs) return logits def get_action(self, obs): logits self.forward(obs) dist torch.distributions.Categorical(logitslogits) action dist.sample() return action.item(), dist.log_prob(action)这里有个细节值得新手注意不要在 forward 里直接输出 softmax 概率而是输出 logits然后在需要概率的地方用torch.distributions.Categorical去构建分布。这样做的原因很实际Categorical 内部会把 logits 过 softmax并且log_prob的计算因为数值稳定性的关系会比你先 softmax 再取 log 更稳。手工写F.log_softmax也行但直接用分布对象最省事也避免很多极端数值下溢出。你可能会好奇为什么隐藏单元数不用太大。CartPole 这种玩具环境状态只有 4 维动作只有 2 个策略本身非常简单网络只要不太小都能收敛。但如果你把它当模板用到更复杂环境至少要把隐藏维度提到 64 甚至 128否则表达能力不够策略学不进去。3.2 核心训练循环拆解REINFORCE 的训练循环最精简的版本长这样def train_one_episode(policy_net, optimizer, gamma0.99): obs, _ env.reset() done False log_probs [] rewards [] while not done: obs_t torch.FloatTensor(obs) action, log_prob policy_net.get_action(obs_t) obs, reward, terminated, truncated, _ env.step(action) done terminated or truncated log_probs.append(log_prob) rewards.append(reward) # 计算折扣回报 returns [] G 0.0 for r in reversed(rewards): G r gamma * G returns.insert(0, G) returns torch.FloatTensor(returns) # 标准化回报等价于一种基线处理 returns (returns - returns.mean()) / (returns.std() 1e-8) # 计算损失 policy_loss [] for log_prob, R in zip(log_probs, returns): policy_loss.append(-log_prob * R) policy_loss torch.cat(policy_loss).sum() optimizer.zero_grad() policy_loss.backward() optimizer.step() return sum(rewards)这段代码里最关键的一行就是policy_loss.append(-log_prob * R)。负号是因为 PyTorch 的优化器是梯度下降而我们想最大化期望回报等价于最小化负的回报加权对数概率。如果 R 为正带着负号求梯度时log_prob 的梯度方向是上升的动作概率被推高R 为负则相反。我特意在代码里做了 returns 标准化。这其实是一种非常廉价的基线替代方案把所有轨迹回报减去均值除以标准差使得每条轨迹的权重有正有负。它在玩具环境上已经能让训练稳定不少。实际工程里如果你的任务允许收集一批轨迹再更新强烈建议用 mini-batch 的方式做标准化比单轨迹在线更新稳定得多。3.3 损失函数里的隐藏细节严格来说这里的 policy_loss 并不是传统意义的监督损失而是负期望回报的蒙特卡洛估计。因为梯度里包含了 log_prob 乘回报而如果你把它当成普通 loss 去 backward计算图会自动帮你算出正确梯度。很多人会困惑为什么损失是负数还能收敛答案是这里的损失只是梯度的载体数值本身没有物理意义。这跟监督学习里 loss 越小越好是两码事。另外一个重要细节是当你在一个 episode 里多次调用log_prob每次动作的概率都不同最终的损失是这些项的求和。有些实现会写成 (-\sum_t \log \pi(a_t|s_t) \cdot R_t)有些写成 (-\sum_t \log \pi(a_t|s_t) \cdot \sum_{t \ge t} \gamma^{t-t} r_{t})。理论上前者也是 REINFORCE 的一个合法变体但实践中后者带折扣回报显著更好因为同一个动作只对它之后产生的回报负责之前已经发生的奖励不该算到这个动作头上。这一点在长 horizon 任务里尤其关键。还有一点必须提醒如果回馈的 reward 一直是正数比如 CartPole 的 1 到游戏结束在没有基线的情况下网络会把所有动作概率一起推高策略会变得过度自信最终概率分布趋向 one-hot。这就是为什么哪怕玩具实验也要做回报标准化或者引入基线。4. 调参与避坑实操里的真实战场4.1 学习率到底该设多大策略梯度对学习率极其敏感这是我踩过最深的一个坑。学习率太大参数一步跨太远策略概率分布容易被推成极端 one-hot然后智能体只会做单一动作环境里一旦出现没见过的状态直接崩盘。学习率太小训练慢得让人怀疑人生。一个实用的经验值Adam 优化器下REINFORCE 类算法在 CartPole 这种简单环境里学习率取 1e-3 到 3e-3 比较稳妥。但如果你把策略网络换成更深的网络或者任务回报稀疏、方差很大学习率要跟着调低到 1e-4 量级。我自己的习惯是先拿一个 episode 的累计回报当粗估指标如果曲线在几十个 episode 内一点不动先别急着加学习率先检查回报标准化有没有做再看是不是基线缺失。4.2 Discount Factor 的双重角色(\gamma) 不只是未来奖励打折那么简单它同时控制着梯度的方差和偏差。(\gamma) 越接近 1回报考虑的步数越长信息更完整但方差也越大训练越不稳定(\gamma) 偏小比如 0.9方差小但智能体会变得短视动作只对近几步负责。我在实操中的选择逻辑是如果任务是稀疏奖励比如只有走到终点才给一次大奖励那么 (\gamma) 至少要 0.99 以上否则智能体根本没有办法把远处的奖励信号回传到之前的动作上。反过来如果任务每一步都有稠密奖励比如机器人走路每步都有接触反馈那 (\gamma0.95) 左右就够用了还能顺带降低方差。4.3 回报标准化到底能不能随便用很多开源实现里会把 returns 做标准化但这里有个争议点标准化会改变目标函数的尺度严格说改变了优化目标。不过我个人的观点是在策略梯度里标准化的收益远远大于它带来的理论不严谨。原因很简单策略梯度里回报的绝对尺度并不重要重要的是不同轨迹之间的相对比较标准化相当于自动调整了基线并压缩了梯度尺度。唯一要注意的是标准化必须在每个 batch 内进行而不是用全局统计量。因为策略在训练中不断变化不同时期轨迹的回报分布也不一样跨时期的标准化会把早先的尺度错误地套到后来的样本上。我在一个项目中就踩过这个坑把回报标准化用了全局均值结果训练后期策略明明变好了更新步长却被压缩得越来越小收敛变得异常缓慢。4.4 常见问题排查速查表症状可能原因排查方向累计回报来回大幅震荡学习率过大 / 基线缺失调小学习率检查是否减基线或做标准化策略迅速退化成单一动作概率分布被推成 one-hot降学习率、加入熵正则项、检查回报尺度训练明明在涨但突然崩溃方差过大碰到极端回报轨迹降 (\gamma)、增大 batch、加基线前几步就 NaN高斯策略里 (\sigma) 过小导致梯度爆炸给 (\sigma) 设置下限或改用 tanh 约束回报为正但策略不提升所有轨迹权重都为正梯度方向打架做回报标准化或引入 value baseline连续动作场景输出恒为边界值高斯策略均值被推出有效范围输出层接 tanh并重参数化动作范围这张表算是我自己做策略梯度实验时的速查手册。遇到问题时我通常先检查回报的形状再看梯度的范数。如果梯度范数在个别 step 突然比平时大几百倍那基本就是回报异常或者 (\sigma) 过小引起的。5. 从 REINFORCE 出发下一步该往哪走5.1 方差问题与 Actor-Critic 的登场REINFORCE 是策略梯度家族最基础的成员但它的方差问题在复杂任务里会变得不可接受。每一条完整轨迹采样只能给你一个带噪声的回报估计如果环境随机性很大几十条轨迹之间回报差异巨大梯度更新就会被少数运气好的轨迹主导。一个自然的改进方向是用当前状态的价值估计来代替整条轨迹的回报。这就是 Actor-Critic 的核心Critic 网络估计状态价值 (V(s))Actor 网络的梯度变成 (\nabla_\theta \log \pi_\theta(a_t|s_t) \cdot (r_t \gamma V(s_{t1}) - V(s_t)))其中括号里被称为 TD 误差。TD 误差的直觉是比起用整条轨迹的总回报评论动作好坏我们只评论这一步比预期好多少。这个信号方差小因为每一步都可以更新不用等完整轨迹结束。我在 A2C、PPO 这类算法里反复看到 TD 误差的影子它们的基石就是策略梯度思想。理解 REINFORCE 之后再上手 PPO 就会顺很多——PPO 无非是在策略梯度每次更新上加了个 KL 或 clip 约束防止更新太猛而已。5.2 我踩过的几个经典坑第一坑把 log_prob 的梯度方向搞反。我见过同学在自定义损失时写了log_prob * reward然后用了梯度上升结果完全训练不动。其实用 PyTorch 的自动求导工具标准做法是-log_prob * reward配合 optimizer 的梯度下降。第二坑忘记 detach 回报。回报向量在构造时如果从某个可导网络输出里带了过来梯度就会沿着回报回流到价值网络引发奇怪的双重梯度问题。严格来说策略梯度里回报应当被当作常数权重绝对不能让它携带梯度。第三坑batch 大小设置不合理。REINFORCE 这类 on-policy 算法数据用一次就扔batch 太小梯度噪声大batch 太大计算开销高但方差确实会降。实际中我一般取 1000 到 5000 步为一批具体看任务复杂度。5.3 关于策略梯度更深远的一点体会如果让我用一句话总结策略梯度的精髓我会说它把强化这个动作直接建模成了概率参数空间里的移动。你不需要精确知道环境模型不需要一步一算最优动作只需要让智能体去试、去采样、去比较不同动作在回报上的差异然后像爬山一样调整策略。我自己的经验是策略梯度虽然有方差大、样本效率低、超参敏感这些毛病但它让策略本身成为可优化的对象这是很多复杂任务里绕不开的核心。你在做机器人控制、游戏 AI、推荐系统排序策略的时候最终都会回到这个框架里来。建议想深入的同学把这篇文章里的最小实现亲手跑一遍把每个变量的形状打印出来看手动算一算某个 step 的梯度值会把公式彻底变成肌肉记忆。之后再去看 PPO、TRPO、SAC 这些进阶算法的原论文会发现它们讨论的所有问题——方差、步长、约束、基线——在一开始这个简单公式里就埋下了伏笔。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑