在强化学习的策略优化家族里TRPO和PPO是绕不开的两个名字。尤其是PPO几乎成了现代深度强化学习默认的基线算法。很多人直接调库跑实验但对它们背后的逻辑——为什么TRPO要搞“信赖域约束”为什么PPO又简化成“近端裁剪”——往往一知半解。这篇博文我会从实际调参和复现的角度把这两个算法掰开揉碎讲清楚它们的来龙去脉、核心公式的直觉、实现时的关键细节以及你在自己的项目里该怎么选、怎么调踩过的坑也一并整理出来。我默认读者已经懂一点点强化学习基础比如策略梯度、Actor-Critic结构。如果这些概念还比较模糊也不用担心我会在讲原理时顺便用生活化的类比解释清楚。1. 从“策略梯度害怕大步子”说起1.1 策略梯度为什么训练不稳定我们先回顾一下最朴素的策略梯度REINFORCE或者带baseline的版本。它的更新方向是让策略在“好动作”上的概率增大在“坏动作”上的概率减小。数学上写成g E[ ∇θ log πθ(a|s) * A(s,a) ]这里的A(s,a)是优势函数表示这个动作比平均水平好多少。直觉很简单如果这个动作比平均好就加大它的对数概率如果比平均差就减小。问题出在哪这个“梯度”是期望形式的但我们实际用的是采样估计。策略πθ一旦更新下一步生成数据的分布就变了。如果学习率设得稍大一点每一步更新都会让策略分布发生剧烈变化导致训练曲线忽上忽下甚至直接发散。我最初在某个模拟环境的连续控制任务上调REINFORCE那个loss曲线简直像心电图奖励一上来就崩阈值怎么调都不稳。根本原因就是“步长”没有约束。普通的梯度下降在监督学习里没问题因为数据分布是固定的只要loss函数平滑小步长总能收敛。但在强化学习里数据由策略自己生成策略一变数据分布跟着变大步长更新就容易把策略推到“数据稀薄”的危险区——新策略下采样的样本分布和旧策略差异太大优势函数估计失效训练自然崩溃。1.2 为什么需要“信赖域”这种约束大家想想在小步迭代时用旧策略采样的数据来估计新策略的梯度这个偏差有多大有一个量化指标叫KL散度衡量两个分布之间的差异。如果新旧策略的KL散度足够小那么用旧数据估计新策略的效果就是可信的。反过来如果KL散度很大旧数据就几乎没什么参考价值。于是“信赖域”这个思想本质上就是给策略更新加一道紧箍咒允许你更新但必须保证新旧策略的KL散度不超过某个阈值。类比一下就像修水管你当然要拧扳手但每次只能拧几度不能一下子转一整圈否则水管直接崩裂。TRPO就是这种“拧几度”的算法。这个思路不是拍脑袋想出来的它背后有严格的数学证明在某种近似下只要新旧策略的KL散度受限新策略的真实性能就有一个下界保证。这就是“信赖域”得名的由来——在这个区域内的更新策略提升是有理论背书的。2. TRPO硬约束下的策略更新2.1 目标函数与约束的数学表达TRPO想要优化的是这样一个带约束的问题maximize θ E[ πθ(a|s) / π_old(a|s) * A_old(s,a) ] subject to E[ KL[π_old(·|s) || πθ(·|s)] ] ≤ δ这里πθ/π_old是重要性采样比率用旧策略的数据来估计新策略的收益。约束条件是“新旧策略的平均KL散度不超过δ”。δ就是信赖域半径通常取0.01左右。这个约束让问题变得复杂很多。参数θ是高维的直接带约束做梯度上升没法简单迭代。TRPO的经典做法是用“共轭梯度法”来近似求解这个约束优化问题。具体做法是先对目标函数做一阶近似也就是求梯度)对约束做二阶近似也就是求Fisher信息矩阵H然后求解线性系统 Hx g其中g是梯度。得到的x就是考虑了约束后的更新方向再通过线搜索确定满足KL约束的最大步长。2.2 共轭梯度与线搜索TRPO的具体实现细节实操时TRPO的每一步更新大致分四步用当前策略采样一批轨迹计算优势函数A(s,a)。计算目标函数的梯度g。用共轭梯度法求解 Hx g。这里的H是Fisher信息矩阵的样本近似计算方式是 E[ ∇θ log π * (∇θ log π)^T ]注意不需要显式构造H矩阵只需要定义H乘以向量v的操作然后用共轭梯度迭代求解。得到搜索方向x之后从当前参数出发沿x方向做线搜索找到一个步长λ使得更新后的策略满足KL约束同时目标函数有一定提升。我在复现TRPO时最头疼的就是第3步。共轭梯度法本身不难难的是Fisher矩阵-向量积的计算。如果你的策略网络是高斯分布输出连续动作那么∇θ log π其实包含两部分均值的梯度和标准差的梯度。计算Hx时要小心处理否则结果差得离谱。我的做法是对每个样本单独算∇θ log π然后求外积的平均再乘以向量v。这一步如果用PyTorch的autograd可以有两种方式方法一先算出所有样本的log π对θ的梯度矩阵grads形状是[样本数, 参数维度]然后Hx (grads v) * grads然后在样本维度求平均。这个做法直白但内存开销大参数多时grads矩阵很大。方法二利用PyTorch的torch.autograd.grad计算(v * logπ).sum()的梯度这里v作为与梯度同形状的向量先对logπ做线性组合再求梯度等价于Hx。这个做法更省内存也是主流实现里用的。线搜索也很关键。不能随便按固定步长走那样约束很容易超。我的经验是先设初始步长为1然后不断折半步长乘0.5或0.8同时检查更新后的KL散度是否小于δ以及新旧策略的目标函数差值是否为正。如果折半很多次之后还是失败就放弃这步更新直接用旧策略。2.3 TRPO的优点与尴尬TRPO最大优点是稳定。训练过程中KL散度被死死压在δ附近策略不会突然漂移所以很多任务上它的样本效率比原始策略梯度高不少。当年在仿真环境里的很多连续控制任务上TRPO都能稳定拿到不错的分数这也是它一度成为主流的原因。但尴尬也很明显。每步更新都要做大量共轭梯度迭代一般几十次到几百次而且每次迭代都要在整个batch上算Hx计算量非常大。我的实际体验是同样的网络和样本量TRPO每一步的耗时比后来的PPO慢5到10倍。如果任务本身simulation很快那还能忍一旦simulation模型复杂、状态空间大这个开销就非常劝退。另一个问题是实现复杂度高Fisher矩阵、共轭梯度、线搜索这些组件任何一个写错都会导致训练异常调试起来头痛。3. PPO把硬约束变成软惩罚3.1 近端裁剪的直觉与公式PPO的核心思路是别搞那么复杂的约束了直接在目标函数里加一个裁剪项限制重要性采样比率。它的目标函数长这样L_CLIP(θ) E[ min( r(θ) * A, clip(r(θ), 1-ε, 1ε) * A ) ]其中r(θ) πθ(a|s) / π_old(a|s)ε通常取0.2。这个公式的直觉很清晰如果A大于0说明这个动作比平均好我们希望增大r但最多增大到1ε再大就不再提供额外梯度如果A小于0我们希望减小r但最少减到1-ε再减小也没用。换句话说裁剪相当于给策略更新加了一个隐形的“信赖域”边界比率不能偏离1太远也就是新旧策略差异不能太大。我经常用一个类比来帮助理解就像开车时的油门限速器。你想加速但油门踩到一定深度就不再增加燃料防止车速过猛你想减速刹车到一定程度也限制住防止你慌不择路一脚跺死。这个限速器虽然不像TRPO那样精确监督KL散度但它在实际中几乎能达到同样的稳定效果而且计算极便宜。3.2 PPO的两种变体CLIP与KL惩罚PPO原文里给了两种目标一个是上面说的CLIP另一个是带KL惩罚项的目标简单说就是在原目标函数后面加上-β * KL。但我在实践里几乎只用CLIP。KL惩罚版本不好调ββ太大更新太保守太小又等于没约束而且KL散度的尺度会随网络宽度变化没有CLIP版本那种“归一化”的自适应特性。CLIP版本的ε0.2是一个普适性很好的设置几乎不需要为不同任务调整。只有极少数任务我会考虑把ε调小到0.1或调大到0.3后面我会讲什么时候需要动它。3.3 PPO的Actor-Critic实现要点实际工程里的PPO几乎都是Actor-Critic结构共享一个特征提取网络。整个训练流程分几个阶段用当前策略在环境里跑T个时间步常用T2048或4096收集状态、动作、奖励、下一个状态。计算回报和优势函数。这里推荐用GAE广义优势估计它能平衡偏差和方差。GAE的关键参数是λ通常取0.95到0.99。λ越大方差越小但偏差越大λ越小偏差越小但方差越大。我一般默认0.95在任务奖励比较稀疏时调到0.99。把采集的数据用于多轮小批量梯度更新。每轮更新时把数据和当前策略的比率r算出来然后按CLIP目标求梯度。通常一个batch数据会被更新4到10轮称为epochmini-batch大小取64到256不等。更新完策略之后还要更新价值网络Critic通常用MSE损失和策略损失一起回传或者分开更新。这里有个容易搞错的地方计算r(θ)时分母π_old是采样时的行为策略它在整个更新过程中必须是“固定”的。你会在代码里记录采样时的策略参数或者保存采样时每个动作的对数概率log_prob_old。所有更新轮次中r exp(log_prob_new - log_prob_old)其中log_prob_new是在当前更新轮次的网络参数下重新计算的动作概率。这一点如果处理不当比如在同一个迭代中反复更新log_prob_old就完全破坏了重要性采样的前提。4. TRPO与PPO的全方位对比与选型建议4.1 关键差异对照表维度TRPOPPO-CLIP约束方式显式KL散度硬约束目标函数裁剪软限制数学复杂度高Fisher矩阵、共轭梯度、线搜索低只有裁剪项每步计算开销高共轭梯度迭代低和普通策略梯度差不多稳定性理论保证强非常稳定实际稳定但缺少严格单调保证超参数敏感性需要对δ、CG迭代数敏感对ε不敏感主要调GAE和batch调试难度难容易出数值问题易PyTorch/框架都有现成实现适合场景计算充足、需要严格约束的研究常规RL任务、工业落地、快速原型这张表是我在这些年跑各种任务后的真实体会。TRPO的“理论保证”虽然漂亮但在多数实际任务上PPO能达到同等的稳定效果而且训练速度快好几倍调试成本低一个量级。所以除非你是在研究策略优化理论本身否则我强烈建议直接用PPO。4.2 什么时候选TRPO什么时候选PPO我的选择标准很简单如果你的环境simulation极快比如一步只需要几微秒且你不在乎训练时间同时对策略更新有严格的KL约束需求比如某些安全敏感的控制任务TRPO仍然有价值。如果你用的是深度网络、高维状态、多任务或者训练过程需要频繁调整PPO是默认选项。特别是机器人控制、游戏AI、推荐系统模拟PPO都是稳妥的基线。如果GPU计算量大但环境simulation慢PPO的优势更明显——因为“环境采样”才是瓶颈时TRPO额外增加的计算量就是纯浪费。我之前在某个控制任务上做过对比同样一千步环境交互预算PPO已经到达平台期TRPO还在慢慢爬。因为TRPO每步更新太吃计算相同时间内能做的策略更新次数远少于PPO导致实际样本效率反而低。所谓TRPO的“样本效率高”是在忽略单步计算开销的前提下说的。端到端看PPO才是更高效的选择。5. 实操过程我用PPO训练一个连续控制智能体5.1 环境与网络结构设计为了验证和演示我常用常见的连续控制环境来测试。假设我们要让一个机器人关节臂去触碰目标点。状态是关节角度和角速度动作是关节力矩。策略网络是一个多层感知机隐藏层256256输出高斯分布的均值和标准差标准差用log_std参数表示并初始化为-0.5左右让初始探索比较充分。价值网络类似结构输出标量价值估计。5.2 训练循环与关键代码片段我用PyTorch写一个精简的PPO训练循环伪代码如下for iteration in range(max_iterations): # 1. 采集数据 batch collect_trajectories(env, policy, steps2048) # batch包含 states, actions, rewards, dones, old_log_probs # 2. 计算优势和回报 advantages, returns compute_gae(batch, value_net, gamma0.99, lam0.95) # 3. 多轮更新 for epoch in range(4): for mini_batch in get_mini_batches(batch, batch_size128): # 重新计算当前策略下动作的log prob log_probs policy.get_log_prob(mini_batch.states, mini_batch.actions) ratio torch.exp(log_probs - mini_batch.old_log_probs) # 裁剪目标 surr1 ratio * mini_batch.advantages surr2 torch.clamp(ratio, 1.0 - clip_eps, 1.0 clip_eps) * mini_batch.advantages actor_loss -torch.min(surr1, surr2).mean() # 注意负号因为我们要最大化 # 价值网络损失 value_pred value_net(mini_batch.states) critic_loss F.mse_loss(value_pred, mini_batch.returns) # 总损失可以加上entropy bonus鼓励探索 entropy policy.entropy(states).mean() loss actor_loss 0.5 * critic_loss - 0.01 * entropy optimizer.zero_grad() loss.backward() # 梯度裁剪有助于稳定 torch.nn.utils.clip_grad_norm_(policy.parameters(), max_norm0.5) optimizer.step()这里有几个细节需要注意compute_gae的实现要处理donesGAE只在非结束状态使用下一状态的价值预测。很多bug都出在“末期状态”处理上。old_log_probs必须是在采样时用行为策略算出来的不能随着更新变化。ratio的计算要小心数值稳定性建议先算log_probs - old_log_probs再exp避免直接除。梯度裁剪是一个“保命”设置。就算有CLIP我还是会加一个max_norm0.5防止极端batch造成的梯度爆炸。5.3 训练曲线观察与调整技巧训练PPO时我最常看的几个指标是平均回报、策略的KL散度新旧策略分布差异、裁剪比例ratio超出[0.8,1.2]的样本占比、价值损失。这些指标能帮你判断训练是否健康。如果KL散度每步都在0.05以上说明裁剪约束没有生效可能ε过大或学习率太大需要降低学习率。如果裁剪比例总是接近0.3以上说明策略更新太快也要降低学习率。如果价值损失长期不下降说明Critic不够准可能网络容量不足或者GAE参数不合适。我踩过一个典型的坑entropy bonus设成0.01时初期探索还可以但到后期策略已经收敛entropy还在惩罚导致策略微微抖动。后来我把entropy系数改成0.005并在后期线性退火到0曲线平稳了很多。另一个坑是在多进程环境采样时不同进程的奖励尺度差异大导致优势计算被某些极端奖励主导。我的解决办法是给优势做标准化——所有advantages除以它们的标准差。这个小技巧对训练稳定性的提升非常明显。6. 常见问题排查与避坑实录6.1 训练初期就发散怎么办如果发现回报一路下滑或者NaN频繁出现先检查这几个地方网络输出的log_std是否初始化合理。我见过新手把log_std初始化成0相当于标准差为1可能太大也可能太小。如果动作范围很小标准差太大的话探索过度训练容易发散。建议按任务的奖励尺度来先在环境里随机跑几步看看动作分布是否接近合理区域。优势标准化有没有做。没做标准化时如果奖励量级是1000优势也会很大CLIP目标几乎只受符号影响梯度尺度过大。加上标准化后基本能解决这个问题。学习率是否过高。PPO常用的策略学习率在3e-4到1e-3之间。如果使用Adam我一般从3e-4起步。超过1e-3很容易出现问题。6.2 训练后期难以收敛或者震荡后期震荡往往是“探索方差”和“更新步长”不匹配。此时可以逐渐降低探索方差设计一个熵系数退火或者让高斯策略的log_std在训练后期被正则化防止过大的标准差。降低PPO的更新epoch数比如从4降低到2减少过度利用同一批数据。增加batch大小如果你的显存允许把batch size从128提到256或512往往能平滑后期损失。6.3 PPO的“隐形bug”共享网络造成梯度冲突如果Actor和Critic共享特征层可能会发现训练时actor loss下降但回报不动。原因是价值网络反馈的梯度干扰了特征提取。我的经验在小型任务上共享是好的但在复杂任务上最好分开或者至少在不同层用不同的学习率、使用更稳健的归一化层。我曾在共享网络中加过LayerNorm效果比BN好因为RL的batch样本之间强相关BN容易失效。6.4 TRPO调试的专属问题如果你坚持要跑TRPO我也给你几个提醒Fisher矩阵的数值不稳定时可以加上一个小的阻尼项比如0.01 * I防止共轭梯度求解过程中出现矩阵奇异。这个阻尼系数需要小心调整。线搜索开始时建议先用默认步长1如果KL超限再逐步减半。有些实现里还需要检查“约束的线性近似误差”太复杂的话可以跳过。注意KL计算的方向KL是 old || new 还是 new || old两者不对称。TRPO原始论文用的是从旧策略到新策略的KL别搞反。7. 一些我最后想说的经验做了这么久RL项目我的体会是把策略优化说成“配方”其实很贴切。TRPO和PPO就像是两个烹饪流派一个精工细作重金打造一个快手下锅口味稳定。两者都是在解决同一个痛点——如何让策略更新既不冒进又不畏缩。实际上不需要执着于哪个更高级关键是你手里的资源和任务性质适合哪一种。PPO虽然“土”一些但它在大多数场景下性价比最高这也是为什么那么多开源库里默认跑的就是PPO。最后分享一个小技巧当你拿到一个新环境时先用极少的几万步跑一个“快速测试”只观察KL散度、裁剪比例和早期回报曲线是否正常。这一步能过滤掉八成以上的实现bug和超参问题比直接跑完整训练高效得多。把这些基本功做扎实TRPO和PPO在你手里就不再是黑盒而是随时可以调整的可靠工具。