我刚入行异步 RL 的时候,最先被搞晕的不是 PPO 的 clip,也不是分布式框架怎么搭,而是这么一个问题:为什么同一个强化学习算法,单机跑得好好的,一放到异步架构里,loss 曲线就跟心电图一样,动不动就血崩。
后来我花了很长时间才搞明白,这里面相当一部分问题的根源,可以追溯到一行看起来不起眼的数学:策略梯度里的 score function。而很多人挂在嘴边的 baseline、centering、V-trace、Importance Sampling,其实都是在跟“异步导致的 mismatch”做搏斗。
这篇文章不打算搞高大上的理论证明,就靠大白话,从 score function 一路拆到异步 RL 里的 Trainer-Generator Mismatch,把我自己踩过的坑和想明白的逻辑都写下来。适合那些刚接触分布式强化学习、被各种术语绕晕的同学,也适合想重新理顺“为什么异步这么不稳定”的从业者。
1. 为什么偏偏要从 score function 讲起
1.1 score function 只是“概率对参数的敏感度”
先别管强化学习,只看一个最简单的分类问题。假设我们有一个策略网络,输入状态,输出动作的概率分布。网络参数是 θ,对某个动作 a,网络给出的概率是 π(a|s; θ)。
现在的问题是:如果我们把参数 θ 稍微拨动一点点,动作 a 的概率会变大还是变小、变得有多快?
这个“变得有多快”的度量,恰好就是对数概率对参数的导数:
∇θ log π(a|s; θ)
这个量在统计学里有个正式的名字,叫 score function。因为它本质上衡量的是“样本对参数的敏感程度”,也就是样本在多大程度上推动参数更新。
我用一个更接地气的类比:你把政策网络想象成一个销售员,动作是销售员选择的推销话术。score function 就是“话术 X 对销售员经验参数 θ 的敏感度”。经验 θ 变了一点点,这个话术被选中的概率是涨是跌,幅度多大,全看这个量。
不要被名字吓到,score function 不是什么新奇的神经网络模块,就只是“梯度”而已。它唯一的特殊之处在于:它作用在一个概率分布上,而不是一个普通的函数上。
1.2 策略梯度里为什么会出现 score function
强化学习中,我们要最大化的是期望回报:
J(θ) = Eτ ~ π(·|·;θ) [Σt γt rt]
对这个目标直接求梯度,会遇到一个经典的问题:期望里面包含了对动作的采样,而采样这个过程本身不可导。我们不能像监督学习那样,直接对 loss 求导然后反向传播。
REINFORCE 算法给出的答案是:把对“轨迹分布”的梯度,转化成对“轨迹概率的 log”的梯度,再乘上回报:
∇θ J(θ) = Eτ [ (Σt ∇θ log π(at|st; θ)) · R(τ) ]
这里那个 ∇θ log π(at|st; θ),就是这条轨迹上每个时刻的 score function。整个期望梯度的来源,就是 score function 与回报的乘积在不同轨迹上的均值。
策略梯度算法五花八门,A3C、PPO、IMPALA,本质上都还是在反复利用这条公式。甚至可以说,策略梯度家族的算法差异,主要就集中在两件事上:
- 怎么估计这里的回报 R(τ)(用蒙特卡洛、用 TD、用 GAE);
- 怎么处理这个期望(原样采样、重要性采样、裁剪)。
而 score function 始终是那个内核。理解了它的性质,你才能真正看懂后续所有的花活到底在解决什么问题。
2. Score Function Centering 到底在做什么
2.1 一个想当然的梯度,和它暗藏的偏差
真实的训练里,我们不可能算期望,只能用一批采样轨迹去近似:
g ≈ (1/N) Σi (Σt ∇θ log π(a_t|s_t; θ)) · R_i
然后拿着这个 g 去更新参数。
这里有个看起来没什么问题、实际上很要命的点:R_i 的数值可能很大。比如某个环境里,所有动作的回报都集中在 100 左右,另外一些环境里回报可能只有 0.1 左右。如果直接用 R_i 乘 score function,那么更新步长会被回报的绝对值严重干扰。回报整体大了 100 倍,梯度也整体大了 100 倍,但我们真正想关心的是“哪个动作比别的动作更好”,而不是“动作的绝对得分是多少”。
如果没有对 score function 乘的系数做任何中心化处理,策略更新的方向就会包含一个非常大的、无意义的偏移。你本来想让好动作的概率涨一点、差动作的概率跌一点,结果所有动作的概率都被同一个大常数推着朝一个方向跑。
2.2 baseline 为什么能“无损”进场
这时候 score function 的一个关键性质就派上用场了:对一个概率分布求参数梯度,所有可能动作的 score 之和等于 0。
Ea~π [∇θ log π(a|s; θ)] = ∇θ Σa π(a|s; θ) = ∇θ 1 = 0
这个性质翻译成人话就是:score function 在策略分布自身的期望下,均值是 0。正因为如此,如果我们从回报 R 里减去一个只依赖状态、不依赖动作的量 b(s),那么:
Ea~π [score · (R - b(s))] = Ea~π [score · R] - Ea~π [score · b(s)] = Ea~π [score · R] - b(s) · 0 = Ea~π [score · R]
也就是说,减去的 b(s) 那一项,对整个期望梯度的贡献是 0。从这个意义上,减去 baseline 是无偏的。策略梯度估计的期望没变,但方差通常会显著下降,因为常数项带来的扰动被消掉了。
这个操作在大多数文章里叫 baseline subtraction,但我个人更喜欢叫它 Score Function Centering。因为从数学本质上看,它就是在利用 score function 均值为 0 这个性质,把乘在 score function 前面的系数“居中”了。
2.3 “减均值”这个动作的直觉
很多刚接触这个概念的读者会问:那 baseline 是不是随便选?只要不依赖动作就行?
答案是:只要不依赖动作,无偏性就成立。但方差效果好不好,取决于 baseline 跟实际回报有多接近。最理想的 baseline 是状态的价值函数 V(s),因为这时候 R - V(s) 就是优势函数,正好消除了状态本身好坏带来的影响。这就是为什么几乎所有现代策略梯度算法里,你都能看到 critic 网络在不停地估值一个状态价值,然后拿它当 baseline。
关于减均值这件事,我打个比方。你在一家餐厅当评委,要给十道菜打分。如果直接看绝对分数,有的餐厅整体水平高所有菜都打 9 分,有的餐厅整体水平低所有菜都打 5 分,你很难判断“哪道菜相对更好”。但如果你先按餐厅把分数居中,把 9 分和 5 分都变成相对均值的偏差,就能看出哪道菜在它自己餐厅里是出挑的。Score Function Centering 干的就是这件事:帮你在动作分布内部比较相对好坏,而不是被绝对回报牵着鼻子走。
3. 异步 RL 中的 Trainer-Generator Mismatch 是什么
3.1 异步 RL 的基本分工
在异步架构里,整个系统通常被拆成两种角色:
- Trainer:也叫 Learner、训练器。它维护一份最新策略参数,负责计算梯度、更新参数。
- Generator:也叫 Actor、生成器。它从环境中采集经验,生成轨迹,把数据送给 Trainer。
单机同步训练里,这两个角色是合二为一的:你校准一个参数,马上用它采样,马上算梯度,马上更新,每一步用的都是同一个策略。异步训练的问题在于,这两个角色之间会产生时间差。Generator 采到的数据,是它在某个时刻从 Trainer 那里拿到的参数快照下生成的。等这批数据传到 Trainer 手里时,Trainer 的参数可能已经更新过很多次了。
于是产生了一个尴尬的局面:你想用这批数据来优化当前策略,但数据其实是从上一个策略分布里采样出来的。我管这种不一致叫 Trainer-Generator Mismatch,也就是标题里说的训练器-生成器不匹配。
3.2 mismatch 产生的三种典型路径
我在实际工程里见到的 mismatch,不只是延迟这一种。总结下来主要有三条路径:
第一,参数版本延迟。这是最经典的一种。Generator 在 t 时刻拷贝了参数 θ_t,然后在本地跑了几千步,等它把轨迹发出去,Trainer 已经更新到了 θ_{t+k}。差异越大,数据越旧,mismatch 越严重。很多异步框架会用“队列+采样器”的方式解耦,导致 Reporter 和 Trainer 之间的参数版本差完全失控。
第二,梯度过期。这种出现在类似 A3C 的架构里。每个 thread 本地维护一份参数,算好梯度以后提交给全局网络。你提交梯度的时候,参数已经不是你算梯度时用的那版了。虽然提交的是一个“梯度”,但它是在旧参数点上计算的梯度,放到新的参数点上可能根本不指向最优方向。相当于你在地图上的旧位置问路,却拿着这个答案在新位置做决策。
第三,经验重放的分布漂移。很多异步实现会用回放池存数据,从里面均匀采样训练。回放池里既有旧策略生成的数据,也有新策略生成的数据。如果回放池太大,训练器长期在混合分布上更新,实际优化目标就变成了一个不知道是谁的混合策略。
3.3 当 mismatch 存在时,score function 还“中间”吗
前面讲过一个关键结论:score function 在它自己的策略分布 π 下,期望为 0。center 这个概念,天然依赖一个前提——采样分布就是当前策略分布。
异步训练破坏的恰恰是这个前提。
假设当前训练器手里的策略是 π_current,但 Generator 实际采样用的策略是 π_old,那么:
Ea~π_old [∇θ log π_current(a|s; θ)] ≠ 0
这是因为求导是对 π_current 求的,但期望是在 π_old 上算的。两个分布不一致,均值为 0 的性质就失效了。
这个“不再为 0”的后果非常隐蔽。你在训练时,如果还在用 π_current 的价值函数当 baseline,你自认为自己在做 centering,但实际采集分布的期望根本不满足 centering 条件。于是 baseline 那一项不再自动消失,它会引入一个真正的偏差。你不仅没把方差降下来,反而把原本干净的策略梯度也污染了。
这让我想起一个很常见的情况:很多人只是把异步训练的 batch size 调大,发现训练反而更不稳定,然后一头雾水地去调学习率。其实真正的问题就是:数据分布已经偏离了当前策略,baseline 的“免费午餐”不再免费了。
4. 用 Score Function Centering 重新读 mismatch
4.1 核心等式:行为分布下的 score 期望不再为 0
把上一节小结成数学语言。理想情况下,策略梯度是:
g_π = Ea~π [score_π · A_π]
在异步情况下,我们实际估计的是:
g_actual = Ea~q [score_π · A_hat]
这里 q 是 Generator 实际使用的行为分布,也就是旧策略;A_hat 是用当前策略的 critic 估计出的优势。因为 Ea~q [score_π] 不等于 0,所以:
Ea~q [score_π · b(s)] = b(s) · Ea~q [score_π] ≠ 0
于是 baseline 就不干净了。每走一步更新,都会带上一份由“分布偏移”和“baseline 状态值”共同决定的额外漂移。这个漂移跟回报大小无关,纯粹是异步训练“送”给你的。
所以从 score function centering 的视角看,Trainer-Generator Mismatch 的本质,就是:center 的前提条件被破坏了。
4.2 偏差、方差和“谁在中心化”
我们复盘一下,有 mismatch 时,你到底是在更新一个什么样的目标。可以分拆成两部分看:
第一部分:来自回报的梯度信号。这部分希望把策略推向高回报动作,是我们要的那部分。
第二部分:来自 centering 失效的漂移。这部分由 baseline 乘以一个非零的 score 期望组成。它的方向基本是随机的,大小取决于状态价值函数和分布偏离程度。
有意思的是,第二部分的随机性才是异步训练里梯度噪声的大头。很多人喜欢把不稳定归因于“网络的随机种子”或“环境的随机性”,其实一片一片的分布式数据,本身就是一个极不均匀、不断漂移的采样分布,再加上 baseline 不再抵消,噪声自然很大。
那能不能通过“更高质量的 estimate”来解决?能缓解,但治标不治本。你不管用 GAE 还是更复杂的 advantage 估计,只要数据分布 q 不等于 π_current,条件期望这一步始终是歪的。
4.3 V-trace 与 IS:把 mismatch 补回来
既然问题出在分布不匹配,那最直接的思路就是把它校正回来。经典的校正是 Importance Sampling:
ρ(a|s) = π_current(a|s) / q(a|s)
在算每一个样本的贡献时,乘上这个权重。这样 Ea~q [ρ · score_π · A] 的期望又重新接近于 Ea~π [score_π · A]。
理论很漂亮,但实际做起来有两个问题。第一,ρ 的方差可能非常大,尤其两个策略开始分化以后,比值可以到几百几千,训练一步直接炸掉。第二,在长轨迹上连乘 ρ,方差会指数增长。
所以你会发现,真正的工程实现里用的都是“加了限制”的版本。比如 V-trace 的改进是:对每个时间步的重要性权重上下裁剪,把权重限制在一个安全范围内:
ρ_t = min( ρ_max, π_current(a_t|s_t) / q(a_t|s_t) )
这么做会引入轻微偏差,但能把方差压住,整体训练稳定很多。说白了就是:完全校正做不到,那就做一个便宜的、温和的、有界的校正。
从 score function centering 的角度看,这些校正手段本质上是在“恢复中心化的前提”。你没法让 Generator 重新回到过去去采样,但你可以通过权重调整,把非当前策略采样带来的中心化偏差压低。这也是为什么我建议所有做异步 RL 的同学,先彻底理解 score function 的 centering 原理,再去看 V-trace 这些公式,否则你只是记住了一个符号,并没有真正理解它在补什么洞。
5. 实操中的处理方式与踩坑记录
5.1 先把自己的分布式结构画清楚
我拿到一个异步训练任务,第一件事不是调参,而是画一张数据流图。就一张纸画清楚:谁产生数据、数据经过什么缓冲、训练器多久取一次数据、训练器更新多少次以后往生成器推一次新参数。
这张图画完,你基本能肉眼判断 mismatch 的严重程度了。我见过不少项目,生成器和训练器之间的参数同步策略是“每训练 N 步同步一次”,但这个 N 完全是拍脑袋定的。画完图你会发现,数据从生成到抵达训练器,中间还可能经过一个巨大的 FIFO 队列,队列里的数据可能是上千条旧策略轨迹。这时候你可能第一步要做的不是上算法校正,而是先把这个队列的容量降下来,或者改成按参数版本分桶。
5.2 我实测中真正生效的手段
以下几条是我在多个分布式强化学习项目里实测下来有效的方式,尤其是组合起来用之后,训练稳定性肉眼可见地上升。
第一,把 KL 散度当作健康指标。在训练日志里同时记录 π_current 和最近一批数据来源策略之间的 KL 散度。这个指标比 loss 曲线更直接。KL 突然升高,几乎必然伴随训练崩塌。我一般会设置一个阈值,超过它就把策略同步频率提高,或者停止训练做一次参数拉齐。你不需要一开始就知道阈值是多少,多跑几次看分布趋势,就能找到一个靠谱的警戒线。
第二,限制单份数据的使用寿命。如果一个 rollout 在回放池里待了太长时间,策略已经迭代了很多版,它对当前策略的参考价值接近为零,却又在贡献梯度更新。最粗暴有效的做法是给每条轨迹记录它的“策略版本号”,训练采样时只允许使用最近 K 个版本内生成的数据。这个操作看着笨,但比我见过的很多花哨校正都稳。它直接从源头上限制 mismatch 的上限。
第三,对重要性权重做裁剪,甚至温和降权。如果要上 IS 或 V-trace,务必裁剪权重。裁剪上限怎么选?我的经验是从小往大试。先设一个比较小的上限,看训练是否稳定,再慢慢放大,找到一个“既能保留部分校正能力、又不至于让单条样本主导梯度”的点。不需要追求无偏,稳定比无偏重要。无偏的期望在模拟器里很美好,但在真实的分布式训练里,一次极端权重就可能让整个模型飞到十万八千里外。
第四,熵正则不要当摆设。很多算法里熵正则只是为了探索,但在异步训练里,它还有一个隐藏作用:维持策略分布一定宽度,避免 π_current 和 q 迅速拉大。策略过早变尖,会让分布偏差爆炸式增长,重要性权重也会失控。把熵系数调大一点点,让策略保持“糊”一点,mismatch 的破坏力会下降很多。这不是什么教科书理论,是我反复试出来的实用技巧。
第五,梯度裁剪和更新频率解耦。异步环境下,梯度爆炸往往不是学习率太大,而是某一条过时数据产生了超大梯度。先做梯度范数裁剪,再做学习率调整,不然你可能把学习率调得很低,还是无法稳定收敛。
5.3 一个典型的调试案例
说一个我最近实际处理的例子。某个模拟环境,任务目标是通过控制机械臂末端到达目标位置,奖励比较密集。初始版本是一个简单的同步 A3C,训练曲线稳定。后来为了提速,把采样部分拆成多个 Generator 进程,通过 Redis 队列传输数据,参数每 200 步广播一次。
结果 loss 曲线开始出现周期性的尖峰,每跑一段时间就性能骤降,然后慢慢恢复,再骤降。一开始我怀疑是环境随机性太大,把 GAE 的 λ 调低,没用。后来怀疑是学习率,从 3e-4 一路降到 1e-4,尖峰还在。
最后我用日志记录了每一批数据对应的策略版本号,发现尖峰出现的时间点和参数广播的时间点高度重合。原因是这样:参数广播后,Generator 们拿到了新策略,但队列里还堆着大量按旧策略生成的轨迹。Trainer 同时吃新旧数据,梯度方向互相打架,于是出现尖峰。等到队列里的旧数据被清空,训练又恢复正常。
解决方案不复杂:把 Redis 队列改成按版本分区,训练采样时优先从当前版本附近的数据区取,超出版本阈值的直接丢弃。改动完成以后,尖峰完全消失,收敛速度反而比原来更快了。这个案例让我彻底明白了一个道理:异步 RL 里的不稳定,往往不是优化器的问题,而是数据分布的问题。
6. 常见问题速查
如果你时间有限,可以直接看这张表:
| 问题 | 一句话回答 | 我的建议 |
|---|---|---|
| 为什么减去 baseline 叫 centering? | 因为 score function 在自身策略下期望为 0,减去 baseline 就是让乘在 score 上的系数居中了 | 记住 E[score]=0 这个等式,它是一切的起点 |
| 异步训练里 mismatch 一定存在吗? | 只要有参数同步延迟和队列缓冲,就一定存在,只是严重程度不同 | 别问是否存在,问“大到什么程度了” |
| 既然有偏差,那我用更大的 batch 能解决吗? | 不能,batch 增大只会让偏差更稳定地发挥影响,不会消除偏差 | 优先减小数据陈旧度,再调 batch |
| V-trace 和普通 IS 的区别是什么? | IS 做无偏校正但方差大,V-trace 对权重裁剪,有偏但方差小 | 分布式场景先考虑 V-trace 这类有界方法 |
| 熵正则为什么能缓解 mismatch? | 熵大的策略分布更宽,行为策略和当前策略的偏差增长更慢,重要性权重不容易爆炸 | 当训练不稳时,不要只调学习率,试试加熵权重 |
| KL 散度监控应该看哪个值? | 看最近一批数据的行为策略与当前策略之间的平均 KL | 设置警戒线,触发后至少做一次参数拉齐 |
最后再分享一个我自己的习惯。我会在每天的异步训练跑起来之后,先不看总奖励,而是看梯度范数和 KL 散度两张图。只要这两张图中任何一个出现异常形态,比如 KL 阶梯式上升、梯度范数周期性尖峰,那大概率是 mismatch 在作怪。先把数据流的同步问题处理好,再回头调算法细节。很多同学把大量时间花在改网络结构和调学习率上,却没有意识到训练器手里的参数和生成器正在执行的策略已经隔了好几代。先把这句话记在脑子里,比什么技巧都管用。
我写过不少分布式强化学习的代码,也踩过不少坑,每次回头重新看 score function 和 centering 的关系,都会有新的理解。这个角度你不能指望从论文摘要里直接读到,但确实能帮你在面对一堆看不懂的 loss 曲线时,找到一个靠谱的排查方向。希望这篇文章能给你同样的启发。