news 2026/10/1 15:41:36

策略梯度算法详解:从REINFORCE到PPO的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
策略梯度算法详解:从REINFORCE到PPO的实战指南

做强化学习的人,迟早会和策略梯度(Policy Gradient)打交道。无论是玩CartPole、训练机器人走路,还是调大语言模型的RLHF,核心引擎基本都是它。很多教程把策略梯度讲得云里雾里,要么堆公式,要么直接甩代码,两头都不挨着。这篇算是我的个人笔记整理:把策略梯度的来龙去脉、几种主流算法的设计逻辑、实现时的坑和调参心得都串一遍,尽量做到看完能理解、能上手、能排错。适合刚入门RL的读者,也适合那些已经跑过PPO但总觉得哪里没吃透的朋友。

先说清楚策略梯度是什么。强化学习的任务是找一个策略(policy),让智能体在和环境交互的过程中拿到尽可能多的累计回报。传统值函数方法先估计Q值或V值,再根据值函数推策略;策略梯度则直接对策略本身做参数化,沿着“让好动作概率变大、坏动作概率变小”的方向更新参数。这个思路看起来简单,但涉及到目标函数怎么设计、梯度怎么估计、方差怎么控制等问题,稍不留神就会训练崩掉。所以这篇文章不光是讲公式,更想把公式背后的直觉、实现时的取舍、以及我踩过的那些坑一起讲清楚。

1. 策略梯度到底在做什么

1.1 从“先估值再决策”到“直接给动作打分”

最早接触强化学习时,大家一般先学Q-learning:维护一张Q表或Q网络,算出每个状态下每个动作的预期回报,然后选Q值最大的动作。这种“间接”方法在离散动作空间表现不错,但遇到连续控制(比如机器人关节力矩)、高维动作空间(比如语言模型输出token序列)时就会非常尴尬。Q-learning要找出所有动作的max,连续空间里根本无法穷举;策略梯度则天然适合连续动作——策略网络直接输出动作分布(例如高斯分布的均值和方差),采样即得动作,不需要求max。

所以策略梯度的第一个优点:它把策略本身作为优化对象,搜索空间就是参数空间,而不是动作空间。第二个优点是随机策略的天然探索机制:策略分布自带随机性,不会像ε-greedy那样需要额外维护探索逻辑。第三个优点是当回报函数复杂甚至不可导时,策略梯度可以用采样估计梯度,不用对环境求导——这一点在RLHF这种场景里特别关键,人类偏好反馈根本无法写成平滑函数,但我们可以通过策略梯度去优化它。

1.2 目标函数:三种写法,一个本质

策略梯度的目标函数有多种等价写法,常见的有三种:

  • 期望回报:J(θ) = E_{τ~πθ}[R(τ)],其中τ表示一个完整的轨迹(状态、动作、奖励序列),R(τ)是累积奖励。
  • 平均奖励形式:J(θ) = Σ_s dπ(s) Σ_a πθ(a|s) R(s,a),dπ(s)是策略下的稳态状态分布。
  • 初始状态形式:J(θ) = V^{πθ}(s0),即从初始状态出发的期望回报。

这三种写法本质都在衡量“当前策略跑出来的平均成绩”。优化目标就是找一组参数θ,让J(θ)最大。梯度上升的直觉是:如果某个动作在某个状态下带来了高于平均的回报,就提高这个动作的条件概率;反之就降低。梯度方向就是状态-动作对的对数概率对参数的梯度乘以回报值。

1.3 策略梯度定理:跳过未知的微分,直接采样估计

严格推导过程不细写,但结果很干净:

∇θ J(θ) = E_{τ~πθ} [ Σ_t ∇θ log πθ(a_t|s_t) * R(τ) ]

