☰
策略梯度与PPO实战:从梯度上升原理到工业级强化学习算法
2026/10/14 7:29:14 网站建设 项目流程

1. 从“会做题”到“会改策略”:梯度上升到底在优化什么

很多人第一次接触强化学习里的策略梯度方法时,脑子里冒出来的第一个疑问往往是:监督学习里我们用梯度下降最小化损失,怎么到了强化学习,突然就变成梯度上升了?这个方向上的反转,其实藏着策略优化最核心的直觉。

先把场景摆出来。假设你训练一个智能体玩某种棋盘游戏,它每一步都要从若干个合法动作里挑一个。策略梯度的思路很直接:把策略本身参数化,比如用一个神经网络输出每个动作的概率,然后让“能带来更高回报的动作”在未来被选中的概率变大。注意这里的关键词是“变大”,而不是“让损失变小”。所以我们要最大化的是期望回报,自然就用梯度上升去更新参数。

用最朴素的话讲,梯度上升就是在参数空间里朝着“回报期望增长最快的方向”挪一小步。公式上写出来就是 θ ← θ + α∇J(θ),其中 J(θ) 是期望回报,α 是学习率。这个加号就是它和梯度下降最直观的区别。

但真正让初学者卡住的,不是这个加号,而是 ∇J(θ) 到底怎么算。因为期望回报里包含了对动作采样、对状态转移采样的双重随机性,你没法直接对这个期望求导。于是就有了策略梯度定理,它把 ∇J(θ) 转化成一个可以用采样估计的形式:∇J(θ) = E[∇log π(a|s) · G],其中 G 是从当前时刻起的累计回报。

这个式子的物理意义非常值得反复咀嚼。∇log π(a|s) 表示“如果我想让动作 a 在状态 s 下更容易被选中,参数该往哪个方向调”;G 则是一个标量权重,代表这次动作最终带来的回报好坏。两者相乘,就实现了“好的动作加大力度,坏的动作减小力度”。我第一次真正理解这个式子,是在把它拆成“方向 × 幅度”两个部分之后——∇log π 给方向,G 给幅度。

这里有个特别容易踩的坑:G 的数值范围可能非常大,比如累计回报动辄几百上千,导致梯度爆炸。所以实际实现里几乎都会对 G 做标准化或者用折扣因子 γ 来压缩。折扣因子 γ 的作用不只是让远期回报衰减,它还有一个隐含效果——降低回报的方差。γ 越接近 0,智能体越短视,但梯度估计越稳定;γ 越接近 1,智能体越有远见,但方差越大。这个权衡在实操中需要根据任务长度反复调。

还有一个概念必须澄清:策略梯度优化的是“策略的参数”,而不是“价值函数的参数”。这一点和基于价值的方法(比如 Q-learning)有本质区别。基于价值的方法是先学好 Q 值,再隐式地导出策略;策略梯度则是直接对策略下手。这个区别决定了它们在连续动作空间、随机策略等场景下的适用性差异。连续动作空间里,基于价值的方法要对动作做 argmax,几乎没法直接做;而策略梯度天然输出动作分布的参数,处理连续动作非常自然。

理解了这些,你再看“gradient ascent”这个词,就不会只把它当成一个符号反转,而会意识到它背后是一整套“用采样估计梯度、用回报加权方向”的优化哲学。这套哲学后面衍生出了 REINFORCE、Actor-Critic、PPO 等一系列方法,但根都在这里。

2. 用REINFORCE把公式跑起来:一次完整的策略梯度实现

光看公式容易飘,必须动手写一遍才能把那些符号变成肌肉记忆。REINFORCE 是最原始、最干净的策略梯度算法,没有基线、没有 critic,就靠一条轨迹的回报来更新策略。虽然它在实际项目里很少直接用,但作为理解策略梯度的“最小可运行示例”,价值极高。

2.1 轨迹采样与回报计算的具体写法

先明确一次迭代的流程:用当前策略跑完一个完整的 episode,记录下每一步的 (状态, 动作, 奖励),然后从后往前计算每一步的累计回报 G_t。这里有个细节,G_t 通常用折扣累计,即 G_t = r_t + γ·r_{t+1} + γ²·r_{t+2} + …。从后往前算的好处是可以用递推:G_t = r_t + γ·G_{t+1},避免重复求和。

代码上大概长这样:

def compute_returns(rewards, gamma): returns = [] G = 0 for r in reversed(rewards): G = r + gamma * G returns.insert(0, G) return returns

