策略梯度这块内容,我其实很早就想写一篇足够系统的梳理了。外面讲策略梯度的文章要么只讲一个PPO,要么数学推导一笔带过,要么代码和理论完全对不上,初学者想靠碎片信息搭起完整认知框架,确实很难。这篇我打算换个思路,把策略梯度从“为什么要用它”到“它到底在优化什么”,再到“REINFORCE、Actor-Critic、PPO这些变体各自在折腾什么”,最后到“训练时挂掉的常见原因和排查方法”完整串一遍。内容比较多,建议收藏了慢慢看,或者直接拉到对应章节。
1. 策略梯度的核心思想:从“选动作”到“调概率”
1.1 为什么强化学习需要策略梯度?
强化学习解决的核心问题是:智能体在未知环境里,通过试错学会一套决策方式,让长期累积奖励最大化。传统方法里,Q-learning这类基于价值的方法先把每个状态-动作对的价值函数估出来,然后靠贪心策略“选当前价值最高的动作”——这种做法简单有效,但有几个先天问题。
第一,动作空间一旦连续,Q-learning就非常难受。你没办法枚举所有动作来求最大化,只能靠优化算法去逼近那个最大值,而逼近过程本身又引入误差。第二,基于价值的方法学出来的是确定性策略,很多场景下最优策略其实是带随机性的。比如扑克里的虚张声势,如果每次都做同样的动作,对手很快就能摸透你的套路。第三,价值方法对策略的改进是“间接”的——价值函数估准了,策略自然就好了。但价值函数稍微估偏一点,策略就可能跟着跑偏,这种级联误差在复杂环境里尤其致命。
策略梯度换了一条路:我不估价值再转策略,我直接把策略本身参数化,然后用梯度上升去调参数,让累积奖励的期望值变大。这就像“既然是爬山,我直接朝山脚坡度最陡的方向迈步,而不是先画一张等高线地图再找路”。对于连续控制、高维动作空间、随机策略建模这些场景,策略梯度天然更契合。
1.2 一行公式看懂策略梯度
策略梯度的目标函数是所有轨迹的累积奖励期望,通常写作:
J(θ) = E_{τ ~ π_θ}[ R(τ) ]这里的 τ 是一条轨迹(状态、动作、奖励的序列),R(τ) 是这条轨迹的总回报,π_θ 是神经网络参数化的策略。要最大化 J(θ),最直接的办法就是对 θ 求梯度然后做梯度上升。但这个梯度不是简单求导能算出来的,因为期望的分布本身就依赖 θ,而且环境动态(转移概率)通常是未知的。
这里就需要用到那个经典的技巧——log derivative trick,把对期望求梯度转化成对策略对数概率求梯度:
∇J(θ) = E_{τ ~ π_θ}[ ∇log π_θ(τ) * R(τ) ]这个式子推导的核心是想办法把“采样轨迹”和“计算梯度”解耦:我们无法对 R(τ) 求导,但我们可以对 log π_θ(τ) 求导,而 π_θ(τ) 只由策略本身决定,不依赖环境动态。所以哪怕环境是完全黑盒,只要我们能采样轨迹,就能用蒙特卡洛的方式估计出策略梯度,然后用梯度上升不断改进策略。
后面所有的变体,像REINFORCE的减baseline、Actor-Critic的引入价值网络、PPO的clip操作,本质上都是在想方设法让这个梯度的估计更稳定、方差更低,同时不引入过大的偏差。记住这条主线,你再看任何策略梯度算法都不会迷路。
2. 数学推导与关键公式:梯度为什么长这样
2.1 log derivative trick的前世今生
刚才给的公式有点“从天而降”,我把它拆开推导一遍。先把目标函数按定义展开:
J(θ) = ∫ π_θ(τ) R(τ) dτ对 θ 求梯度,因为 R(τ) 不依赖 θ,所以梯度只作用在 π_θ(τ) 上:
∇J(θ) = ∫ ∇π_θ(τ) R(τ) dτ现在问题是,∇π_θ(τ) 本身不是概率分布,没法直接做蒙特卡洛期望。这时候用恒等式 ∇log z = ∇z / z,得到 ∇z = z * ∇log z,代入:
∇J(θ) = ∫ π_θ(τ) ∇log π_θ(τ) R(τ) dτ这个形式就是 E_{τ}[ ∇log π_θ(τ) * R(τ) ],齐活。这一串变换只有一个目的:把梯度搬到一个我们能在采样中算出来的量上。这个“搬移”的过程一点都没丢失信息,所以它是个无偏估计。
接下来是马尔可夫链的乘积分解,轨迹的概率 π_θ(τ) 等于初始状态概率、每一步的转移概率和策略概率的连乘:
π_θ(τ) = p(s_0) ∏_{t=0}^{T} π_θ(a_t|s_t) p(s_{t+1}|s_t, a_t)取对数后,连乘变成连加,而转移概率项 p(s_{t+1}|s_t, a_t) 跟 θ 无关,求导时就没了。这就是策略梯度的“美丽之处”:你不需要知道环境的动力学模型,就能算出策略的梯度。
2.2 从轨迹公式到单步公式
上面那个公式是对整条轨迹求梯度,实际应用时通常会把轨迹展开成逐时间步的形式。因为轨迹的回报 R(τ) 可以拆成每一步的折扣回报,经过一系列重排和推导,可以得到更常用的形式:
∇J(θ) = E_{s_t, a_t ~ π_θ}[ ∇log π_θ(a_t|s_t) * A_t ]这里 A_t 是优势函数,代表“当前状态-动作对相对平均水平的超额回报”。这个变换有一个直观解释:如果某个动作带来的回报高于平均水平,我就提高它的概率;如果低于平均水平,我就压低它的概率。调整幅度和优势大小成正比,调节速度由学习率控制。
这跟人类从经验中学习的方式很像——做对了就记下“下次还这么做”,做错了就记下“下次换个做法”。策略梯度只是把这个直觉变成了严格的数学操作。
2.3 为什么需要baseline和优势函数
REINFORCE算法(Williams在1992年提出的经典策略梯度算法)直接用轨迹回报作为 R(τ) 来估计梯度,理论上是无偏的,但实际使用中方差大得惊人。方差大的原因是,不同轨迹之间回报的绝对数值差异很大,而梯度更新方向被这些绝对数值主导,导致策略参数剧烈震荡,训练极度不稳定。
解决方向是找到一个“减法项”——一个只依赖状态、不依赖动作的量,从回报中减掉。这个量叫baseline,最常用的是状态价值函数 V(s)。减去baseline后梯度期望不变(这是可以严格证明的),但方差会显著下降。
减去baseline在数学上为什么不会改变期望?关键在于减掉的项和动作无关。具体来说,把梯度拆开看,那个减掉的部分可以分解成“每个状态的概率加权和”乘以“该状态下期望的梯度”,而每个状态的概率加权和正好是1,所以整体加总为0。这就是baseline不损害无偏性的原因。
从baseline再往前走一步,就是优势函数 A(s,a) = Q(s,a) - V(s)。QA告诉你这个动作在期望上有多好,V告诉你在当前状态下平均有多好,两者相减就是“这个动作比一般动作好多少”。用优势替代回报,相当于把回报先按状态标准化了一遍,梯度的“信噪比”大幅提升。这一改进是策略梯度从理论走向实践的关键一步。
3. 主流变体详解:REINFORCE、Actor-Critic与PPO
3.1 REINFORCE:最朴素的策略梯度
REINFORCE是策略梯度家族的老祖宗,思路极其直白:采样一整条完整的轨迹,计算累积回报,然后对这条轨迹中的每一步动作都计算梯度,用累积回报作为权重去更新策略。算法代码核心就几行:
# 伪代码:REINFORCE for episode in range(n_episode): states, actions, rewards = collect_trajectory(env, policy) # 从轨迹末尾往回计算折扣回报 returns = compute_discounted_returns(rewards, gamma=0.99) # 每一步用一个baseline(通常是状态均值)减一下,降低方差 advantage = returns - returns.mean() # 最大化优势期望,最小化交叉熵 policy_loss = -(log_prob(actions) * advantage).mean() optimizer.zero_grad() policy_loss.backward() optimizer.step()实际代码中,log_prob一般通过神经网络输出动作概率分布后取对数得到。在PyTorch里,这通常用categorical.log_prob(action)直接算出来。整个过程不需要任何价值网络,实现简单,逻辑直观。
但 REINFORCE 的致命弱点也很明显:一条轨迹从头到尾充满随机性,某个动作在早期可能不是最优的,但后续几步运气好拿了大奖励,这个动作也跟着被“奖励”,贡献了噪声,方差爆炸。另外,整条轨迹采样完才能更新,学习速度极慢,样本效率很低。再一个,如果某条轨迹回报特别高,梯度更新幅度会异常大,直接崩掉训练。
实操中我用REINFORCE跑过最简单的CartPole环境,轨迹够短(20~50步),勉强能收敛,但学习曲线几乎没有平滑过,像心电图一样。稍微复杂一点的环境,比如二维机器人导航,基本就是白费算力。所以REINFORCE现在更多是教学工具,用来帮助理解策略梯度的基础原理,真正实用的是下面这些改进版本。
3.2 Actor-Critic:让策略梯度用上“价值估计”
Actor-Critic的核心改进是引入一个参数化的价值网络,专门负责预估状态价值(或状态-动作价值),这个网络就是Critic。策略网络是Actor,负责做决策;价值网络是Critic,负责给Actor的每个动作打分。两者一起训练:Critic用TD误差(时间差分误差,即目标价值和当前预测价值之差)来拟合真实回报,Actor利用Critic提供的优势信号更新策略。
Actor-Critic和REINFORCE最大的区别在于:REINFORCE用一条轨迹的真实回报做“事后评价”,而Actor-Critic用价值网络的预测做“实时评价”。事后评价虽无偏但方差大,实时评价偏差更大但方差小。两者是一种权衡,而最终实用的做法是找一个折中方案。
# 伪代码:Actor-Critic(单步更新) for step in range(max_steps): state = env.reset() log_prob, value = actor_critic(state) action = sample(log_prob) next_state, reward, done = env.step(action) next_log_prob, next_value = actor_critic(next_state) # TD误差 td_target = reward + 0.99 * next_value * (1 - done) critic_loss = mse(value, td_target.detach()) # Actor用优势(TD误差)做权重 advantage = td_target - value.detach() actor_loss = -(log_prob(action) * advantage).mean() loss = actor_loss + critic_loss optimizer.zero_grad() loss.backward()Actor-Critic比REINFORCE幸运的地方在于,它可以每步更新而非整条轨迹更新,样本效率大幅提升。但它也有自己的麻烦:Actor和Critic同时在学,是“两个菜鸟带路”的状态,价值网络估不准,策略网络就会接收到垃圾信号,训练不稳定。实际调参时,我经常遇到Critic快速收敛、Actor还在原地打转的情况,或者反过来,策略震荡导致价值网络也跟着震荡。
这个问题在后来有了一个系统性解法——GAE(Generalized Advantage Estimation)。GAE本质上是对“多步优势”做指数加权平均,在偏差和方差之间用 λ 参数进行连续调节。λ=0时它退化成一步TD,偏差大但方差小;λ接近1时它近似于蒙特卡洛,无偏但方差大。实际中使用 λ=0.95 是常见起点,这个值在大量任务上都表现不错。
3.3 PPO:稳定性和实现复杂度之间的最佳平衡
PPO(Proximal Policy Optimization)在2017年由OpenAI提出,它解决的核心问题是:策略梯度更新步长怎么选。普通策略梯度对更新步长非常敏感——太大了容易把策略一下推出悬崖,太小了训练慢得让人怀疑人生。TRPO(Trust Region Policy Optimization)用二阶信息严格约束新旧策略的KL散度,效果好但计算复杂、实现麻烦,不适合大规模推广。
PPO的clip机制是它的灵魂。它裁掉(clip)新旧策略概率比,将其限制在目标函数的阈值范围内,粗暴但有效地防止了策略突变。具体公式展开如下:
# 伪代码:PPO-Clip 核心计算 ratio = torch.exp(new_log_prob - old_log_prob) # 新旧策略概率比 unclipped = ratio * advantage clipped = torch.clamp(ratio, 1 - eps, 1 + eps) * advantage actor_loss = -torch.min(unclipped, clipped).mean()这里的 eps 通常取0.2,含义是“单次更新中,任何动作的概率变化幅度不超过20%”。如果新旧策略的概率比超出这个范围,梯度就会被裁断,从源头阻止更新步长过大。PPO用一阶优化实现了接近TRPO的稳定性,实现难度却小得多,因此成为现代强化学习的事实标准。
PPO还有一个关键的配套点是重要性采样。采样数据是用旧策略跑的,更新却要滚到新策略上,中间需要一个修正因子——新旧策略概率比。这个比率保证即使偶尔一次超出clip范围,更新方向也不会失控。实践中我通常跑多个环境并行采样,一次性收集上千步数据,然后做多轮小batch更新,再把旧策略更新为当前策略重新采样。这个流程在OpenAI的baselines实现里已经写得很通用,稍微改改就能迁移到自己的任务上。
3.4 变体对比与选型建议
写到这里,把三类算法放到一起对比:
| 算法 | 核心思路 | 更新方式 | 方差/偏差 | 适用场景 |
|---|---|---|---|---|
| REINFORCE | 轨迹回报加权 | 整条轨迹结束后 | 方差极高 | 教学、极简单环境 |
| Actor-Critic | 价值网络实时评分 | 每步或n步 | 方差中,引入偏差 | 能够稳定训出价值网络的任务 |
| PPO | 新旧策略比clip | 多步批量更新 | 方差低且受约束 | 复杂环境、连续控制、大规模并行训练 |
选型建议:如果是做课程作业或理解概念,老老实实从REINFORCE开始;如果是真实的科研或工程项目,直接上PPO,不要浪费时间在裸Actor-Critic上打磨。裸Actor-Critic更像教科书里的过渡产物,实际工程中很少单独用。
4. 实践中的关键技巧:让策略梯度真正work
4.1 奖励归一化:容易被忽视的一步
很多人把策略梯度代码写完,一跑发现损失曲线嗡嗡乱跳,然后开始怀疑算法写错了。我踩过的坑是,忘了对奖励或优势做归一化。策略梯度的更新幅度直接受优势值大小影响,如果不同轨迹的奖励尺度差异巨大(比如有的环境单步+0.1,有的环境单步+100),梯度方向就会被大奖励轨迹霸占,小奖励但高效的动作被无视。
通用的做法是计算一个batch里的advantage均值和方法,然后标准化:
# 归一化 advantage advantage = (advantage - advantage.mean()) / (advantage.std() + 1e-8)加小常数1e-8是防除零。这个trick在PPO和A2C(Advantage Actor-Critic)里几乎是标配。需要注意的是,这个操作只改梯度尺度,不改变相对方向,因此不会破坏最优性。
4.2 熵正则:防止策略过早“锁死”
训练后期常见的一个问题是策略退化——网络输出某个动作的概率接近1,其他动作概率全部趋近于0。这听起来像是“学得很好了”,但通常其实是策略提前收敛到局部最优,尤其当环境探索不足时,策略过早失去了探索能力。
解决办法是在loss中加一个策略熵的负项,让策略保持一定随机性:
# 熵正则项(PyTorch) probs = torch.softmax(logits, dim=-1) entropy = -(probs * torch.log(probs + 1e-8)).sum(dim=-1).mean() actor_loss = actor_loss - entropy_coef * entropy熵正则系数entropy_coef一般取0.01到0.1之间。系数太小,防不住策略坍缩;系数太大,策略倾向“摆烂”保持高随机性,学不到有效动作。我习惯在前20%的训练轮次里用较大的系数(0.1),之后线性衰减到0.01,这样既保护了早期的探索,又不影响后期的精调。
4.3 折扣因子与GAE的选择
折扣因子 γ 决定了模型看多远。γ接近1表示关注长期回报,但方差会变大;γ太小则短视,容易陷入局部最优。一个常见的经验组合是 γ=0.99,GAE的 λ=0.95。GAE在策略梯度的框架下起着“平滑优势”的作用,让每一时间步的优势都被前后几步的回报信息“抹匀”。如果训练不稳定,优先把 λ 调到0.9试试;如果感觉学习太慢,可以升到0.98。这个参数非常值得多调几次。
4.4 学习率:策略网络和价值网络最好分开设
这算是一个进阶技巧。Actor网络(策略)和Critic网络(价值)在同一个loss里相加,但收敛速度往往不一致。Critic学得通常比Actor快,因为价值拟合是一个回归任务,信号密度高;Actor要靠Critic提供的信号更新,存在延迟。如果共用一个学习率,经常是Critic趋稳而Actor还在波动,或者反过来。
更精细的做法是分设学习率:比如Actor用3e-4,Critic用1e-3,两者相差几倍,给Critic更大步长让它更快跟上真实价值。许多高效实现还会用独立的优化器分别维护两个网络,避免梯度互相拉扯。如果你发现训练时价值损失明显较大而策略损失很小,可以试试提高Critic的学习率。
5. 常见问题与排查技巧实录
5.1 训练时loss一直没有下降,正常吗?
先说结论:策略梯度的loss不下降并不代表没在学。传统监督的loss下降是因为模型在不断逼近目标;而策略梯度的actor loss是“期望回报的负值”,如果策略已经在一个不错的状态,loss曲线可能就不会再大幅下降,甚至会上涨。真正该看的是回报均值曲线,而不是loss曲线。
如果回报均值曲线长时间平平无奇,排查点按优先级排序:检查奖励信号是否正确(有没有符号搞反)、检查价值网络是否收敛(看TD误差曲线)、检查优势归一化是否生效、检查熵值是否过早归零。
5.2 reward曲线上下剧烈震荡,怎么排查?
reward剧烈震荡是最常见的问题。可能原因有三类:学习率太大导致单一batch的梯度步覆盖了太多参数空间,batch_size太小导致回报信号噪声过大,clip阈值设置不当导致更新幅度失控。第一优先把学习率降到原来的1/5甚至1/10,经验上这个调整就能解决七成以上的震荡问题。
如果降学习率没有用,检查GAE的λ是不是太小,λ小会让优势曲线“锯齿化”,噪声信号直接传导到策略更新里。另外,多环境并行采样能显著平滑训练曲线,建议一次至少跑8~16个环境实例收集数据。
5.3 训练后期策略完全退化,动作概率全集中到一个动作上
这是熵坍缩的典型症状。可以先看策略熵曲线:如果熵在训练中期就掉到接近0,说明策略过早放弃了探索。处理方式是加大熵正则系数,甚至临时把熵正则作为主要优化目标跑几百步“解冻”策略。极端情况下,可以给动作概率分布加一个小的均匀噪声(比如ε-greedy),强制维持探索。
另外一个容易被忽视的原因是reward scale太小,比如单步奖励在0.01量级,网络输出差异被淹没在数值精度里,学习不到区分度。这种情况把奖励乘以一个常数放大,或者对advantage做强归一化,效果立竿见影。
5.4 一个重要心得:监控熵和advantage分布
训练策略梯度类算法,我强烈建议在TensorBoard或wandb里至少监控四条曲线:回报均值、策略熵、advantage的均值和标准差、新旧策略的KL散度。前两条判断训练健康度,后两条判断更新是否合理。KL散度如果一直飙升,说明每步更新都离旧策略太远,PPO的clip机制可能没生效,检查一下是不是忘了把old_log_prob包在no_grad里。
6. 我的实操经验与建议
策略梯度这个家族,从REINFORCE到PPO,表面上公式越来越复杂,但本质思路一直没有变:采样轨迹、评价好坏、调整动作概率。所有后来的改进都围绕一个目标——让“评价”的信号更稳定、更高效。
我给想入门的人建议是:不要直接跳到PPO就完事。先手写REINFORCE跑通CartPole,感受一下什么叫“方差爆炸”;再实现一个最简Actor-Critic,看看价值网络的引入如何缓解这个问题;最后再上手PPO,你才能真正理解为什么clip操作对训练稳定性这么重要。跳过这些中间步骤,你会面对一个黑盒调参器,很难积累出真正的经验。
最后分享一个我常用来调试的最小环境组合:CartPole验证算法能不能学,LunarLander连续版验证算法的稳定性,MuJoCo的Hopper验证算法在连续控制上的表现——跑通这三个,大部分策略梯度问题你已经有足够手感了。