强化学习学到这个阶段,大部分公式推导和代码实现都能跑通了,但有一类问题始终绕不过去:理论上讲,策略优化的目标应该让性能越改越好,实际训练里却经常出现"越改越差"。这个问题的系统解答,就是 Monotonic Improvement Theory(策略的单调提升)。这是系列的第四十三篇,我打算把这块从数学原理到工程实现彻底捋清楚。适合正在啃策略优化论文、或者在调参路上被各种玄学坑过的朋友——这篇文章会把"为什么策略更新会翻车"和"怎么改才能尽量不翻车"两件事一次性讲透。
1. 策略更新"越改越差"的根因:一阶近似与分布偏移
1.1 复现一次典型的"更新即崩溃"
先把最典型的失败模式挂在这里,后面所有的推导都是在回应这个问题。标准策略梯度做的事情非常简单:用当前策略采一批轨迹,估计优势函数,然后朝策略梯度的方向走一步。这套流程单看每一步都有道理,但放到实际情况里,会出一个非常诡异的结果——前几百次更新平平稳稳,某一次更新之后,评估奖励突然塌掉一大截,而且往往是在你"信心最足"的时候。
这不是代码写错了,也不是环境出了 bug。原因是:更新前的梯度是用旧策略采样的轨迹估计出来的,更新完之后,新策略已经和采样时不是同一个策略了。策略梯度公式里那个以"当前策略分布"为期望的记号,更新之后分布立刻变了,梯度信息却还停留在旧分布上。可以理解为拿着昨天的地图找今天的新路,地图本身没画错,但路已经动了。
1.2 策略梯度只保证"无穷小步长"下的正确性
更严格地说,策略梯度是一阶近似,它保证的是"在策略参数空间里沿梯度方向走无穷小一步,性能会按照一阶项提升"。注意是无穷小。实际训练里步长不可能无穷小,只要步长有限,泰勒展开的余项就会产生误差。这一点在监督学习里问题不大,损失函数在参数空间里通常足够光滑,稍微跨大一点也只是轻微变差。
但强化学习的目标函数有一个特殊性:策略一改变,状态的访问分布也会跟着改变,而且是递归式地改变。旧分布下采样得到的优势函数,对新分布下的状态完全可能是错误的。这就是分布偏移(distribution shift)。两个策略差得越远,原来那个代理目标对真实目标的近似就越差。学习率一旦偏大,更新后的策略偏离旧策略太远,所谓的"梯度上升"实际上是在一个已经失效的目标函数上做上升,越升越弱。
1.3 性能差分解:把"差多少"写成精确等式
既然一阶近似会失真,自然的想法是找一个不需要近似的表达式,把新旧策略的性能差精确写出来。强化学习里有一个现成的工具——性能差分解:
$$ J(\pi') - J(\pi) = \frac{1}{1-\gamma}\mathbb{E}{s \sim d{\pi'},, a \sim \pi'}\left[A_\pi(s,a)\right] $$
这个式子的意思是:新策略相对旧策略的性能增益,等于"从新策略的折扣状态分布出发,按新策略采样动作,然后计算旧策略优势函数"的期望。推导的关键是折扣状态分布下优势函数累加与性能差的卷积关系。它最妙的地方在于始终保持精确性,没有丢弃任何高阶项。后面所有单调提升的结论都从这个等式出发。
2. 单调提升理论的推导主线:从精确等式到KL下界
2.1 代理目标(surrogate objective)与它的"一阶匹配"性质
上面的分解式虽然精确,但它要求按照新策略的分布 d_{π'} 去采样状态,而这是没法直接算的——如果我已经知道新策略的分布,那迭代就已经完成了。于是自然产生一个近似:把 d_{π'} 换成 d_π,得到代理目标函数:
$$ L_\pi(\pi') = J(\pi) + \frac{1}{1-\gamma}\mathbb{E}{s \sim d{\pi},, a \sim \pi'}\left[A_\pi(s,a)\right] $$
为什么这个代理目标有用?因为当 π'=π 时,L_π(π') 和 J(π') 不仅函数值相等(都等于 J(π)),它们对策略参数的导数也相等。也就是说,在旧策略附近,真实目标函数和代理目标函数的一阶行为完全一致。策略梯度能 work,本质上就是因为我们一直在优化这个代理目标的梯度,在小步长范围内它与真实目标的梯度方向一致。
但是"一阶一致"只能支撑局部结论。一旦策略参数移动得稍远,代理目标的曲面就会逐渐偏离真实目标,这个偏离量不是固定的,而是随新旧策略的分布差异增大而增大。所以接下来的问题变成:这个"偏离量"到底有多大?有没有办法用一个可计算的量把它界住?
2.2 两个分布之间到底差多少:KL散度进入视野
要度量分布差异,最常用的工具是 KL 散度。为什么不是参数空间里的欧氏距离?因为策略的参数和策略的行为之间不是一一对应的。参数向量差 0.01,对应的动作分布可能已经差异很大;反过来参数看起来不一样,策略行为也可能完全相同。KL 散度直接度量的是动作分布的差异,这才是我们真正关心的量。
由 Pinsker 不等式,总变差距离可以被 KL 散度控制;再通过状态分布的递推累加,可以证明 d_{π'} 与 d_π 之间的差异也被新旧策略在每个状态上的 KL 散度最大值所界住。这就是为什么很多推导的最后会出现"max_s KL(π'(·|s) ‖ π(·|s))"这样的项——它把"策略在每个状态上偏离了多少"打包成一个标量,用来控制由分布偏移造成的误差。
2.3 三明治式论证:单调提升定理的骨架
把上面的界限带回代理目标与真实目标的关系,可以得到一个下界不等式:
$$ J(\pi') \ge L_\pi(\pi') - C \cdot \max_s D_{KL}\left(\pi'(\cdot|s),|,\pi(\cdot|s)\right) $$
其中 C 是一个只与折扣因子 γ 以及优势函数界有关的常数,在常见推导里是 2εγ/(1-γ)² 的量级。这个下界的含义是:无论 π' 选择什么,真实性能 J(π') 都至少不低于"代理目标减去 KL 惩罚"。
于是逻辑就完整了。令 M_π(π') = L_π(π') - C·max_s KL(...)。我们有 J(π') ≥ M_π(π')。同时,在 π'=π 时,KL 项为 0,L_π(π)=J(π),所以 M_π(π)=J(π)。如果每次迭代都选取使 M_π 最大的新策略 π',那么 M_π(π') ≥ M_π(π) = J(π),又因为 J(π') ≥ M_π(π'),最终推出 J(π') ≥ J(π)。这就完成了单调提升的证明。
整个证明的精髓在于:我们不需要去估计 J(π') 本身,只需要优化一个带惩罚的代理目标,就能保证真实性能在每次迭代后只升不降。这是策略优化领域少有的"兼顾可计算性和理论保证"的结论。
3. TRPO与PPO如何把理论边界变成可运行的更新规则
3.1 第一个妥协:max 换成 mean,严格性让位于可行性
理论很漂亮,但直接实现会遇到一个尴尬的问题:max over states 的 KL 无法求解,因为我们根本没有所有状态的分布。于是 TRPO 的做法是退而求其次,把"每一个状态上的 KL 都不超过 δ"改成"平均 KL 不超过 δ":
$$ \theta_{k+1} = \arg\max_\theta L_{\theta_k}(\theta), \quad \text{s.t.} \quad \bar{D}_{KL}(\theta_k, \theta) \le \delta $$
这一步丢失了严格保证,但在实践中效果稳定。道理也直观:神经网络是参数共享的,如果平均 KL 被约束住,那些最大偏差的状态通常也不会跑得太离谱,至少不会出现整体分布剧变。TRPO 是"理论上严格但工程上近似"的典型产物。
3.2 TRPO 的求解:共轭梯度与线搜索
实现 TRPO 时,约束优化里会出现一个二阶项:KL 散度对参数的 Hessian 矩阵。如果直接求逆,参数维度稍大就扛不住。常用解法是共轭梯度法,它只通过 KL 的梯度迭代逼近 H⁻¹g,避免显式求逆矩阵。求完方向后还要做线搜索:从完整步长开始,如果发现新策略与旧策略的 KL 超过约束,或代理目标改进不足,就逐步缩短步长。
这个"逐步缩短步长"的机制,其实就是把上一节的下界思想写进了工程代码里——虽然用的是 mean KL,但每次更新前都会检查"这次步子是否越界"。在很多开源实现里,这一行检查往往就是决定训练是否稳定的关键。
3.3 PPO的截断式目标:把约束变成梯度开关
TRPO 每轮都要做二阶近似和共轭梯度,计算开销偏大。PPO 的做法更直接:不做显式约束,在损失函数里做截断。定义新旧策略的动作概率比 r_t(θ)=π_θ(a_t|s_t)/π_old(a_t|s_t),损失为:
$$ L^{CLIP}(\theta) = \mathbb{E}\left[\min\left(r_t(\theta) A_t,\ \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon), A_t\right)\right] $$
当 r_t 落在 [1-ε, 1+ε] 内时,这个目标与普通策略梯度一致;一旦 r_t 越界,对应的梯度被 clip 遮挡,相当于自动停止在该样本上继续扩大更新幅度。它是把"约束"翻译成"梯度信号开关",粗鲁但有效,而且只用一阶梯度,工程实现极其友好。
3.4 三种方案对比与一个提醒
我整理一个简单的对比,方便理解理论、半理论半工程、纯工程三者之间的差别:
| 方案 | 是否保证单调 | 分布约束形式 | 计算复杂度 | 实际特点 |
|---|---|---|---|---|
| 严格单调提升(最大化下界) | 严格单调 | max over states KL | 理论可行,实际不可解 | 参考基准 |
| TRPO | 近似/经验上稳定 | 平均 KL | 二阶近似 + 共轭梯度 | 稳定但迭代较慢 |
| PPO-clip | 启发式近似 | 无显式约束 | 一阶即可 | 快,多数场景够稳定 |
需要特别说清楚一个常见误区:单调提升定理保证的是"期望意义下真实回报不下降",并不是"每次用采样回报评估都不下降"。训练时评估的奖励是带噪声的估计,加上神经网络函数近似误差,即使是 TRPO 也可能看到单次下跌。判断算法稳不稳定,要看足够长窗口内的趋势,以及 KL、熵这类分布指标,而不是盯着某一轮的数字。
4. 用单调提升的视角做实战调试:KL指标、熵与排查流程
4.1 训练日志里必须盯住的两个分布指标
跑策略优化实验时,很多人只看 reward 曲线,这是不够的。我建议至少记录 mean KL 和策略熵。mean KL 告诉你"每一步更新策略实际变了多少",是分布偏移的直接度量;策略熵告诉你"策略还有没有在探索"。这两者一个管稳定性,一个管探索能力,都是 reward 曲线无法直接反映的。
前文已经解释了 KL 的理论含义,这里只强调实操价值。当 mean KL 长期在 0.01 以下,说明策略几乎没动,通常意味着学习率太小或 KL 惩罚过重;当某一步 mean KL 突然跳到 0.05 以上,说明这一步的更新幅度严重越界,哪怕当前 reward 还在涨,也要警惕后续崩盘。
4.2 自适应 KL 惩罚的落地配置
在很多实现里,我们会维护一个自适应的 KL 惩罚系数 β,目标是让 mean KL 保持在 target_kl 附近。逻辑很简单:
# 伪代码逻辑,完整实现请结合具体框架 target_kl = 0.01 beta = 1.0 for epoch in range(...): update_policy(beta) kl = estimate_mean_kl() if kl > 1.5 * target_kl: beta *= 2.0 elif kl < 0.5 * target_kl: beta /= 2.0这个策略的实质是"用上一次更新的 KL 观测值来调整下一次更新的惩罚强度"。我个人的经验是 target_kl 取 0.01 到 0.02 在多数连续控制任务里比较好用。如果任务奖励尺度很大,可以适当放宽到 0.03,但不要超过 0.05,否则策略更新幅度过大,训练曲线会变得很躁。初始 β 不用精调,给个差不多的值让它自适应就行,关键是每次更新后要真正去观测 KL,而不是只在代码里写上自适应逻辑就不管了。
4.3 一次"更新后崩掉"的排查顺序
假设你在某个模拟任务上遇到了开头说的崩溃:训练曲线前几天还好,某次更新后评估奖励突然大幅下降。别急着调学习率、改网络结构,按下面的顺序查:
- 打开日志,看那次更新前后的 mean KL 和策略熵。如果 mean KL 超过 0.03,甚至接近 0.1,直接判定是更新幅度过大。
- 查看优势估计的标准差和奖励尺度。奖励尺度很大时,同样的 KL 会带来更大的性能波动。
- 回看这次更新用的样本是否大量来自旧策略的"过期轨迹"。如果数据池里混了太多旧样本,概率比可能异常,PPO 里会体现为 clip 比例骤增。
- 最后再调超参数:优先降低学习率或提高 target_kl 对应的惩罚,重新跑一版,观察 KL 是否回到稳定区间。
这套流程我试过很多次,大部分情况下第一步就能定位到原因。真正难的是那些"KL 不大但奖励也掉了"的情况,这时候一般要从奖励函数设计、环境噪声、优势估计的 bias 方面去找,那就超出单调提升理论能解释的范围了。
4.4 几条可以长期复用的实操建议
最后把散落的经验集中一下。首先,每次更新前保存旧策略参数,或者至少保存一份旧的 checkpoint,这样即使单步崩了也能快速回退,而不是从头重训。其次,使用线搜索或 early stopping 机制:TRPO 的线搜索、PPO 的早停都是把理论下界思想工程化的手段,自己实现框架时别忘了加。
再者,把 mean KL、策略熵、clip 比例都写进日志。我见过不少实验复盘,reward 曲线复盘半天说不出所以然,但一翻 KL 日志立刻明白是哪一步更新越界了。最后,理解"单调提升"不等于"每次提升"。它更像一个安全护栏:在你选择优化方向时,保证这个方向不会把策略推下悬崖。
我现在再看训练曲线,心态完全不同了。以前看到某一步涨了就高兴,跌了就心慌,现在更关心的是:每次更新之后,那些保证单调提升的代理指标是否还处在安全范围内。理论给你的不是"永远不崩"的承诺,而是一套判断"什么时候危险"的标尺。这个认知上的转变,比多调几个超参数值钱得多。