资讯动态

策略梯度原理与实现:从REINFORCE到PPO的完整演进攻略

发布时间:2026/10/22 3:38:40 来源:尧图企业网站定制
深度强化学习这几年最常被问到的一个问题就是既然Q-learning能把值函数逼近得那么好为什么还要费劲去直接优化策略这个问题我在做连续控制任务时找到了答案。假设你在做一个机械臂控制动作是关节力矩是一个连续值用Q-network输出离散动作的Q值根本没法解决。而策略梯度直接把策略建模成带参数的概率分布用梯度上升一步步把期望回报拉高天然支持连续空间。第6讲我就把自己从REINFORCE到Actor-Critic再到PPO这条路上踩过的坑、验证过的细节完整梳理一遍。这篇内容适合已经开始接触强化学习基础、想深入理解策略梯度原理并且准备动手实现自己第一个策略梯度算法的朋友。我会把推导过程、代码实现、调试经验混在一起讲尽量说人话不堆公式吓人。看完你至少能独立实现一个可运行的REINFORCE并知道它为什么容易崩、怎么改进。1. 为什么直接优化策略是刚需1.1 从Q-learning到策略优化的自然演进先回顾一下基于值函数的方法在做什么。Q-learning的思路是先估计每个状态下所有动作的期望回报然后每次选Q值最大的动作。这相当于通过“间接”的方式告诉策略该干什么——策略只是值函数的一个附属品。DQN在Atari上表现很好但只能在离散动作空间里工作因为argmax操作要求你把所有动作的Q值算出来然后挑最大。这就引出了两个核心问题。第一如果动作空间是连续的比如力矩、速度、坐标你不可能把动作空间离散化成无限多个点去穷举Q值。第二即使动作离散很多任务的真实最优策略本质上是带随机性的比如扑克中的虚张声势、躲避球里的假动作。确定性策略没法表达“以某个概率选择动作A以另一个概率选择动作B”这种需求而策略梯度天然是一个概率分布刚好能覆盖这两类问题。另外一个容易被忽略的点基于值函数的方法在训练中通常把目标函数设为“使得估计值和目标值之间的TD误差最小”这本质上是在做一个回归任务。但回归目标本身会因为探索、经验回放而不断变化导致训练不稳定。策略梯度直接对着“期望回报”这个真正的优化目标求梯度逻辑上更干净也更适合跟深度学习结合去做端到端训练。1.2 策略梯度的直觉理解偏导数的巧妙传递策略梯度的核心公式写出来是这样[ \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(\tau) \right] ]其中 (\tau) 是一条完整的轨迹(R(\tau)) 是从 (t0) 开始到结束的累积奖励。你别被这个积分式子吓到直觉其实非常朴素如果某条轨迹的回报比平均值高那就增大这条轨迹上所有动作的概率回报越低就减小这些动作的概率。也就是说做完一条轨迹之后回头看给表现好的动作“加分”给表现差的动作“减分”。用代码逻辑来说明就更清楚了。计算每个时间步动作的对数概率然后乘上这条轨迹的总回报作为损失函数取负号再做梯度下降。这相当于一个带权重的最大似然估计权重就是回报。关键在于这个权重是数据本身告诉我们的而不是预先知道的标签。这样做的好处是你不需要再去求解最优值函数、不需要argmax只需要能对策略做梯度运算就行。所以从原理上讲策略梯度是理解所有现代策略优化算法ACTOR-CRITIC、TRPO、PPO的基石。1.3 解决探索与利用的天然机制基于值函数的方法通常靠epsilon-greedy来探索也就是大部分时候选贪婪动作偶尔随机一下。这种探索方式比较生硬而且在高维连续空间中效率低。策略梯度则完全不同——策略网络输出的就是一个概率分布初始时分布比较均匀随着训练逐渐集中到高回报的动作上。这个过程中探索是“内嵌”在策略里的不需要额外的随机机制。举个例子我在调一个简单的机器人走迷宫任务时用DQN要专门调epsilon衰减曲线衰减快了容易陷在局部最优点衰减慢了又无法收敛。换用策略梯度后虽然没有完全消除这个问题但探索方式自然多了——它在迷宫分叉口本身就存在概率分配哪个方向回报高概率就会自动变大。这种柔性探索机制让新手更容易上手。2. 策略梯度定理与算法家族2.1 期望回报的梯度推导要真正理解策略梯度还是得走一遍推导过程但我会把中间过程缩到最短。假设策略 (\pi_{\theta}) 是可微的我们的目标是让期望回报 (J(\theta)) 最大化。[ J(\theta) \mathbb{E}{\tau \sim \pi{\theta}} [R(\tau)] ]求梯度时因为轨迹的分布是由策略决定的不能直接对期望里的函数求导。这里用到一个小技巧把概率的导数转化成对数概率的导数。[ \nabla_{\theta} P(\tau|\theta) P(\tau|\theta) \cdot \nabla_{\theta} \log P(\tau|\theta) ]轨迹概率可以分解成环境转移概率和策略概率的乘积。环境转移概率跟策略参数无关求梯度会被消掉最后只留下策略部分的对数梯度。[ \nabla_{\theta} \log P(\tau|\theta) \sum_{t0}^{T} \nabla_{\theta} \log \pi_{\theta}(a_t|s_t) ]把这个代回期望梯度的积分式就得到了策略梯度定理。推导里最体现“为什么是log”的地方就在于环境动态产生的梯度贡献是零只剩下策略自己的贡献。这跟监督学习里用交叉熵梯度很像但前面的权重不是离散标签而是回报。2.2 REINFORCE最朴素的蒙特卡洛策略梯度把策略梯度公式直接变成算法就是REINFORCE。完整走一条轨迹收集所有状态、动作、奖励然后对每个时间步计算带回报权重的梯度。由于是蒙特卡洛采样梯度估计的方差很大——同一策略跑10次轨迹总回报可能从10到1000不等导致梯度的方向和步长随机性很强。为了缓解方差最常用的技巧是引入baseline。减去一个只依赖状态的基线函数比如 (b(s))可以保持梯度估计无偏的同时降低方差。直观理解我们关心的是“这条轨迹比我平时表现的预期好多少”而不是“这条轨迹的绝对回报有多高”。如果所有轨迹的回报都整体加了100绝对权重全变了但相对优劣关系没有变减掉baseline之后权重就变成“超出基线的那部分”更稳定。最简单的baseline可以用一个独立的网络去估计状态价值 (V(s))这就是Actor-Critic的雏形。REINFORCE with baseline通常已经能在简单任务上训练出不错的策略但样本效率仍然很低因为要等一整条轨迹结束才能更新一次而且没有利用后一步的回报信息。2.3 Actor-Critic让梯度估计更聪明Actor-Critic的思路是让两个网络协同工作Actor负责输出动作概率Critic负责估计状态价值或者状态动作价值。Actor用策略梯度做更新Critic用TD误差做更新。为什么这样比纯REINFORCE好关键在于每一步都可以获得一个即时反馈——Critic给出的价值估计相当于给Actor提供了一个“当前状态值多少钱”的参考而TD误差则告诉策略“采取这个动作比平均预期好还是差”。从公式上看Actor-Critic的梯度权重从整条轨迹的回报 (R(\tau)) 变成了优势函数 (A(s,a) Q(s,a) - V(s))。优势函数表示“在状态s采取动作a的相对优劣”不仅方差更小而且利用了一步–步的时序差分信息不需要等到轨迹结束。我自己在做连续控制任务时发现Actor-Critic最直接的优势是训练速度肉眼可见地变快。把环境从单步奖励改成稀疏奖励场景后REINFORCE基本学不动而A2C同步版本的Actor-Critic还能勉强在前期探索到一些有效经验。当然Actor-Critic也有自己的麻烦两个网络互相耦合调参复杂不少这就是为什么后面出现了PPO这种更稳定的变体。2.4 从REINFORCE到Actor-Critic的演进路线图如果按复杂度排个序大致是REINFORCE → REINFORCE with baseline → Actor-CriticQAC/A2C/A3C → TRPO → PPO。每一步的改进都是为了解决一个具体痛点REINFORCE方差大所以加baseline基线不准确所以用TD学习单步采样仍然不稳定所以TRPO限制策略更新步长TRPO实现复杂所以PPO用clip替代约束优化。理解这条演进路线特别重要。很多新人直接上手PPO训练失败后一脸懵不知道问题出在哪个环节。如果你从REINFORCE开始一步步自己动手加baseline、改critic、试clip你很容易就能感知到每个模块对训练稳定性到底做了什么贡献。我在带团队的时候要求新人必须先把REINFORCE跑通再让他写PPO就是这个原因。3. 从零实现一个策略梯度Agent以CartPole为例3.1 环境与网络设计CartPole是测试策略梯度算法最好的入门环境因为动作空间只有两个离散动作状态是四维连续向量训练快且容易观察规律。我们用PyTorch实现一个简单的策略网络输入状态输出两个动作的逻辑值然后用softmax转成概率分布。网络结构很简单一个输入层4维中间一个128维的隐藏层ReLU激活输出层2维。不需要太复杂因为CartPole本身任务不重。关键是你要清楚我们输出的不是动作本身而是动作的概率分布参数。训练过程中从分布中采样动作同时记录对应的对数概率用于后续的梯度计算。3.2 损失函数与反向传播细节策略梯度在实现里有个容易绕晕的点损失函数到底是什么REINFORCE的损失是负的对数概率乘以回报即[ L -\sum_{t} \log \pi(a_t|s_t) \cdot G_t ]其中 (G_t) 是从时间步t到轨迹结束的折扣回报。PyTorch里我们不需要手动求梯度只需要构造这个损失然后调用backward()。但这里有一个细节监督学习中的损失是标量策略梯度的损失也是标量但它的梯度意义完全不同。监督学习希望模型预测接近标签策略梯度希望“加大回报高的动作概率”所以乘上了回报。另外一个细节是折扣因子的选择。CartPole里每个非结束状态都有1的存活奖励所以折扣回报 (G_t \sum_{kt}^{T} \gamma^{k-t} r_k)。(\gamma) 建议设成0.99这样既保留远期信息又不会让方差失控。我在早期实现中曾经把 (\gamma) 设为1结果训练后期策略波动加大因为所有时间步的权重都一样梯度无法区分“近期的动作”和“远期的动作”哪个更重要。3.3 一份可运行的PyTorch代码下面是我在实际调试中验证过能稳定跑通的REINFORCE实现。完整代码可以直接复制到你的项目里配合gym的CartPole-v1环境运行。import gym import numpy as np import torch import torch.nn as nn import torch.optim as optim # 策略网络输出动作概率分布 class PolicyNet(nn.Module): def __init__(self, state_dim4, hidden128, action_dim2): super().__init__() self.fc nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, action_dim) ) def forward(self, x): logits self.fc(x) probs torch.softmax(logits, dim-1) return probs # 计算一条轨迹的折扣回报 def compute_returns(rewards, gamma0.99): returns [] G 0 for r in reversed(rewards): G r gamma * G returns.insert(0, G) return torch.tensor(returns, dtypetorch.float32) # 训练REINFORCE def train(env, policy, optimizer, episodes500, gamma0.99): for episode_idx in range(episodes): state, _ env.reset() log_probs [] rewards [] done False while not done: state_t torch.tensor(state, dtypetorch.float32).unsqueeze(0) probs policy(state_t) action_dist torch.distributions.Categorical(probs) action action_dist.sample() log_prob action_dist.log_prob(action) next_state, reward, terminated, truncated, _ env.step(action.item()) done terminated or truncated log_probs.append(log_prob) rewards.append(reward) state next_state returns compute_returns(rewards, gamma) log_probs torch.stack(log_probs) # 策略梯度损失- log_prob * return loss -(log_probs * returns).sum() optimizer.zero_grad() loss.backward() optimizer.step() if episode_idx % 20 0: total_reward sum(rewards) print(fEpisode {episode_idx}, reward: {total_reward}) env gym.make(CartPole-v1) policy PolicyNet() optimizer optim.Adam(policy.parameters(), lr1e-2) train(env, policy, optimizer, episodes500)这个代码跑下来通常300步左右能让CartPole稳定存活到接近500步。有几个地方大家可以自己调隐藏层大小改成256会更快学习率调到5e-3有时候更平滑但低于1e-3基本学不动。核心还是要在你的环境里多试几次。3.4 关键参数与训练技巧学习率是REINFORCE里最敏感的参数。我在用Adam时lr1e-2通常效果不错但换到SGD就得重新调。原因是策略梯度的轨迹回报尺度差异非常大Adam的适应性学习率能自动归一化不同参数维度的更新幅度但Adam的初始步长仍会影响收敛速度。另一个技巧是奖励归一化。把每条轨迹的回报减去均值、除以标准差变成零均值单位方差。这样能显著缓解不同轨迹回报尺度差异大的问题而且不会改变优化方向只是让梯度更新更稳健。尤其是当环境奖励的绝对值很大时比如数值游戏里单步奖励可能上万不做归一化几乎必然梯度爆炸。我在实际项目里发现即使只做简单的“除以历史平均奖励的绝对值”训练稳定性也能提升一个档次。4. 常见问题与调试实录4.1 训练不收敛loss反复横跳这是策略梯度最经典的问题。loss也就是负的期望回报加权和会在训练过程中出现剧烈波动甚至前期上升、然后又下跌。原因是单条轨迹的采样误差太大了——一条轨迹表现好权重高梯度加大其概率下一条轨迹表现差权重低又反过来把刚加大的概率压下去。这就形成了恶性循环。我的排查习惯是先看奖励的分布而不是看loss曲线。如果你打印每100步的平均奖励发现它在100到300之间来回震荡且没有明显上升趋势大概率是方差问题。先做奖励归一化再把baseline加上去。纯REINFORCE在CartPole上还能狗住在很多复杂任务上就完全不行了。4.2 梯度爆炸与NaN问题当回报量级变大比如超过10000log_prob乘以回报之后梯度会非常巨大容易造成NaN。常见原因包括奖励没有归一化、学习率太高、网络输出层没有初始化好。我的排查步骤检查奖励数值范围如果平均值超过100先做归一化。设置梯度裁剪torch.nn.utils.clip_grad_norm_(policy.parameters(), 0.5)限制梯度的最大范数。确保网络初始化合理通常用Xavier初始化或干脆用PyTorch默认的初始化。在某个机器人仿真项目里我遇到过一次梯度爆炸导致policy输出全部变成NaN。检查后发现是回报里包含了一个时间步奖励累加到几百万的稀疏事件而归一化之前没有做任何clip。加了一个max绝对回报上限比如500之后训练就稳住了。4.3 策略过早崩溃熵塌缩策略梯度训练中策略网络可能会过早变成确定性分布导致探索能力消失。表现就是动作概率几乎全压到某一个动作上稍微有一点噪声就崩盘。在连续动作空间里更容易发生因为高斯分布的方差会被优化到趋近于零。解决办法是在损失函数中加入策略熵正则项(L L_{pg} - \lambda H(\pi))其中 (H(\pi)) 是策略熵。熵越大代表随机性越强所以减去它相当于鼓励探索。通常在训练初期设 (\lambda0.01)后期可以调小。这个技巧在PPO里被直接内置了但REINFORCE里很多人会忘记加。我见过一个案例在连续控制任务中策略熵从大约0.5快速降到0.01之后训练就停滞了。加上熵正则后前100步的熵保持在一个合理范围内探索更充分最终性能反而更高。所以在实现策略梯度时不要怕“加正则化”会影响性能适度探索是长期赢家。4.4 样本效率太低怎么办策略梯度是on-policy算法每一条轨迹用完就扔不能像DQN那样放进经验回放池反复利用所以样本效率很低。一个CartPole任务还好500条轨迹就能收敛但一个高维机器人任务可能几百万步还学不到什么。改进方向有两个。一是引入重要性采样通过重要性权重来复用旧策略采样到的数据这就是PPO能多epoch更新的原因。二是把单步的Actor-Critic改成多步回报如GAE在偏差和方差之间做折中让训练信号更密集。我会在下一节展开PPO的思路因为它是实际应用最广的解决样本效率问题的方法。5. 从REINFORCE到PPO工业级策略梯度5.1 TRPO的核心思想限制步长REINFORCE和Actor-Critic用普通的梯度上升更新参数但神经网络参数互相影响一个过大的步长可能让策略瞬间变差而且很难恢复。TRPOTrust Region Policy Optimization就是针对这个问题的每一步更新要求新旧策略之间的KL散度不超过某个阈值。这相当于在策略空间中画了一个“信任区域”保证更新后策略不会偏离太远。TRPO的理论性很强但它用共轭梯度法去近似求解带约束的优化问题实现复杂度很高。我在项目里直接用现成库还好说自己手写时发现要调很多数值稳定性参数。所以除非你需要严格的单调改进保证否则没必要从TRPO开始写。理解其动机就够了。5.2 PPO的clip目标函数PPO用极简的方式实现了TRPO类似的效果。它的核心在于对重要性采样比率 (r_t(\theta) \frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}) 做clip目标函数为[ L^{CLIP}(\theta) \mathbb{E} \left[ \min(r_t \cdot A_t, \ \text{clip}(r_t, 1-\epsilon, 1\epsilon) \cdot A_t) \right] ]这个公式看起来复杂实际行为很直观当优势 (A_t 0) 时说明这个动作比平均好我们想增大其概率但最多只能让 (r_t) 增加到 (1\epsilon)防止更新太猛。当 (A_t 0) 时想减小该概率但同样限制 (r_t) 至少 (1-\epsilon)。这样更新就始终保持在一个“安全范围”内。我在实际项目中使用PPO时通常将 (\epsilon) 设为0.2mini-batch大小调成256或2048更新epoch设3次。相比REINFORCEPPO能在同样样本量下稳定得多而且对于连续动作空间和高维观察空间都表现优秀。但要注意PPO的超参数依然敏感尤其是学习率和 (\epsilon) 之间的匹配关系。输出层的初始化、奖励归一化、GAE中的 (\lambda) 参数都会影响最终结果。5.3 实际项目中怎么选什么时候用REINFORCE什么时候用PPO不是说有了PPO就完全抛弃REINFORCE。在一些极简单、需要快速验证自己想法的任务上REINFORCE几十行代码就能跑通调试成本低。例如检查一个环境封装是否正确、一个reward shaping策略是否有效先用REINFORCE打通流程再切换到PPO做正式实验效率会高很多。如果任务涉及高维状态、稀疏奖励、连续控制我一般直接上PPO。但这里的“上PPO”不是调用一个库就完事而是需要把网络结构、输入归一化、奖励归一化、熵系数、GAE参数都仔细调一遍。我在某机器人移动任务里对比过REINFORCE、A2C和PPO最终PPO以约80%的样本达到同样性能而且训练曲线平滑几乎没有崩溃过。5.4 策略梯度在真实场景的落地体会我参与过的项目中策略梯度实际用得最多的三个方面是游戏AI、机器人控制、模拟环境中的参数优化。游戏AI方面策略梯度天然支持多智能体互动和部分可观测状态PPO已经成了标准配置。机器人控制方面策略梯度可以输出连续动作而且能直接处理力矩、位置等物理量但真实机器人上样本获取成本高通常先在仿真里训练再迁移。参数优化方面如给推荐系统调参、给硬件控制系统调参策略梯度可以把“调整策略”本身当作策略来学习搜索效率比网格搜索高很多。我的实际体会是策略梯度并不是一个“开箱即用”的算法它更像一个框架。理解它的核心原理、方差来源、步长限制才能在实际项目中做出正确的调试决策。如果你能亲手实现一遍REINFORCE调过几个环境的参数再看PPO的代码很多以前觉得玄学的内容都会豁然开朗。6. 写在最后我的几个经验原则练了这么久策略梯度我自己沉淀出几条经验原则算是给大家的额外礼物。第一条先跑通再调优。不管用的是REINFORCE还是PPO第一次实现务必使用最简单的环境、最小的网络、默认的超参数让整个流程闭环跑通。很多人在第一步就因为奖励归一化没做而得到一个看似高大上但完全不工作的代码然后就开始怀疑人生其实问题往往很简单。第二条调试时同时盯三个指标平均奖励、策略熵、梯度范数。奖励告诉你学没学到东西熵告诉你探索是否还在梯度范数告诉你更新是否稳定。很多情况下先看熵是否塌缩比看奖励曲线更快定位问题。如果熵过早掉到接近零基本可以确定是探索不足而不是学习率太低。第三条不要迷信某一种算法。REINFORCE虽然“老”但它的简单性让它成为理解整个RL理论的最佳工具。PPO虽然是默认选择但在小型、离散、节奏快的实验中未必比得上调好的A2C。在你面对一个新任务时先用最简单的方法做基线再迭代改进。策略梯度这条技术路线最吸引我的地方就在这里从最原始的想法出发你可以沿着方差缩减、步长限制、分布式训练等方向无限深入而且每一步的改进都有清晰的数学直觉和工程验证在后面撑着。希望这篇内容能帮你少踩一些我当年踩过的坑。

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

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

免费获取报价 →
↑