这段代码看起来简单,但有一个隐藏问题:如果 episode 很长,G 的数值会累积得很大,直接拿去乘 ∇log π 会导致梯度尺度失控。所以下一步通常要做标准化。

2.2 策略网络的输出层设计与动作采样

策略网络的输出层设计取决于动作空间是离散还是连续。离散动作空间用 softmax 输出每个动作的概率;连续动作空间通常输出高斯分布的均值和标准差,然后从该分布里采样。这里有个新手常犯的错误:连续动作空间里直接把标准差设成固定值,导致探索能力不足。正确做法是把标准差也作为网络输出,并且用 softplus 或 exp 保证它为正。

动作采样时要用torch.distributions或者手动实现,关键是采样完要保留log_prob,因为更新时要用。很多人采样完就把 log_prob 丢了,回头再重新算一遍,既浪费计算又容易出错。

2.3 损失函数的符号陷阱:为什么是负的 log_prob 乘回报

这是 REINFORCE 实现里最容易翻车的地方。我们想最大化 J(θ) = E[log π(a|s) · G],但深度学习框架默认是 minimize loss。所以损失函数要写成 loss = -log_prob * G。这个负号如果漏了,训练会朝着完全相反的方向走,表现就是智能体越来越差。

我见过不止一个同学在这里卡了半天,打印出来的 loss 在下降,但智能体表现却在恶化。原因就是符号搞反了,loss 下降实际上是在最小化 -J,也就是在最小化回报。

2.4 一个可复现的完整训练循环

把上面几块拼起来,一个最小可运行的 REINFORCE 训练循环大概是:

for episode in range(num_episodes): states, actions, log_probs, rewards = [], [], [], [] state = env.reset() done = False while not done: state_tensor = torch.FloatTensor(state) probs = policy(state_tensor) dist = torch.distributions.Categorical(probs) action = dist.sample() log_prob = dist.log_prob(action) next_state, reward, done, _ = env.step(action.item()) states.append(state) actions.append(action) log_probs.append(log_prob) rewards.append(reward) state = next_state returns = compute_returns(rewards, gamma) returns = torch.FloatTensor(returns) returns = (returns - returns.mean()) / (returns.std() + 1e-8) loss = 0 for log_prob, G in zip(log_probs, returns): loss += -log_prob * G optimizer.zero_grad() loss.backward() optimizer.step()

这段代码里,回报标准化那一步非常关键。如果不做标准化,不同 episode 之间的回报尺度差异会让梯度极不稳定。标准化之后,梯度只关心“哪些动作比平均好”,而不是绝对数值大小。

实测下来,REINFORCE 在简单任务(比如 CartPole)上几百个 episode 就能收敛,但在稍复杂的任务上就非常吃力,因为方差太大。这就引出了后面要讲的基线技巧。

3. 方差 reduction:让梯度上升从“能跑”变成“好用”

REINFORCE 最大的问题不是理论,而是方差。同一条策略,跑两次 episode,回报可能差好几倍,导致梯度方向忽左忽右,训练曲线像心电图。解决方差是策略梯度从玩具走向实用的必经之路。

3.1 基线的直觉:为什么减去一个常数不改变期望

基线(baseline)的核心操作是:把回报 G 替换成 G - b(s),其中 b(s) 是一个只依赖状态、不依赖动作的量。数学上可以证明,E[∇log π(a|s) · b(s)] = 0,因为 ∇log π(a|s) 对动作的期望是零。所以减去基线不改变梯度的期望,但能显著降低方差。

直觉上理解:如果某个状态下所有动作的回报都差不多是 100,那 G=100 并不能告诉我们这个动作到底好不好;但如果减去基线 100,G-b 就变成了 0 附近的小波动,梯度只关注“比平均好多少”。这就是基线的价值。

3.2 用价值网络估计基线:Actor-Critic的雏形

基线怎么选?最简单的可以用回报的滑动平均,但更好的做法是训练一个价值网络 V(s) 来估计当前状态的期望回报。这个价值网络就是 Critic,策略网络就是 Actor,两者合起来就是 Actor-Critic。

Actor-Critic 的更新逻辑是:Actor 用 ∇log π(a|s) · (G - V(s)) 更新,Critic 用 (G - V(s))² 的梯度更新。这里的 G - V(s) 叫做优势函数 A(s,a) 的一个估计,表示“这个动作比平均水平好多少”。

