Off-Policy 策略梯度:重要性采样、裁剪与 PPO
策略梯度必须用最新策略采样,所以每批数据只能走一步梯度。本讲用重要性采样把「旧数据」救回来,并解释为什么必须裁剪权重才能真正跑通。
0. 本讲导读
第 5–6 讲把策略梯度(policy gradient)和 actor-critic 建立起来,第 7–8 讲则走了完全另一条路:值函数方法(Q-learning、DQN 及其工程实践)。这两条路线的分水岭是一个非常实际的问题——数据能不能重复使用。Q-learning 之所以能塞进百万级的回放缓冲区(replay buffer)反复训练,是因为 Bellman 方程本身不依赖数据是谁产生的;而策略梯度的期望是明明白白写在 $\tau\sim p_\theta(\tau)$ 下面的,参数一动,这个分布就变了,旧样本立刻「过期」。
于是我们陷入一个尴尬处境:策略梯度稳定、无偏、对 critic 的错误免疫,但样本效率低得离谱;Q-learning 样本效率高,却难调、可能不收敛。本讲要问的问题非常具体:
能不能对旧策略采到的数据做某种「修正」,使得我们可以在同一批数据上连续走 $K$ 步梯度?
答案是重要性采样(importance sampling, IS)。但重要性采样在长时域序贯问题上有一个恶名昭著的失败模式:轨迹级权重是 $T$ 个比值的连乘,方差随时域指数爆炸。本讲的主线就是:先老老实实推出精确的 off-policy 策略梯度(它是正确但没法用的),再一步步做近似和工程加固——丢掉未来项、丢掉状态边缘分布比、裁剪权重、加 $\min$——最后落地成今天工业界用得最多的算法 PPO(Proximal Policy Optimization)。
- 精确的 off-policy 策略梯度需要轨迹级 IS 权重 $\prod_t \frac{\pi_{\theta'}(a_t|s_t)}{\pi_\theta(a_t|s_t)}$,其方差随时域 $H$ 指数增长,$H\sim 10^3$ 时轻易达到 $10^{17}$ 量级,实践中完全不可用。
- 利用因果性可以把权重拆成「$t$ 时刻之前的连乘」和「$t$ 时刻之后的连乘」。后者可以丢掉——丢掉之后得到的不是策略梯度,而是一个策略迭代(policy iteration)式的算法,下一讲会证明这个替换的代价可控。
- 再做一步一阶近似:忽略状态边缘分布 $d^{\pi_{\theta'}}$ 与 $d^{\pi_\theta}$ 的差异,只保留单步动作 IS 比值,得到实际可用的 off-policy 策略梯度。这个近似只在两个策略足够接近时成立,因此必须约束更新幅度——这正是下一讲信赖域方法的动机。
- 即便只剩单步比值,权重远离 $1.0$ 时估计量依然失效。工程上的三件套是:权重裁剪、自归一化 IS、有效样本量(ESS)诊断。
- 把裁剪写成 $\min\{w\hat A,\ \text{clip}(w,1-\epsilon,1+\epsilon)\hat A\}$(而不是直接裁剪 $w$),才能保证「跑得太远」的方向没有梯度、「跑回来」的方向仍有梯度。这就是 PPO 的 $\mathcal{L}_{\text{CLIP}}$。
1. 我们为什么喜欢策略梯度
在动手改造之前,先说清楚我们到底在保护什么。本讲的起点是第 6 讲结尾那个「带 GAE 的 on-policy 批量 actor-critic」算法,Levine 把它完整重述了一遍:
算法本身逐条写出来是这样的($i$ 索引轨迹,$t$ 索引时间步):
- 用当前策略 $\pi_\theta$ 与环境交互,采样一批轨迹 $\{\tau^{(i)}\}$;
- 计算自举目标 $y^{(i)}_t = r(s^{(i)}_t,a^{(i)}_t) + \hat V^\pi_\phi(s^{(i)}_{t+1})$;
- 用回归把 $\hat V^\pi_\phi(s)$ 拟合到目标 $\{y^{(i)}_t\}$ 上;
- 计算 GAE 优势 $\hat A^\pi_{\text{GAE}}(s_t,a_t)=\sum_{t'=t}^{\infty}(\gamma\lambda)^{t'-t}\delta_{t'}$,其中 $\delta_{t'}=r_{t'}+\gamma \hat V^\pi_\phi(s_{t'+1})-\hat V^\pi_\phi(s_{t'})$;
- 组装梯度 $\nabla_\theta J(\theta)\approx \sum_i\sum_t \nabla_\theta\log\pi_\theta(a^{(i)}_t|s^{(i)}_t)\hat A^\pi(s^{(i)}_t,a^{(i)}_t)$;
- 更新 $\theta \leftarrow \theta + \alpha\nabla_\theta J(\theta)$,然后回到第 1 步。
无偏性是一张「保险单」
幻灯片右侧特意点出 $\lambda=1$ 这个特例。当 GAE 的 $\lambda$ 取 1 时,优势估计退化成纯蒙特卡洛形式:
$$ \hat A^\pi_{\text{MC}}(s_t,a_t)=\sum_{t'=t}^{\infty}\gamma^{t'-t}r(s_{t'},a_{t'}) - \hat V^\pi_\phi(s_t) $$这里 $\hat V^\pi_\phi(s_t)$ 只作为基线(baseline)出现,而基线只依赖状态、不依赖动作,因此按第 5 讲的推导它对期望的贡献为零:$\E_{a\sim\pi_\theta}[\nabla_\theta\log\pi_\theta(a|s)\,b(s)]=b(s)\nabla_\theta\sum_a \pi_\theta(a|s)=b(s)\nabla_\theta 1=0$。这就是幻灯片上那句「no matter how “wrong” the critic is」的含义:
$\lambda=1$ 时 critic 只影响梯度估计的方差,不影响它的期望。critic 拟合得好,方差小;critic 完全是随机噪声网络,方差大得离谱,但梯度方向的期望依旧正确,跑得慢却不会跑歪。Q-learning 没有这张保险单:$Q_\phi$ 一旦偏了,$\argmax_a Q_\phi(s,a)$ 就直接选错动作,而且这个错误会通过自举不断放大。
Levine 由此给出一个非常实际的判断:如果你要的是一个稳定、可靠、不用反复调参就能跑起来的 RL 方法,而且你对样本效率没那么在意,策略梯度是最好的选择。他特别点名两个场景——
- sim-to-real 机器人:仿真器里样本近乎免费,可以并行开几千个环境,瓶颈在「训练出来的策略能不能稳定迁移」,而不在「用了多少帧」。
- 训练大语言模型(LLM):一次 rollout 就是生成一段文本,代价可控且高度可并行;反过来,RLHF/RLVR 场景里训练极其怕不稳定(一崩就是模型退化成乱码),所以宁可用无偏但慢的方法。今天 LLM 后训练的主力算法 PPO / GRPO 全部属于本讲要推导的这一族。
2. 致命伤:一批数据只能走一步
请注意上面算法框里绿色箭头的位置:它从第 6 步直接绕回第 1 步。这就是全部问题所在。用一个粗略的账算一下就明白代价有多大:
| 环节 | 监督学习 | on-policy 策略梯度 |
|---|---|---|
| 一批数据能走几步梯度 | 反复走很多个 epoch | 严格一步 |
| batch size | 32 – 512 | 常见 $10^3$ – $10^5$ 个时间步 |
| 学习率 | $10^{-3}$ 量级 | $10^{-4}$ – $10^{-5}$,还常需自适应 |
| 数据来源 | 磁盘上现成的 | 每一步都要跑仿真 / 真机 / 生成模型 |
| 总样本消耗 | 数据集大小 | batch × 迭代数,两者都很大 |
为什么策略梯度非要大 batch?因为它的方差来自两处:一是轨迹回报本身的随机性(环境噪声 + 策略随机性),二是 $\nabla_\theta\log\pi_\theta$ 在动作空间上的高维波动。第 5 讲已经看到,即使加了 baseline 和因果性截断,梯度估计的相对标准差仍然常常是 $O(1)$ 甚至更大。要把噪声压到能安全走一步梯度的水平,只能靠 $1/\sqrt{N}$ 硬堆样本。而学习率又不能大——因为策略梯度是在参数空间里走直线,一步走远了策略分布可能剧变,下一批数据的分布也随之剧变,训练直接发散。
两个约束互相牵制:大 batch 意味着每次迭代很贵,小学习率意味着需要很多次迭代。代价是相乘的,所以 Levine 说「this quickly becomes intractable」。
我们想要的算法长什么样
把这个「不合法」讲透一点。策略梯度的定义式是
$$ \nabla_\theta J(\theta) = \E_{(s_t,a_t)\sim p_\theta(s_t,a_t)}\big[\nabla_\theta \log\pi_\theta(a_t|s_t)\,\hat A^\pi(s_t,a_t)\big] = \sum_{s_t,a_t} p_\theta(s_t,a_t)\,\nabla_\theta\log\pi_\theta(a_t|s_t)\,\hat A^\pi(s_t,a_t) $$我们用样本平均去估计它,靠的是「样本按 $p_\theta$ 分布」这个前提,把 $\sum_{s_t,a_t}p_\theta(\cdot)$ 换成 $\frac1N\sum_i$。走完第一步梯度后参数变成 $\theta'$,此时正确的梯度是在 $p_{\theta'}(s_t,a_t)$ 下取期望,但我们手上的样本仍然来自 $p_\theta$。被积函数换了没关系($\nabla\log\pi_{\theta'}$ 是解析式,代入即可),换不了的是采样分布。
「反正 $\theta'$ 离 $\theta$ 很近,直接把旧数据再用一遍不就行了?」——很多人第一次写 PPO 时会把 IS 比值删掉,发现「好像也能跑」。它确实经常能跑,因为在 $\theta'\approx\theta$ 时 IS 比值 $\approx 1$。但删掉比值后你就失去了唯一的诊断信号:比值告诉你策略已经漂多远了。裁剪、早停、ESS 全都建立在这个比值之上。没有它,训练是在无仪表盘的情况下飞行,$K$ 稍微调大一点就会静默发散。
所以真正的问题是:能不能把 $p_\theta$ 下的样本「重新加权」,使它等效于 $p_{\theta'}$ 下的样本?这正是重要性采样要干的事。
3. 重要性采样:把期望搬到另一个分布下
重要性采样是一个纯粹的恒等式,和 RL 没关系。设我们想算 $\E_{x\sim p(x)}[f(x)]$,但只能从 $q(x)$ 采样,那么只要 $q$ 的支撑集覆盖 $p$(即 $p(x)>0 \Rightarrow q(x)>0$),就有:
$$ \E_{x\sim p(x)}[f(x)] = \int p(x)f(x)\,dx = \int \frac{q(x)}{q(x)}p(x)f(x)\,dx = \int q(x)\frac{p(x)}{q(x)}f(x)\,dx = \E_{x\sim q(x)}\!\left[\frac{p(x)}{q(x)}f(x)\right] $$推导只用了「乘以 $q/q$ 再重新分组」这一个动作。$\frac{p(x)}{q(x)}$ 叫重要性权重(importance weight)。它的含义很直白:某个 $x$ 在 $q$ 下被采到得太频繁,就给它降权;采到得太少,就给它升权。
为什么模型项能约掉——这是整套方法能成立的前提
轨迹概率写全是
$$ p_\theta(\tau) = p(s_1)\prod_{t=1}^{H}\pi_\theta(a_t|s_t)\,p(s_{t+1}|s_t,a_t) $$其中 $p(s_1)$ 是初始状态分布,$p(s_{t+1}|s_t,a_t)$ 是环境转移。这两项不含 $\theta$——它们是环境的性质,不管你用哪个策略采样都一样。于是权重变成
$$ \frac{p_\theta(\tau)}{\bar p(\tau)} = \frac{\cancel{p(s_1)}\prod_{t=1}^{H}\pi_\theta(a_t|s_t)\cancel{p(s_{t+1}|s_t,a_t)}}{\cancel{p(s_1)}\prod_{t=1}^{H}\bar\pi(a_t|s_t)\cancel{p(s_{t+1}|s_t,a_t)}} = \prod_{t=1}^{H}\frac{\pi_\theta(a_t|s_t)}{\bar\pi(a_t|s_t)} $$轨迹级重要性权重只依赖策略概率,完全不需要知道环境模型。这是 model-free off-policy 学习能成立的根本原因:我们无法计算 $p(s'|s,a)$,但幸运的是根本不需要计算它。代价是这个权重是 $H$ 个数的连乘——下一节我们会看到,这个连乘就是万恶之源。
记号约定:本讲里 $\bar\pi$(或 $\pi_\theta$,当我们把当前参数 $\theta$ 当作「采样策略」时)表示行为策略 / 旧策略,即真正与环境交互产生数据的那个策略;$\pi_{\theta'}$ 表示我们正在优化的目标策略 / 新策略。二者一开始是同一个($\theta'\leftarrow\theta$),随着内层循环推进逐渐分开。
4. 用重要性采样推导策略梯度
现在把 IS 从「估计目标值」升级到「估计梯度」。Levine 在这里给出了一个很漂亮的推导,顺便重新导出了 on-policy 策略梯度作为特例。
逐步展开
第一步:写出可微的目标。 我们想知道「换成新参数 $\theta'$ 后目标会变成多少」,但只有旧策略的样本:
$$ J(\theta') = \E_{\tau\sim p_\theta(\tau)}\!\left[\frac{p_{\theta'}(\tau)}{p_\theta(\tau)}r(\tau)\right] $$这里 $r(\tau)=\sum_t r(s_t,a_t)$ 是轨迹总回报。注意这个式子里 期望的下标是 $\theta$(固定的),被积函数里唯一含 $\theta'$ 的是 $p_{\theta'}(\tau)$。这就是为什么它可以直接对 $\theta'$ 求导——梯度算子可以穿过期望符号,因为期望本身不依赖 $\theta'$。这跟 on-policy 时「必须用 log-derivative trick 才能把梯度搬进期望」形成鲜明对比。
第二步:求梯度。
$$ \nabla_{\theta'}J(\theta') = \E_{\tau\sim p_\theta(\tau)}\!\left[\frac{\nabla_{\theta'} p_{\theta'}(\tau)}{p_\theta(\tau)}r(\tau)\right] $$现在用那个「方便的恒等式」(convenient identity):由链式法则 $\nabla_{\theta'}\log p_{\theta'}(\tau) = \frac{\nabla_{\theta'}p_{\theta'}(\tau)}{p_{\theta'}(\tau)}$,两边乘 $p_{\theta'}(\tau)$ 得
$$ p_{\theta'}(\tau)\nabla_{\theta'}\log p_{\theta'}(\tau) = \nabla_{\theta'}p_{\theta'}(\tau) $$把它代回去:
$$ \nabla_{\theta'}J(\theta') = \E_{\tau\sim p_\theta(\tau)}\!\left[\frac{p_{\theta'}(\tau)}{p_\theta(\tau)}\nabla_{\theta'}\log p_{\theta'}(\tau)\,r(\tau)\right] $$这就是精确的 off-policy 策略梯度:一个 IS 权重乘以熟悉的 score function 乘以回报。它对任意 $\theta'\neq\theta$ 都成立,没有任何近似。
第三步:验证一致性。 令 $\theta'=\theta$(即在采样策略处「就地」估计梯度),此时 $\frac{p_{\theta}(\tau)}{p_\theta(\tau)}=1$,立刻得到
$$ \nabla_{\theta}J(\theta) = \E_{\tau\sim p_\theta(\tau)}\big[\nabla_{\theta}\log p_{\theta}(\tau)\,r(\tau)\big] $$正是第 5 讲的 REINFORCE 梯度。所以 on-policy 策略梯度只是 off-policy 版本在 $\theta'=\theta$ 处的特例,两条线索在这里合上了。
再展开 $\nabla_{\theta'}\log p_{\theta'}(\tau)$:因为 $\log p_{\theta'}(\tau)=\log p(s_1)+\sum_t[\log\pi_{\theta'}(a_t|s_t)+\log p(s_{t+1}|s_t,a_t)]$,而模型项不含 $\theta'$,求梯度后全部消失,只剩 $\sum_{t=1}^{H}\nabla_{\theta'}\log\pi_{\theta'}(a_t|s_t)$。再次强调:无需环境模型。 这一点在 off-policy 情形下依然成立,因为分子分母共用同一个环境。
5. 因果性拆分与指数爆炸的方差
上一节的公式还很抽象,把 $p_{\theta'}(\tau)/p_\theta(\tau)$ 和 $\nabla\log p_{\theta'}(\tau)$ 都展开成逐时刻的形式,问题的严重性才会暴露出来。
5.1 展开与重排
把两个连乘/求和代入,精确梯度是
$$ \nabla_{\theta'}J(\theta') = \E_{\tau\sim p_\theta(\tau)}\!\left[\left(\prod_{t=1}^{H}\frac{\pi_{\theta'}(a_t|s_t)}{\pi_\theta(a_t|s_t)}\right)\left(\sum_{t=1}^{H}\nabla_{\theta'}\log\pi_{\theta'}(a_t|s_t)\right)\left(\sum_{t=1}^{H}r(s_t,a_t)\right)\right] $$第 5 讲我们已经知道,对 on-policy 版本可以用因果性(causality)把它改写:$t$ 时刻的动作不影响 $t$ 之前的奖励,所以第二个求和的第 $t$ 项只需要配上 $t'\ge t$ 的奖励。off-policy 版本还多一层因果性:$t$ 时刻之后的动作也不影响 $t$ 时刻这个 score 项的「合法性」。严格重排后得到
$$ \nabla_{\theta'}J(\theta') = \E_{\tau\sim p_\theta(\tau)}\!\left[\sum_{t=1}^{H}\nabla_{\theta'}\log\pi_{\theta'}(a_t|s_t)\left(\prod_{t'=1}^{t}\frac{\pi_{\theta'}(a_{t'}|s_{t'})}{\pi_\theta(a_{t'}|s_{t'})}\right)\left(\sum_{t'=t}^{H}r(s_{t'},a_{t'})\left(\prod_{t''=t}^{t'}\frac{\pi_{\theta'}(a_{t''}|s_{t''})}{\pi_\theta(a_{t''}|s_{t''})}\right)\right)\right] $$这个式子里出现了两段权重,含义完全不同:
| 项 | 范围 | 含义 | 能否丢弃 |
|---|---|---|---|
| 过去项 $\prod_{t'=1}^{t}$ | 从起点到当前 | 修正「新策略走到状态 $s_t$ 并选出动作 $a_t$ 的概率」与旧策略的差异,即状态-动作访问分布的修正 | 丢了就不再是 $J$ 的梯度 |
| 未来项 $\prod_{t''=t}^{t'}$ | 从当前到未来某个奖励 | 修正「在 $s_t$ 之后,新策略取得这些未来奖励的概率」与旧策略的差异,即把 $Q^{\pi_\theta}$ 修正成 $Q^{\pi_{\theta'}}$ | 可以丢——丢掉相当于用旧策略的 $Q$ 值来评估新策略 |
丢掉未来项 = 「用旧策略的价值函数评估动作,然后按这个评估去改策略」。这不就是策略迭代吗:策略评估(算 $Q^{\pi_\theta}$)→ 策略改进(往 $Q^{\pi_\theta}$ 高的方向挪 $\pi_{\theta'}$)。所以幻灯片说「if we ignore this, we get a policy iteration algorithm」。这不是随便的近似,而是换了一个有自己理论保证的算法族。下一讲的性能差分引理(performance difference lemma)会精确刻画这个替换带来的误差有多大,并证明只要 $\pi_{\theta'}$ 和 $\pi_\theta$ 的 KL 散度足够小,误差就被 $O(\epsilon^2)$ 级别的量控制住。
顺带一提:把上式中的「过去项权重 + 未来奖励」组合起来,就是把 $\sum_{t'\ge t} r$ 换成优势估计 $\hat A^\pi_t$ 的更实用形式。后面我们统一写成 $\hat A_t$。
5.2 权重为什么随 $H$ 指数爆炸
就算丢掉未来项,过去项 $\prod_{t'=1}^{t}\rho_{t'}$(记 $\rho_t = \frac{\pi_{\theta'}(a_t|s_t)}{\pi_\theta(a_t|s_t)}$)在 $t$ 接近 $H$ 时依然是 $H$ 个数的连乘。下面用具体数字说明这有多糟。
基本事实。 每一步的比值满足 $\E_{a\sim\pi_\theta}[\rho_t]=\sum_a \pi_\theta(a|s)\frac{\pi_{\theta'}(a|s)}{\pi_\theta(a|s)}=1$。所以权重的均值恒为 1,看起来很温和。但方差不是:记二阶矩 $\E[\rho_t^2]=1+\sigma^2$,若各步近似独立,则
$$ \E[w^2] = \prod_{t=1}^{H}\E[\rho_t^2] = (1+\sigma^2)^H,\qquad \operatorname{Var}(w) = (1+\sigma^2)^H - 1 $$数值例子一(离散动作)。 设两个动作,旧策略 $\pi_\theta=(0.5,\,0.5)$,新策略 $\pi_{\theta'}=(0.6,\,0.4)$——这是一个非常温和的更新,谁看了都会说「两个策略几乎一样」。此时
$$ \E[\rho^2] = 0.5\times 1.2^2 + 0.5\times 0.8^2 = 0.72+0.32 = 1.04 $$| 时域 $H$ | $\E[w^2]=1.04^H$ | $\operatorname{Var}(w)$ | 标准差 $\approx$ | 解读 |
|---|---|---|---|---|
| 10 | 1.48 | 0.48 | 0.69 | 完全可用 |
| 100 | 50.5 | 49.5 | 7.0 | 已经需要几百个样本才稳 |
| 500 | $3.3\times10^{8}$ | $3.3\times10^{8}$ | $1.8\times10^{4}$ | 基本报废 |
| 1000 | $1.1\times10^{17}$ | $1.1\times10^{17}$ | $3.3\times10^{8}$ | 估计量毫无意义 |
读一下最后一行:估计量的均值是 1,标准差是 3 亿。这意味着在几乎所有样本上权重都接近 0,偶尔某一条轨迹的权重是天文数字,整个 batch 的梯度被这一条轨迹独占。这就是 IS 崩溃的典型形态——不是「误差大一点」,而是估计量退化成「一条样本说了算」。
数值例子二(连续动作,高斯策略)。 设 $\pi_\theta(\cdot|s)=\mathcal N(\mu,\sigma^2)$,$\pi_{\theta'}(\cdot|s)=\mathcal N(\mu',\sigma^2)$,两者只差均值。此时单步二阶矩有闭式解:
$$ \E_{a\sim\pi_\theta}\!\left[\left(\frac{\pi_{\theta'}(a)}{\pi_\theta(a)}\right)^{\!2}\right] = \exp\!\left(\frac{(\mu-\mu')^2}{\sigma^2}\right) $$于是 $\E[w^2]=\exp\big(H\,\Delta^2/\sigma^2\big)$,其中 $\Delta=|\mu-\mu'|$。取 $\Delta = 0.1\sigma$(均值只挪了十分之一个标准差,肉眼看不出差别),$H=1000$:$\E[w^2]=e^{10}\approx 2.2\times10^4$,标准差约 148。若 $\Delta=0.3\sigma$,同样 $H=1000$:$\E[w^2]=e^{90}\approx 1.2\times10^{39}$。指数上的指数——这就是「exponential in $T$」的字面含义。
这里的爆炸与状态空间、网络结构、奖励尺度都无关,它纯粹来自「概率连乘」这个结构。所以你不可能通过换更大的网络、调 batch size 或做奖励归一化来解决它。唯一的出路是结构性地改写估计量:要么把连乘变成单项(第 6 节的一阶近似),要么强行限制权重的取值范围(第 7 节的裁剪)。
5.3 有效样本量:给崩溃一个可测量的指标
「方差太大」这句话在实践中需要一个可以打印到日志里的数字。标准做法是有效样本量(effective sample size, ESS):
$$ \widehat{\text{ESS}} = \frac{\left(\sum_{i=1}^{N} w_i\right)^2}{\sum_{i=1}^{N} w_i^2} $$它的含义是:这 $N$ 个加权样本,其统计效力相当于多少个等权样本。若所有权重相等,$\widehat{\text{ESS}}=N$;若只有一个权重非零,$\widehat{\text{ESS}}=1$。可以证明当 $N$ 较大时 $\widehat{\text{ESS}}\approx \frac{N}{1+\operatorname{Var}(w)}$。代入上面表格 $H=1000$ 的那一行:$\operatorname{Var}(w)\approx10^{17}$,即使采 $N=10^6$ 条轨迹,$\widehat{\text{ESS}}$ 仍然远小于 1。
import torch
def ess(logp_new, logp_old):
"""logp_new / logp_old: 形状 (N,) 的对数概率(轨迹级则为整条轨迹求和)。
返回 (ESS, ESS 占比, 权重张量)。全程在对数域做减法,避免连乘溢出。"""
logw = logp_new - logp_old
logw = logw - logw.max() # 数值稳定:整体平移不改变 ESS
w = logw.exp()
ess_val = w.sum() ** 2 / (w ** 2).sum()
return ess_val.item(), (ess_val / w.numel()).item(), w
# 经验法则:ESS 占比 < 0.3 时该批数据已经不可信,应停止内层循环、重新采样。
注意代码里的两个细节:一是永远在对数域计算,$\prod_t \rho_t = \exp(\sum_t \log\rho_t)$,直接连乘在 $H=1000$ 时必然下溢成 0;二是减去最大值再取指数,这个平移会同时出现在 ESS 的分子分母上,因此结果不变,但避免了溢出。
6. 一阶近似:只保留单步动作比值
第 5 节告诉我们连乘不能要。这一节做第二次「有原则的偷懒」:把连乘砍成一项。
6.1 从轨迹视角切换到状态-动作视角
先把 on-policy 策略梯度写成边缘分布形式(第 5 讲已推过):
$$ \nabla_\theta J(\theta) \approx \frac{1}{N}\sum_{i=1}^{N}\sum_{t=1}^{H}\nabla_\theta\log\pi_\theta(a^{(i)}_t|s^{(i)}_t)\,\hat A^{(i)}_t,\qquad (s^{(i)}_t,a^{(i)}_t)\sim \pi_\theta(s_t,a_t) $$这里 $\pi_\theta(s_t,a_t)$ 指的是策略 $\pi_\theta$ 诱导的状态-动作联合边缘分布(marginal),即「跑策略 $\pi_\theta$ 时,第 $t$ 步落在 $(s,a)$ 的概率」。既然样本按这个分布来,换成 $\theta'$ 时用 IS 修正就是把联合分布的比值乘上去:
$$ \nabla_{\theta'} J(\theta') \approx \frac{1}{N}\sum_{i=1}^{N}\sum_{t=1}^{H}\frac{\pi_{\theta'}(s^{(i)}_t,a^{(i)}_t)}{\pi_{\theta}(s^{(i)}_t,a^{(i)}_t)}\nabla_{\theta'}\log\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)\,\hat A^{(i)}_t $$关键在于用条件概率的链式法则把联合比拆开:
$$ \frac{\pi_{\theta'}(s_t,a_t)}{\pi_{\theta}(s_t,a_t)} = \underbrace{\frac{\pi_{\theta'}(s_t)}{\pi_{\theta}(s_t)}}_{\text{状态边缘比:难算、爆炸}}\cdot \underbrace{\frac{\pi_{\theta'}(a_t|s_t)}{\pi_{\theta}(a_t|s_t)}}_{\text{单步动作比:一个除法就完了}} $$两项的性质天差地别。动作比是两个网络前向传播的输出之比,$O(1)$ 的计算量,取值范围也容易控制。状态边缘比则要求我们知道两个策略各自的状态占用分布——这需要在环境里展开整条轨迹,实际上它就是那个 $\prod_{t'=1}^{t-1}$ 连乘的另一种写法(先前的所有动作选择共同决定了你落在哪个状态)。所以它既算不出来,就算算得出来也会爆炸。
6.2 扔掉状态边缘比,得到实用形式
于是我们直接把它设为 1:
$$ \nabla_{\theta'} J(\theta') \approx \frac{1}{N}\sum_{i=1}^{N}\sum_{t=1}^{H}\frac{\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)}{\pi_{\theta}(a^{(i)}_t|s^{(i)}_t)}\nabla_{\theta'}\log\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)\,\hat A^{(i)}_t $$写成期望形式($d^{\pi}$ 表示状态占用分布 / state visitation distribution)就是本讲最重要的公式:
$$ \nabla_{\theta'} J(\theta') \approx \E_{s\sim d^{\pi_\theta}}\ \E_{a\sim \pi_\theta(\cdot|s)}\!\left[\frac{\pi_{\theta'}(a|s)}{\pi_{\theta}(a|s)}\nabla_{\theta'}\log\pi_{\theta'}(a|s)\,A^{\pi_\theta}(s,a)\right] $$逐项对照就能看出它做了两次替换:状态从 $d^{\pi_{\theta'}}$ 换成了 $d^{\pi_\theta}$(这一步是「错的」),优势从 $A^{\pi_{\theta'}}$ 换成了 $A^{\pi_\theta}$(这是第 5 节丢掉未来项的结果)。剩下的动作分布差异由 IS 比值精确修正。
还有一个漂亮的等价写法。注意 $\frac{\pi_{\theta'}}{\pi_\theta}\nabla_{\theta'}\log\pi_{\theta'} = \frac{\nabla_{\theta'}\pi_{\theta'}}{\pi_\theta} = \nabla_{\theta'}\!\left(\frac{\pi_{\theta'}(a|s)}{\pi_\theta(a|s)}\right)$,因为分母 $\pi_\theta$ 关于 $\theta'$ 是常数。也就是说上面这个梯度恰好是某个标量目标的梯度:
$$ \mathcal L^{\text{IS}}(\theta') = \E_{s\sim d^{\pi_\theta},\,a\sim\pi_\theta}\!\left[\frac{\pi_{\theta'}(a|s)}{\pi_{\theta}(a|s)}A^{\pi_\theta}(s,a)\right] $$这个 $\mathcal L^{\text{IS}}$ 就是幻灯片里说的「代理目标(surrogate objective)」,也是 TRPO/PPO 论文里那个 $L(\theta')$。实践上你不需要手写梯度公式,只要把 $\mathcal L^{\text{IS}}$ 写成 PyTorch 的标量 loss,反向传播自动给出正确梯度。而且它可以对同一批数据反复求梯度——每次 $\theta'$ 变了,比值和 loss 都跟着变,这正是我们要的「多步」。
6.3 这个近似什么时候可靠
Levine 在幻灯片上只写了一句「this is a little bit wrong, we’ll see next time why it’s mostly ok」。这里先给出结论的形状,下一讲会补上证明:
- 被忽略的是 $d^{\pi_{\theta'}}(s)$ 与 $d^{\pi_\theta}(s)$ 的差异。可以证明,若两个策略在每个状态上的总变差距离都不超过 $\epsilon$,则它们的状态占用分布的总变差距离不超过 $2\epsilon H$(有限时域)或 $\frac{2\epsilon\gamma}{1-\gamma}$(折扣无限时域)。
- 把这个偏差代回目标,$J(\theta')$ 与代理目标 $\mathcal L^{\text{IS}}$ 的差被 $O(\epsilon^2)$ 的项界住(在合适的常数下)。也就是说,只要策略变化是「小量」,误差就是「小量的平方」——这正是「一阶近似」这个名字的来源:它在 $\theta'=\theta$ 处与真实目标一阶相切。
- 反过来:$\epsilon$ 一旦不小,界就失效,代理目标可以任意地误导优化。你会看到 $\mathcal L^{\text{IS}}$ 一路上升而真实回报一路下跌。
「用旧状态分布代替新状态分布」这个近似只在两个策略足够接近时成立。因此 off-policy 策略梯度天生带着一个附加约束:你必须主动限制 $\pi_{\theta'}$ 离 $\pi_\theta$ 有多远。怎么限制?下一讲给出原理性答案(KL 约束 + 自然梯度 = 信赖域),本讲第 7–8 节给出工程答案(裁剪)。二者解决的是同一个问题。
6.4 真正能写出来的「多步」算法
- 用 $\pi_\theta$ 采样 $\{\tau^{(i)}\}$,并记录每个动作的旧对数概率 $\log\pi_\theta(a^{(i)}_t|s^{(i)}_t)$(这一步极易漏掉,漏了就没法算比值);
- 计算目标 $y^{(i)}_t=r^{(i)}_t+\hat V^\pi_\phi(s^{(i)}_{t+1})$;
- 拟合 $\hat V^\pi_\phi$;
- 计算 GAE 优势 $\hat A^{(i)}_t$ 并冻结;
- $\theta'\leftarrow\theta$;
- 内层重复 $K$ 次:$\nabla_{\theta'}J(\theta')\approx\sum_i\sum_t \frac{\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)}{\pi_{\theta}(a^{(i)}_t|s^{(i)}_t)}\nabla_{\theta'}\log\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)\hat A^{(i)}_t$,然后 $\theta'\leftarrow\theta'+\alpha\nabla_{\theta'}J(\theta')$;
- $\theta\leftarrow\theta'$,回到第 1 步。
$K$ 是核心超参:$K=1$ 退化成原来的 on-policy 算法;$K$ 越大样本利用率越高,但 $\pi_{\theta'}$ 漂离 $\pi_\theta$ 越远,近似越不可靠。典型取值是 $K=4\sim10$ 个 epoch(每个 epoch 内再切成若干 minibatch)。
7. 还没完:让重要性采样真正跑起来
做完两次近似,公式里只剩下单步比值 $w_t=\frac{\pi_{\theta'}(a_t|s_t)}{\pi_\theta(a_t|s_t)}$,看上去问题已经解决了。但只要 $K$ 稍微调大一点,你就会发现训练仍然崩。
7.1 单步比值也会失控
为什么单步比值也不安全?做个小实验:设动作空间有 10 个动作,旧策略是均匀分布(每个 0.1)。内层循环跑了几步之后,新策略在某个动作上的概率涨到 0.9,那么这个样本的权重是 $0.9/0.1=9$;同一状态下另一个概率跌到 0.005 的动作,权重是 $0.05$。一个 batch 里如果有 1% 的样本权重是 9,它们贡献的梯度分量就相当于 9% 的样本量——少数样本主导了整个更新。更糟的是,$w_t$ 与 $\hat A_t$ 是相关的:优化器会主动去增大那些 $\hat A_t>0$ 的样本的概率,也就是主动把权重推向大值。这是一个正反馈。
方差的形式化表达:对单步 IS 估计量 $\hat g = \frac1N\sum_i w_i \nabla\log\pi_{\theta'}(a_i|s_i)\hat A_i$,其方差含有 $\E[w^2\|\nabla\log\pi\|^2\hat A^2]$ 这一项。$w$ 的分布是重尾的——极端情况下(比如 $\pi_\theta$ 在某个动作上概率极小而 $\pi_{\theta'}$ 概率不小)$\E[w^2]$ 可以直接发散,此时中心极限定理都不再适用,样本平均不再以 $1/\sqrt N$ 收敛。
7.2 三件套之一:权重裁剪
裁剪的定义就是把权重强行压回区间 $[1-\epsilon,\,1+\epsilon]$:
$$ w_c = \max\Big\{1-\epsilon,\ \min\big\{1+\epsilon,\ w\big\}\Big\} = \operatorname{clip}(w,\,1-\epsilon,\,1+\epsilon) $$$\epsilon=0.1$ 是 Levine 建议的起手值(PPO 原论文常用 0.2;LLM 后训练里常用 0.2 甚至更小的非对称区间)。它带来两个立竿见影的好处:
- 方差有界。 $w_c\in[0.9,1.1]$,所以 $\E[w_c^2]\le 1.21$,$\operatorname{Var}$ 被硬性封顶,无论 $\theta'$ 漂多远。
- 隐式的信赖域。 一旦某个样本的比值超出 $[1-\epsilon,1+\epsilon]$,裁剪函数在那里是常数,梯度为零。优化器不再有动力继续推大这个动作的概率。这就等价于给「概率能改变多少」加了一道闸。
裁剪引入的是偏差:$\E[w_c f]\ne\E[wf]$。我们是在用「一个有偏但方差有限的估计量」换掉「一个无偏但方差无限的估计量」。这在 RL 里是完全值得的交易——无偏估计如果方差是 $10^{17}$,它的实际用途为零。但要清楚这笔账:裁剪版本不再是任何目标函数的无偏梯度,收敛性分析也随之失效,PPO 至今没有干净的收敛保证。它靠的是大量经验验证。
7.3 三件套之二:自归一化重要性采样
另一个独立的技巧:与其用 $\frac1N\sum_i w_i f_i$(普通 IS,无偏),不如用
$$ \hat\mu_{\text{SNIS}} = \frac{\sum_{i=1}^{N} w_i f_i}{\sum_{i=1}^{N} w_i} $$即用权重的和而不是 $N$ 来归一化,这叫自归一化重要性采样(self-normalized IS, SNIS)。它有几个重要性质:
| 性质 | 普通 IS | 自归一化 IS |
|---|---|---|
| 偏差 | 无偏 | 有偏,偏差 $O(1/N)$ |
| 方差 | 常常更大 | 通常显著更小(权重的整体缩放被抵消) |
| 需要归一化常数 | 需要 $p,q$ 都是正确归一化的密度 | 只需知道 $p/q$ 相差一个常数因子 |
| 数值稳定性 | 差(一个巨大权重直接毁掉均值) | 好(巨大权重同时出现在分子分母) |
| 在 RL 中的对应 | 标准 IS 策略梯度 | 常见于 off-policy 评估、GRPO 一类的组内归一化 |
直觉:如果所有权重整体偏大 10 倍(比如新策略普遍比旧策略更「确定」),普通 IS 会把梯度整体放大 10 倍,学习率相当于翻了 10 倍;SNIS 则自动把这个共同因子除掉,只保留样本之间的相对权重。RL 里我们关心的本来就是相对的优劣排序,所以这个性质非常合用。
7.4 三件套之三:把诊断打进日志
裁剪和自归一化都是被动防御,主动的做法是监控,一旦指标恶化就提前结束内层循环(early stopping)。下面是一份可以直接嵌进训练循环的实现:
import torch
import torch.nn.functional as F
def is_diagnostics(logp_new, logp_old, eps=0.2):
"""返回一批 off-policy 诊断指标。logp_* 形状均为 (B,)。"""
logr = logp_new - logp_old
ratio = logr.exp()
# 1) 有效样本量占比:越接近 1 越健康,低于 ~0.3 说明数据已失效
ess_frac = ratio.sum() ** 2 / (ratio.pow(2).sum() * ratio.numel())
# 2) 近似 KL(old || new)。直接用 -logr 的均值是无偏但方差大且可能为负;
# Schulman 的 k3 估计量恒非负、方差更小:k3 = r - 1 - log r
approx_kl = (ratio - 1.0 - logr).mean()
# 3) 被裁剪样本的比例:PPO 里典型健康值是 0.05 ~ 0.3
clip_frac = ((ratio - 1.0).abs() > eps).float().mean()
return {"ess_frac": ess_frac.item(),
"approx_kl": approx_kl.item(),
"clip_frac": clip_frac.item()}
def should_stop_inner_loop(diag, kl_target=0.015, ess_floor=0.3):
"""内层循环的早停判据:任一指标越线就停止本批数据的训练。"""
return diag["approx_kl"] > 1.5 * kl_target or diag["ess_frac"] < ess_floor
三个指标各管一段:approx_kl 衡量策略整体漂移了多少(这是下一讲 TRPO 约束的经验版本);clip_frac 太低说明 $\epsilon$ 设得过松、裁剪没起作用,太高说明学习率或 $K$ 太大;ess_frac 是最本质的——它直接回答「这批数据还剩多少统计效力」。实践中 KL 早停(PPO 的一个常见变体,也是 OpenAI Spinning Up 里推荐的做法)往往比调 $\epsilon$ 更有效。
用 $-\E[\log r]$ 来估计 $\KL(\pi_\theta\|\pi_{\theta'})$ 是无偏的,但它可能取到负值(KL 本身恒非负),而且方差很大,做早停判据时会疯狂误触发。代码里的 $k_3 = r-1-\log r$ 估计量同样无偏(因为 $\E_{\pi_\theta}[r]=1$,所以 $\E[r-1]=0$),却逐样本恒非负($x-1\ge\log x$),方差也小得多。这是一个非常值得记住的小工具。
8. 从裁剪到 PPO:那个 min 是干什么的
直接裁剪权重有一个微妙但致命的漏洞,Levine 用一整页专门讲它。
8.1 为什么 min 是必要的
把两种写法逐情形对照一遍就清楚了。设 $w=\frac{\pi_{\theta'}(a|s)}{\pi_\theta(a|s)}$,$w_c=\operatorname{clip}(w,1-\epsilon,1+\epsilon)$,目标是最大化。
| 情形 | 朴素裁剪 $w_c\hat A$ | PPO 的 $\min\{w\hat A,\ w_c\hat A\}$ | 我们想要的行为 |
|---|---|---|---|
| $\hat A>0$,$w\in[1-\epsilon,1+\epsilon]$ | 正常梯度 | 两项相等,正常梯度 | 推大这个动作的概率 ✓ |
| $\hat A>0$,$w>1+\epsilon$(已推得太多) | 削平,无梯度 | $w\hat A>w_c\hat A$,取 $w_c\hat A$,无梯度 | 停止继续推 ✓ |
| $\hat A>0$,$w<1-\epsilon$(反而压小了) | 削平,无梯度,卡死 | $w\hat A<w_c\hat A$,取 $w\hat A$,有梯度 | 应该把它推回来 ✓ |
| $\hat A<0$,$w<1-\epsilon$(已压得太狠) | 削平,无梯度 | 取 $w_c\hat A$,无梯度 | 停止继续压 ✓ |
| $\hat A<0$,$w>1+\epsilon$(反而推大了坏动作) | 削平,无梯度,坏动作被永久锁在高概率上 | $w\hat A<w_c\hat A$(因为 $\hat A<0$ 且 $w>w_c$),取 $w\hat A$,有强梯度把它压回来 | 赶紧压回来 ✓ |
第三行和第五行就是幻灯片右图那句「not paying any price for drifting away from data here!」所指的漏洞。朴素裁剪是对称地削平两侧,它不区分「已经漂出去了但方向是对的」和「已经漂出去了而且方向是错的」。而 $\min$ 是单边的:它永远取两者中较小(即较保守)的那个,效果是「往外漂时封顶,往回收时放行」。
$\min\{w\hat A, w_c\hat A\}$ 可以理解为「代理目标的悲观下界」。我们最大化的是一个 pessimistic bound:只要有可能高估真实改进,就取更保守的那一支。往数据分布外走时,目标不再增加(没有收益),但如果发现走错了,损失照旧全额计入(有惩罚)。这种「收益封顶、损失不封顶」的不对称性,正是它能约束住策略漂移的原因。
8.2 PPO 完整算法
PPO 的裁剪损失(clipped surrogate objective):
$$ \mathcal L_{\text{CLIP}}(\theta') = \sum_{i=1}^{N}\sum_{t=1}^{H} \min\left\{ \frac{\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)}{\pi_{\theta}(a^{(i)}_t|s^{(i)}_t)}\hat A^{(i)}_t,\ \ \operatorname{clip}\!\left(\frac{\pi_{\theta'}(a^{(i)}_t|s^{(i)}_t)}{\pi_{\theta}(a^{(i)}_t|s^{(i)}_t)},\,1-\epsilon,\,1+\epsilon\right)\hat A^{(i)}_t \right\} $$算法流程与第 6.4 节完全一样,只是把第 6 步的梯度换成 $\nabla_{\theta'}\mathcal L_{\text{CLIP}}(\theta')$。另外两个细节:
- critic 目标也用 GAE。 与其用单步目标 $y_t=r_t+\hat V_\phi(s_{t+1})$,不如用 $\lambda$-return $y_t = \hat A^{\text{GAE}}_t + \hat V_\phi(s_t)$。这在偏差-方差之间取得与 actor 一致的折中,实现上也只是一行加法。
- 熵正则化(entropy regularization)。 在目标里加上 $\beta\sum_i\sum_t \mathcal H\big(\pi_{\theta'}(\cdot|s^{(i)}_t)\big)$,其中 $\mathcal H(p)=-\sum_a p(a)\log p(a)$。作用是防止策略过早坍缩成确定性——一旦某个动作概率接近 1,其他动作再也采不到,探索停止,而且 IS 比值会变得极度不稳定(分母趋于 0)。典型 $\beta\in[0.0,\,0.01]$,连续控制里常设 0,离散动作和 LLM 场景常设小正数。
8.3 最小可运行实现
import torch
import torch.nn.functional as F
def ppo_loss(logp_new, logp_old, adv, entropy,
clip_eps=0.2, ent_coef=0.01):
"""
logp_new : (B,) 当前 θ′ 下 log π_θ′(a|s),带梯度
logp_old : (B,) 采样时记录的 log π_θ(a|s),detach 过,无梯度
adv : (B,) 冻结的 GAE 优势
entropy : (B,) 当前策略在这些状态上的熵
"""
# 优势标准化:这不是理论要求,但对学习率的鲁棒性影响极大
adv = (adv - adv.mean()) / (adv.std() + 1e-8)
ratio = (logp_new - logp_old).exp() # w = π_θ′ / π_θ
surr1 = ratio * adv # 未裁剪支
surr2 = ratio.clamp(1 - clip_eps, 1 + clip_eps) * adv # 裁剪支
policy_loss = -torch.min(surr1, surr2).mean() # 取 min 后最大化 → 加负号
return policy_loss - ent_coef * entropy.mean()
def train_on_batch(policy, value_fn, opt_pi, opt_v, batch,
K=4, minibatch=64, kl_target=0.015):
obs, act, logp_old, adv, ret = batch # 全部 detach 过
n = obs.shape[0]
for epoch in range(K): # 内层 K 次复用同一批数据
perm = torch.randperm(n)
for i in range(0, n, minibatch):
idx = perm[i:i + minibatch]
dist = policy(obs[idx])
logp_new = dist.log_prob(act[idx])
loss = ppo_loss(logp_new, logp_old[idx], adv[idx],
dist.entropy())
opt_pi.zero_grad(); loss.backward()
torch.nn.utils.clip_grad_norm_(policy.parameters(), 0.5)
opt_pi.step()
v_loss = F.mse_loss(value_fn(obs[idx]).squeeze(-1), ret[idx])
opt_v.zero_grad(); v_loss.backward(); opt_v.step()
# 每个 epoch 结束检查一次整体漂移,越线就提前结束内层循环
with torch.no_grad():
logr = policy(obs).log_prob(act) - logp_old
approx_kl = (logr.exp() - 1.0 - logr).mean()
if approx_kl > 1.5 * kl_target:
break
几个容易踩的实现细节:logp_old 必须是采样当时记录的值,不能在训练循环里用「上一个 minibatch 后的策略」重算,否则比值的基准一直在动,裁剪区间就失去意义;adv 的标准化要在整批上做(而不是每个 minibatch 单独做)才不会引入额外偏差;梯度裁剪 clip_grad_norm_ 是 PPO 事实上的标配,与 IS 权重裁剪是两回事,前者管参数更新的步长,后者管分布漂移。
9. 走得更远:replay buffer 与 off-policy actor-critic
PPO 的 off-policy 程度其实很有限:数据只在被采集后的几次内层迭代里复用,然后立刻丢掉。它常被称为「近-on-policy(near-on-policy)」。那能不能像 DQN 一样,维护一个百万条转移的 replay buffer,让策略梯度也享受高样本效率?可以,但要付出的代价完全不同。
9.1 buffer 里的数据来自「一群陈年策略」
replay buffer 里的样本 $(s,a,r,s')$ 来自过去许多个不同的策略 $\pi_{\theta_1},\pi_{\theta_2},\dots$,合起来是一个混合行为策略 $\beta$。要把第 6 节的公式用上,我们需要
$$ \nabla_{\theta}J(\theta) \approx \E_{s\sim d^{\beta}}\ \E_{a\sim\beta(\cdot|s)}\!\left[\frac{\pi_{\theta}(a|s)}{\beta(a|s)}\nabla_{\theta}\log\pi_{\theta}(a|s)\,A^{\pi_\theta}(s,a)\right] $$比 PPO 的情形多出三个新麻烦:
| 麻烦 | 具体表现 | 常见对策 |
|---|---|---|
| $\beta$ 未知 | 混合策略的密度没有解析式(除非你为每条转移都存下当时的 $\log\beta(a|s)$) | 存 logp_behavior 到 buffer;或干脆假装 $\beta$ 就是采集时的那个策略 |
| 比值巨大 | 老策略与当前策略差异远大于 $\epsilon$,$\frac{\pi_\theta}{\beta}$ 可能是 $10^3$ 量级 | 激进裁剪($\bar\rho=1$ 的 V-trace)、只保留新鲜数据、优先采样新样本 |
| 状态分布错配 | $d^{\beta}\ne d^{\pi_\theta}$,且这次差距不小,第 6 节的一阶近似彻底失效 | 无法用 IS 修正(状态边缘比不可计算),只能接受这个偏差 |
请把两种偏差分清楚:动作分布的错配是可以用 IS 精确修正的(我们知道 $\pi_\theta$ 和 $\beta$ 的密度);状态分布的错配原则上无法修正(需要环境模型或整条轨迹的连乘)。所有 off-policy 策略梯度算法,从 PPO 到 SAC,本质上都是在「状态分布错配的偏差」上下注:赌它足够小,或者赌它不影响最终收敛到的策略。这也是 off-policy 方法调参更玄学的根源。
9.2 用 critic 绕开动作上的 IS:DDPG 与 SAC 的思路
还有一条更彻底的路线:既然动作上的期望难采,那就不要采样,直接对当前策略重新采样动作。 注意上式里对动作的期望本来就是 $\E_{a\sim\pi_\theta}$——如果我们有一个能对任意 $(s,a)$ 求值的 critic $Q_\phi(s,a)$,那么给定 buffer 里的状态 $s$,我们可以就地从 $\pi_\theta(\cdot|s)$ 重新采一个动作 $\tilde a$,代入 $Q_\phi(s,\tilde a)$,这样动作分布天然就是 on-policy 的,IS 比值直接消失:
$$ \nabla_\theta J(\theta) \approx \E_{s\sim d^{\beta}}\ \E_{\tilde a\sim\pi_\theta(\cdot|s)}\big[\nabla_\theta\log\pi_\theta(\tilde a|s)\,Q_\phi(s,\tilde a)\big] $$这就是 off-policy actor-critic 的核心构造。它把「$Q$ 值从哪来」和「动作从哪来」解耦了:$Q_\phi$ 用 buffer 里的转移做 Bellman 回归(这部分本来就是 off-policy 安全的),策略梯度则用重新采样的动作。代价是:Monte-Carlo 优势估计不能用了,你必须依赖 critic,于是第 1 节那张「无偏保险单」被撕毁——critic 有偏,梯度就有偏。这正是 Levine 在开头强调的那个取舍。
| 算法 | 动作期望怎么处理 | 数据复用程度 | 梯度偏差来源 |
|---|---|---|---|
| on-policy PG / A2C | 用采样动作,$w\equiv1$ | 用一次即丢 | 只有 critic 引入的方差($\lambda=1$ 时无偏) |
| PPO | IS 比值 + 裁剪 | 同一批复用 $K$ 轮 | 状态分布错配 + 裁剪偏差 |
| IMPALA / V-trace | 裁剪的 IS 比值($\bar\rho,\bar c$ 两个上限) | 分布式,数据滞后几十步 | 裁剪导致收敛到 $\pi_{\bar\rho}$ 而非 $\pi$ |
| DDPG / TD3 | 确定性策略,用 $\nabla_a Q\cdot\nabla_\theta\mu$ 链式法则 | 百万级 buffer | critic 的近似误差 + 状态分布错配 |
| SAC | 重参数化重采样 $\tilde a$,最大熵目标 | 百万级 buffer | 同上,但熵项缓解了过度确定化 |
DDPG(deep deterministic policy gradient)走到了极致:策略是确定性的 $a=\mu_\theta(s)$,此时 $\log\pi_\theta$ 根本没有定义,梯度改用链式法则 $\nabla_\theta J=\E_{s\sim d^\beta}[\nabla_a Q_\phi(s,a)|_{a=\mu_\theta(s)}\nabla_\theta\mu_\theta(s)]$。SAC(soft actor-critic)则保留随机策略,用重参数化技巧(reparameterization trick)$\tilde a = f_\theta(\varepsilon;s)$ 让梯度穿过采样,并在目标里加上熵项 $\alpha\mathcal H(\pi_\theta(\cdot|s))$——这与第 8 节的熵正则化是同一个想法,只是 SAC 把它提升为目标函数的一部分而非附加惩罚。这两个算法是第 17–18 讲的主题,这里只需要建立一个认识:
处理 off-policy 数据只有三种手段,且各有代价:(1)用 IS 权重修正——正确但方差爆炸,必须裁剪,于是引入偏差;(2)用 critic 重新采样动作——消除动作错配,但把正确性完全押在 critic 上;(3)限制数据的陈旧程度——最简单也最有效,PPO 就是这么做的。真实系统里三者常常混用。而没有任何手段能修正状态分布的错配,这是 off-policy 学习不可消除的偏差底线。
9.3 一张决策表
| 你的情况 | 推荐做法 | 理由 |
|---|---|---|
| 仿真器很快、可大规模并行 | on-policy PG / PPO,$K$ 取小 | 样本便宜,买稳定性最划算 |
| 真机采样、每条数据都昂贵 | SAC / TD3 + 大 buffer | 样本效率是唯一硬约束,接受偏差 |
| LLM 后训练(RLHF / RLVR) | PPO / GRPO 一族,强裁剪 + KL 惩罚 | 怕崩溃甚于怕慢;生成可并行;KL 还兼作「别跑离基座模型太远」的正则 |
| 动作空间离散且很小 | 可以考虑 Q-learning 系 | $\argmax$ 廉价,值函数方法样本效率优势明显 |
| 动作空间连续且高维 | 策略梯度系 | $\argmax_a Q$ 在连续空间要么解不出、要么很贵 |
本讲小结
本讲的逻辑是一条完整的「妥协链」:每一步都明确写出「这样做是错的,但错得可控」,最后落在一个能真跑的算法上。
| 步骤 | 做了什么 | 付出的代价 | 为什么值得 |
|---|---|---|---|
| ① IS 恒等式 | $J(\theta')=\E_{\tau\sim p_\theta}\big[\frac{p_{\theta'}(\tau)}{p_\theta(\tau)}r(\tau)\big]$ | 无(精确) | 模型项约掉,只剩策略概率连乘 |
| ② 对 $\theta'$ 求导 | 用 $p\nabla\log p=\nabla p$ 得到 off-policy PG | 无(精确) | $\theta'=\theta$ 时还原成 REINFORCE |
| ③ 丢掉未来项连乘 | 用 $A^{\pi_\theta}$ 代替 $A^{\pi_{\theta'}}$ | 不再是 $J$ 的梯度,变成策略迭代 | 策略迭代自身有单调改进保证(下一讲) |
| ④ 丢掉状态边缘比 | $d^{\pi_{\theta'}}\to d^{\pi_\theta}$,只留单步动作比 | 一阶近似,误差 $O(\epsilon^2)$ | 把指数级连乘降成一次除法 |
| ⑤ 裁剪权重 | $w\to\operatorname{clip}(w,1-\epsilon,1+\epsilon)$ | 引入偏差,失去收敛保证 | 方差硬性封顶;隐式信赖域 |
| ⑥ 取 $\min$ | $\min\{w\hat A,\ w_c\hat A\}$ | 目标不再光滑 | 堵住「往外漂不付代价」的漏洞 |
一页速查
- on-policy 策略梯度的死穴:一批数据只能走一步梯度,而高方差又逼你用大 batch、小学习率,代价相乘。
- IS 恒等式:$\E_{p}[f]=\E_{q}\big[\frac pq f\big]$,前提是 $q$ 覆盖 $p$ 的支撑集。
- 轨迹权重:$\frac{p_{\theta'}(\tau)}{p_\theta(\tau)}=\prod_{t=1}^{H}\frac{\pi_{\theta'}(a_t|s_t)}{\pi_\theta(a_t|s_t)}$,初始分布与转移全部约掉,不需要模型。
- 方差爆炸:若单步 $\E[\rho^2]=1+\sigma^2$,则 $\operatorname{Var}(w)=(1+\sigma^2)^H-1$。$(0.5,0.5)\to(0.6,0.4)$ 这种微小改动在 $H=1000$ 时就把方差推到 $10^{17}$。
- 因果性拆分:过去项修正访问分布(不能丢),未来项修正 $Q$ 值(可以丢,丢了得到策略迭代)。
- 可用形式:$\nabla J\approx\E_{s\sim d^{\pi_\theta},a\sim\pi_\theta}\big[\frac{\pi_{\theta'}(a|s)}{\pi_\theta(a|s)}\nabla\log\pi_{\theta'}(a|s)A^{\pi_\theta}(s,a)\big]$,等价于最大化代理目标 $\E\big[\frac{\pi_{\theta'}}{\pi_\theta}A\big]$。
- 诊断三件套:ESS 占比(> 0.3 才可信)、近似 KL(用 $r-1-\log r$,恒非负)、裁剪比例(0.05–0.3 为宜)。
- PPO:$\mathcal L_{\text{CLIP}}=\sum\min\{w\hat A,\ \operatorname{clip}(w,1-\epsilon,1+\epsilon)\hat A\}$,$\epsilon$ 取 0.1–0.2,内层 $K$ 取 4–10,配 GAE + 熵正则 + 优势标准化 + 梯度裁剪。
- 偏差的两个来源:动作分布错配(可用 IS 精确修正)与状态分布错配(无法修正,只能靠「策略别变太快」来控制)。
与前后讲的接口
上一讲(Q-learning 的工程实践)解决的是「值函数方法如何在深度网络下不崩」,本讲解决的是「策略梯度方法如何不那么浪费样本」。两条线在第 9.2 节短暂交汇:off-policy actor-critic 同时需要 replay buffer(Q-learning 的技术)和策略梯度(本讲的技术)。
本讲留下的最大欠账是那句「this is a little bit wrong」:我们凭什么相信忽略状态分布差异是可以接受的?下一讲会用性能差分引理给出严格答案,并顺势推出以 KL 散度定义的信赖域、Fisher 信息矩阵与自然梯度,以及 TRPO。到那时你会发现,本讲的「裁剪」只是信赖域约束的一个粗糙但极其好用的近似——理解了原理再看 PPO,会觉得它每一处设计都恰好落在正确的位置上。
延伸阅读
本讲直接对应的论文
- Proximal Policy Optimization Algorithms (Schulman et al., 2017) — 本讲第 8 节的原始出处,$\mathcal L_{\text{CLIP}}$、自适应 KL 惩罚变体、以及那张著名的「六种代理目标对比图」都在这里。必读。
- Trust Region Policy Optimization (Schulman et al., 2015) — PPO 的前身,把「策略别变太快」写成硬性 KL 约束并给出单调改进的证明。下一讲的主要素材,可以先读第 3 节的引理。
- High-Dimensional Continuous Control Using Generalized Advantage Estimation (Schulman et al., 2015) — GAE 的原始论文。本讲算法框里的 $\hat A^{\text{GAE}}$ 就是它。
重要性采样与方差控制
- IMPALA: Scalable Distributed Deep-RL with Importance Weighted Actor-Learner Architectures (Espeholt et al., 2018) — V-trace 用两个独立的裁剪上限 $\bar\rho,\bar c$ 分别控制「收敛到哪个策略」和「方差」,是理解裁剪代价的最佳材料。
- Safe and Efficient Off-Policy Reinforcement Learning (Munos et al., 2016) — Retrace($\lambda$),用截断的 IS 比值构造对任意行为策略都安全的多步回报算子。
- Doubly Robust Off-policy Value Evaluation for Reinforcement Learning (Jiang & Li, 2015) — 把 IS 与模型估计结合,方差远低于纯 IS,是 off-policy 评估的基准方法。
- Pareto Smoothed Importance Sampling (Vehtari et al., 2015 起多个版本) — 统计学界处理重尾 IS 权重的标准工具,其中的 $\hat k$ 诊断量比 ESS 更敏感,值得作为日志指标借鉴。
off-policy actor-critic 一族
- Off-Policy Actor-Critic (Degris et al., 2012) — 最早系统讨论「丢掉状态分布修正项」的工作,本讲第 6.2 节那个近似的理论源头。
- Continuous Control with Deep Reinforcement Learning (Lillicrap et al., 2015) — DDPG,确定性策略 + replay buffer 的组合。
- Soft Actor-Critic (Haarnoja et al., 2018) — 最大熵框架下的 off-policy actor-critic,本讲第 8 节的熵正则化在这里被提升为目标函数本身。
- Q-Prop: Sample-Efficient Policy Gradient with An Off-Policy Critic (Gu et al., 2016) — 用 critic 做控制变量,在保持无偏的前提下降低方差,是「两条路线融合」的一个优雅样本。
工程实践与复现
- Implementation Matters in Deep Policy Gradients: A Case Study on PPO and TRPO (Engstrom et al., 2020) — 证明 PPO 的大部分性能来自优势标准化、学习率退火、正交初始化等「小技巧」而非裁剪本身。看完再回头读第 8.3 节的代码会有新体会。
- What Matters In On-Policy Reinforcement Learning? A Large-Scale Empirical Study (Andrychowicz et al., 2020) — 上千次实验的超参消融,是调 PPO 时的实用查表手册。
- DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models (Shao et al., 2024) — GRPO 的出处:去掉 critic,用组内多次采样的回报做优势的自归一化基线,是本讲 SNIS 思想在 LLM 场景下的直接应用。