☰
策略梯度推导与实战:从REINFORCE到PPO的方差控制
2026/10/27 11:28:05 网站建设 项目流程

1. 策略梯度到底在解决什么问题

搞强化学习的人,早晚会撞上策略梯度这堵墙。监督学习里我们有明确的标签告诉模型“这个输入对应这个输出”,损失函数一算,梯度一回传,事情就结了。但强化学习不是这样——智能体在环境里跑完一整条轨迹,最后只拿到一个标量回报,中间每个动作到底贡献了多少、该往哪个方向调参数,这件事天然是模糊的。策略梯度就是专门啃这块硬骨头的工具:它不通过价值函数间接地推导策略,而是直接对策略参数求梯度,让“表现好的动作概率上升、表现差的动作概率下降”这件事变成一个可计算的数学过程。

我第一次认真推策略梯度的时候,最大的困惑不是公式本身,而是“凭什么一条轨迹的总回报能拆解到每个动作头上”。后来想明白了,这背后是一套非常朴素的逻辑:既然策略本身就是一个以参数θ为条件的概率分布π(a|s;θ),那么“让期望回报变大”这件事就可以直接对θ求导,而求导过程中出现的那个∇log π(a|s;θ),本质上是在说“这个动作在当前状态下被选中的概率,对参数有多敏感”。回报高的动作,我们就顺着这个敏感方向推一把;回报低的,反着推。整个策略梯度的推导,说到底就是在把这句话严格化。

这篇内容适合两类人看:一类是刚学完MDP、值函数方法,准备啃策略梯度的朋友;另一类是用过PPO、A2C这些算法但一直没搞懂loss里那个log概率项从哪来的人。我会从最基础的推导讲起,把每一步的“为什么”都摊开,然后落到实操里那些文档不会写的坑。推导部分我会尽量用生活化的类比配合严格的式子,实操部分给可直接抄的参数和排查表。

2. 从目标函数到梯度表达式:一步步拆推导

2.1 先把优化目标写清楚

策略梯度的起点是一个期望回报的最大化问题。定义轨迹τ = (s₀, a₀, r₀, s₁, a₁, r₁, …),一条轨迹的概率由环境动力学和策略共同决定:

p(τ|θ) = ρ₀(s₀) · ∏ₜ π(aₜ|sₜ;θ) · P(sₜ₊₁|sₜ,aₜ)

这里ρ₀是初始状态分布,P是环境转移概率。注意一个关键点:环境动力学P和初始分布ρ₀都不含θ,只有策略π含θ。这个观察是后面所有推导能成立的前提,很多人推着推着就忘了这一点,把环境的梯度也算进去了,那是错的。

优化目标是期望回报:

J(θ) = E_{τ~p(τ|θ)}[R(τ)],其中R(τ) = ∑ₜ rₜ

我们要的是∇_θ J(θ)。难点在于期望的分布本身依赖θ,不能直接把梯度挪进期望里面——这是策略梯度和普通监督学习求导最本质的区别。监督学习里数据分布固定,梯度可以直接进期望;这里分布随θ变,得用点技巧。

2.2 log-derivative trick:整个推导的枢纽

处理“分布依赖参数”的标准手法是log-derivative trick,也叫似然比技巧。它的核心恒等式只有一行:

∇_θ p(τ|θ) = p(τ|θ) · ∇_θ log p(τ|θ)

这个式子怎么来的?就是对log求导的链式法则反过来用:∇log f = (1/f)·∇f,两边乘f就得到∇f = f·∇log f。看着简单,但它是整个策略梯度能落地的关键,因为它把“对概率求导”转化成了“对log概率求导再乘回概率”,而后者可以重新塞回期望里。

把它用到目标函数上:

∇_θ J(θ) = ∇_θ ∫ p(τ|θ)R(τ)dτ = ∫ ∇_θ p(τ|θ) R(τ)dτ = ∫ p(τ|θ) ∇_θ log p(τ|θ) R(τ)dτ = E_{τ}[∇_θ log p(τ|θ) · R(τ)]

