ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

策略梯度方法详解:从REINFORCE到PPO的方差优化与实战避坑

策略梯度方法详解:从REINFORCE到PPO的方差优化与实战避坑

策略梯度方法一直没有花单独一篇来写,这次补上。前面那些文章主要围绕值函数展开,也就是先估计Q(s,a)或者V(s),再用它们间接决定策略。而策略梯度直接对策略本身做参数化,不去绕道值函数。这种思路上的转变,解决了不少值函数方法很头疼的问题,比如连续动作空间、随机策略的表达、以及策略退化时无法自救的情况。

这一篇我会从最基础的推导开始,把为什么要减基线、REINFORCE和Actor-Critic的区别、以及PPO里那个clip到底解决了什么,讲清楚。最后附上自己实际操作中踩过的坑和排查经验。

1. 为什么绕开值函数:策略梯度方法的核心思路

1.1 值函数方法在连续空间里的尴尬

在之前写的DQN那几篇里,整个训练流程是:拿状态s输入网络,输出每个动作的Q值,然后选Q值最大的动作。这个逻辑在离散动作空间里很自然,比如上下左右四个方向,输出维度就是4,比较大小非常方便。但一旦动作用连续变量表示,比如机械臂关节角度、自动驾驶的方向盘转角,事情就麻烦了。

你可能想,把连续动作离散化不就行了?确实有人这么做,但维度稍微一高就撑不住。假设一个机械臂有7个关节,每个关节离散成100个档位,动作空间就变成100的7次方,这个数大到任何网络都输出不了。所以离散化只适合动作维度极低、精度要求不高的场景。

另外一个更难察觉的问题是,值函数方法本质上只能表达确定性策略。它通过贪婪选择或者ε-贪婪来选择动作,这意味着在同一个状态下,要么选那个"最优"动作,要么随机乱试。那些需要随机策略的任务,比如石头剪刀布、扑克牌游戏,用值函数方法就很别扭,因为你明明知道对手在观察你的行为模式,纯随机才是最优解,但DQN这类方法天生不擅长表达"均匀随机地出拳"这种策略。

1.2 直接对策略求梯度,绕开"先估后选"的弯路

策略梯度方法换了一个角度:定义一个参数为θ的策略网络π_θ(a|s),这个网络直接输出在状态s下选择动作a的概率,然后优化目标就是让累积回报的期望最大化。整个过程不显式地估计Q值或V值(至少最原始的版本如此),而是对目标函数直接求梯度,让参数沿着梯度方向更新。

这样做有几个立竿见影的好处。动作空间连续时,策略网络的输出可以是动作分布的均值或方差,比如高斯分布的μ和σ,采样得到实际动作。随机策略也是天然支持的,因为网络的输出本身就是概率分布,完美的随机策略就是各个动作概率相等。

最重要的是,策略梯度方法在"策略变差"的时候有自我修正的能力。值函数方法如果Q值估计偏了,策略会被带偏,而且很难自己绕回来;而策略梯度是朝着"回报更大的方向"一步步挪,哪怕步子偶尔迈歪了,整体趋势是能纠正的。

1.3 策略梯度 vs 值函数:一张表看清各自优势

维度值函数方法(如DQN)策略梯度方法
动作空间适合离散、低维离散连续通吃
随机策略很难自然表达天然支持
策略单调性可能因Q值偏估而退化梯度方向保证局部改进
方差相对较低偏高,需要减基线等技巧
收敛性可能不收敛收敛性更稳定
实现复杂度相对简单需要处理采样与基线

2. 从期望回报到梯度:数学推导与直觉

2.1 目标函数与"用采样代替积分"的关键一步

策略梯度的起点很朴素:定义一个目标函数J(θ),表示在策略π_θ下,一条完整轨迹的累积回报的期望:

J(θ) = E_{τ ~ π_θ}[R(τ)]

其中τ表示一条完整的状态-动作序列,R(τ)就是这条轨迹上所有奖励的总和。优化目标就是让J(θ)最大,所以梯度方向是∇J(θ),参数更新就是θ ← θ + α∇J(θ)。

核心难题在于,J(θ)是对轨迹分布的期望,而这个分布取决于策略本身,策略一变分布就变,普通求导法则没法直接套。这里需要用到log-ratio trick,也叫score function trick,本质上就是利用恒等式:

∇π_θ(a|s) = π_θ(a|s)∇log π_θ(a|s)

把它代进期望的表达式,经过几步代数操作,就能得到:

∇J(θ) = E_{τ ~ π_θ}[Σ_t ∇log π_θ(a_t|s_t) · R(τ)]

这个式子的直观意义可以这样理解:如果某条轨迹的回报R(τ)很高,那么沿着这条轨迹上所有动作的log概率梯度方向去调整参数,让这些动作再出现的概率增大;如果回报很低,就让这些动作的概率减小。最后把所有轨迹的贡献汇总,就是策略的更新方向。

这个公式的关键价值在于:期望里不再包含策略的梯度,只包含log概率的梯度,这样就能用实际采样的轨迹来估计期望,也就是蒙特卡洛采样。你不需要知道所有状态和动作的确切分布,只需要不断地在环境里跑轨迹,记录状态、动作、回报,然后代入公式更新参数就行。

2.2 为什么这个梯度方向是"对的":重力场类比

用一个类比来帮助理解梯度方向为什么合理。想象你不是在做强化学习,而是在一个滑雪坡上调整滑雪板的角度。坡上每个位置的高度就是累积回报,滑雪板的角度就是策略参数。你没法直接看到整个山坡的全貌,只能在自己脚边感受一下哪个方向往下倾斜得多一点,然后朝那个方向微调。

策略梯度做的事本质上就是这样:通过采样几条轨迹,估算出"当前策略参数附近哪个方向的回报上升最快",然后沿着那个方向挪一小步。它不需要知道整个回报曲面长什么样,只需要局部的坡度信息。这也是为什么策略梯度方法就算在环境稍微有扰动的时候也还能继续工作,因为它永远只是在探测局部梯度。

但这里有个代价:采样只是估计,估计就有噪声。几条轨迹的回报可能差距巨大,梯度方向的波动也就特别大。这就是后面要讲方差问题和基线技巧的出发点。

2.3 核心公式的完整推导过程

为了让你能真正搞懂而不只是记住结果,我把关键的推导步骤列出来。里面有一步涉及期望和梯度的交换,其他都是代数操作。

目标函数的梯度展开如下两步。第一步把期望展开成积分形式,利用概率分布的对数求导恒等式把梯度作用到log概率上:

∇J(θ) = ∫ π_θ(τ)∇log π_θ(τ) R(τ) dτ

第二步,轨迹概率分解成每一步的乘积,取对数后变成求和,梯度也就随之分配到每个时间步:

π_θ(τ) = Π_t π_θ(a_t|s_t),所以∇log π_θ(τ) = Σ_t ∇log π_θ(a_t|s_t)

代回原式就得到逐时间步的形式了。整个过程没有复杂的近似,全部是精确的数学变形。这也是策略梯度方法和很多启发式方法不一样的地方:它有个严格的优化目标,每一步更新都是在朝目标函数梯度的无偏估计方向走。

2.4 方差问题初现:为什么这个"朴素"版本很难直接用

理论很漂亮,但直接照着这个公式写代码,训练起来往往让人怀疑人生。原因就是方差。我举个例子,某个状态s下,策略以0.6的概率选动作a1,回报是+10;以0.4的概率选动作a2,回报是0。理论上期望回报是6,梯度会温和地提升a1的概率。但你实际采样的时候,如果这一轮只采到一条轨迹,恰巧走了a2,回报是0,梯度就认为a1和a2都该降低概率,这就完全反了。

虽然大量的采样会让估计逐渐接近真实值,但训练过程中梯度的大幅波动会导致训练曲线像心电图一样狂跳。更致命的是,不同时间步的动作共享整条轨迹的总回报,这意味着早期那个动作的更新,会受到后面几十步中所有随机性的影响,甚至包括其他动作的贡献。这种"信用分配"问题让梯度的噪声变得非常大。

所以实际使用策略梯度方法时,几乎没有人生般直接用最原始的版本。接下来要说的减基线、优化子等技术,本质上都是在不改变梯度期望的前提下降低方差,让训练能真正跑起来。

3. 减基线:让梯度估计不再"噪声爆炸"

3.1 不用基线时的直观感受和数学后果

我用一个非常简单的实验说明问题。在某仿真环境里,我训练一个两层MLP的策略网络,动作空间是2维连续,reward在0到1之间波动。如果直接用朴素的REINFORCE算法,学习率设到0.01,训练到第2000步时loss曲线已经完全乱掉,策略输出直接崩到边界值,再也回不来。

原因可以用一个简单的量级估算来看。假设一条轨迹有50步,每步的reward标准差是0.5,整条轨迹的总回报方差就是50×0.25=12.5,标准差约3.5。而梯度更新量里要乘上这个总回报,所以更新方向的噪声相当可观。在真正的高维问题里,reward的尺度可能更大,噪声更离谱。

