简介:本资源是一份面向强化学习初学者与进阶实践者的深度原理推导笔记,聚焦Actor-Critic框架与PPO算法的核心思想、数学推导及实现逻辑,适用于AI方向学生、算法工程师及RL自学者系统理解策略梯度类方法。内容完整覆盖Actor与Critic网络的协同机制、TD误差计算、优势函数(Advantage Function)引入动机、基线(baseline)与信用分配(credit assignment)两大关键改进,以及PPO中重要性采样(Importance Sampling)如何实现on-policy向off-policy的稳健过渡。资源为1个599KB的PDF文件,排版清晰,含公式推导链、框架流程图与关键梯度表达式展开,便于逐行对照学习。目前已有6067人学习下载,是少有的将PPO理论细节(如∇Rθ推导、Aθ(s,a)定义、γ折扣累积奖励设计)讲透且不依赖代码实现的纯原理型资料。
1. 这不是又一个PPO公式搬运工:它是一份能让你亲手调通Actor-Critic、真正理解PPO Clip机制的推导实录
你是不是也见过太多“PPO公式堆砌”?推完KL散度就戛然而止,讲完clip ratio就贴PyTorch代码,结果自己一跑——loss不降、reward震荡、actor和critic互相拖后腿。我去年在某高校强化学习课程助教时,带过17个学生复现PPO,12人卡在“为什么advantage算出来是负的但loss还在涨”,5人死在“ratio clip后梯度突然消失”。问题不在代码,而在推导和实现之间那层薄薄的、没人捅破的窗户纸:比如,为什么GAE里的δₜ必须用V(sₜ₊₁)−V(sₜ)而不是rₜ?为什么clip区间设成[1−ε,1+ε]比直接截断ratio更稳?为什么Critic的MSE loss里要减去baseline,而Actor的loss里却用advantage乘logprob?这份《Actor-Critic原理+PPO算法推导》不是教科书复述,它是从TD error出发,一步步把“策略梯度怎么带优势函数”、“重要性采样如何控制更新步长”、“clip机制怎样防止策略突变”全拆开、重装、再拧紧的实战笔记。它专为那些已经写过DQN、试过REINFORCE、但面对PPO论文里那个min{·, clip(·)}符号仍会愣住三秒的工程师准备——你要的不是“它是什么”,而是“它为什么必须这样,以及不这样会翻车在哪”。
2. Actor-Critic不是两个网络拼起来就行:从TD error到Advantage Function的四层逻辑闭环
Actor-Critic框架常被简化为“Actor选动作,Critic打分数”,但这掩盖了它最精妙的设计闭环:Critic不是给Actor打分,而是在帮Actor把梯度信号从稀疏奖励中解耦出来。我们从最原始的TD error开始,一层层剥开这个闭环。
2.1 TD error是起点,但不是终点:为什么单靠r + γv′ − v无法指导策略更新?
TD error(δ = r + γV(s′) − V(s))本质是Critic对状态价值预测的瞬时误差。它的物理意义很清晰:如果当前状态s的价值被低估了(δ > 0),说明s比想象中更有潜力;反之则需降低其价值。但问题来了——这个误差不能直接喂给Actor。原因有二:
第一,δ是标量,而Actor需要的是“在s下选a比选其他动作好多少”的向量信号;
第二,δ混杂了环境随机性(r的方差)和Critic估计偏差(v′和v的误差),直接用于策略更新会导致梯度噪声爆炸。
所以,TD error只是“燃料”,真正驱动Actor的是经过加工的“引擎”——Advantage Function。
2.2 Advantage Function:从Q−V定义到GAE实现的工程妥协
Advantage Function A(s,a)的严格定义是:
A(s,a) = Q(s,a) − V(s)
其中Q(s,a)是状态-动作值函数(执行a后能获得的期望回报),V(s)是状态值函数(所有动作的期望回报均值)。这个差值直白地说:在s下选a,比“随便选个动作”的平均表现好多少。正A值鼓励提升π(a|s),负A值则抑制它——这正是Actor需要的、带方向的梯度信号。
但Q(s,a)不可观测(需知道所有后续轨迹),于是我们用GAE(Generalized Advantage Estimation)来逼近它。GAE不是数学技巧,而是在偏差(bias)和方差(variance)之间做工程权衡:
- 若λ=0,Âₜ = δₜ → 完全无偏,但方差极大(只依赖单步r);
- 若λ=1,Âₜ = ∑ᵢ₌ₜᵀ γⁱ⁻ᵗ rᵢ − V(sₜ) → 偏差小,但方差随T指数增长(需完整episode);
- PPO论文默认λ=0.95,即Âₜ = δₜ + 0.95γδₜ₊₁ + (0.95γ)²δₜ₊₂ + …
这个加权和的本质,是让远期奖励以γⁱ衰减,同时用λ控制“多远的未来该被信任”。实践中,λ=0.95~0.99是安全区,低于0.9易震荡,高于0.99则训练慢得像爬行。
2.3 Critic的损失函数:为什么用MSE(advantage)而非MSE(td_error)?
看项目正文第(4)条:“c_loss = MSE(adv)”。这和传统AC框架(c_loss = td_error²)有本质区别。原因在于:
- td_error²优化的是V(s)对真实回报R的拟合精度;
- MSE(adv)优化的是A(s,a)对真实advantage的逼近质量。
而PPO中,Actor的梯度是∇logπ(a|s) × Â(s,a),Critic的唯一使命就是让Â(s,a)尽可能准。若用td_error²,Critic会过度拟合V(s)的绝对值(比如把V(s)=100和V(s)=101都学得很准),但Â(s,a)=Q̂(s,a)−V̂(s)的误差可能依然很大。用MSE(adv)则强制Critic聚焦于“相对差异”,这是PPO稳定性的底层保障。
下面这段Python代码展示了GAE计算的核心逻辑,注意三个关键点:next_non_terminal、gamma、gae_lambda:
import numpy as np def compute_gae(rewards, dones, values, next_value, gamma=0.99, gae_lambda=0.95): """ Compute Generalized Advantage Estimation. Args: rewards: array of shape (T,), rewards at each timestep dones: array of shape (T,), True if episode ended at this step values: array of shape (T,), predicted state values V(s_t) next_value: scalar, V(s_{T+1}) for the last step gamma, gae_lambda: discount and GAE decay factors Returns: advantages: array of shape (T,), A_hat_t for each t """ advantages = np.zeros_like(rewards) gae = 0 # Process backwards: from last step to first for t in reversed(range(len(rewards))): # δ_t = r_t + γ * V(s_{t+1}) * (1-done_t) - V(s_t) delta = rewards[t] + gamma * next_value * (1 - dones[t]) - values[t] # A_hat_t = δ_t + γ * λ * A_hat_{t+1} gae = delta + gamma * gae_lambda * (1 - dones[t]) * gae advantages[t] = gae next_value = values[t] # for next iteration, V(s_t) becomes V(s_{t+1}) return advantages提示:
next_value在每轮循环中更新为values[t],这是为了在t-1步计算δₜ₋₁时,能正确拿到V(s_t)。很多初学者在这里写成固定next_value,导致GAE全错。
2.4 Actor的梯度来源:从∇R_θ到∇logπ×Â的完整链路
项目正文推导了∇R_θ = E[∑A(sₜ,aₜ)∇logπ(aₜ|sₜ)],但没说清为什么A(sₜ,aₜ)能替代R(τ)。关键在两点:
- Baseline减法:E[A(s,a)] = 0,所以加减任意baseline不改变期望梯度,但能大幅降低方差;
- Credit Assignment:A(sₜ,aₜ)只反映aₜ对后续回报的贡献,而非整个trajectory的总奖励,解决了“a₂不该为a₁的失误背锅”的问题。
因此,Actor的梯度不再是“所有动作一起升/降”,而是“a₁的A值高就猛提π(a₁|s₁),a₃的A值低就狠压π(a₃|s₃)”。这才是策略梯度真正的样子。
3. PPO不是AC的简单升级:Clip机制如何用一行min()守住策略更新的生死线
PPO的核心创新不在算法结构,而在对策略更新步长的硬约束。AC框架中,每次更新都用新数据重新训练,策略突变风险高;而PPO允许用旧策略(π_old)采集的数据,多次更新新策略(π_new),但必须确保π_new和π_old“长得不要太离谱”。这就是clip机制存在的全部理由。
3.1 Importance Sampling是桥梁,ratio是度量尺
当用π_old采集数据,却想更新π_new时,需用重要性采样修正分布偏移:
π_new的梯度 ≈ E_{s,a∼π_old} [ (π_new(a|s)/π_old(a|s)) × A(s,a) × ∇logπ_new(a|s) ]
其中ratio = π_new(a|s)/π_old(a|s)是核心。若ratio=1,说明新旧策略在(s,a)上完全一致;ratio=2表示π_new认为a在s下概率是π_old的两倍;ratio=0.1则表示π_new几乎放弃a。ratio越大,更新越激进;ratio越小,更新越保守。
3.2 Clip机制:用min{}函数给ratio套上“安全锁”
PPO2的loss定义为:
L^{CLIP}(θ) = E[min( r(θ)A, clip(r(θ),1−ε,1+ε)A )]
其中r(θ)=π_new/π_old。这个min操作的物理意义是:
- 当r(θ)∈[1−ε,1+ε]时,用原始ratio更新(保留信号);
- 当r(θ)>1+ε时,强行截断为1+ε(防止过度提升高频动作);
- 当r(θ)<1−ε时,强行截断为1−ε(防止过度打压低频动作)。
ε不是超参,而是策略变化容忍度的量化表达。ε=0.2意味着:任何动作的概率变化幅度超过20%,就视为危险操作,梯度被削平。这是PPO鲁棒性的灵魂。
3.3 为什么不用KL Penalty(PPO1)而用Clip(PPO2)?
项目正文提到了PPO1的KL penalty:J(θ) = J(θ_k) − βKL(θ,θ_k)。它的问题在于:
- KL散度难精确计算(需积分),实践中用sample近似,噪声大;
- β需自适应调整(adaptive KL),增加实现复杂度;
- KL衡量的是整个分布距离,而PPO真正关心的是单个(s,a)对的ratio是否失控。
Clip机制直接作用于ratio,无需估计KL,无需调β,且效果更稳定。OpenAI的实验表明,在多数任务中PPO2的性能和稳定性均优于PPO1。
3.4 Critic与Actor的协同更新节奏:经验池填满→批量更新→参数同步
PPO的框架图(项目正文第(1)~(8)条)隐含了一个关键工程设计:异步更新节奏。
- Step 1-1: 用π_new与env交互,填满经验池(如4096 steps);
- Step 2-1: 固定π_old,用池中数据批量更新π_new(如10 epochs);
- Step 2-2: 每次epoch内,对经验池shuffle后mini-batch训练(如64 batch size);
- Step 3: 所有epochs结束后,将π_new参数复制给π_old,开始下一轮。
这个节奏保证了:
- Critic每次更新都基于同一组高质量advantage(由固定π_old生成);
- Actor在单轮内可充分利用数据,避免AC的“采一训一”低效;
- π_old和π_new的差异可控,clip机制才能生效。
下面是一个标准的PPO loss计算函数,重点看ratio、surrogate_obj和clip_ratio三者的联动:
import torch import torch.nn.functional as F def ppo_loss( log_probs_new, # shape: (batch_size,), logπ_new(a|s) log_probs_old, # shape: (batch_size,), logπ_old(a|s) advantages, # shape: (batch_size,), A_hat(s,a) eps_clip=0.2, entropy_coef=0.01 ): """ Compute PPO-CLIP loss for actor update. Args: log_probs_new: log probability under current policy log_probs_old: log probability under old policy (collected data) advantages: estimated advantage, already normalized or not eps_clip: epsilon for clipping ratio entropy_coef: coefficient for entropy bonus (encourage exploration) Returns: total_loss: scalar tensor for backprop """ # Compute importance sampling ratio ratio = torch.exp(log_probs_new - log_probs_old) # π_new/π_old # Surrogate objective: min( r*A , clip(r)*A ) surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1.0 - eps_clip, 1.0 + eps_clip) * advantages surrogate_obj = torch.min(surr1, surr2).mean() # Entropy bonus: encourage exploration entropy = -(torch.exp(log_probs_new) * log_probs_new).mean() # Total loss: maximize surrogate_obj + entropy_bonus # So we minimize negative of it total_loss = -surrogate_obj - entropy_coef * entropy return total_loss # Usage example in training loop: # logp_new = actor.get_log_prob(states, actions) # (B,) # logp_old = old_actor.get_log_prob(states, actions) # (B,) # adv = compute_gae(...) # (B,) # loss = ppo_loss(logp_new, logp_old, adv, eps_clip=0.2) # loss.backward()注意:
torch.clamp(ratio, 1-eps, 1+eps)是clip的核心,它确保ratio不会超出安全区间。很多新手误用torch.clip或手动if判断,导致梯度中断。
4. 避坑:PPO训练中五个血泪教训,每个都曾让我重启三次训练
PPO看似公式简洁,实则处处是坑。这些不是理论假设,而是我在某跨平台系统仿真项目中,用3台A100反复验证过的真问题。现象、原因、解法,一条条列给你,省得你再踩一遍。
4.1 现象:Advantage全为负值,Actor loss持续上升,reward不增反降
原因:Critic过拟合,V(s)被系统性高估,导致A(s,a)=Q−V整体为负。常见于Critic网络太深(>3层)、学习率过高(>3e-4)、或未对advantage做标准化。
解决:
- 在compute_gae后,对advantages做z-score标准化:
adv = (adv - adv.mean()) / (adv.std() + 1e-8); - Critic学习率设为Actor的0.5倍(如Actor用3e-4,Critic用1.5e-4);
- Critic最后一层输出前加
nn.Tanh()(若reward范围已知,如[-1,1])或nn.Sigmoid()(若reward为[0,1])。
4.2 现象:Ratio频繁触发clip,surrogate_obj中surr2占比超80%,更新极其保守
原因:ε设得太小(如0.1),或π_old和π_new差异过大(因Actor学习率太高或batch size太小)。
解决:
- ε初始设0.2,若发现clip率>50%,先检查Actor学习率是否>3e-4;
- 增大batch size(从32→128),让ratio统计更稳定;
- 在loss中加入entropy bonus(coef=0.01),防止单一动作概率坍缩。
4.3 现象:训练初期reward暴涨后断崖下跌,Critic loss剧烈震荡
原因:GAE中next_value初始化错误。项目正文说“将经验池中最后一个s_输入Critic得v_”,但若最后一步是done=True,则s_无效,v_应为0。
解决:
- 在compute_gae中,
next_value必须根据dones[t]动态置零:next_value = 0 if dones[t] else values[t] # 而非固定用池末尾s_ - 或统一用
dones数组标记终止状态,GAE计算时自动处理。
4.4 现象:多轮训练后,所有动作概率趋同(如[0.33,0.33,0.34]),策略丧失区分力
原因:Entropy bonus系数过大(>0.02)或advantage标准化过度(std过小导致A值压缩)。
解决:
- Entropy coef从0.01起步,若发现exploration过强,逐步降至0.005;
- Advantage标准化时,
std加1e-8防除零,但避免用adv.std() < 0.1时强制设为0.1——这会放大噪声。
4.5 现象:Critic loss下降快,Actor loss下降慢,且两者loss scale相差百倍
原因:Actor和Critic用了同一学习率,但二者梯度量级天然不同。Critic优化MSE,目标是数值拟合;Actor优化策略梯度,目标是方向优化。
解决:
- Actor学习率设3e-4,Critic设1e-3(因Critic需更快收敛以提供准advantage);
- 或用Separate Optimizers:
actor_opt = torch.optim.Adam(actor.parameters(), lr=3e-4) critic_opt = torch.optim.Adam(critic.parameters(), lr=1e-3)
5. 把PPO从“能跑通”升级到“敢上线”:三个验证技巧与一个必做习惯
PPO训练不像监督学习,没有validation set上的准确率告诉你“成了”。它的验证是渐进式的、多维度的。以下三个技巧,是我从某图像处理Demo项目中沉淀出的“上线前 checklist”,每个都直击PPO落地的核心痛点。
5.1 技巧一:用“Policy Divergence Heatmap”可视化策略漂移
光看reward曲线不够,因为reward可能因环境随机性波动。真正可靠的是看策略本身是否健康演化。做法很简单:
- 每10个episode,用当前π_new在固定100个state样本上,计算所有动作的概率分布;
- 计算与初始策略π₀的JS散度(Jensen-Shannon Divergence),画成热力图(x轴:episode,y轴:state index,颜色:JS散度);
为什么用JS而非KL?KL不对称(KL(P||Q)≠KL(Q||P)),且当π_new(a|s)=0时KL发散。JS散度对称、有界[0,1],完美适配策略比较。
若热力图显示“左上角深色(早期漂移大)→ 右下角浅色(后期稳定)”,说明策略在合理探索后收敛;若整片泛红,说明clip失效或ε太小;若整片浅蓝,说明策略根本没学动。
5.2 技巧二:Advantage Sign Analysis——诊断Critic是否学会“归因”
Advantage的符号分布是Critic健康的晴雨表。理想情况下:
- 正advantage占比应在55%~65%(因baseline=E[R],略高于一半正常);
- 若正adv占比<40%,说明Critic严重高估V(s),或reward设计有问题(如稀疏reward未加shaping);
- 若正adv占比>80%,说明Critic低估V(s),或环境存在大量“伪正向反馈”(如撞墙得小reward但实际有害)。
在训练日志中加这一行:
pos_adv_ratio = (advantages > 0).float().mean().item() print(f"Episode {ep}: Pos Adv Ratio = {pos_adv_ratio:.3f}")连续5个episode该值<0.45,立刻停训,检查Critic架构或reward函数。
5.3 技巧三:Rollout Consistency Check——用旧策略重跑验证数据新鲜度
PPO依赖“π_old采集的数据仍适用于π_new”。但若π_new更新过快,旧数据会失效。验证方法:
- 每轮更新后,用当前π_new在相同env中rollout 100 steps,记录states和actions;
- 用π_old对这批states重算logπ_old(a|s),与π_new的logπ_new(a|s)对比;
- 计算ratio的均值和std:若mean(ratio)偏离1.0超±0.1,或std(ratio)>0.3,说明数据已过期,需缩短rollout周期或增大ε。
这个检查成本极低(一次forward),却是防止“用过期数据毒害新策略”的最后防线。
5.4 一个必做习惯:每次修改超参,先跑3个seed,再决定是否继续
PPO对随机性极度敏感。我曾因一个seed=42的偶然好结果,坚持了两天错误方向,直到换seed=13、seed=99发现reward全崩。现在我的铁律是:
- 任何超参调整(ε、lr、batch_size、gae_lambda),必须用3个不同seed(如0、1、2)各跑100k steps;
- 只有3个seed的reward曲线均呈上升趋势,且最终reward std < mean×0.15,才认定该配置有效;
- 否则,回退到上一版,查代码逻辑而非怪随机性。
从那以后我每次改完eps_clip,都强制走一遍三seed验证。不是怕失败,是怕把运气当能力。希望帮到你。
本文还有配套的精品资源,点击获取