上一节的结尾留下了一个明显的缺口:基线选什么。两种极端都不好用——用整局回报 Gt 当「优势」的无偏估计(REINFORCE),方差大到信噪比接近 1;用一个学出来的值函数当基线,方差骤降但引入了偏差(值函数本身在漂移)。这一节的起点就是把这两种极端中间的路走通:学策略的 Actor 与学价值的 Critic 互相喂饭,前者用 Critic 给出的优势更新自身,后者用前者采集的数据修正价值——这就是 Actor–Critic。
但 Actor–Critic 只解决了「优势怎么算」,没有解决一个更致命的工程问题:步长。策略梯度对步长极其敏感——步长太大,一次更新就能把策略推进一个「所有动作都变差」的区域;而策略一坏,后续采样的数据全废,你甚至无法判断是哪里坏了(这正是 RL 难调试的核心)。TRPO(Trust Region Policy Optimization)用「限制新旧策略的 KL 距离」优雅地解决了它,代价是要解带约束的优化问题、需要二阶信息。PPO(Proximal Policy Optimization)则用一个四行的裁剪目标达到几乎相同的效果,只需一阶梯度——它因此成为今天最通用的默认算法,也是 5.6 里 RLHF 第一阶段的执行器。
这一节的组织:优势函数与 Actor–Critic(5.5.1)→ GAE(5.5.2,把偏差—方差做成一根旋钮)→ TRPO 的信任域(5.5.3,讲清「为什么必须限幅」)→ PPO 的裁剪目标(5.5.4,本讲最该会推的一段)→ 熵正则与稳定性诊断(5.5.5–5.5.6)→ 一个能跑的最小更新步骤(5.5.7)。
定义 5.5.1 · 优势函数(advantage)
Aπ(s,a)=Qπ(s,a)−Vπ(s),Vπ(s)=a∑π(a∣s)Qπ(s,a).
Aπ(s,a) 度量「在该状态下执行 a 比按策略平均好多少」。两条基本性质:
- ∑aπ(a∣s)Aπ(s,a)=0:在策略的分布下,优势的均值为零(这是基线无偏性的直接推论,定理 5.4.3);
- Aπ(s,a)=0 的动作是「平均水平」的动作;只有 A>0 的动作才值得抬高概率。
优势与 Q 的差别:把「状态有多好」除掉
Q 的数值里混了两部分:这个状态本身好不好(比如「离终点只有一步」)与这个动作选得对不对。前者对同一状态下的所有动作是公共项,对排序毫无贡献,却会主导梯度的大小——在值很大的状态里,所有动作的权重都很大,学习率实际被放大;在值很小的状态里,学习几乎停滞。
减去 V(s) 就是把这层「公共背景」除掉,只留下「差异」。这也是为什么优势归一化(把一个 batch 的 A^ 标准化到均值 0、方差 1)在实践中几乎总是有用:它让不同状态、不同时间的梯度尺度可比。
Actor–Critic 与 A2C:两个网络,各管一件事
结构:
- Actor:策略网络 πθ(a∣s),负责「演」,按策略梯度更新;
- Critic:值网络 Vϕ(s),负责「评」,用 TD 学习拟合回报,并向 Actor 提供优势估计。
最常用的优势估计是「一步 TD 残差」:
A^t=rt+1+γVϕ(st+1)−Vϕ(st)=δt,
它既有 Critic 的低方差,又只需走一步就能更新(不必等整局结束)。Critic 的损失就是让 Vϕ 逼近「回报估计」:
LV(ϕ)=Et[(Vϕ(st)−R^t)2],R^t=A^t+Vϕ(st) (用同一批数据算出的“不变的目标”).
A2C(Advantage Actor–Critic):把上面这件事并行化——同时跑 N 个环境副本,把 N×T 步数据凑成一个 batch 做一次更新(A3C 是它的异步版本)。并行环境的作用不是提速,而是降方差:不同副本的随机性互相抵消,相当于把 batch 变大。
| 优势估计 |
公式 |
偏差 |
方差 |
何时用 |
| 蒙特卡洛 |
A^t=Gt−Vϕ(st) |
无偏(Vϕ 准确时) |
大(含整局随机性) |
回合短、奖励稠密 |
| 一步 TD |
A^t=δt=rt+1+γVϕ(st+1)−Vϕ(st) |
依赖 Vϕ 质量 |
小 |
通用默认,A2C 的主力 |
| n 步 |
A^t(n)=∑l=0n−1γlδt+l |
随 n 减小 |
随 n 增大 |
想要更强信号时 |
| GAE |
A^tGAE=∑l≥0(γλ)lδt+l |
由 λ 连续调节 |
由 λ 连续调节 |
PPO 的事实标准(5.5.2) |
上面那张表里,「n 步」是一个离散的选择:n=1,5,20,…。GAE 的做法是把所有 n 的估计按指数权重混起来——于是不必再挑 n,只要调一个连续的 λ∈[0,1]。
定理 5.5.1 · 广义优势估计(GAE)
记 δt=rt+1+γV(st+1)−V(st)(用 Critic 估计)。定义 n 步优势
A^t(n)=l=0∑n−1γlδt+l(n≥1),A^t(∞)=Gt−V(st).
对所有 n 取指数加权平均(权重 (1−λ)λn−1,λ∈[0,1)):
A^tGAE(γ,λ)=(1−λ)n≥1∑λn−1A^t(n)= l≥0∑(γλ)lδt+l
等价地,它满足一个从后往前的递归:
A^t=δt+γλA^t+1,A^T=0 (超出轨迹末端的项为 0).
两个极端:λ=0 时 A^t=δt(一步 TD,偏差大、方差小);λ=1 时 A^t=∑lγlδt+l=Gt−V(st)(蒙特卡洛,偏差小、方差大)。
为什么 γλ 会「一起」出现,以及怎么调 λ
闭式里的 (γλ)l 有两个来源:γl 来自折扣(第 l 步之后的奖励本来就该打折),λl 来自我们故意给远处的 TD 残差打的额外折扣。因此调 λ 就是在调「多信任远期数据」:
- λ→0:几乎只信一步之内的信息,估计主要来自 Critic——如果 Critic 学得差,梯度方向就被带偏(偏差);
- λ→1:几乎完全信实际回报,估计无偏但每条轨迹的噪声全部进来(方差);
- 实践中的默认是 λ=0.95 上下(与 γ=0.99 搭配,γλ≈0.94,有效记忆约 1−γλ1≈17 步)。
实现提示:闭式看起来要算无穷项,但递归式只需一次从后往前的循环(代码见 5.5.7)。它同时也回答了「为什么 PPO 里总能看到 λ 这个超参数」——它就是偏差—方差的旋钮,没有更深的含义(更紧凑的版本见附录 C.11)。
推导(指数加权求和 → 闭式)
把 n 步优势展开为双重求和,并交换求和次序(n≥1,0≤l≤n−1):
(1−λ)n≥1∑λn−1l=0∑n−1γlδt+l=(1−λ)l≥0∑γlδt+ln≥l+1∑λn−1=(1−λ)l≥0∑γlδt+l⋅1−λλl=l≥0∑(γλ)lδt+l,
其中用到几何级数 ∑n≥l+1λn−1=1−λλl(λ<1)。
递归式的证明:把闭式在 t 处拆出 l=0 项:
A^t=δt+l≥1∑(γλ)lδt+l=δt+γλl≥0∑(γλ)lδt+l+1=δt+γλA^t+1. ■
∎
例 5.5.1 · 手算 GAE:λ 如何改变同一个序列的估计
给定 γ=0.99、λ=0.95,四步的 TD 残差为
δ0=1.0,δ1=−0.8,δ2=0.6,δ3=−0.4.
求 A^0 在 λ=0、λ=0.95、λ=1 三种设置下的值,并解释差异。
解
记 γλ=0.99×0.95=0.9405。用递归 A^t=δt+γλA^t+1(A^4=0):
(a) λ=0:A^t=δt,故 A^0=1.0。
(b) λ=0.95(自后往前):
A^3=−0.4,A^2=0.6+0.9405×(−0.4)=0.2238,
A^1=−0.8+0.9405×0.2238=−0.5895,A^0=1.0+0.9405×(−0.5895)=0.4455.
(c) λ=1(此时 γλ=0.99):
A^3=−0.4,A^2=0.6+0.99×(−0.4)=0.204,
A^1=−0.8+0.99×0.204=−0.598,A^0=1.0+0.99×(−0.598)=0.408.
解释:三者对 A^0 的估计从 1.0(只信第一步)降到 0.445(λ=0.95)再到 0.408(完全信实际回报)。差别来自后面的负残差:越信任远期数据,越会把「后面变差了」这件事计入当前优势。注意 λ=0.95 与 λ=1 在这里只差 0.037——说明 λ 的作用是「平滑」,而不是「换一个量级」;它的价值主要体现在方差上(λ=1 会把整条轨迹的噪声全带进来)。这也提示一个实用判据:如果 λ 从 0.95 调到 1.0 后回报曲线剧烈振荡,说明 Critic 尚可、但轨迹噪声太大;反过来若调到 0 反而更好,说明 Critic 比实际回报更可信(不常见,通常出现在奖励极度稀疏时)。■
∎
策略梯度与监督学习最大的工程差异是失败是不可逆的。监督学习一步走坏,下一批数据仍然是那个固定的数据集,梯度还在;RL 里一步走坏,后续采样的数据全部来自坏策略——你可能再也采不到「好策略才会遇到的状态」,学习信号随之消失,错误被永久固化。
TRPO 的信任域(trust region)
用重要性采样把「新策略的回报」写成一个可以用旧数据估计的替代目标(CPI, conservative policy iteration):
LCPI(θ)=Es∼dπold,a∼πold[πθold(a∣s)πθ(a∣s)A^πold(s,a)].
注意状态分布仍是旧的 dπold——这是「替代」两字的由来:它只是真实目标的一阶近似。TRPO 在这上面加一个硬约束:
θmax LCPI(θ)s.t.Es[KL(πθold(⋅∣s)∥πθ(⋅∣s))]≤δ.
为什么用 KL 而不是参数距离:策略的行为只由输出分布决定,参数走多远并不直接对应行为变化(softmax 的饱和区可以让参数变化很大而分布几乎不动,反之亦然)。KL 是行为层面的度量,因此更可靠。
代价:这个约束优化在实现上要做二阶近似(用 Fisher 信息矩阵)、共轭梯度求解方向、再做线搜索保证约束满足。它能用,但工程上不轻——这正是 PPO 出现的动机。
为什么「步长」是 RL 的独有难题
监督学习的步长只需满足「损失下降」,因为数据分布固定、目标函数固定。RL 里步长还额外控制数据分布的变化速度:策略变太快,旧的样本(以及 Critic 的估计)就迅速失效。更麻烦的是,失败看起来像成功——回报可能因为随机性暂时上升,而策略已经不可恢复。
因此 RL 的稳定化技巧可以分成三类,分别对应三个不稳定源:限幅(PPO / TRPO,控制策略变化)、降噪(基线、GAE、并行环境、优势归一化)、限速(学习率衰减、梯度裁剪、定期重新初始化 Critic)。看一个 RL 实现时,先找出它这三类里各做了什么。
PPO 的信任域 intuition(本教程计算绘制,ε=0.2)。红线是实际目标min(ρA, clip(ρ, 1−ε, 1+ε)·A):当新旧策略比 ρ 越出 [1−ε, 1+ε],目标拉平、梯度归零——一步更新被硬性限幅,这是 PPO 比 REINFORCE 稳定的全部秘密(见 5.5 节)。
定义 5.5.2 · PPO 的裁剪替代目标
设 πθold 是采样时用的策略,A^t 是优势估计(通常是 GAE),定义概率比
ρt(θ)=πθold(at∣st)πθ(at∣st)(ρt=1 表示与采样时完全一致).
PPO 最大化的是裁剪替代目标
LCLIP(θ)=Et[min(ρt(θ)A^t, clip(ρt(θ),1−ε,1+ε)A^t)],
其中 ε 是超参数(论文与社区默认常取 0.2;等价地,也可以用「单侧减半」的参数化写成 ε 与 0.5ε)。
裁剪到底裁掉了什么
ρt 衡量新策略相对老策略「偏爱这个动作的程度变了多少」:ρt=1 表示没变,ρt=5 表示把这个动作的概率抬到了 5 倍。
- 当 A^t>0(好动作):目标希望 ρt 越大越好,但被 min 与 clip 卡在 1+ε——抬到一定程度就够了,再抬不给额外收益;
- 当 A^t<0(坏动作):目标希望 ρt 越小越好,但被卡在 1−ε——压低到一定程度也不再多要。
于是「越过裁剪区间的更新」在继续外推的方向上拿不到梯度(梯度为 0),策略不会因为一个 batch 的噪声被一步推飞。这就是它替代 TRPO 的原因:同样的「别走太远」的约束,却只需一阶梯度、实现简单、对超参数鲁棒。
它也是「近似 on-policy」的:ρt 是重要性采样比,修正了「用老策略数据估计新策略目标」的偏差,因此同一批数据可以重复做几个 epoch 的梯度上升(论文常用 3–10),样本效率比 REINFORCE 高不少;但数据仍来自 πθold,跑完这几轮后必须重新采样。
推导(分情形:哪两个象限梯度为 0,哪两个象限被“拉回”)
记 f(ρ)=min(ρA, clip(ρ,1−ε,1+ε)A),其中 A 视为与该样本的 θ 无关(优势用旧数据估计,是常数)。注意 clip 关于 ρ 在区间外是常数,故分四个象限讨论。
情形 ①:A>0 且 ρ>1+ε。 此时 clip=1+ε<ρ,所以 clip⋅A=(1+ε)A<ρA,min 取常数项:
f=(1+ε)A⟹∂ρ∂f=0.
梯度为 0:不再奖励「继续抬高概率」。
情形 ②:A<0 且 ρ<1−ε。 此时 clip=1−ε>ρ。因 A<0,乘以更大的正数得到更负的值:(1−ε)A<ρA,min 仍取常数项:
f=(1−ε)A⟹∂ρ∂f=0.
梯度为 0:不再奖励「继续压低概率」。
情形 ③:A>0 且 ρ<1−ε。 此时 clip=1−ε,比较 1−ε 与 ρ 得 (1−ε)A>ρA,于是 min 取 f=ρA:
∂ρ∂f=A>0.
梯度仍然为「抬高 ρ」——方向恰好是把自己拉回区间。
情形 ④:A<0 且 ρ>1+ε。 由 clip=1+ε<ρ 与 A<0 得 (1+ε)A>ρA,故 min 取 f=ρA:
∂ρ∂f=A<0.
梯度仍然为「压小 ρ」——同样是把自己拉回区间。
情形 ⑤:1−ε≤ρ≤1+ε:clip=ρ,两项相等,f=ρA,梯度 =A(这就是带重要性权重的普通策略梯度)。
作为下界的意义:因为 min 恒取两个候选里较小的那个,故
LCLIP(θ) ≤ Et[ρtA^t]=LCPI(θ)(逐点成立),
即裁剪目标始终是无裁剪替代目标的保守下界——它不会高估「一步更新能带来的改善」。把这一点与情形 ①②③④ 合起来看,就得到 PPO 的全部行为:区间内正常优化;越界后「继续外推」的梯度被归零,而「回到区间」的梯度保留。这就是「软信任域」:TRPO 用硬约束禁止越界,PPO 允许越界但不再给你奖励。
注意:这个推导说明的是「一旦越界,目标函数对该样本的梯度为 0」,不等于「该样本对参数 θ 完全没有梯度贡献」——因为 ρt 本身通过 πθ 依赖 θ,只是链式法则中 ∂ρ∂f=0 这一环断开了。工程上因此仍要用 clip_fraction 监控有多少样本落在越界区。■
∎
例 5.5.2 · 裁剪目标的分情形数值验算(ε=0.2)
对以下四种组合,写出 LCLIP 的取值与当前梯度分支(是否被截断):
| 组合 |
A^ |
ρ |
ρA^ |
clip(ρ,0.8,1.2)A^ |
min 取值 |
梯度分支 |
| (a) |
+2 |
1.5 |
3.0 |
2.4 |
2.4 |
截断(∂f/∂ρ=0) |
| (b) |
+2 |
0.5 |
1.0 |
1.6 |
1.0 |
活跃,方向为抬高 ρ(拉回) |
| (c) |
−2 |
1.5 |
−3.0 |
−2.4 |
−3.0 |
活跃,方向为压低 ρ(拉回) |
| (d) |
−2 |
0.5 |
−1.0 |
−1.6 |
−1.6 |
截断(∂f/∂ρ=0) |
解
逐格验算(ε=0.2,故裁剪区间 [0.8,1.2]):
(a) A^=+2,ρ=1.5>1.2:未裁剪项 3.0,裁剪项 1.2×2=2.4,min=2.4。属情形①:梯度为 0——尽管「事实上的目标值」仍是正的(2.4),但继续抬高 ρ 不再带来任何收益。
(b) A^=+2,ρ=0.5<0.8:未裁剪项 1.0,裁剪项 0.8×2=1.6,min=1.0。属情形③:梯度 =A^=+2>0,即鼓励增大 ρ(从 0.5 向区间靠拢)。
(c) A^=−2,ρ=1.5>1.2:未裁剪项 −3.0,裁剪项 1.2×(−2)=−2.4,min=−3.0。属情形④:梯度 =A^=−2<0,即鼓励减小 ρ(从 1.5 向区间靠拢)。
(d) A^=−2,ρ=0.5<0.8:未裁剪项 −1.0,裁剪项 0.8×(−2)=−1.6,min=−1.6。属情形②:梯度为 0——已经压低到这个坏动作的幅度,不再追加惩罚。
读法:把 A^ 当「方向」、把 ρ 当「已走多远」。(a)(d) 是「走太远且方向一致」→ 停手;(b)(c) 是「方向一致但走到反向区间去了」→ 继续走。这正是「软信任域」的四格图。■
∎
PPO 的完整目标与超参数
实践中的 PPO 从来不是只优化裁剪项,而是三项相加(符号约定:最大化目标、最小化其负值):
LPPO(θ,ϕ)=策略(Actor)Et[min(ρtA^t, clip(ρt,1−ε,1+ε)A^t)]− c1价值(Critic)Et[(Vϕ(st)−R^t)2]+ c2熵正则Et[H(πθ(⋅∣st))].
熵正则:H(π)=−∑aπ(a∣s)logπ(a∣s) 度量策略的随机性。加它有两个作用:维持探索(防止策略过早坍缩到单一动作、熵归零后再也回不来)与正则化(抑制策略过度自信)。系数 c2 常取 0–0.01:太小则熵塌缩,太大则拒绝收敛(回报曲线上表现为「一直很随机、学不快」)。
| 超参数 |
PPO 论文(MuJoCo 设置)与社区默认(以 Stable-Baselines3 的 PPO 默认值为参照) |
作用与调法 |
| 裁剪系数 ε |
0.2 |
越小越保守;0.1–0.3 是常见范围 |
| 折扣 γ |
0.99 |
与任务时间尺度匹配(5.2) |
| GAE λ |
0.95 |
偏差—方差旋钮(5.5.2) |
| 学习率 |
3×10−4(Adam) |
偏大是 PPO 不稳定的首要原因 |
| rollout 长度 T / 并行环境数 N |
T=2048(单环境 SMDP)或 N×T≈2048–8192 |
决定一次更新的数据量与方差 |
| minibatch / epochs |
64 / 10 |
epochs 越大越省样本、越容易越界(看 clip_fraction) |
| 价值系数 c1 |
0.5 |
太小 Critic 学不动,太大梯度被价值项主导 |
| 熵系数 c2 |
0.0(论文)/0.0–0.01(社区) |
熵塌缩时上调 |
| 梯度裁剪 |
max-norm 0.5 |
抑制单批异常更新 |
| 优势归一化 |
开(batch 级标准化) |
让不同任务的学习率可比 |
| KL 早停阈值 |
常见 0.015–0.03(社区经验值) |
超过阈值就丢弃本批剩余 epoch |
PPO 相对「稳定」不等于「不会坏」。它坏的时候通常不会给出明显报错,只会悄悄变差(或看起来在变好)。下面这张表是本讲最实用的工程清单之一。
PPO 的六个诊断指标
| 指标 |
正常表现 |
异常与含义 |
处理 |
| 近似 KL E[ρ−1−logρ] |
每批更新后约 10−3–10−2 |
持续 >0.03:策略每批变化过大 |
降学习率、降 epochs、降 ε、开 KL 早停 |
| 裁剪比例(越界样本占比) |
约 5%–20% |
长期 >50%:更新被大量截断,学不动;接近 0:裁剪几乎没起作用 |
调 ε;检查优势尺度是否过大 |
| 策略熵 |
缓慢下降 |
突然塌到 ~0:探索死锁;一直不降:c2 太大或任务没学会 |
调 c2;检查奖励是否有信号 |
| 价值损失 |
平缓、与回报同尺度 |
爆炸:Critic 目标尺度太大 |
奖励缩放、调 c1、检查 done 掩码 |
| 解释方差(explained variance,1−Var(R−V)/Var(R)) |
上升趋近 1 |
<0:Critic 还不如「用均值预测」 |
Critic 单独多训几步;检查观测归一化 |
| 回报与回合长度分布 |
多种子下均值上升、方差收敛 |
只有单条曲线好:不能下结论(5.7) |
补种子;报分布而不是最优值 |
本节常见误解
- 「PPO 是 on-policy,所以同一批数据只能用一次」:严格策略梯度确实是;PPO 用 ρ 做了重要性修正,因此可以在同一批数据上跑几个 epoch。但能跑几轮是有上限的——KL / 裁剪比例就是那个上限的度量。跑太多轮 ⇒ 数据严重 off-policy ⇒ 训练崩塌。
- 「裁剪对所有越界情形都起作用」:不是。5.5.4 的推导 表明 4 个越界象限里只有 2 个梯度归零(A>0 且 ρ>1+ε;A<0 且 ρ<1−ε),另 2 个仍然活跃且方向是「拉回」。
- 「ρ 的裁剪等价于把概率比硬截断」:被裁剪的是目标函数,不是策略概率本身。策略概率依旧可以变到任意值(只是没有奖励激励),这与「把概率截断到 0」完全不同——后者会导致 log0 与熵塌缩。
- 「优势不归一化也行」:优势的尺度随奖励设计变化,不归一化等于让有效学习率随任务漂移。归一化是几乎零成本的稳定器(但要注意:归一化会改变乘性尺度,若奖励的绝对水平有意义,应先确认它不会破坏语义)。
- 「TRPO 已过时,不用理解」:PPO 的裁剪是 TRPO 约束的一阶近似版本。不理解信任域,就无法回答「ε 到底在限制什么」——这也是面试里区分「背过 PPO」与「理解 PPO」的常见分水岭。
- 「PPO 能直接用于所有 RL 问题」:它对奖励尺度与回合结构很敏感。稀疏奖励(5.1)、连续控制的精确动作、需要深度探索的任务,往往需要额外的探索机制或换用其它算法族。
下面的片段把本节所有公式落成代码:GAE 的从后往前递归、裁剪目标、价值与熵项,以及可以直接打印的诊断量。完整训练循环(含环境与多种子评估)在 5.7。
import torch
import torch.nn as nn
def compute_gae(rewards, values, dones, last_value, gamma=0.99, lam=0.95):
"""rewards/values/dones 是长度 T 的张量;last_value 由 Critic 对最后一步的 s' 估计给出。"""
T = len(rewards)
adv = torch.zeros(T + 1) # adv[T] = 0:轨迹末端的后继优势视为 0
for t in reversed(range(T)): # ← GAE 的递归必须从后往前
next_val = last_value if t == T - 1 else values[t + 1]
nonterm = 1.0 - dones[t] # 终止后不再自举(同 5.3 的 done 掩码)
delta = rewards[t] + gamma * next_val * nonterm - values[t]
adv[t] = delta + gamma * lam * nonterm * adv[t + 1]
adv = adv[:T]
returns = adv + values # 价值回归的目标:Â + V(“不变的目标”)
return adv.detach(), returns.detach()
def ppo_update(policy, value_fn, optimizer, batch,
eps_clip=0.2, vf_coef=0.5, ent_coef=0.01, max_grad_norm=0.5):
"""batch = (s, a, logp_old, adv, ret),均为同一批 rollout 上算好的量。"""
s, a, logp_old, adv, ret = batch
adv = (adv - adv.mean()) / (adv.std() + 1e-8) # 优势归一化:让尺度与任务无关
dist = torch.distributions.Categorical(logits=policy(s))
logp = dist.log_prob(a)
ratio = torch.exp(logp - logp_old) # ρ = π_θ / π_θold
unclipped = ratio * adv
clipped = torch.clamp(ratio, 1.0 - eps_clip, 1.0 + eps_clip) * adv
policy_loss = -torch.min(unclipped, clipped).mean() # 最大化目标 ⇒ 取负最小化
value_loss = nn.functional.mse_loss(value_fn(s).squeeze(-1), ret)
entropy = dist.entropy().mean()
loss = policy_loss + vf_coef * value_loss - ent_coef * entropy
optimizer.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(list(policy.parameters()) + list(value_fn.parameters()), max_grad_norm)
optimizer.step()
# 诊断量:k3 估计的近似 KL(非负、无偏)与裁剪比例
with torch.no_grad():
approx_kl = ((ratio - 1.0) - (logp - logp_old)).mean().item()
clip_frac = ((ratio - 1.0).abs() > eps_clip).float().mean().item()
return {"loss": loss.item(), "approx_kl": approx_kl,
"clip_frac": clip_frac, "entropy": entropy.item()}
if __name__ == "__main__":
B, OBS, ACT = 64, 8, 4
shared = nn.Sequential(nn.Linear(OBS, 64), nn.Tanh()) # 共享躯干(常见做法)
policy = nn.Sequential(shared, nn.Linear(64, ACT)) # Actor 头
value_fn = nn.Sequential(shared, nn.Linear(64, 1)) # Critic 头
opt = torch.optim.Adam(list(policy.parameters()) + list(value_fn.parameters()), lr=3e-4)
s = torch.randn(B, OBS); a = torch.randint(0, ACT, (B,))
with torch.no_grad():
logp_old = torch.distributions.Categorical(logits=policy(s)).log_prob(a)
adv = torch.randn(B); ret = torch.randn(B)
info = ppo_update(policy, value_fn, opt, (s, a, logp_old, adv, ret))
assert 0.0 <= info["approx_kl"] < 1.0 and 0.0 <= info["clip_frac"] <= 1.0
print(info)
# 判据(每次更新后打印,直接用它们排查):
# 1) approx_kl 应在 1e-3 ~ 1e-2 量级;> 0.03 说明这一步走得太远;
# 2) clip_frac 应在 5%~20%;长期 > 50% 说明更新几乎被截断(学不动);
# 3) entropy 应缓慢下降;若第一轮就接近 0,说明策略塌缩,检查奖励与 entropy 系数。
三点读代码的提示:
logp_old 必须用采样时的策略算、并用 no_grad() 固定:它是 ρ 的分母。若在更新循环里重算(或在多 epoch 之间更新),比率就不再是「新旧策略之比」,裁剪的前提失守。
returns = adv + values 是 PPO 里最容易被写错的一处:Critic 的回归目标应当是「优势估计 + 当前价值估计」,它在一批数据内是常数,绝不能对 ret 求梯度(代码里用 .detach() 保证)。
- 诊断量不是装饰:
approx_kl 与 clip_frac 是 PPO 的「仪表盘」。工业实现(如 RLHF 训练框架)通常还把它们写进日志并对 approx_kl 设早停阈值——这是 PPO 能在生产环境里长期跑下去的关键工程手段。
即时自测 · 5.5
Q1 写出优势函数的定义,并说明「优势均值为 0」这一性质来自哪条更基本的引理。
Q2 在 GAE 中,γλ 为什么会一起出现?若把 λ 从 0.95 调到 1.0,偏差与方差各朝哪个方向变化?
Q3 用例 5.5.2的记号,给定 A^=−1.5、ρ=0.6、ε=0.2,min 取哪一项?梯度是否被截断?
Q4 训练日志显示:approx_kl = 0.08、clip_frac = 0.62、entropy = 0.02。三个数字合起来说明什么?给出两条优先处理措施。
参考要点
Q1 Aπ(s,a)=Qπ(s,a)−Vπ(s),其中 Vπ(s)=∑aπ(a∣s)Qπ(s,a)。故 ∑aπ(a∣s)Aπ(s,a)=0——本质上就是定理 5.4.3 用的那条引理:任何只依赖状态的量在策略梯度期望里可自由加减(这里是减去 V)。
Q2 γl 来自折扣(第 l 步以后的奖励本来就该打折),λl 是人为给远处 TD 残差打的折扣,二者在闭式里天然乘在一起成为 (γλ)l。λ→1:更接近蒙特卡洛 ⇒ 偏差减小、方差增大;λ→0:更依赖 Critic ⇒ 方差减小、偏差增大(当 Critic 不准时偏差显著)。
Q3 ρ=0.6<0.8=1−ε,裁剪项为 0.8×(−1.5)=−1.2,未裁剪项为 0.6×(−1.5)=−0.9;min(−0.9,−1.2)=−1.2,即取裁剪项。属情形②(A^<0 且 ρ<1−ε):∂f/∂ρ=0,梯度被截断——坏动作的概率已经压得够低了,不再追加惩罚(继续压会破坏探索与熵)。
Q4 approx_kl=0.08 远超常见阈值(0.015–0.03)⇒ 每批更新走得太远;clip_frac=0.62 说明六成样本的更新被截断 ⇒ 有效学习信号被大量丢弃,训练会表现为「loss 在动但回报不涨」;entropy=0.02 逼近 0 ⇒ 策略已接近确定性,探索死锁(一旦塌缩很难恢复)。优先措施:① 立刻把学习率降到原来的 1/3–1/10,并把每批的 epochs 从 10 降到 3–5(或加 KL 早停);② 上调熵系数 c2(例如从 0.01 提到 0.03)并检查奖励尺度是否需要缩放。两项做完再观察 approx_kl 是否回到 10−2 量级、entropy 是否回升到 0.3 以上。
延伸阅读
- Schulman, Moritz, Levine, Jordan & Abbeel, High-Dimensional Continuous Control Using Generalized Advantage Estimation(2016, arXiv:1506.02438)——GAE 原文(含 γλ 的最优性与实验)。
- Schulman, Levine, Abbeel, Jordan & Moritz, Trust Region Policy Optimization(2015, arXiv:1502.05477)——TRPO 与替代目标的理论(单调改进的界)。
- Schulman, Wolski, Dhariwal, Radford & Klimov, Proximal Policy Optimization Algorithms(2017, arXiv:1707.06347)——PPO 原文(裁剪目标、KL 惩罚变体、超参数表)。
- Mnih et al., Asynchronous Methods for Deep Reinforcement Learning(2016, arXiv:1602.01783)——A3C / A2C 的并行环境做法。
- Stable-Baselines3 文档的「PPO」页——逐项列出社区默认超参数,与本节表格互为参照(工程文档,非论文)。
- OpenAI Spinning Up 的 Intro to Policy Optimization 与 Trust Region 两节——把 TRPO→PPO 的动机讲得很清楚,适合作为本节的第二遍材料。
本节小结:Actor–Critic 把「学策略」与「学价值」分成两个网络互相喂饭,优势函数 A=Q−V 是它们之间的接口;GAE 用 γλ 把「单步 TD(低方差高偏差)」与「蒙特卡洛(高方差低偏差)」连成一根可调的旋钮,成为 PPO 的事实标准。TRPO 用 KL 硬约束限制策略变化幅度,代价是二阶优化;PPO 用裁剪替代目标在越界时把「继续外推」的梯度归零、同时保留「拉回区间」的梯度,用一阶实现近似同样的效果,并配以价值损失与熵正则。它是今天的默认算法——也是 5.6 里 RLHF 第一阶段的执行器。下一节处理两个更现实的约束:不能交互(离线 RL)与写不出奖励(RLHF 与偏好优化)。