数学上可以证明,梯度的方差与"回报的方差"成正比。所以如果直接采样几次就更新,每条轨迹之间差异那么大,更新方向几乎被噪声主导,完全不具备一致性。

3.2 数学推导:为什么减去一个只依赖于状态的函数不影响期望

基线技巧在数学上非常简洁。假设我们给原始的梯度项减去一个函数b(s),这个函数只与状态s有关,与动作a无关。那么对于单个时间步,考虑期望:

E[∇log π_θ(a|s) · b(s)] = Σ_a π_θ(a|s) · ∇log π_θ(a|s) · b(s)

把∇log拆成∇π/π,代入得到:

Σ_a b(s) · ∇π_θ(a|s) = b(s) · ∇(Σ_a π_θ(a|s)) = b(s) · ∇1 = 0

这里用到Σ_a π_θ(a|s) = 1这个概率归一化性质。所以不管b(s)取什么值,期望上它对梯度没有任何影响。这就给了我们一个巨大的自由度:可以选择一个适当的b(s)参与计算,在不改变梯度期望的前提下降低方差。

直觉上,减去基线的效果是把"绝对回报"变成了"相对回报"。比如一条轨迹在某个状态下回报是1.0,但如果该状态之前的平均回报就是1.5,那这个1.0其实是偏低的,应该降低该动作的概率。减去基线后,梯度信号就变成了"这个动作相比平均水平好多少",而不是单纯的"这个动作带来的回报数值是多少"。

3.3 状态值函数作为基线的理由与实现细节

最常用的基线就是状态值函数V(s)。它表示从状态s出发,按照当前策略能获得的期望累积回报。用它做基线,梯度项变成:

∇J(θ) = E[Σ_t ∇log π_θ(a_t|s_t) · (G_t - V(s_t))]

其中G_t是t步以后的累积回报,V(s_t)是基线。两者的差值在概念上就是"这个动作的回报相对于平均水平好了多少",这个量正是优势函数A(s,a)的定义。

实现V(s)的方式有很多种。最简单的是用一个单独的神经网络,结构通常和策略网络共享底层特征,只是输出维度变成1。也可以用线性回归拟合,但效果差一些。实际工程里我一般直接用Actor-Critic架构,让评论员网络去估计V(s),然后用TD误差来替代真实的优势值,这个后面细说。

3.4 基线选不好时会发生什么:一个实测记录

我在一次实验里踩过一个很典型的坑。当时用的是最简单的REINFORCE加状态基线,基线网络是一个单层线性层,没有和策略网络共享特征提取层。训练到第3000步,基线网络本身还没收敛,输出的V(s)全是近似常数。这时候减去基线的效果约等于没减,但多了额外一层梯度计算,训练速度反而更慢。

后来又换了一种做法,让基线网络和策略网络共享前两层隐藏层,只在输出层分开。效果立刻好了很多,因为共享特征提取层意味着基线能更快地学到有价值的状态表示,而不需要从零开始单独学习特征。

从这个经验里得到的教训是:基线网络必须足够强,否则效果有限。减基线的核心是让V(s)尽量准确地预测当前策略下的平均回报,V(s)越准,方差降低越明显。如果你的基线网络一直在瞎猜,那不如不要。

4. 实战中的三个关键版本:REINFORCE、Actor-Critic、PPO

4.1 REINFORCE:最原汁原味的蒙特卡洛策略梯度

REINFORCE是策略梯度方法里最基础的一种,特点是用整条轨迹的完整回报G_t来估计优势。算法流程非常清晰:用当前策略跑一条轨迹,收集每一步的状态、动作、回报,到轨迹结束后计算每一个时间步的累积回报G_t,然后对每个时间步计算∇log π(a_t|s_t)·(G_t - b(s_t)),求和取平均作为梯度,更新网络参数。

这个算法在概念上有一种天然的"事后诸葛亮"意味:先打完整场,等结果明朗了再回头反思每一步该不该那么走。如果整场赢了,所有被选中的动作都会被鼓励;如果输了,所有动作都会被惩罚。这种粗糙的信用分配方式效率不高,但好在无偏——期望上不会误导策略。

我在一个小型导航任务上跑过REINFORCE,训练5000个episode才能达到比较稳定的水平,而同样的任务用Actor-Critic只用1500个episode左右就能达到同样水平。所以REINFORCE现在更多是作为教学工具来理解策略梯度的基本原理,实际工程里很少直接用它。

