GAE 优势估计把轨迹变成固定的优势估计。接下来更新 Actor 时,能否对同一批数据多做几轮优化,又不让已经改善的样本无限推动策略远离采样时的状态?

PPO-Clip 修改的是优化目标:某些方向上的改进达到阈值后,不再给该样本额外收益。它没有把新策略投影回概率比区间,也没有保证所有状态上的 KL 小于某个常数。下面从动作概率比进入分段目标,并运行一个可以独立核验梯度的标准库实验。

先冻结采样时的三个量

一批数据由行为策略 π_old 采集,记录状态 s、实际执行动作 a,以及当时这个动作的 log π_old(a|s)。再按固定 Critic 快照构造优势 A_hat。Actor 优化期间,这些旧 log 概率和优势作为常量使用,不对它们反向传播。

量 一批数据内如何处理 混用的后果
实际执行动作 a 保留采样记录 用新策略重新采样动作,再拿旧优势套上去
old log probability 保留采样时数值 每轮覆盖分母,使概率比总回到 1
A_hat 在本次 Actor 更新中固定 不小心把梯度传入优势计算或 Critic
当前 log probability 随 Actor 参数更新 错误地复用旧 log 概率,Actor 没有梯度

概率比是实际采样动作在同一状态下的新旧概率之比:

1
2
r(θ) = π_θ(a|s) / π_old(a|s)
= exp(log π_θ(a|s) − log π_old(a|s))

分母必须在采样动作上为正。若旧策略永远不选某动作,旧数据中也就没有这个动作的直接观测;不能靠比值制造未采到的反馈。连续动作时使用相同动作的密度比,不能把密度值误叫作单点概率。

在固定状态 s 且旧动作分布有足够支持时,Σ_a π_old(a|s)r(a)f(a)=Σ_a π_θ(a|s)f(a)。但这只改变动作条件分布的权重,并没有同时修复旧策略与新策略的状态访问分布差异,也不是完整轨迹的重要性权重。

裁剪目标为什么取较小值

设 ε=0.2,clip(r)=min(max(r,0.8),1.2)。对一个采样点定义最大化目标:

1
2
3
l(r,A) = min(rA, clip(r)A)
L_clip = 一批采样点的 l 的平均
若优化器执行梯度下降,Actor loss = −L_clip

这个目标来自 PPO 原始论文式 7。min 选择两个候选值中较保守的那个;优势为负时,负数乘法会改变比较方向,不能机械地认为 r 离开区间就把梯度清零。

正负优势决定哪一边会变平

当 A>0,希望提高该动作的概率。r 超过 1+ε 后,l 不再继续增长;r 过低时仍应有把它拉回来的梯度。当 A<0,希望降低概率;r 低于 1−ε 后不再奖励继续降低,但 r 过高时仍会受惩罚。

1
2
3
A > 0:l = A × min(r, 1+ε)
A < 0:l = A × max(r, 1−ε)
A = 0:l = 0

在非拐点处,导数为:

1
2
3
∂l/∂r = A,若 A>0 且 r<1+ε,或 A<0 且 r>1−ε
∂l/∂r = 0,其余平坦区
∇_θ l = (∂l/∂r) × r × ∇_θ log π_θ(a|s)

在拐点上目标不可微;实现可以使用选定的次梯度约定。本篇代码选择平坦侧的零梯度,数值梯度检查避开拐点,不声称所有框架在等号处返回相同导数。

ArrAclip(r)Al∂l/∂r
+21.42.82.42.40
+20.61.21.61.2+2
−20.6−1.2−1.6−1.60
−21.4−2.8−2.4−2.8−2

第三行不是“−1.2 比较好,所以取 −1.2”;目标明确定义了 min,因此取 −1.6。第四行则说明“超出区间即没有梯度”是错误实现。

正优势在概率比超过上界后变平,负优势在低于下界后变平;其余越界方向仍保留惩罚梯度
原创分段图;纵轴是最大化的样本目标,横轴是动作概率比,不是参数更新步数。查看完整图

一个目标相同而 KL 不同的反例

只考虑一个状态、两个动作。旧策略对动作 1 的概率为 0.5,数据中两个动作各出现一次,固定优势分别为 +1 和 −1。新策略动作 1 的概率为 p,则两个样本的比值为 2p 与 2(1−p)。

当 p≥0.6,正优势样本已经进入上侧平坦区,负优势样本也进入下侧平坦区,平均目标一直为 (1.2−0.8)/2=0.2。

新 p L_clip KL(旧 ‖ 新),约
0.5 0 0
0.6 0.2 0.020411
0.9 0.2 0.510826

这里可以算出完整分布上的精确 KL,而不是由采样动作近似:

1
KL(旧 ‖ 新) = 0.5 log(0.5/p) + 0.5 log(0.5/(1−p))

对 p→1,KL 可趋于无穷,而目标仍为 0.2。因此“目标被裁剪”不能推出“新旧策略一定足够接近”。即使当前点梯度已经为零,前一优化步也可能跨过边界;共享参数还可能被其他状态的梯度推动。

完整标准库实验与梯度

用一个 logit z 参数化 Bernoulli 策略 p=σ(z)。实际动作 a 的 log 概率导数为 a−p,所以 ∂r/∂z=r(a−p)。代码用稳定的 softplus 计算 log 概率,并返回真实梯度、目标平坦比例与精确 KL。

