先把结论摆在这:异步强化学习里,Trainer-Generator Mismatch 并不只是“数据太旧”这么简单。它真正的麻烦在于,当生成数据的策略和更新梯度的策略不是同一个时,Score Function 的均值会悄悄从零变成非零,而这个非零均值会像慢性毒药一样,一点一点把策略梯度往错误方向带。这篇完全手写、尽量不用黑话的文章,就是把“Score Function 怎么定义、它凭什么 zero-mean、为什么 Mismatch 会破坏这个性质、Centering 又是怎么把它们拉回来的”一条线讲完,最后附上我认为最干净的伪代码和我在实验里踩过的坑。适合两类人读:一类是被异步训练曲线折磨的算法工程师,一类是被论文符号劝退、但想真正搞懂原理的学生。
1. 异步RL里那个让人头大的 Trainer-Generator Mismatch:先用大白话讲清楚问题
1.1 异步RL的标准流水线:一个 actor,一个 learner,两个版本的参数
异步强化学习之所以被大规模使用,核心原因是环境采样很慢,而 GPU 上的梯度更新很快,必须拆成两个工种并行跑才有吞吐量。典型结构是这样:一组生成器(我干脆叫 generator)跑在环境里,它们不干别的,就是拿着当前策略去采集 transition——状态、动作、奖励、终止标志。另一端是一个训练器(trainer,也叫 learner),攒够一批样本之后计算梯度、更新策略参数。
generator 和 trainer 之间通过参数同步来对齐,但问题在于,同步不是每一步都做的。为了躲开“环境采样慢”和“梯度更新快”之间的性能差距,同步频率往往很低,可能是几百步同步一次,也可能是完全异步、各玩各的设计——actor 每隔一段时间从 learner 那边拉一次最新参数,然后继续用自己的本地副本采样。
正是这种设计制造了一个不可回避的事实:generator 手里的策略参数,永远比 trainer 手里的策略参数旧。trainer 在 t 时刻拿到的一批样本,是 generator 在更早时刻、用旧策略跑出来的。而 trainer 算完梯度之后,直接拿这批旧样本去更新当前的新参数。于是,用来采样的分布是旧的,用来更新的目标是新的——这就是 Trainer-Generator Mismatch 最朴素的定义。
1.2 为什么它比“数据不新鲜”更严重
很多人第一反应是:这不就是 off-policy 吗?做重要性采样不就行了?确实是 off-policy,但问题在于,很多异步实现为了效率和简单,根本没有加重要性采样。就算加了,如果生成策略和目标策略之间的差距是持续扩大的——因为 trainer 一直在更新、generator 迟迟不同步——重要性采样的比值也会越来越不稳定,修正本身都可能爆炸。
更隐蔽的是,Mismatch 会直接影响 Score Function 的统计性质。用来更新参数的 ∇logπ,它原本在“自己策略产生的数据”上均值是零,但在“旧策略产生的数据”上,均值就未必是零了。这个变化带来的后果,比“方差变大”严重得多——它会引入一个稳定方向的梯度偏置,而不是那种上下乱晃的噪声。下面第 4 章我会完整推导这个偏置长什么样。这里先记住一句话:Mismatch 不只是 stale data 问题,它是 Score Function 统计性质的质变。
2. 白话 Score Function:梯度里的“指南针”,它凭什么指导策略更新
2.1 Score Function 是谁:一个“方向指示器”
在许多初步接触 RL 的人眼里,∇θ log π(a|s) 是个莫名其妙的符号。其实白话理解非常简单:对于某个具体动作 a,log π(a|s) 告诉你这个动作在当前策略下的“受欢迎程度”。拿这个 log 概率对策略参数 θ 求梯度,得到的就是:如果我把 θ 往梯度的方向挪一点点,这个动作的 log 概率会增加多少。
换句话说,Score Function 是“想让这个动作被更多选择的方向和力度”。我给它的外号是“指南针”——它指向一个方向,那个方向对应着“提高这个动作概率”的最大斜率。
举个例子。假设在某状态下有两个动作,当前策略是 50% 概率选左、50% 选右。你对左动作算了一下 Score Function,发现它指向左边,那就表示参数 θ 往那个方向挪,左动作的概率会上升。对右动作算一下,发现它指向右边——同一个状态,两个动作的 Score 方向可能完全相反。这很合理,一个想让左边更可能,一个想让右边更可能,它们本来就在竞争。
所以真正的问题变成了:到底该听谁的?每个动作都有自己的“指南针”,但策略更新只能选一个合方向。这就需要某种加权机制:好动作的分量被放大,坏动作的分量被反转。于是我们引出了 Advantage 以及期望运算。
2.2 零均值性质:它自己不会“拉偏架”
现在说 Score Function 最决定性的一个性质。如果我们按照策略 π 自己来采样动作,然后把所有动作的 Score 按概率加权平均,会发生什么?
$$ E_{a\sim\pi_\theta}[\nabla_\theta \log\pi_\theta(a|s)] = \int \nabla_\theta \pi_\theta(a|s),da = \nabla_\theta \int \pi_\theta(a|s),da = \nabla_\theta 1 = 0 $$
第一个等号成立,是因为 ∇logπ = (∇π)/π,乘以概率 π 正好消掉分母。最后一个等号是概率分布归一化的必然结果——所有概率加起来恒等于 1,对参数求导当然是 0。
这个性质我用大白话翻译就是:在“自己采自己”的稳态下,Score Function 的平均值是零。它并不会因为想提高某些动作的概率,就整体朝某个方向偏心。这可以说是一切策略梯度算法敢直接把 Score 乘上 Advantage 使用的前提——Score 自己不拉偏架,方向完全由 Advantage 决定。
如果这个零均值性质被破坏,乘出来的梯度就会多出一个不受 Advantage 控制的分量,整个更新方向会莫名其妙被带着跑。而 Trainer-Generator Mismatch 恰恰就是破坏这个性质的头号因素。
2.3 为什么最终要乘 Advantage:协方差的直觉
常规策略梯度的标准形式是:
$$ \nabla J(\theta) = E\left[\nabla_\theta \log\pi_\theta(a|s) , A(s,a)\right] $$
Score 负责“往哪个方向调概率”,Advantage 负责“这个方向值不值得调”。两者相乘再取期望:好动作(A > 0)的 Score 方向被保留甚至放大,坏动作(A < 0)的方向被反转,于是整个梯度就是“提升好动作概率、压低坏动作概率”的合力。
如果从协方差角度看,有一个极其重要、但几乎没人点破的事实:
$$ E[s \cdot A] = \mathrm{Cov}(s, A) + E[s] \cdot E[A] $$
正常情况下,因为 E[s] = 0,上面的式子自动化简成纯协方差项。也就是说,策略梯度的本质是 Score 和 Advantage 的相关性——它们是不是“同步地”往某个方向变化。一旦 Score 的均值不等于零,梯度里就会混入一个跟相关性完全无关的“均值乘均值”污染项。这个认识非常重要,后面第 4 章的推导全靠它。
3. Centering 在干嘛:把 Score Function 的“系统性漂移”剥掉
3.1 Centering 是一次数学手术:从 score 里减去它的均值
所谓 Score Function Centering,就是在使用 Score 之前,先减去它的期望(或者期望的估计):
$$ \tilde{s} = \nabla_\theta \log\pi_\theta(a|s) - c, \quad c \approx E\left[\nabla_\theta \log\pi_\theta(a|s)\right] $$
就这一个动作。很多文献管它叫 centering,但注意它跟数据预处理里的“中心化”不是一回事——它不做缩放、不做标准化,只做平移。为什么只做平移就够了?因为上面已经分析过了:问题出在 Score 的均值不为零,把它减掉,剩下的是一个零均值向量。零均值意味着它恢复了“自己不拉偏架的”的性质,更新方向重新回到主要由 Advantage 说了算。
至于为什么减一个常数向量、不减去其他更复杂的函数,因为我们要去掉的恰好就是均值那一项。平移操作不会改变方差——如果你只关心降方差,它有更激进的工具可用;但在 Mismatch 场景下,它纠正的恰恰是均值偏差,这是缩放、标准化做不到的事情。
3.2 别和 Baseline 搞混:一个减标量,一个减向量
很多讨论里会碰到一道经典的问题:策略梯度里面不是有 baseline 吗?Advantage 不也是减过基线了吗?那 Centering 跟它有什么区别?
区别非常明确。Baseline(比如状态价值函数 V(s))减的是 Advantage 本身的标量水平:A = Q - V,目的是降低梯度估计的方差,它从头到尾都留在“奖励/优势”的维度里。Centering 减的是 Score 这个梯度向量在参数空间里的均值,是一个和 θ 同维的向量。一个是做在标量轴上,一个是做在梯度向量轴上,二者不在一个维度空间。
还有一条重要区别:在纯 on-policy、没有 Mismatch 的情况下,Centering 基本等于什么都没做(因为它减的那个值本来就是零),而 Baseline 即使在 on-policy 下也照样能大幅降方差。所以如果你看到有人把 Centering 当成 Baseline 的替代品,那是在误导。它们解决的问题不同,也完全可以同时使用。
3.3 在经典 RL 里 Centering 的价值被低估了
传统意义上,Score Function Centering 主要被当作一个方差技巧来介绍:既然 E[s] = 0,减去一个接近零的东西没什么坏处,又多少能减小估计波动。它确实能降方差,尤其当样本量不大、Score 和 Advantage 相关性较强的时候方差会明显下降。
但在异步 RL 里,它的价值远不止降方差。因为这里 E[s] 不等于零了,Centering 实际上是在做去偏——把统计中心拉回它应该在的位置。我把 Centering 分成两种身份来看:on-policy 下它是锦上添花的方差技巧;off-policy 或者说 Mismatch 场景下,它是必要的纠偏手段。
很多刚听说这个技巧的人只记住“减均值降方差”,完全忽略了后一种更关键的身份。结果异步训练一发散,根本想不到去检查 Score 的均值,这才是最常见的遗憾。
4. 核心推导:Mismatch 如何偷偷给 Score Function 注入非零均值
4.1 推导:Mismatch 把零均值变成了什么
假设我们用旧策略 p = π_old 采样(generator 在用的那版),去更新新策略 q = π_θ(trainer 正在优化的目标)。我们关心的是:
$$ E_p[\nabla_\theta \log q] = \int p(x) \frac{\nabla_\theta q_\theta(x)}{q_\theta(x)},dx $$
这个式子直接看不出来什么,但注意到 KL 散度对小运算非常关键。算一下 KL 散度对 θ 的梯度:
$$ \nabla_\theta KL(p | q_\theta) = \nabla_\theta \int p(x) \log\frac{p(x)}{q_\theta(x)},dx = -\int p(x)\nabla_\theta \log q_\theta(x),dx = -E_p[\nabla_\theta \log q] $$
整理一下,得到:
$$ E_p[\nabla_\theta \log q] = -\nabla_\theta KL(p | q) $$
这个等式太漂亮了。它说明两件事。第一,当 p 和 q 是同一个分布时,KL 处于最小值 0,梯度是 0,所以熟悉的零均值性质回来了。第二,当 p 和 q 不一样时,Score 的期望等于负的 KL 散度梯度——换句话说,Score 会平均地指向“让新策略偏离旧策略”的方向。
4.2 白话翻译:梯度的“幽灵拉力”
把上面的式子翻译成人话。KL 散度可以理解成“新旧策略之间的差异程度”。∇KL(p || q) 是“这个差异程度随着新策略参数 θ 变化而增长最快的方向”。因为生成数据的样本是旧策略 p,而更新的是新策略 q,Score 的平均方向居然是:把 q 往“让它和 p 的差异变得更大”的方向推。
在无 Mismatch 时,没有这个幽灵拉力,Score 平均为零,方向完全交给 Advantage。可一旦 trainer 比 generator 超前,这个拉力就出现了,而且它是系统性的、单向的——每来一批旧样本,它都会往同一个方向推一把新策略。
这跟“采样噪声大、方差高”有本质区别。方差大是上下乱晃,顶多造成训练不稳定;系统性拉力则会让训练曲线看起来在认真下降,实际上却是在往错误方向积累偏置。你可能见过这种诡异的现象:训练 loss 在降,策略熵也在降,但 eval 分数就是上不去,最后突然崩掉——很大概率就是这种幽灵拉力在起作用。
4.3 协方差分解:为什么污染的是均值项而不是协方差项
现在把完整的梯度期望拆开,这一步是整个论证最干净的地方:
$$ E_p[s \cdot A] = \mathrm{Cov}_p(s, A) + E_p[s],E_p[A] $$
第一项——Score 和 Advantage 的协方差——是策略梯度真正想抓住的信号。它衡量的是:一个动作的 Advantage 好不好,是否与它的 Score 方向同步变化。动作好,Score 方向也正好指向提高这个动作概率的方向,协方差为正;动作差,Score 方向却指向提高概率,协方差为负,梯度就会反转这个动作的权重。
第二项是 Score 均值乘 Advantage 均值。On-policy 时 E_p[s] = 0,这一项自动消失,梯度就是纯协方差。Mismatch 时第一项还在——Score 与 Advantage 的相关性依然存在,这是有效信号,不应该丢弃——但第二项变成了一个叠加在梯度上的整体偏移。
这个分解给了我们非常干净的治理思路:既然污染的只是第二项里面的 E_p[s],那我们把 Score 减去它的均值,让它变回零均值,第二项就被干掉了。而第一项协方差几乎不受影响,因为减去一个常数不改变协方差。这就是 Score Function Centering 能对症下药的完整逻辑链:它不破坏信号,只清除污染。
5. Centering 怎么落地:一个从公式到代码的完整方案
5.1 教学级伪代码:REINFORCE 风格的带 Centering 更新
先给最直白的版本,每算一步就更新一次参数。好处是逻辑透明,坏处是计算量大,实际工程不会这么干,但用来理解足够了。
# 伪代码:generator 采样,trainer 更新(REINFORCE 风格 + score centering) def compute_score(policy, state, action): logp = policy.log_prob(state, action) # log π(a|s) score = list(torch.autograd.grad(logp, policy.parameters())) return score # ∇θ log π(a|s),和 θ 同维 # trainer 主循环 gamma = 0.99 for trajectory in generator.collect(): # 对一条轨迹,用旧策略(generator 版本)采的样本算 advantage adv = compute_advantage(trajectory, gamma) # 旧策略视角的 TD 误差 # 1) 先用本批数据估计 c = E_old[∇θ log π_new] states_actions = [(s, a) for s, a, _, _ in trajectory] scores = [compute_score(policy, s, a) for s, a in states_actions] # c 的每个元素对应策略网络每个参数的均值 c = [sum(s[i] for s in scores) / len(scores) for i in range(len(scores[0]))] # 2) 对每个 transition 做 centering 更新 for (s, a), A in zip(states_actions, adv): s_grad = compute_score(policy, s, a) for p, g, c_p in zip(policy.parameters(), s_grad, c): # 关键是这里:减掉 c,相当于把 score 均值拉回 0 p.data += alpha * (g - c_p.detach()) * A注意c_p.detach()。中心化常数是估计出来的,如果让它参与梯度计算,梯度流会绕回去,造成非常隐蔽的训练退化。
这只是教学版,一次 transition 做多次梯度计算,慢得不行。但它是理解“参数空间里做减法”的最直接方式。真正工程上怎么做得快,看下一节。
5.2 工程上怎么做得高效
第一个痛点是,每个 transition 算一次 Score(对参数求梯度),开销很大。实际做法一般是:先攒一个 batch,用torch.func.vmap或者autograd.grad的批量形式一次算出所有样本的 Score。或者更进一步,把策略参数的更新改成标准策略梯度的 loss 形式,只在参数梯度上手动叠加-c * A的修正。
第二个痛点是,c 的估计可以用滑动平均(EMA)替代每个 batch 重算,这样更稳定:
c = 0.0 ema = 0.9 # 每个 batch: batch_scores = compute_all_scores(policy, batch) # shape [B, theta 维度] batch_mean = batch_scores.mean(dim=0) c = ema * c + (1 - ema) * batch_mean.detach() # 缓慢跟踪漂移 # 更新时用 c,注意 detachEMA 的系数不能太大,否则 c 跟着噪声跑,反而引入新的偏差。我习惯把 ema 系数放在 0.9~0.99 之间。如果任务本身很稳定、Mismatch 又不严重,可以调到 0.99,让 c 尽量平滑。
第三个痛点是,如果策略网络参数很多——比如几十万维——保存一份和参数同维的 c,内存和更新开销都不小。这时可以只在部分参数上做 Centering,比如只对策略输出的 logits 层做,或者最后两层做。真正受 Mismatch 影响最大的往往是策略分布的形状,底层特征提取网络受分布漂移的影响相对小。这样做能大幅节省计算量,实战效果也不会差太多。
5.3 和 Importance Sampling 的关系:是不是有了 IS 就不需要 Centering
一个非常自然的问题:既然我们知道是 off-policy,直接用重要性采样修正不就行了吗?
确实,理论上算严格的重加权因子 ρ = q/p,在分布层面做修正,得到的估计是无偏的:
$$ g_{IS} = E_p[\rho(s,a) , \nabla_\theta \log q \cdot A] $$
但实践中有两个大问题。第一,ρ 的方差很大。随着 q 和 p 越拉越远,ρ 可能爆炸,也可能趋近于零。异步 RL 里策略更新频率高、新旧策略差距变化快,ρ 很容易失去控制,所以很多显式 off-policy 算法都会对 ρ 做截断。一旦截断,修正就不完全了。
第二,也是很多人没想明白的:IS 修正修的是“分布不同的期望”,它并不会显式处理 Score 均值漂移。换句话说,即便乘了 ρ,Mismatch 导致的 Score 均值非零问题依然存在,只是被重新缩放了一下而已。
Centering 和 IS 解决的是不同层面的事:IS 修整体期望,Centering 修 Score 自身的统计中心。实践里完全可以同时用:先做截断的 IS,再做 Centering。一个处理分布差异,一个拉平 Score 的均值,两者不冲突。
5.4 怎么验证 Centering 真的有效
验证 Centering 有没有起作用,最直接的办法是监控原始 Score 的均值范数:
# 每隔 N 个 batch,用当前 buffer 的数据估计: scores = compute_all_scores(policy, buffer) score_norm = scores.mean(dim=0).norm().item()如果这个 norm 在训练中从接近 0 逐渐涨到很大,说明 Mismatch 正在污染梯度,此时 Centering 必须在工作。如果你把 Centering 关掉,同时监控策略熵和 KL 散度,你会发现:Center on 的版本,熵下降得更有规律,新旧策略之间的 KL 稳定增长;Center off 的版本,熵可能在某一段突然暴跌,KL 出现尖刺。我用这个指标排查过不少“神秘发散”的 bug,屡试不爽。
另一个测量方法是看更新的方向变化。分别记录一次迭代的原始梯度方向和 Centered 梯度方向,算它们的余弦相似度。如果长期偏离很大——比如小于 0.7——说明 Centering 在频繁地重定向,你确实需要它。如果一直接近 1,说明 Mismatch 不严重,Centering 基本是摆设,那就可以关掉省资源。
6. 我的实战观察:什么时候有效、什么时候小心翻车
6.1 我在异步任务里见过的三种典型症状
第一种,训练中期突然熵崩。前面明明还挺正常,到了某一个阶段策略熵直线下降,最后变成一个几乎确定性的策略,eval 分数却很差。这多半不是学习率太大,而是 Score 漂移积累到一定程度,系统性的幽灵拉力把策略往确定性方向猛推,然后 Advantage 又拦不住。
第二种,Advantage 均值长期不为零。正常理想情况下,Advantage 的均值应该接近零,但稀疏奖励、reward scale 没配好的任务里,它可能是一个正的或负的常数。一旦它非零,Mismatch 的污染项 E[s]·E[A] 就会被放大——因为 E[s] 已经非零,再乘一个非零的 E[A],梯度偏移会非常可观。我在某次稀疏奖励任务里看到这个乘积比有效梯度还大,那一刻才真正理解为什么异步环境里“玄学”这么多。
第三种,加了 Centering 之后训练变稳但变慢。这很正常。Centering 去掉的那个幽灵拉力,本质上在“帮”你更快推到某个方向——只不过那是错的方向。去掉错误推力后,收敛自然会变慢一点点,但只要最终效果更好,这些都是值得的。
6.2 实现里的几个坑
坑一:c 要 detach。我在伪代码里强调过。如果你让 c 参与梯度计算,梯度流会绕回去,不仅 Centering 失效,还可能让整个训练崩溃。
坑二:EMA 更新 c 时,不要把当前 batch 的梯度混进去。很多人在实现时会把 Score 均值直接加进 loss 里,以为没关系,结果 c 本身也在随 batch 变化,形成正反馈,训练更不稳。
坑三:Centering 最好只作用于策略参数的更新,不要顺手把 Critic 的梯度也 Centering 了。Critic 用的是回归损失,里面没有 Score Function,对它的梯度做 Centering 没有理论依据,还容易把价值函数搞坏。
坑四:别忘了配合 Advantage 的 baseline。Centering 不是 Baseline 的替代品。它们一个在 Score 维度上减均值,一个在 Advantage 维度上减基线,两个都要做。只做 Centering、不做 Advantage 的 baseline,方差照样大得离谱。
6.3 什么时候该坦白:Centering 救不了你
如果一个任务的 Mismatch 已经到了近乎离线状态——比如 generator 和 trainer 的参数好长时间才同步一次,或者更新 batch 内新旧策略的 KL 已经大得离谱——Centering 只能缓解,不能根治。它把 Score 均值去掉了,但样本分布和真实目标分布的差异还在那里,协方差项本身也会失真。这种时候真正该做的是降低同步间隔,或者干脆换用显式的 off-policy 修正方案,然后在这个基础上再做 Centering。
另外,如果 Advantage 估计本身质量有问题——bootstrap 步数太少、reward scale 没归一化、价值函数根本没收敛——Centering 同样无能为力。它只能保证 Score 维度干净,保证不了更新信号的标量质量。我见过有人把 Centering 当成万能药,结果该发散照样发散,最后才发现 TD 误差早就炸了,Score 这边再干净也救不回来。
最后分享一点个人体会。我现在每跑一个异步训练任务,第一反应永远不是调学习率,也不是加 batch size,而是先看 Score 均值范数和 Advantage 均值的乘积。如果乘积长期不接近零,那几乎一定存在 Mismatch 污染。把这一层理顺之后,后面的调参才不会被“玄学”反复折腾。这个问题在同步 RL 里几乎不存在,所以很多在同步算法上验证过的手段,一到异步环境就失灵,其实背后就是这种统计偏移在作怪。希望这篇手写解读,能帮你少走一段弯路。