实操中,Critic 的学习率通常要比 Actor 小一些,因为 Critic 需要稳定估计,而 Actor 需要快速适应。如果两者学习率一样,Critic 估计不准会直接带偏 Actor。

3.3 优势函数的几种估计方式与选择建议

优势函数的估计有好几种做法,常见的有:

估计方式公式特点
蒙特卡洛A = G - V(s)无偏但方差大
TD(0)A = r + γV(s') - V(s)方差小但有偏
GAE多步加权组合偏差方差折中

GAE(广义优势估计)是目前最常用的,它用一个参数 λ 在偏差和方差之间调节。λ=0 退化成 TD(0),λ=1 退化成蒙特卡洛。实践中 λ 常取 0.95 左右,兼顾稳定性和准确性。

我自己的经验是:任务越短,越可以偏向蒙特卡洛;任务越长,越需要 GAE 或 TD 来压制方差。如果任务有明确的终止条件且 episode 长度在几十步以内,直接用 G - V(s) 也够用。

3.4 回报标准化与梯度裁剪的配合使用

除了基线,回报标准化和梯度裁剪也是标配。回报标准化让不同 episode 的回报尺度一致;梯度裁剪防止个别极端梯度把参数带飞。两者配合使用,训练稳定性会有质的提升。

梯度裁剪的阈值一般设在 0.5 到 1.0 之间。太小会限制学习,太大会失去保护作用。我通常先用 0.5 试,如果发现学习太慢再往上调。

这里有个细节:梯度裁剪要在loss.backward()之后、optimizer.step()之前做,用torch.nn.utils.clip_grad_norm_对参数做范数裁剪。如果对 loss 做裁剪,那是另一回事,效果不一样。

4. 从策略梯度到PPO:为什么工业界更偏爱它

REINFORCE 加上基线之后已经能用了,但它有一个致命缺陷:每次更新只能用当前策略采的数据,采完一次就得扔掉重新采。样本效率极低,在真实环境里根本耗不起。PPO(近端策略优化)就是为解决这个问题而生的。

4.1 重要性采样:让旧数据也能参与更新

PPO 的核心思想是重要性采样。它允许用旧策略 π_old 采的数据来更新新策略 π_θ,通过一个比值 r(θ) = π_θ(a|s) / π_old(a|s) 来修正分布差异。这样一次采样可以做好几轮更新,样本效率大幅提升。

但重要性采样有个前提:新旧策略不能差太远,否则比值会爆炸,估计就失效了。PPO 的做法是把这个比值裁剪到 [1-ε, 1+ε] 区间内,ε 通常取 0.1 或 0.2。这就是 PPO 里那个著名的 clip 操作。

4.2 clip机制到底在裁剪什么

很多人第一次看 PPO 的损失函数会懵:L = min(r·A, clip(r, 1-ε, 1+ε)·A)。这个 min 和 clip 到底在干什么?

拆开看:当 A > 0(动作比平均好)时,我们希望 r 越大越好,但 clip 把 r 限制在 1+ε,防止新策略过度偏离旧策略;当 A < 0(动作比平均差)时,我们希望 r 越小越好,但 clip 把 r 限制在 1-ε。min 的作用是取两者中更保守的那个,确保更新不会太激进。

这个设计的精妙之处在于:它既允许策略改进,又防止策略一步跨太大导致崩溃。相比 TRPO 的复杂约束优化,PPO 用简单的 clip 就达到了类似效果,实现难度低得多。

4.3 实操中超参数怎么调:clip范围、更新轮数、batch大小

PPO 的超参数不多,但每个都影响很大:

  • clip 范围 ε:0.1 到 0.3 之间。任务越简单可以越大,任务越复杂越要小。
  • 更新轮数:同一批数据重复训练的次数,通常 3 到 10 轮。太多会过拟合旧数据,太少样本效率低。
  • batch 大小:太小梯度噪声大,太大更新次数少。一般几千步的数据量比较合适。
  • 学习率:3e-4 是常见起点,配合线性衰减效果更好。

我踩过的一个坑是:更新轮数设成 20,结果策略在旧数据上过拟合,新数据一来表现就崩。后来改成 5 轮,稳定性明显改善。所以不要贪多,PPO 的 clip 虽然能防大步,但防不住多轮累积的偏移。

4.4 和A2C、TRPO的横向对比

算法样本效率实现难度稳定性适用场景
A2C低低中快速原型
TRPO中高高理论要求高
PPO中高中高通用首选

A2C 是同步版的 A3C,简单但样本效率低;TRPO 理论漂亮但实现复杂,二阶优化开销大;PPO 在两者之间找到了平衡点,所以成了工业界默认选择。如果你刚开始做强化学习项目,PPO 基本是不会错的第一选择。

5. 那些文档里不写的实操细节与踩坑记录

理论和代码都过了一遍,但真正决定项目成败的,往往是那些文档里不会写的细节。下面这些是我在实际训练中反复踩坑后总结出来的。

5.1 奖励设计对梯度上升的隐性影响

奖励函数的设计直接决定了梯度上升的方向。如果奖励稀疏,比如只在成功时给 1,其他时候给 0,那大部分 G 都是 0,梯度信号极弱,学习会非常慢。这时候需要做奖励塑形,比如给中间状态一些小的正向奖励,引导智能体逐步靠近目标。

但奖励塑形有个风险:如果塑形奖励设计不当,智能体可能学会“刷奖励”而不是真正完成任务。比如你给“靠近目标”奖励,它可能学会在目标附近来回晃悠。所以塑形奖励要谨慎,最好配合终止条件一起设计。

5.2 策略网络的初始化与探索噪声

策略网络最后一层的初始化很关键。如果初始输出过于极端(比如某个动作概率接近 1),探索能力会很差。常见做法是把最后一层权重初始化得小一些,让初始策略接近均匀分布。

连续动作空间里,探索靠的是高斯分布的标准差。如果标准差初始太小,智能体几乎不探索;太大则动作随机,学不到东西。一般初始标准差设在动作范围的 1/4 到 1/2 之间比较合适。

5.3 训练不稳定的典型症状与排查顺序

训练不稳定是策略梯度的家常便饭。遇到问题时,我通常按这个顺序排查:

  1. 先看回报曲线,是震荡还是发散。震荡通常是学习率太大或 batch 太小;发散可能是梯度爆炸。
  2. 检查梯度范数,如果经常超过 10,说明需要裁剪。
  3. 检查策略熵,如果熵快速降到接近 0,说明探索不足,需要加熵正则。
  4. 检查价值网络损失,如果 Critic 损失不降,说明基线估计不准,Actor 的梯度也会受影响。

这个排查顺序能覆盖大部分常见问题。关键是不要一上来就乱调超参数,先定位问题类型再动手。

5.4 熵正则:防止策略过早收敛的保险丝

熵正则是在损失函数里加一项 -β·H(π),其中 H 是策略熵,β 是系数。它的作用是鼓励策略保持一定的随机性,防止过早收敛到次优动作。

β 的取值通常在 0.01 到 0.1 之间。太小起不到作用,太大会让策略一直随机,学不到确定性行为。我一般从 0.01 开始,如果发现策略熵下降太快就往上调。

熵正则和 clip 是 PPO 里两个最重要的稳定器,一个防策略偏移,一个防探索不足。两者配合,训练稳定性会有明显提升。

6. 从单智能体到多智能体:梯度上升的扩展边界

单智能体的策略梯度已经够复杂了,但现实任务往往是多智能体协作或对抗。这时候梯度上升的假设会发生变化,因为每个智能体的回报不仅取决于自己的策略,还取决于其他智能体的策略。

在多智能体场景里,环境对每个智能体来说都是非平稳的,因为其他智能体在不停学习、不停改变策略。这导致每个智能体的梯度估计都带有额外的噪声。常见的应对方式有集中式训练分布式执行(CTDE),即训练时用全局信息估计价值,执行时只用局部信息做决策。

另一个扩展方向是分层强化学习,把策略分成高层和低层,高层负责设定子目标,低层负责执行。这样梯度上升在两层分别进行,高层用稀疏的长期回报,低层用密集的短期回报。这种结构在长序列任务里特别有用。

不过这些扩展都建立在单智能体策略梯度的基础之上。如果单智能体的梯度估计都没搞稳,直接上多智能体只会更乱。所以我的建议是:先把 REINFORCE 和 PPO 在简单任务上跑通、跑稳,再考虑扩展。

最后分享一个我自己的习惯:每次实现新的策略梯度算法,我都会先在 CartPole 或 Pendulum 这种经典控制任务上验证。这两个任务足够简单,能快速暴露实现错误;又足够有代表性,能反映算法在连续和离散动作空间的表现。跑通之后再迁移到目标任务,能省下大量调试时间。

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

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

立即咨询