☰
策略梯度原理与实现:从REINFORCE到PPO的完整演进攻略
2026/10/22 3:38:30 网站建设 项目流程

深度强化学习这几年最常被问到的一个问题就是:既然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_{t=0}^{T} \nabla_{\theta} \log \pi_{\theta}(a_t|s_t) \cdot R(\tau) \right] ]

其中 (\tau) 是一条完整的轨迹,(R(\tau)) 是从 (t=0) 开始到结束的累积奖励。你别被这个积分式子吓到,直觉其实非常朴素:如果某条轨迹的回报比平均值高,那就增大这条轨迹上所有动作的概率;回报越低,就减小这些动作的概率。也就是说,做完一条轨迹之后回头看,给表现好的动作“加分”,给表现差的动作“减分”。

用代码逻辑来说明就更清楚了。计算每个时间步动作的对数概率,然后乘上这条轨迹的总回报,作为损失函数取负号再做梯度下降。这相当于一个带权重的最大似然估计,权重就是回报。关键在于,这个权重是数据本身告诉我们的,而不是预先知道的标签。

这样做的好处是,你不需要再去求解最优值函数、不需要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_{t=0}^{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-Critic(QAC/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_{k=t}^{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_dim=4, hidden=128, action_dim=2): 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, gamma=0.99): returns = [] G = 0 for r in reversed(rewards): G = r + gamma * G returns.insert(0, G) return torch.tensor(returns, dtype=torch.float32) # 训练REINFORCE def train(env, policy, optimizer, episodes=500, gamma=0.99): for episode_idx in range(episodes): state, _ = env.reset() log_probs = [] rewards = [] done = False while not done: state_t = torch.tensor(state, dtype=torch.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(f"Episode {episode_idx}, reward: {total_reward}") env = gym.make("CartPole-v1") policy = PolicyNet() optimizer = optim.Adam(policy.parameters(), lr=1e-2) train(env, policy, optimizer, episodes=500)

这个代码跑下来,通常300步左右能让CartPole稳定存活到接近500步。有几个地方大家可以自己调:隐藏层大小改成256会更快,学习率调到5e-3有时候更平滑,但低于1e-3基本学不动。核心还是要在你的环境里多试几次。

3.4 关键参数与训练技巧

学习率是REINFORCE里最敏感的参数。我在用Adam时,lr=1e-2通常效果不错,但换到SGD就得重新调。原因是策略梯度的轨迹回报尺度差异非常大,Adam的适应性学习率能自动归一化不同参数维度的更新幅度,但Adam的初始步长仍会影响收敛速度。

另一个技巧是奖励归一化。把每条轨迹的回报减去均值、除以标准差,变成零均值单位方差。这样能显著缓解不同轨迹回报尺度差异大的问题,而且不会改变优化方向(只是让梯度更新更稳健)。尤其是当环境奖励的绝对值很大时,比如数值游戏里单步奖励可能上万,不做归一化几乎必然梯度爆炸。我在实际项目里发现,即使只做简单的“除以历史平均奖励的绝对值”,训练稳定性也能提升一个档次。

4. 常见问题与调试实录

4.1 训练不收敛,loss反复横跳

这是策略梯度最经典的问题。loss(也就是负的期望回报加权和)会在训练过程中出现剧烈波动,甚至前期上升、然后又下跌。原因是单条轨迹的采样误差太大了——一条轨迹表现好,权重高,梯度加大其概率;下一条轨迹表现差,权重低,又反过来把刚加大的概率压下去。这就形成了恶性循环。

我的排查习惯是先看奖励的分布,而不是看loss曲线。如果你打印每100步的平均奖励,发现它在100到300之间来回震荡且没有明显上升趋势,大概率是方差问题。先做奖励归一化,再把baseline加上去。纯REINFORCE在CartPole上还能狗住,在很多复杂任务上就完全不行了。

4.2 梯度爆炸与NaN问题

当回报量级变大,比如超过10000,log_prob乘以回报之后梯度会非常巨大,容易造成NaN。常见原因包括:奖励没有归一化、学习率太高、网络输出层没有初始化好。我的排查步骤:

  1. 检查奖励数值范围,如果平均值超过100,先做归一化。
  2. 设置梯度裁剪torch.nn.utils.clip_grad_norm_(policy.parameters(), 0.5),限制梯度的最大范数。
  3. 确保网络初始化合理,通常用Xavier初始化或干脆用PyTorch默认的初始化。

在某个机器人仿真项目里,我遇到过一次梯度爆炸导致policy输出全部变成NaN。检查后发现是回报里包含了一个时间步奖励累加到几百万的稀疏事件,而归一化之前没有做任何clip。加了一个max绝对回报上限(比如500)之后,训练就稳住了。

4.3 策略过早崩溃(熵塌缩)

策略梯度训练中,策略网络可能会过早变成确定性分布,导致探索能力消失。表现就是动作概率几乎全压到某一个动作上,稍微有一点噪声就崩盘。在连续动作空间里更容易发生,因为高斯分布的方差会被优化到趋近于零。

解决办法是在损失函数中加入策略熵正则项:(L = L_{pg} - \lambda H(\pi)),其中 (H(\pi)) 是策略熵。熵越大代表随机性越强,所以减去它相当于鼓励探索。通常在训练初期设 (\lambda=0.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用普通的梯度上升更新参数,但神经网络参数互相影响,一个过大的步长可能让策略瞬间变差,而且很难恢复。TRPO(Trust 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.2,mini-batch大小调成256或2048,更新epoch设3次。相比REINFORCE,PPO能在同样样本量下稳定得多,而且对于连续动作空间和高维观察空间都表现优秀。但要注意,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。在你面对一个新任务时,先用最简单的方法做基线,再迭代改进。策略梯度这条技术路线最吸引我的地方就在这里:从最原始的想法出发,你可以沿着方差缩减、步长限制、分布式训练等方向无限深入,而且每一步的改进都有清晰的数学直觉和工程验证在后面撑着。希望这篇内容能帮你少踩一些我当年踩过的坑。

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

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

立即咨询