到这一步,梯度已经变成了一个可以采样估计的期望。这就是策略梯度定理的雏形。我建议每个学这块的人都亲手把这个变换推一遍,因为后面所有变体(REINFORCE、带baseline的、A2C的)都是在这个式子上做文章。

2.3 把log概率拆开,环境项自动消失

现在处理∇log p(τ|θ)。把轨迹概率的连乘取log变成连加:

log p(τ|θ) = log ρ₀(s₀) + ∑ₜ [log π(aₜ|sₜ;θ) + log P(sₜ₊₁|sₜ,aₜ)]

对θ求梯度,ρ₀和P都不含θ,梯度直接为零,只剩策略项:

∇_θ log p(τ|θ) = ∑ₜ ∇_θ log π(aₜ|sₜ;θ)

这一步是很多人第一次看会“啊哈”的地方——环境的未知动力学在梯度里彻底消失了。这意味着我们不需要知道环境怎么转移,只要能从环境里采样,就能估计梯度。这就是策略梯度属于model-free方法的根本原因。代回上面的期望:

∇_θ J(θ) = E_{τ}[ (∑ₜ ∇_θ log π(aₜ|sₜ;θ)) · (∑ₜ rₜ) ]

这就是最原始的REINFORCE梯度。它成立、可算,但直接拿来用效果很差,原因下面会讲。

2.4 因果性修正:未来的回报不该影响过去的动作

上面那个式子里,整条轨迹的总回报R(τ)乘在了每一个∇log π上,包括时间步很早的那些。但物理直觉告诉我们:t时刻的动作不可能影响t时刻之前的奖励。用因果性(causality)可以把这一点严格化,得到更紧的估计:

∇_θ J(θ) = E[ ∑ₜ ∇_θ log π(aₜ|sₜ;θ) · ∑_{t'≥t} r_{t'} ]