4.2 Actor-Critic:用偏差换方差

Actor-Critic的核心改动是用TD误差来替代整条轨迹的总回报。TD误差定义为:

δ_t = r_t + γV(s_{t+1}) - V(s_t)

它衡量的是"当前回报加下一状态的价值估计"与"当前状态的价值估计"之间的差距,这个差距本质上就是对优势函数的估计。策略网络(Actor)按δ_t的方向更新,价值网络(Critic)则朝让δ_t变小的方向更新。

这个改动把"等整条轨迹跑完再回顾"变成了"每走一步就立刻反思",因此方差大幅降低。代价是引入了偏差,因为V(s_{t+1})本身是估计值,估计的误差会污染梯度。但在实际操作中,这个权衡非常划算——方差降低带来的训练稳定性提升远大于偏差带来的影响。

实现Actor-Critic架构时,需要注意的是两个网络的学习率通常不同。策略网络的学习率一般设小一点,价值网络可以大一些,因为价值网络需要快速收敛才能提供准确的基线信号。我自己常用的配置是Actor学习率3e-4,Critic学习率1e-3。

4.3 PPO:把"采样优化"变成了"离线更新的安全区"

PPO要解决的问题是:策略梯度每次更新只能用当前的采样数据,数据用完就扔,导致样本效率很低。理想情况下,我们希望同一批数据能被反复利用几次,但要保证更新步长不能太大,否则策略跑偏了,数据分布就不对劲了。

PPO的核心思路是限制新旧策略之间的差距。假设旧策略是π_old,新策略是π_θ,定义两者的概率比值:

r_t(θ) = π_θ(a_t|s_t) / π_old(a_t|s_t)

如果r_t(θ)大于1,说明新策略在当前位置的动作概率比旧策略大;小于1,说明概率变小了。PPO的目标函数在这个比值上做clip:

L(θ) = E[min(r_t(θ) · Â_t, clip(r_t(θ), 1-ε, 1+ε) · Â_t)]

其中Â_t是优势函数估计,ε通常取0.2。当r_t(θ)超出[1-ε, 1+ε]这个区间时,梯度被截断,不再继续增大更新量。这个机制保证了策略每步只能"稳妥地走一小步",不会因为某次采样运气好就大幅改变策略。

从我自己的实验经验看,PPO在大部分连续控制任务上的表现都相当稳定,尤其是和REINFORCE对比时,训练曲线顺滑得多。它不需要精调每步的信任域参数,也不需要像自然梯度方法那样计算费解的Fisher信息矩阵,这让它成为实际使用中最受欢迎的策略梯度算法之一。

4.4 三种算法的对比与选型建议

特性REINFORCEActor-CriticPPO
更新频率每条轨迹结束每个时间步每条轨迹后多次更新
方差很高较低低
偏差无偏有偏(使用估计值)有偏(clip导致的偏移)
样本效率低中高
实现难度简单中等中等偏复杂
适用场景教学、小规模任务实时交互场景大多数实际工程任务

选型时我的建议很简单:刚开始学就用REINFORCE,把公式和代码对应起来;做真正的项目直接用PPO,稳定性比另外两个好太多。Actor-Critic更适合那些每一步都能获得即时奖励、环境交互成本低的场景,比如在线决策类的任务。

5. 训练过程中那些防不胜防的坑

5.1 reward尺度过大导致的梯度爆炸

策略梯度方法对reward的尺度非常敏感。这是因为梯度更新量直接乘以回报值,reward的绝对尺度会直接影响参数更新的幅度。我在一个任务里设置的reward范围是0到100,但网络输出的动作范围只有-1到1。结果训练到第200步,策略网络参数直接变成NaN。

排查过程比较折磨,一开始怀疑是学习率问题,降低了10倍还是爆炸;又怀疑是网络初始化问题,换了初始化方法也不行。最后把reward的历史曲线打出来才发现,回报值偶尔会出现特别大的峰值,乘以梯度后直接把参数推到了数值溢出的边缘。

解决办法有两个方向:一是对reward做缩放,比如除以一个常数十,让它落在0到10的区间;二是对梯度做归一化,比如用梯度裁剪或归一化梯度的L2范数。两者的效果差别不大,但我个人更倾向于先做reward缩放,因为梯度裁剪只是防止数值爆炸,并没有解决尺度对更新幅度的根本影响。

5.2 学习率过大导致策略"猝死"