这个定理的价值在于:J(θ)的梯度可以表示成一个期望形式,期望里面有且仅有策略本身的梯度,不涉及环境动态模型(转移概率P(s'|s,a))的梯度。也就是说,即使我们完全不知道环境怎么跳转,只要能和环境交互采样,就能用蒙特卡洛方法无偏估计出策略梯度。这是策略梯度能落地的理论基石。

回想一下我刚学这里时的困惑:为什么可以无视环境的动态?直觉解释是——环境的跳转概率虽然影响回报,但它不依赖θ,所以对θ求导时这一项就“消失”了。真正需要调整的,是我们自己可控的策略分布。把这句话记牢,后面看Actor-Critic、PPO会顺很多。

2. 从REINFORCE到PPO:策略梯度算法的进化逻辑

2.1 REINFORCE:最朴素的蒙特卡洛策略梯度

REINFORCE是最基础的策略梯度算法,逻辑直白:

  1. 用当前策略采样一整条轨迹。
  2. 计算整条轨迹的总回报R(τ)。
  3. 对轨迹中每个时间步t,用 ∇θ log πθ(a_t|s_t) 乘以整个轨迹的回报R(τ),作为梯度贡献。
  4. 更新参数。

但直接这么干方差大到离谱。想象一个场景:一模一样的状态和动作,可能因为环境随机性导致后续回报波动巨大;一个本来不错的动作,这次运气差拿到负回报,梯度就会错误地压低它的概率。所以REINFORCE理论无偏但高方差,实际中必须配合大量采样和对冲技巧才勉强能用。

它的优点是简单——作为理解策略梯度的入口再合适不过。只要环境采样不太慢,CartPole这种玩具问题还是能跑通的。但如果你直接拿它去训练HalfCheetah,大概率看到loss曲线像心电图。

2.2 Actor-Critic:引入价值函数做基线

为了降低方差,算法设计者想到一个思路:我们不直接用总回报R(τ),而是用回报减去一个“预期水平”基线b(s_t)。如果某个动作的回报高于预期,就加大概率;低于预期,就减小概率。这个预期水平可以用价值函数 V(s_t) 来近似。改造后的梯度为:

∇θ log πθ(a_t|s_t) * (R_t - V(s_t))

其中 (R_t - V(s_t)) 就是优势函数(Advantage)的估计。这里的价值网络V(s)就是Critic,策略网络πθ就是Actor。Actor负责出动作,Critic负责打分,二者交替训练。这个结构已经成为现代策略梯度算法的标准骨架,后面所有算法都在这个框架上做文章。

2.3 TRPO:信任区域的试金石

直接用优势函数更新策略,容易出现步子太大扯着蛋的问题。一次更新太猛,新策略和旧策略差异过大,下一步采样的数据分布就变了,训练直接崩掉。TRPO的核心思路是:给每次更新加一个约束,限制新旧策略的KL散度在某个小范围内。这样做能保证更新后的策略不会离原来的策略太远,从而让训练过程稳定。

TRPO的实现复杂,光是那个共轭梯度求解就够折腾。但它提出了一个关键概念:trust region。理解了它的动机,你会更容易接受PPO的简化方案。

2.4 PPO:把约束变成惩罚

PPO没有严格求解KL约束,而是用两种更简单的办法来近似:

  • PPO-Clip:限制新旧策略的概率比 r_t(θ) = πθ(a_t|s_t) / π_old(a_t|s_t) 在 [1-ε, 1+ε] 之间。具体目标函数写出来就是 min(r_t * A_t, clip(r_t, 1-ε, 1+ε) * A_t)。翻译成人话:如果优势为正,就最大程度鼓励动作,但概率比不能超过1+ε;如果优势为负,就压制动作,但概率比不能低于1-ε。

  • PPO-KL:在目标函数里直接减掉 KL散度项,但需要自适应调整KL惩罚系数。

PPO-Clip在工程上最省心,不需要算KL,也不需要对约束做复杂求解,成为OpenAI等团队的首选。你如果了解它的来历,就知道为什么现在跑实验几乎都用PPO而不是TRPO。

2.5 算法对比速查

算法核心思路优点主要缺点
REINFORCE轨迹总回报 × log概率梯度简单、无偏方差高、样本效率低
Actor-Critic引入V值作为基线比REINFORCE方差低价值网络误差会引入偏差
TRPOKL散度硬约束稳定性极强实现复杂、计算量大
PPO-Clip概率比裁剪稳定且实现简单超参数ε敏感,需要调

选型建议:做实验和工程项目,默认PPO;学习理解,先手写REINFORCE再过渡到Actor-Critic;如果你的环境允许大量采样且不追求效率,TRPO也仍然可用。

3. 实现策略梯度时躲不开的细节

3.1 优势估计与GAE

前面说用 R_t - V(s_t) 当优势,但R_t具体怎么算很讲究。最朴素的做法是用蒙特卡洛回报 G_t = Σ_{k=0}^{T-t} γ^k r_{t+k}。这个无偏但方差大。另一种做法是用TD误差 δ_t = r_t + γV(s_{t+1}) - V(s_t),方差低但有偏差。GAE(Generalized Advantage Estimation)在这两者之间做了加权平均:

A_t^{GAE(γ,λ)} = Σ_{l=0}^{∞} (γλ)^l δ_{t+l}

当λ=0时,GAE退化成一次TD误差;当λ=1时,接近蒙特卡洛回报。实际中λ取0.95到0.99之间,是一个很稳的起点。GAE这个公式背下来,写PPO时几乎一定会用到。

3.2 基线、归一化与奖励尺度

即使有了GAE,优势数值可能忽大忽小。强烈建议在每轮batch内对优势做标准化:减去均值、除以标准差。这不是理论必须,但实践下来能显著稳定训练。原因在于策略梯度的更新幅度受优势量级影响,量级不稳定时,同一个学习率在不同batch里可能发出误导性的更新。

奖励尺度和折扣因子也需要认真对待。如果环境奖励都是5左右,γ取0.99,V值会累积到几百,网络需要学很大的数值,容易振荡。可以适当调小γ(比如0.95,对短视任务够用),或者在奖励上做缩放。但这些改动要理解其影响:γ越小,智能体越“短视”,尤其对于需要长程信用分配的任务,γ太小就废了。

3.3 熵正则:对抗策略坍缩

随着训练推进,策略会逐渐收敛到确定性的最优动作。但在收敛前,如果策略过早变成“几乎总是选同一个动作”,探索就停止了,可能永远找不到更好的策略,这叫熵坍缩。常见做法是在loss里加入一项:β * H(πθ(s)),其中H是策略分布的熵。鼓励策略保持一定随机性。β初始值可以从0.01开始,根据熵的下降速度调整。如果训练早期就看到熵骤降,把β升高到0.05甚至0.1;如果熵一直不降,可能是β太大,限制了策略收敛能力。

3.4 网络设计与参数初始化

策略网络和价值网络的一个实用建议:用两层MLP,隐层256或512,激活函数用tanh或ReLU都行,但连续动作输出层一定要配好初始scale。例如连续控制环境里,策略输出通常是高斯分布的mean和log_std;log_std初始值设为0或-0.5,别一开始就设成绝对值很大的负数,否则探索噪声太小,策略很难跳出局部。

Critic网络学习率一般比Actor低些,或者两个网络共用学习率但梯度裁剪。我自己的习惯是:Actor和Critic在同一个优化器下,但把Critic的loss loss乘以0.5或0.25,再算总loss。这让Critic更新稍微慢一点,减少它对Actor的误导。

4. 实操全程:从零实现一个可用的PPO-CartPole

4.1 环境搭建与整体流程

我这里以CartPole-v1为例,用PyTorch实现一个最小可用的PPO。虽然CartPole简单,但流程完整:采样 → 计算GAE → 更新策略 → 重复。你没有必要照着抄,重点看结构和关键参数怎么选。

import gymnasium as gym import torch import torch.nn as nn import torch.optim as optim import numpy as np class ActorCritic(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.common = nn.Sequential( nn.Linear(obs_dim, 128), nn.Tanh(), nn.Linear(128, 128), nn.Tanh() ) self.pi = nn.Linear(128, act_dim) self.v = nn.Linear(128, 1) self.log_std = nn.Parameter(torch.zeros(act_dim)) def policy(self, obs): x = torch.tanh(self.common(obs)) probs = torch.softmax(self.pi(x), dim=-1) return probs def value(self, obs): x = torch.tanh(self.common(obs)) return self.v(x) def dist(self, obs): # 简单起见用离散softmax,其实CartPole可以更简单 probs = self.policy(obs) return torch.distributions.Categorical(probs)

注意这里为了照顾CartPole的离散动作,用了Categorical分布。如果是连续任务,需要改成MultivariateNormal,代码如下:

def dist(self, obs): x = torch.tanh(self.common(obs)) mean = self.pi(x) std = torch.exp(self.log_std) return torch.distributions.Normal(mean, std.to(mean.device))

4.2 采样与GAE计算

PPO需要先跑若干步收集数据。一个完整epoch中,我们用当前策略跑出多个轨迹,存下obs、action、reward、done、value。然后按时间顺序算GAE。

def collect_rollout(env, model, steps=2048, gamma=0.99, gae_lambda=0.95): obs_list = [] action_list = [] reward_list = [] done_list = [] value_list = [] logprob_list = [] obs, _ = env.reset() for _ in range(steps): obs_t = torch.FloatTensor(obs).unsqueeze(0) dist = model.dist(obs_t) action = dist.sample().item() logprob = dist.log_prob(torch.tensor([action])).item() value = model.value(obs_t).item() next_obs, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated obs_list.append(obs) action_list.append(action) reward_list.append(reward) done_list.append(done) value_list.append(value) logprob_list.append(logprob) obs = next_obs if done: obs, _ = env.reset() # compute returns and advantages obs_tensor = torch.FloatTensor(np.array(obs_list)) action_tensor = torch.LongTensor(action_list) reward_tensor = torch.FloatTensor(reward_list) done_tensor = torch.FloatTensor(done_list) value_tensor = torch.FloatTensor(value_list) advantages = [] gae = 0 for t in reversed(range(steps)): next_value = 0 if (t == steps - 1 or done_tensor[t]) else value_tensor[t + 1] delta = reward_tensor[t] + gamma * next_value - value_tensor[t] gae = delta + gamma * gae_lambda * (1 - done_tensor[t]) * gae advantages.insert(0, gae) returns = torch.FloatTensor(advantages) + value_tensor advantages = torch.FloatTensor(advantages) return obs_tensor, action_tensor, logprob_list, returns, advantages

GAE的循环从后往前累加,注意结束时或者done时,next_value要置0,避免跨episode计算价值。

4.3 PPO更新步骤

PPO更新通常会在同一批数据上做多轮(比如4或10轮),每轮打乱数据并切成mini-batch。更新时,重新计算当前策略的logprob,然后算ratio = exp(pi_logprob - old_logprob),就是新旧策略概率比。

def update_ppo(model, optimizer, obs, actions, old_logprobs, returns, advantages, clip_eps=0.2, epochs=4, batch_size=64): total_loss = 0 for _ in range(epochs): inds = torch.randperm(obs.shape[0]) for start in range(0, len(inds), batch_size): idx = inds[start:start + batch_size] batch_obs = obs[idx] batch_actions = actions[idx] batch_old_ll = torch.FloatTensor([old_logprobs[i] for i in idx]) batch_returns = returns[idx] batch_adv = advantages[idx] dist = model.dist(batch_obs) entropy = dist.entropy().mean() pi_logprob = dist.log_prob(batch_actions).sum(-1) ratio = torch.exp(pi_logprob - batch_old_ll) adv = batch_adv # normalize advantages adv = (adv - adv.mean()) / (adv.std() + 1e-8) loss_pi = -torch.min(ratio * adv, torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * adv).mean() loss_entropy = -0.01 * entropy value_pred = model.value(batch_obs).squeeze(-1) loss_value = ((value_pred - batch_returns) ** 2).mean() loss = loss_pi + 0.5 * loss_value + loss_entropy optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.5) optimizer.step() total_loss += loss.item() return total_loss / (epochs * max(1, (obs.shape[0] // batch_size)))

这里有几个细节值得注意:

  • 优势标准化:只对当前batch的advantage做标准化,目的是稳定更新幅度。
  • 熵惩罚:系数取了0.01,CartPole问题简单,太小不会提前坍缩;像连续控制任务就适当加大。
  • 梯度裁剪:clip_grad_norm很关键,尤其当ratio过大或value loss爆炸时能救命。
  • value loss系数:0.5,让critic收敛更平滑。

4.4 训练循环与效果

env = gym.make("CartPole-v1") model = ActorCritic(env.observation_space.shape[0], env.action_space.n) optimizer = optim.Adam(model.parameters(), lr=3e-4) for iteration in range(500): obs, actions, old_ll, returns, adv = collect_rollout(env, model, steps=2048) loss = update_ppo(model, optimizer, obs, actions, old_ll, returns, adv) # 每50轮或定期评估一下 if iteration % 50 == 0: eval_reward = evaluate(model, env) print(f"Iter {iteration}, loss ~ {loss:.3f}, eval reward ~ {eval_reward:.1f}")

CartPole在PPO下通常几百个iteration就能达到500分的上限。如果epoch数太少(比如1),收敛会慢;太多(比如20)容易过拟合当前batch,导致下次采样性能反而下降。4到10是一个合理区间。

5. 训练策略梯度最容易踩的坑

5.1 优势数值量级忽大忽小

现象:loss曲线出现巨大尖刺,参数更新后策略一瞬间变成随机策略。排查方向:先看advantage的均值和标准差,如果某个batch的adv数值是另一个batch的几十倍,说明GAE计算有问题,或者奖励尺度在不同episode差异太大。解决:对adv做标准化,并检查奖励是否异常。还要确认gamma设置是否合理,gamma太接近1时GAE数值会非常大。

5.2 策略熵急速崩塌

现象:训练初期熵就快速掉到接近0,然后性能不再提升。原因通常是探索不足。对策:增加熵系数,从0.01调到0.05甚至0.1;降低学习率;检查log_std初始化是否过小。连续动作任务里,尤其注意log_std不要初始化为负数大值,否则策略一开始就“自信得过头”。

5.3 value loss不下降

现象:Critic的value loss一直在高位,Advantage约等于随机噪声。原因可能是网络容量不够,或者状态观测没有归一化。很多环境的状态尺度差异很大(位置是0.1量级、速度是10量级),建议对obs做RunningMeanStd归一化,或者至少做标准化。对CartPole这种简单任务不敏感,但对HalfCheetah这种连续控制任务,obs不归一化很难训练。

5.4 随机种子与可复现性

强化学习对随机种子的敏感度远超普通监督学习。同一个代码,seed=1能跑到500分,seed=2可能只有200分。这不是代码bug,而是回报分布非平稳导致的正常现象。要写实验时务必固定seed,并且跑多个seed取平均。另外,确定性推理时关闭dropout、把策略均值作为动作,能帮助你评估当前策略的真实能力。

5.5 ratio爆炸问题

PPO-Clip的一个潜在风险是ratio可能特别大。当旧策略把某动作概率压到极低,新策略又认为这个动作很好时,exp(pi_logprob - old_logprob)可能超过几十。如果裁剪限定在0.8到1.2,好像问题不大,但在batch内某些极端ratio依然会让梯度爆炸。我的经验是:尽量保证采样时策略不是太确定,log_std不要过小;如果ratio超过10的比例多于1%,就要检查是不是策略学习率太高。

6. 我的个人实操体会

策略梯度这套东西,理论门槛不高,但工程坑不少。我在跑了至少几十次PPO变体之后,最大的感受是:调公式参数不如调数据。多数时候训练不稳定,问题不在算法本身,而在奖励设计、obs归一化、batch大小和advantage估计这些“外围环节”。比如有一次我在自定义机器人环境里训练,策略一直不收敛,折腾了一周,最后发现是奖励里某个传感器读数存在极大的离群值,把advantage搞出了几个数量级的尖峰。把奖励clip之后,效果立刻上升。

另外一个容易被忽略的点是batch大小。PPO默认2048步往往是个最低门槛,如果任务复杂,比如需要在长轨迹里做信用分配,1024步根本连一个完整episode都覆盖不了,GAE算出来就失去意义。建议先统计一下一个episode的平均长度,让每次采样至少覆盖几十个完整episode的长度。

最后说说“收藏版”这三个字。策略梯度相关的知识确实适合收藏,因为它是RL里最常见、使用面最广的一类算法,从学术论文到工业落地,几乎绕不开。但收藏之后更要做的是:自己动手实现一次REINFORCE,再改成Actor-Critic,再改造成PPO。这个手写过程带来的理解,比读十篇总结都管用。希望这篇笔记能帮你少走些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 15:41:08

预装全套依赖,OpenClaw 3.1.0 本地 AI 自动化完整落地教程

📌 说明 本文基于 OpenClaw 版本展开讲解,全程采用图形化可视交互模式。整合包已内置全部运行依赖,普通使用者无需额外配置,即可完整复现整套部署流程。 ✨核心亮点: 全程可视化图形交互界面,自动补齐全部运…

作者头像 李华
网站建设 2026/10/1 15:40:32

风光储互补微电网Simulink建模与仿真:从拓扑搭建到控制策略全解析

做新能源方向的仿真课题,很多人第一反应就是搭一个“风光储互补微电网”的Simulink模型。这题目看着简单——把光伏、风电、储能接到一条母线上,再挂个负荷,好像就完事了。但真正动手之后你会发现,模型能不能跑起来、跑出来的结果…

作者头像 李华
网站建设 2026/10/1 15:40:06

Trae国际版600次快速请求实测:AI编程免费额度真实价值解析

1. 先搞清楚:Trae到底是什么,怎么突然就火了最近AI编程圈子里,Trae这个名字出现的频率越来越高。如果你经常逛技术社区或者刷B站,应该已经看到不少博主在晒用它写代码的截图。字节跳动推出的这款免费的AI原生IDE,基于V…

作者头像 李华
网站建设 2026/10/1 15:40:03

UE5烘焙光照实战:从Lumen切换到Baked的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 15:39:46

为什么越来越多的酒店学校工厂选择空气能热泵热水工程?

针对目前商用热水市场的变化,我结合这些年在一线项目上的观察,和读者聊聊空气能热泵热水工程被越来越多酒店、学校、工厂选用的原因。我们团队在实践中发现,其根本逻辑在于从“买设备”转向了“买系统保障”。一、行业痛点:预算与…

作者头像 李华
网站建设 2026/10/1 15:37:41

2026成都景区古建牌坊检测排名 TOP5 CMA 资质机构提供牌坊裂缝检测、牌坊倾斜检测、老化检测 联系方式推荐

成都古建牌坊检测市场近年热度攀升,景区石牌坊、乡村古牌楼、文物古建牌坊的结构安全鉴定需求日益旺盛。小编实地走访发现,市面上检测机构虽多,但鱼龙混杂,不少无资质单位出具的检测报告根本无法通过住建、文物部门核验&#xff0…

作者头像 李华