也就是每个动作只和它之后(含当前)的回报挂钩,记Gₜ = ∑_{t'≥t} r_{t'},这就是“回报-to-go”。这个修正不改变期望值(因为被去掉的那些项期望为零),但显著降低方差。我实测过,在同一个任务上,用回报-to-go比用整条轨迹回报,梯度估计的方差能降一个量级,收敛速度肉眼可见地快。这一步的推导细节是:E[∇log π(aₜ|sₜ)·r_{t'}] 当t' < t时为零,因为r_{t'}与aₜ在给定sₜ下条件独立,而∇log π的期望为零。这个“期望为零”的性质后面还会反复用到。

2.5 baseline:不改变期望,只砍方差

再进一步,可以在回报里减去一个只依赖状态的baseline b(sₜ):

∇_θ J(θ) = E[ ∑ₜ ∇_θ log π(aₜ|sₜ;θ) · (Gₜ - b(sₜ)) ]

为什么减去它不影响期望?因为E_{aₜ~π}[∇_θ log π(aₜ|sₜ;θ) · b(sₜ)] = b(sₜ) · ∇_θ ∑_a π(a|sₜ;θ) = b(sₜ) · ∇_θ 1 = 0。策略对所有动作的概率和为1,这个和的梯度恒为零,所以任何只依赖状态的函数作为baseline都不引入偏差。而它能把方差压下来,因为Gₜ - b(sₜ)的波动比Gₜ小。实践中b(sₜ)通常取状态价值函数V(sₜ)的估计,这就自然过渡到了Actor-Critic:策略网络(Actor)负责输出动作分布,价值网络(Critic)负责估计baseline。A2C、PPO里那个advantage,本质就是Gₜ - V(sₜ)的某种估计。

3. 实操落地:从公式到能跑的代码

3.1 最小可复现的REINFORCE实现

理论推完,得落到代码上。下面是一个结构清晰、可直接跑的REINFORCE骨架,我用的是离散动作空间的经典设定,连续动作只需把Categorical换成Normal并把log_prob换成对应分布的实现:

import torch import torch.nn as nn import torch.optim as optim from torch.distributions import Categorical class PolicyNet(nn.Module): def __init__(self, state_dim, action_dim, hidden=128): super().__init__() self.fc = nn.Sequential( nn.Linear(state_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, action_dim) ) def forward(self, x): return Categorical(logits=self.fc(x)) def compute_returns(rewards, gamma): G, returns = 0, [] for r in reversed(rewards): G = r + gamma * G returns.insert(0, G) return returns def train_step(policy, optimizer, trajectories, gamma=0.99): log_probs, returns = [], [] for traj in trajectories: G = compute_returns(traj['rewards'], gamma) returns.extend(G) log_probs.extend(traj['log_probs']) returns = torch.tensor(returns, dtype=torch.float32) # 标准化回报,等价于用一个自适应的baseline,显著降方差 returns = (returns - returns.mean()) / (returns.std() + 1e-8) loss = -(torch.stack(log_probs) * returns).sum() optimizer.zero_grad() loss.backward() optimizer.step()

这段代码里有两个细节值得单独说。第一,compute_returns用的是倒序累加,这是实现回报-to-go最省事也最不容易错的方式,正序写容易把索引搞乱。第二,回报标准化那一步,很多人以为只是“让训练稳一点”的玄学,其实它有严格解释:减去均值相当于用batch均值作为baseline,除以标准差相当于对梯度做了自适应缩放,两者都不改变梯度的期望方向,但能把方差压得很低。我在几个连续控制任务上对比过,加不加标准化,收敛所需episode数能差两三倍。

3.2 折扣因子γ的选择逻辑

γ不是随便填的。它决定了智能体“看多远”,也直接影响回报的量级和梯度方差。γ越接近1,回报-to-go的方差越大(因为累加项多),但越能捕捉长期依赖;γ越小,方差小但可能短视。经验上:

任务类型推荐γ理由
回合制短任务(步数<200)0.95~0.99回合本身不长,不需要太强的折扣
长回合任务(步数上千)0.99~0.995需要覆盖足够长的信用分配链
持续型任务(无终止)0.99左右必须靠折扣保证回报有界
奖励稀疏且延迟大0.995~0.999否则早期动作拿不到有效信号

有个容易忽略的点:γ改变后,回报的量级会变,如果用了价值网络做baseline,价值网络的输出尺度也得跟着调,否则advantage的尺度失衡,学习率得重新调。我踩过这个坑——把γ从0.99改到0.999,没动学习率,结果价值网络直接发散,因为目标值量级大了十倍。

3.3 梯度估计的方差从哪来,怎么压

策略梯度的方差来源主要有三块,对应三种压制手段:

  • 回报本身的随机性(环境随机+策略随机):用baseline和回报标准化压。
  • 轨迹长度带来的累加噪声:用回报-to-go和折扣压。
  • 策略分布的熵:熵太低时策略接近确定性,采样多样性差,梯度估计噪声大;熵太高又学不动。实践中加一个熵正则项(loss里减一个熵的系数,通常0.01左右)能明显改善探索。

这里给一个我常用的组合配方:回报-to-go + 回报标准化 + 价值baseline + 熵正则系数0.01。这套组合在多数离散和连续任务上都能稳定起步,不需要太多调参。如果还不行,再考虑GAE(广义优势估计),它本质上是baseline和折扣的进一步精细化,用λ在偏差和方差之间做插值。

4. 常见问题与排查实录

4.1 训练完全不涨,梯度是不是算错了

这是最高频的问题。排查顺序我一般这样走:先确认log_prob是不是真的对应当前采样的动作(用Categorical时,log_prob要传采样的action,别传argmax);再确认loss的符号——策略梯度是最大化,但优化器是最小化,所以loss要取负,这个负号漏了会导致策略往反方向学,表现就是越训越差;最后确认回报和log_prob的时间对齐,回报-to-go的索引错位一位是很隐蔽的bug,表现是能学但学得很慢。

提示:写完后先在一个极简环境(比如只有两三个状态、奖励确定的小格子)上跑,看策略能不能收敛到最优动作。极简环境能过,再上复杂环境,否则你分不清是算法错还是调参问题。

4.2 方差大到梯度方向乱跳

如果loss曲线剧烈震荡、策略在几个动作间反复横跳,基本是方差问题。先上回报标准化,再上baseline,最后考虑增大batch(多采几条轨迹再更新一次)。还有一个常被忽略的点:不要用单条轨迹更新。REINFORCE理论上单条轨迹就能估梯度,但方差极大,实践中至少采4~8条轨迹做一次更新,效果立竿见影。

4.3 策略过早坍缩到单一动作

这是探索不足的典型症状,往往伴随熵快速下降。原因可能是熵正则系数太小、学习率太大、或者回报标准化把差异放得过大。处理办法:把熵系数调大(0.01→0.05试),学习率降一个量级,检查回报标准化的分母是不是太小导致数值爆炸。另外,如果任务奖励设计本身有“捷径”(某个动作总能拿到不低的即时奖励),策略会很快锁死在那,这时候得回头改奖励设计,光调算法救不回来。

4.4 连续动作空间下的特有坑

连续动作把Categorical换成Normal后,有几个额外注意点。第一,标准差参数不能太小,否则采样几乎确定,探索消失;通常给标准差设一个下界(比如0.1)或者用可学习的log_std并做截断。第二,log_prob的计算要用分布自带的log_prob,别自己手写高斯密度,容易漏掉归一化项导致梯度有偏。第三,动作如果有范围限制(比如关节角度),要么在环境侧裁剪,要么用tanh变换,但tanh变换后log_prob要做相应的雅可比修正,这一步漏了梯度就是错的。

症状最可能原因快速验证处理
loss不降反升loss符号错/学习率过大打印单步梯度范数检查负号,降学习率
策略反复横跳梯度方差大看不同batch的梯度方向夹角加baseline、回报标准化、增大batch
熵骤降后卡死探索不足监控策略熵曲线加熵正则、降学习率
连续动作学不动log_std坍缩/雅可比漏算打印log_std值设下界、补雅可比修正
前期能学后期崩价值网络发散/尺度失衡看value loss调γ后重调学习率、加梯度裁剪

4.5 一个容易被忽视的实操心得

策略梯度对随机种子的敏感度比监督学习高得多。同一个配置,换个种子可能一个收敛一个不收敛。所以评估一个改动是否有效,别只看一次运行,至少跑3~5个种子看均值和方差。我早期吃过亏,调了个参数觉得效果好,结果换种子就崩,白高兴一场。另外,记录训练时的策略熵、梯度范数、回报均值这三条曲线,比只看回报曲线能早很多发现问题——回报往往滞后,熵和梯度范数才是先行指标。

5. 从REINFORCE到现代算法的演进脉络

把推导走完、代码跑通之后,回头看PPO、A2C这些算法,会发现它们都是在同一个策略梯度定理上做工程加固。A2C用优势函数替代原始回报,本质就是baseline的推广;PPO用重要性采样比率的裁剪限制每次更新的步长,解决的是“策略更新太猛导致性能崩塌”的问题,而它优化的目标仍然是那个带log概率的替代目标。理解了∇log π这个核心项,再看这些算法的loss,就不会觉得是一堆凭空冒出来的公式了。

我个人在实际操作中的体会是,策略梯度的推导值得反复推、亲手推,尤其是log-derivative trick和因果性修正这两步,推熟之后,很多算法的设计动机就变得透明了。至于工程上,先把REINFORCE在简单环境上调稳,把方差压制的手段用熟,再去碰PPO这类更复杂的算法,路径会顺很多。最后分享一个小技巧:调试策略梯度时,把策略网络和价值网络的梯度范数分开打印,很多时候问题出在其中一个网络发散拖垮了另一个,分开看能立刻定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询