策略梯度方法在训练初期最容易出现的一个现象是:训练曲线原本在上升,突然某个episode之后回报暴跌到最低值,之后再也回不来。这个现象的根本原因是策略过度更新——某次采样恰好走到了回报特别高的轨迹,梯度计算出来非常大,参数更新这一步迈得太大,策略直接变成确定性策略,而且是无意中选了一条很差的路。

我在实验里给学习率设到0.1测试过一次,从训练曲线上看,策略在第37个episode就完全崩掉了。崩掉前的几个episode回报还在快速上升,说明策略确实在学习,但学习率太大导致策略一步跨过头。

有效的应对手段是降低学习率,比如从1e-3开始调。另外可以用一个简单的检查手段:在训练过程中记录策略网络输出概率分布的熵。如果熵在某一步骤突然掉到接近0,说明策略变成了几乎确定性的,这时大概率陷阱出现了,需要回退到之前的模型检查点。

5.3 梯度方差大的直观信号和系统的排查法

训练曲线跳得像锯齿电图一样,是策略梯度方法方差大的最直接表现。判断这种情况是否正常,要看整体趋势:如果曲线总体在上抬、只是波动幅度大,属于正常现象;如果曲线是水平振荡甚至长期不抬升,那方差大的问题已经影响到收敛了。

我常用的排查步骤是这样的:先检查reward的scale是否合理,把reward打印出来看最大值、最小值、均值;再看基线网络是否已经收敛,可以把V(s)的预测值和实际回报做相关性分析,如果相关性很低说明基线没学到东西;最后才看学习率。

这三个步骤的次序不能乱。reward scale影响的是全局,基线问题影响的是梯度质量,学习率影响的是更新幅度。从全局到局部逐步排查,能避免在错误的方向上浪费时间。

5.4 熵正则项:防止策略过早收敛到次优

策略梯度方法还有一个容易被忽视的问题:训练进行到中后期,策略会逐渐收敛到一个确定的动作模式。如果这个模式恰好是局部最优,策略就陷入其中无法跳脱了。我在某任务中遇到过策略收敛到"原地转圈"的行为,回报稳定在一个比较低的值,怎么调学习率都没用。

解决办法经常是加熵正则项。在损失函数里加一个策略熵的惩罚项,熵越大代表动作分布越均匀,加这个项等于鼓励策略保持一定的随机性。实际公式是:

J(θ) = E[Σ_t ∇log π(a_t|s_t) · A_t] + β · H(π_θ)

这里的β是熵正则系数,通常在0.005到0.02之间调。β增大会让策略更随机,探索增多;减小则让策略更倾向确定性。从实验经验看,加熵正则后,策略中后期的收敛稳定性和最终回报都有提升,虽然提升幅度不算大,但避免了不少次陷入局部最优的尴尬。

5.5 常见的训练失败模式速查表

现象可能原因排查顺序
梯度变成NaNreward尺度过大检查reward统计量
训练曲线骤降后再不回升学习率过大/策略过度更新检查学习率与概率熵
曲线长期震荡不收敛方差过大检查基线网络收敛度
策略收敛到次优探索不足加熵正则项
训练前期有效后期停滞基线网络过拟合降低Critic学习率

6. 我是怎么调策略梯度算法的:个人实操习惯

顺手写一点个人经验,给刚入门的朋友参考。我每跑新的任务,最开始永远先用最小的配置验证逻辑通通:一个非常小的网络、纯CPU训练、轨迹数量也设少。优先目的是确认梯度更新后回报均值确实在往上走,然后再逐步放大规模。很多新手一上来就开大模型大batch size,出了问题反而不知道是逻辑错还是资源不够。

reward scale这件事我几乎每次都会踩。所以我现在的习惯是在环境封装层就把reward做一次归一化,比如除以历史回报的滑动标准差,避免每次跑新任务都要重新调这个参数。

再就是日志。强烈建议每次训练都把reward均值、V(s)预测均值、策略熵、梯度L2范数这四样东西打出来。它们组合起来基本能定位90%的运行问题。比如梯度的L2范数如果从10^0突然跳到10^3,那大概率是reward出现异常峰值了。

关于PPO的参数,ε我基本不动,固定在0.2;学习率如果训练不太稳定,降到1e-4通常能救;batch size小一点比如256比大batch更不容易过拟合到当前采样分布。这些经验在不同任务之间迁移性挺强的。

如果只是做实验验证算法效果,我会先试Actor-Critic,因为它代码量少、调试直接。做真正的决策控制任务,直接用PPO,它能规避的坑比它引入的复杂度多得多。这两种方案覆盖了我遇到的绝大多数场景。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进