为避免把教学程序当成通用数值库,本实现显式限制 |z|、|old_z|≤20、|A|≤10^6;它拒绝不符合范围的输入,不通过偷偷修改概率比来掩盖溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import json
import math


def finite(value, bound, name):
if type(value) not in (int, float) or not -bound <= value <= bound:
raise ValueError("invalid " + name)
return float(value)


def sigmoid(z):
return 1 / (1 + math.exp(-z))


def log_probability(z, action):
t = -z if action == 1 else z
return -(max(t, 0) + math.log1p(math.exp(-abs(t))))


def sample_objective(ratio, advantage, epsilon=0.2):
ratio = finite(ratio, 1e30, "ratio")
advantage = finite(advantage, 1e6, "advantage")
epsilon = finite(epsilon, 1, "epsilon")
if ratio <= 0 or not 0 < epsilon < 1:
raise ValueError("ratio must be positive; epsilon must be in (0,1)")
clipped = min(max(ratio, 1-epsilon), 1+epsilon)
value = min(ratio * advantage, clipped * advantage)
active = ((advantage > 0 and ratio < 1+epsilon) or
(advantage < 0 and ratio > 1-epsilon))
return value, advantage if active else 0.0


def batch(z, old_z, actions, advantages, epsilon=0.2):
z = finite(z, 20, "z")
old_z = finite(old_z, 20, "old_z")
epsilon = finite(epsilon, 1, "epsilon")
if not 0 < epsilon < 1:
raise ValueError("invalid epsilon")
actions, advantages = tuple(actions), tuple(advantages)
if not actions or len(actions) != len(advantages):
raise ValueError("batch lengths must match and be positive")
p = sigmoid(z)
values, gradients = [], []
outside = flat = 0
for action, advantage in zip(actions, advantages):
if type(action) is not int or action not in (0, 1):
raise ValueError("action must be integer 0 or 1")
ratio = math.exp(log_probability(z, action) - log_probability(old_z, action))
value, derivative = sample_objective(ratio, advantage, epsilon)
values.append(value)
gradients.append(derivative * ratio * (action-p))
outside += ratio < 1-epsilon or ratio > 1+epsilon
flat += advantage != 0 and derivative == 0
q = sigmoid(old_z)
kl = sum(weight * (log_probability(old_z, action)-log_probability(z, action))
for action, weight in [(1, q), (0, 1-q)])
n = len(actions)
return {"p": p, "objective": math.fsum(values)/n,
"gradient": math.fsum(gradients)/n, "kl_old_new": kl,
"outside_fraction": outside/n, "flat_fraction": flat/n}


if __name__ == "__main__":
old_z = 0.0
actions, advantages = [1, 0], [1.0, -1.0]
z, trajectory = 0.0, []
for step in range(4):
report = batch(z, old_z, actions, advantages)
trajectory.append({"step": step, "z": z, **report})
z += 0.8 * report["gradient"] # Ascent; old_z remains frozen.
plateau = [batch(math.log(p/(1-p)), old_z, actions, advantages)
for p in (0.6, 0.9)]
print(json.dumps({"trajectory": trajectory, "plateau": plateau}, indent=2))

实际运行中,第一次上升把 z 从 0 推到 0.4,p 约为 0.5987;第二次更新使 p 超过 0.6,随后该批两个样本都进入平坦区。这个演示没有环境、GAE、Critic、神经网络或完整采样循环,只核验裁剪目标与更新方向。

像 p=0.6 这样的理论拐点,经过 log 和 exp 的浮点运算后,比值可能略落在边界某一侧;不能要求其数值梯度一定等于某个选定次梯度。梯度验证使用远离拐点的样本,目标值仍在舍入误差内符合手算。

outside_fraction 统计概率比是否在区间外,flat_fraction 统计非零优势样本是否落在目标平坦区,两者不是同一个指标。真实实现常见的 clip fraction 也必须结合其具体定义解释。

设 batch 大小为 N,这个标量示例用 O(N) 时间、O(N) 额外存储;真实 Actor 还要加上网络前向、反向与优化器成本。用 log 概率计算比值改善计算方式,但不保证任意输入的指数化都不会溢出。

接入 PPO 训练时还缺什么

检查点 失败边界
优势与价值目标 Actor 优势标准化不应覆盖 Critic 原始回归目标
动作分布 离散 log 概率取实际动作;连续多维动作通常要合并各维 log 密度
行为策略 old log 概率必须对应采样时的同一分布、动作变换和掩码
批量更新 同一批可重复优化有限轮,不等于旧轨迹可以永久重放
更新距离 裁剪不提供硬 KL 约束;监控或早停要独立制定
真实效果 目标改善不自动推出期望回报单调上升,需要独立评测

原论文还讨论了价值损失和熵奖励的组合。本篇故意只实现 PPO-Clip Actor 项,不能把上述二动作轨迹称为完整 PPO 训练结果。

自检:ε=0.2,是否意味着所有动作的新旧概率比永远位于 0.8 到 1.2?

不是。目标在部分方向变平,并没有投影或约束新策略。越界的相反方向仍有梯度,共享参数和一次较大的优化步也可能把比值推离区间。本文 p=0.6 与 0.9 的反例具有相同裁剪目标而 KL 不同。

验证程序 tests/verify-ppo-clipping-article.cjs 直接运行完整代码,另外用非裁剪区间的独立目标、中心差分和动作重命名检查梯度。继续沿策略梯度 → GAE → 本文阅读;评估更新效果时,再参考配对 Bootstrap与配对置换检验,先确认评测单位与推断假设。