ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

策略梯度算法全解析:从REINFORCE到PPO的工程实践指南

策略梯度算法全解析:从REINFORCE到PPO的工程实践指南 第一次跑通策略梯度的时候我盯着终端里的reward曲线发呆——它跳得比股票分时图还猛。损失函数明明在下降reward却一路乱颤换个随机种子出来的又是另一条曲线。这不是我的bug而是策略梯度Policy Gradient这一类方法骨子里的特性。策略梯度不是某一个算法而是一整类方法的统称REINFORCE、Actor-Critic、TRPO、PPO、A3C全都挂在它名下。它们的共同点是直接参数化一个策略然后通过采样的回报信号去更新参数。换句话说只要你的训练循环里出现了动作的对数概率乘以回报这种东西你就跑不出策略梯度的框架。这篇文章我尽量按从原理到代码再到调参的顺序写包含完整推导、最小实现、方差分析和现代算法演进脉络。无论你是刚入门想搞懂REINFORCE为什么有效还是已经在用PPO炼丹却说不清它内部在做什么都可以收藏起来当参考资料。1. 为什么学强化学习绕不开策略梯度1.1 价值学习的天花板连续动作和随机策略先聊点背景。强化学习很长一段时间里被DQN这类价值方法刷屏价值学习的思想非常优雅先学会估计在状态s下采取动作a有多好即Q(s,a)然后策略就是简单地取argmax Q(s,a)。这套路在动作空间是离散且维度不高的时候很好用比如Atari游戏。但你一旦往更真实的任务上靠两个问题就会跳出来。第一个问题是动作空间连续。机械臂的关节角度、自动驾驶的方向盘转角、连续控制里的力度都是连续量。DQN要取的是argmax Q(s,a)那意味着要对a做最优化——在连续空间里这通常是个费劲的非凸优化问题每一步决策都现算一个最优化成本和延迟都受不了。第二个问题更深也常被忽略价值学习只能表达确定性策略而很多任务的最优策略本身就是随机的。最经典的是猜拳和扑克里的诈唬你如果总是出同样的动作对手很容易适应秒杀。自博弈场景里随机策略往往是纳什均衡的一部分。价值方法对这种需求无能为力。1.2 策略梯度的思路直接parameterize策略策略梯度的思路是换个角度干脆把策略本身定义成一个带参数的分布。离散动作空间策略可以是一个softmax网络输出动作的离散概率分布连续动作空间策略可以输出高斯分布的均值和方差从里面采样得到动作。这样做的直接好处是不需要对动作空间做最优化采样就行连续动作和离散动作天然统一。策略天然是随机策略熵可控探索和利用可以直接通过熵来调节。策略的参数化可以非常简单网络输入状态输出动作分布下一步做什么由这个分布决定。1.3 一个直观的类比策略是如何被推着走的想象你闭着眼站在一座山坡上目标是找到一个海拔更高的位置。你看不见全貌只能试着朝某个方向迈一步然后感受脚底是上坡还是下坡。策略梯度的更新逻辑很接近这个画面。先让当前策略在环境里跑几轮收集轨迹和回报然后根据这些轨迹把带来高回报的动作对应的策略概率调大把带来低回报的动作对应的概率调小。参数θ往前走一步策略就微调一步。反复这个过程。但这句高回报的动作概率调大说起来简单实际做起来有个很深的数学问题你怎么知道某个动作的功劳有多大一个轨迹里的回报是所有动作共同作用的结果而且环境本身还有随机性。策略梯度定理就是用来解决这个分配问题的下面把它彻底推导一遍。2. 策略梯度定理完整推导与那只看不见的归一化2.1 目标函数把策略好坏写成可求导的形式我们的优化目标是一个期望总回报。定义策略πθ(a|s)轨迹τ是一连串的状态和动作τ (s0, a0, r0, s1, a1, r1, ..., sT, aT, rT)轨迹发生的概率取决于策略参数θ和环境转移概率pθ(τ) p(s0) · Π πθ(at|st) · p(st1|st, at)注意这里面的环境转移p(s|s,a)和初始状态分布p(s0)我们都不会去管它它们不是θ的函数。目标函数定义为期望回报J(θ) Eτ~pθ [ R(τ) ] ∫ pθ(τ) R(τ) dτ其中R(τ)可以是一个轨迹的总回报也可以是折扣回报。2.2 梯度重写log-ratio魔术现在要求J(θ)关于θ的梯度严格推导如下∇θ J(θ) ∇θ ∫ pθ(τ) R(τ) dτ ∫ ∇θ pθ(τ) R(τ) dτ ∫ pθ(τ) ∇θ log pθ(τ) R(τ) dτ Eτ~pθ [ R(τ) ∇θ log pθ(τ) ]这里用的是对数导数技巧∇θ log pθ(τ) ∇θ pθ(τ) / pθ(τ)所以pθ(τ)乘上∇θ log pθ(τ)就等于∇θ pθ(τ)。这个技巧的意义在于我们没法知道环境的概率分布也就没法直接算∇θ pθ(τ)但在给定一条采样的轨迹后∇θ log pθ(τ)是可求的。期望依然在但我们可以通过采样去近似它。接下来把pθ(τ)展开梯度的对数只剩下策略部分∇θ log pθ(τ) ∇θ [ log p(s0) Σ log πθ(at|st) Σ log p(st1|st, at) ] Σ ∇θ log πθ(at|st)环境项全部消掉了。这是策略梯度最迷人的地方你完全不需要知道环境的动力学模型只需要按当前策略采样然后计算动作概率的对数梯度就行。2.3 为什么要用对数概率×回报作为更新方向把上面的结论合起来梯度可以写成∇θ J(θ) E [ (Σ ∇θ log πθ(at|st)) · R(τ) ]也就是说一条轨迹的贡献是把每一步动作的梯度方向加起来再乘以整条轨迹的回报。从直觉上理解这个式子干了这样一件事如果一条轨迹最终得到正回报R0那么沿∇θ log πθ(at|st)方向调整参数可以提高该轨迹中所有动作的概率如果R0就降低所有动作的概率。这些动作里当然有好的也有坏的R0就把好坏动作的概率一起抬高了R0就一起压低了。这正是方差问题的雏形后面专门分析。2.4 因果性小技巧从整条回报换成当前时刻之后的回报另一个重要细节是轨迹中某个动作at其实只影响它之后的累积回报而不影响它之前的回报。但在上述推导中我们用的是整条轨迹的R(τ)理论上是无偏的方差却不必要地大。于是实际算法里几乎都会做一步因果化替换把R(τ)换成从当前时刻开始的折扣回报Gt rt γ rt1 γ² rt2 ...为什么这样做期望不变因为动作at对过往时刻的回报没有因果影响那些过往回报乘以∇log πθ(at|st)的期望为0。既然不影响期望方向却能显著减少单个样本带来的随机波动这就是一个免费的方差改良手段。2.5 基线的出现公式里天然留了一个口子还有一个关键点上述梯度期望中如果我们在回报上减去一个常数b期望会怎么变∇θ J(θ) E [ ∇θ log πθ(τ) · (R(τ) - b) ]因为E [ ∇θ log πθ(τ) ] Σ ∇θ πθ(τ) ∇θ 1 0所以减b不会改变梯度的期望梯度依然是无偏的。但方差却可以大幅降低。这个减一个数不影响期望但降低方差的操作就是baseline也可以叫基线。很多第一次接触的人会问既然不减也正确为什么要减因为R本身就是随机变量不同轨迹的回报差异极大从几百到几千都有一个巨大正回报乘以梯度会造成参数一步冲得太远。减去一个基线b让被乘的数值围绕0波动方差立刻小很多。这是后面所有策略梯度加速方法——从REINFORCE with baseline到Actor-Critic到GAE——的数学起点。3. REINFORCE最小实现第一份能跑的代码与它的致命方差3.1 网络、采样器和轨迹收集理解推导后写一个最简的策略梯度算法REINFORCE其实只需要很少的代码。核心组件只有三个策略网络、采样循环、回报计算。我用PyTorch写一个CartPole上的最小版本离散动作环境。策略网络就是一个输入状态、输出动作logits的MLPimport torch import torch.nn as nn import gymnasium as gym class PolicyNet(nn.Module): def __init__(self, obs_dim, act_dim, hidden64): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, act_dim) ) def forward(self, obs): return self.net(obs)这里输出的是logits不是softmax后的概率。因为PyTorch的Categorical分布会自动做softmax我们只需要logits。这里有个实现细节保存概率的对数要直接调用dist.log_prob(action)不要先算概率再手动log数值稳定性差很多。单个回合的采样循环把每一步的log_prob和reward存下来注意action要从分布中sample而不是argmax否则就退化成确定策略了def collect_episode(env, policy): obs, _ env.reset() log_probs [] rewards [] done False while not done: obs_t torch.as_tensor(obs, dtypetorch.float32) logits policy(obs_t) dist torch.distributions.Categorical(logitslogits) action dist.sample() log_probs.append(dist.log_prob(action)) obs, reward, terminated, truncated, _ env.step(action.item()) done terminated or truncated rewards.append(reward) return log_probs, rewards3.2 从后往前算折扣回报折扣回报的计算有一个经典技巧从回合末尾向前累加这样一次遍历就可以得到每一步的Gt不用每次重新求和。def compute_returns(rewards, gamma0.99): g 0 returns [] for r in reversed(rewards): g r gamma * g returns.insert(0, g) return torch.tensor(returns, dtypetorch.float32)为什么要折扣除了经济直觉上未来的不确定性和机会成本折扣因子还能有效压缩方差。折扣越大越考虑长远折扣越小越短视但这个参数其实也承担了让远端回报对梯度的贡献不爆炸的作用。3.3 主循环梯度上升变梯度下降最后是更新过程。注意我们的目标是最大化回报的期望所以理论上是梯度上升。在优化器里统一做梯度下降那就给loss加个负号policy PolicyNet(obs_dim4, act_dim2) optimizer torch.optim.Adam(policy.parameters(), lr1e-3) for episode in range(500): log_probs, rewards collect_episode(env, policy) returns compute_returns(rewards) # 减去回合平均回报等价于一个最简单的baseline returns (returns - returns.mean()) / (returns.std() 1e-8) loss -torch.stack(log_probs) * returns loss loss.mean() optimizer.zero_grad() loss.backward() optimizer.step()这里的returns归一化其实就是批内baseline加上缩放控制能让梯度没那么狂野。这个实现的完整逻辑闭环是采样一条轨迹 - 算每个时刻的回报 - 让高回报动作的概率增大 - 更新参数 - 继续采。3.4 第一次训练曲线为什么像心电图如果你亲手跑这个版本大概率会看到reward曲线在0到200之间疯狂跳动完全不像某些教程里那些平滑上升的图。这不是环境问题而是REINFORCE的天然属性。原因很简单单条蒙特卡洛轨迹的回报方差极大。一个回合里随便哪个动作走偏一点拐杖倒了、小车翻了回报就从200直接变0而这个随机波动会被直接乘进梯度里再叠加到参数更新上。所以REINFORCE有个广为人知的评价理论上正确实际慢且抖。不过这个代码的意义和价值在于它是策略梯度家族的最小信息基准。后面所有高级方法都是在同一个骨架上打补丁——要么减小方差要么提升样本效率要么限制更新步幅。4. 与方差的漫长斗争baseline、优势函数和actor-critic4.1 方差从哪来一个轨迹和一个梯度的差距要理解为什么策略梯度训练这么不稳定得拆开方差的构成方差的第一层来自蒙特卡洛回报本身。一条轨迹的回报是所有随机转移和随机动作叠加出来的结果长度越长涉及的不确定性越多单次回报的波动范围就越大。数学上等价于用一次样本来估计一个高方差分布的期望。第二层来自整条轨迹乘以整条轨迹的梯度。一个不好的动作在低回报轨迹里它的log_prob梯度会被一个大负数放大一个碰巧好的动作也会被高分进一步放大。这导致每个单个动作的更新方向里都混了大量环境噪声。第三层来自相邻参数更新之间的不独立性。策略梯度是on-policy的参数一更新下一批采样数据的分布就变了我们相当于时刻在移动的靶子上射箭。4.2 先减一个常数baseline免费但不彻底上一章代码里我顺手做了returns均值减法这就是一个常数baseline。它把所有回报中心化一下「这个回合比其他回合好还是差」作为信号。这能解决一部分问题但不彻底。原因是不同状态下的回报尺度完全不同。某些状态天然action都是高分某些状态下天然低分。用一个统一的常数去减减不到点子上。你需要的是一个与状态相关的baseline。4.3 状态价值baselineactor-critic正式登场最自然的baseline就是状态价值Vπ(s)给定当前状态按当前策略继续走期望能得到多少回报。用V(s)当baselineA(s,a) Q(s,a) - V(s)这个A就是优势函数advantage它回答的问题是在这个状态下我采取这个动作比平均情况好多少A0说明这是个好动作A0说明是个坏动作。梯度变成∇θ J(θ) E [ ∇θ log πθ(a|s) · A(s,a) ]这个变换的意义不仅仅是降方差。它还改变了学习信号的语义从这条轨迹好不好变成这个动作在这一状态下好不好。信号更细粒度了策略更新更有针对性。为了得到A(s,a)我们通常用一个神经网络Vφ(s)去逼近真实价值函数。于是就有了两个网络actor策略网络πθ负责输出动作分布。critic价值网络Vφ负责估计状态价值给actor提供优势信号。critic的训练就是一个普通的回归问题让Vφ(s)逼近蒙特卡洛回报Gt或者TD目标。actor继续按策略梯度更新。这种结构就是actor-criticAC算法。它名字起得很形象「评论家」告诉「演员」每个动作相对于平均水平好多少「演员」根据评价调整自己的表演。4.4 为什么优势函数这么重要我自己的体会是从REINFORCE到actor-critic算法最大的进步不是跑得更快而是信号的语义变了。REINFORCE学到的是全局统计规律高分轨迹里的动作整体概率上调actor-critic学到的是局部因果关系这个状态下这个动作比平均水平好多少。为了更直观可以类比成考试分析。如果你想找出「哪些题型值得重点练习」只看总分是完全不够的因为总分受太多因素影响。最有效的方式是每个题目得分对比全班平均分。某道题你比平均分高很多说明这块是你的强项低很多说明是弱项。优势函数干的就是这件事它不是关心这个动作本身得分多少而是关心它相对该状态下平均水平高多少。这个差比绝对分可靠得多。4.5 用TD误差近似优势进一步降低方差实践中我们往往不会完全用蒙特卡洛来估计A因为MC方差依然很大。更常见的是用TD风格的误差。举一个最简单的GAE前身单步优势A(s,a) ≈ r γ V(s) - V(s)这个式子的好处是它不需要跑完整个回合就能估计优势方差比MC小。坏处是如果Vφ估计不准偏差会大。偏差和方差之间的权衡正是后面GAE要做文章的地方。5. 从REINFORCE到PPO现代策略梯度算法到底在改什么5.1 为什么能用旧数据再更新重要性采样REINFORCE每采集一批数据更新一次参数然后这批数据就扔掉。原因是策略变了以后旧数据不再符合新的采样分布直接复用在数学上不对。数据效率很低。TRPO和PPO都在解决同一个问题如何让旧数据可以被多次更新。关键工具是重要性采样。设旧策略为π_old新策略为π_new对于旧数据中的(s,a)它的概率在两种策略下是不同的。如果我们想用旧策略采样下的期望来估计新策略下的期望需要乘以一个比率ρ π_new(a|s) / π_old(a|s)这个ρ就叫做重要性权重。如果新旧策略很接近ρ接近1旧数据仍然有效如果差别很大ρ偏离1说明旧数据已经不足以代表新策略这时权重就要被控制住。TRPO和PPO都围绕这个ρ做文章区别在于如何控制新旧策略不能差太远。5.2 TRPO的硬约束和PPO的软裁剪TRPO的做法是给更新加一个KL散度约束保证新旧策略的距离不超过某个δ。理论很漂亮但实现很重需要用共轭梯度法求解约束优化还要算KL散度的二阶近似工程上非常吃力日常调参更是噩梦。PPO的思路则是把约束软化与其硬性限制KL不如在目标函数里直接加一道保险。PPO的经典目标是L_clip(θ) E [ min( ρ · A, clip(ρ, 1-ε, 1ε) · A ) ]这里的clip含义是把ρ限制在[1-ε, 1ε]区间内常见ε0.2。min操作保证当A0时我们最多让概率提高到1ε倍当A0时最多让概率降低到1-ε倍。超出这个范围的更新梯度为0不起作用。打个比方TRPO是你最多能走10米PPO是你可以随便走但每走出一步都检查一下自己是不是离起点太远了超过一个范围就罚停。PPO用极小的工程代价获得了接近TRPO的效果而且超参数不那么敏感这就是它成为事实标准的原因。5.3 GAE偏差和方差的滑杆现代PPO实现里优势估计使用的是GAEGeneralized Advantage Estimation它其实是用一个参数λ来平滑地控制到底用多长的回报窗口A_t Σ (γλ)^l δ_{tl} 其中 δ_t r_t γ V(s_{t1}) - V(s_t)λ0时A就是单步TD误差方差最小但偏差大依赖价值函数估计准。λ1时A退化为蒙特卡洛回报的优势偏差小但方差大。GAE把方差和偏差的权衡变成一个可以调的旋钮。实践中λ0.95是相当稳的起点如果你发现训练过程中critic loss很难降下来可以适当调低。这里值得记住的原则是价值函数估计越准越可以用偏MC的优势价值函数噪声大就更得靠TD类的平滑。5.4 熵正则防止策略过早锁死训练策略梯度模型时常会遇到一种尴尬训练还没见起色策略的熵先塌了。熵塌缩意味着策略几乎变成了确定性策略只敢输出一个动作不再探索。解决方法很简单在目标函数里加一项熵正则。L L_policy 熵正则系数 β · H(π)熵的定义是H(π) -Σ π(a|s) log π(a|s)熵大意味着分布均匀探索充分熵小意味着分布尖锐确定性高。加熵正则就是给探索装个地保险不让策略把所有概率压到一个动作上。常见β取0.001到0.01稀疏奖励环境需要更大。5.5 PPO一次训练的标准流程把上面所有组件串起来一次标准PPO训练循环长这样用当前策略π_old在环境中采集一批数据记录每个step的s,a,r,done以及π_old(a|s)。用GAE和当前价值网络计算优势估计A和回报目标G。从这批数据里切minibatch重复多个epoch更新计算新策略πθ(a|s)得到重要性权重ρ。计算clip目标。actor损失 -期望min(ρA, clip(ρ)A) - β熵。critic损失 均方误差(V(s), G)。更新完θ和φ后清空数据回到第1步。PPO与REINFORCE最大的差别除了clip之外就是它能让一组数据小步地更新多个epoch样本效率高很多。现代库里的PPO会配合向量化环境并行采样一批几千条transition效率和稳定性都是REINFORCE不能比的。6. 实现策略梯度的常见坑与调试清单强调一下策略梯度是少数看着end-to-end很顺、实际一跑就崩的算法家族。这里列几个我自己在实战中踩过的坑每条都对应一个可以检查的方向。6.1 坑一奖励尺度不对梯度像一个醉汉策略梯度的梯度大小与回报尺度成正比。如果reward量级动辄上千梯度值也会巨大参数一步就飞出物理边界如果reward本身就接近0梯度信号又太小模型一动不动。我调试的第一个策略梯度项目是机械臂仿真环境奖励在几十到几百之间波动直接把REINFORCE跑起来loss直接NaN。对策有两类环境层检查reward的工程定义必要时压缩尺度。比如对奖励做clip或者除以一个常数。算法层对所有优势或回报做标准化例如减均值除以标准差或者使用running normalization。推荐优先从算法层处理因为环境reward的语义往往和你对算法的修改纠缠在一起改环境容易伤及任务本身的定义。6.2 坑二熵塌缩探索提前死亡训练到中后期你可能会发现策略几乎不更新了表现也不涨。这时候打印一下策略的熵如果离散动作的概率分布几乎one-hot连续动作的std已经降到极小那就是熵塌缩。一个小技巧是连续控制里关注log_std参数。有些框架直接输出log_std如果训练中log_std一路降到-5以下说明探索能力几乎归零。对策在目标函数里加熵正则。检查是否更新幅度过大。PPO的clip参数ε可以适当调小比如0.1。连续动作策略里给log_std设一个下限避免它无限缩小。我见过不止一次去掉熵正则条件后算法跑着跑着突然崩溃加上正则后虽然奖励略低但能稳定收敛。探索这个东西在策略梯度里不是态度问题是数学上必须有。6.3 坑三只看一次实验的曲线被随机性骗了策略梯度实验的随机性大得惊人。同一个代码、同一个超参数换个随机种子收敛速度可能差三倍甚至有的种子成功、有的种子失败。所以我的习惯是每个实验至少跑5个种子。汇报结果时画中位数曲线同时画分位数区间。判断一个改动有没有效看的是几个种子上的分布而不是单条reward曲线。很多论文里的对比图大家默认也是有这个统计意识的。收藏这篇文章后如果你只记一条经验那就是多跑几个种子。6.4 坑四复现性细节gym版本、线程、种子初始化复现实验是策略梯度里面最生气的事同一个seed在别人机器上跑就是另一个结果。常见的原因不完全在算法而在工程环境gymnasium/gym版本差异会影响环境动力学比如termination和truncation的处理不同。转置seed时没有同时固定numpy、torch、环境各自的随机源。多进程采样时线程并发导致采样顺序不稳定进而影响批内数据的分布。我的做法是写一个setup函数统一处理seed并固定到进程级别def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False6.5 坑五使用了旧策略状态继续训练分布不匹配这个问题常发生在加载一个训练到一半的模型继续训练时。策略梯度的采样分布依赖当前策略加载模型前后如果熵值、reward scale完全不同新一批数据的分布和旧数据分布差异巨大梯度更新就会变得极不稳定。遇到过的情况是用A模型训练了100万步保存某天改了一行代码或者换了环境版本重新加载A模型继续在旧代码上训练reward从原本的300直接跌到50之后再也回不来。排查下来是reward normalization的统计量没有重置旧均值旧方差把新信号的尺度压坏了。对策加载模型继续训练前务必清空running statistics、重置熵系数、甚至考虑让初始KL不再惩罚。调试策略梯度有点像调一艘很敏感的船方向大致正确但任何一节参数变化都可能带来不可忽视的震荡。建议一次只改一个变量日志里至少打印五项reward均值、策略熵、clip比例、critic loss、优势均值。clip比例特别有用——如果clip比例超过20%说明新旧策略差异过大学习率大概率太高。写在最后的一点个人建议如果你现在正准备从零开始学策略梯度我的建议是不要一上来就调PPO库先手工把REINFORCE在CartPole上完整写完、跑通、观察一遍。这个过程花的不是冤枉时间你会亲眼看见方差长什么样知道减baseline到底减掉了什么也知道打印熵这种习惯为什么重要。有了这个底子后面再上PPO、接分布式框架都不会只是调参机器人。策略梯度这块内容理论上环环相扣实践上处处有坑。希望这份整理能帮你把从第一个REINFORCE到现代PPO之间的路都走明白也欢迎你把自己实际调试中遇到的反常现象拿出来一起讨论——很多我自己的经验恰恰就是被各种反直觉的曲线教会的。
返回列表