☰
Rollout Correction 数学拆解:从蒙特卡洛回收到 GAE 与重要性采样
2026/10/19 3:58:23 网站建设 项目流程

做强化学习或者在线决策优化的朋友,看到“Rollout Correction Math”这个标题,第一反应大概率是两条线:一条是蒙特卡洛树搜索里模拟 rollout 之后的回传修正,另一条是离策略评估里对行为策略产生的轨迹做偏差校正。这两条线表面上一个做搜索、一个做统计,但底层都在做同一件事——用已知的采样结果,去推算一个更干净、更接近真实目标的估计量。我以前刚开始接触这一块时,总觉得公式东一个西一个,今天补一个基线、明天加一个权重,完全不知道这些修正项到底从哪里冒出来的。后来把数学推导亲手推了几遍,又把代码从一个简陋的估计器迭代到能跑完整实验,才逐渐看清这套“校正数学”的全貌。

这篇文章不打算上来就丢公式。我尽量用从一个实际需求出发的方式,把这套方法拆开来:先说为什么需要校正,再把值函数校正、优势校正、离策略校正这几类核心推导一步步展开,然后给一套可以直接照着写的工具箱实现,最后把我踩过的坑和处理细节一并记录下来。适合谁看呢?适合刚接触策略评估、或者做过简单 rollout 但觉得结果方差大、估计偏的朋友;也适合想在 MCTS 或 MPC 里把“模拟回退”那一步算得更准的读者。

1. 先从核心问题说起:为什么需要 Rollout Correction

1.1 一句话理解:Rollout 是模拟,Correction 是纠偏

所谓 rollout,可以理解成“先把一条可能的路径走完,看看结果是多少”。在强化学习里,它等价于用一个策略从某个状态出发,与环境交互到终止,收集一串奖励,累加出回报;在蒙特卡洛树搜索里,它对应从叶节点开始用默认策略快速下完一盘棋;在模型预测控制里,它近似对应在预测模型上跑一段未来的控制序列,算出累积代价。

但问题来了:你模拟用的策略,往往不是你想评估的策略。MCTS 里的 rollout 策略通常是均匀随机或规则化的快速策略,而你真正想评估的是树上各个节点对应的搜索策略;RL 里你用行为策略去採数据,却想计算目标策略的期望回报;MPC 里你用一个近似模型预测未来,真实环境却总有偏差。这些不一致,就是“偏差的源头”。Rollout Correction 要做的,就是把“用玩世不恭的模拟策略采样得到的结果”修正成“从严肃的目标策略视角看应该得到的结果”。

所以,这套校正数学本质上回答一个问题:给定一批由行为分布产生的样本,如何无偏且低方差地估计目标分布下的期望?

1.2 最常见的三种校正场景

我把日常见到的 rollout correction 归纳成三大类,后面每一类都有一套对应的数学工具:

校正场景产生偏差的原因核心修正手段
值函数估计蒙特卡洛回报在具体状态上加总时,忽略了状态自身的基线差异引入状态值基线与优势函数拆分
策略优势估计单步回报噪声大,并且 rollout 轨迹长短不同导致方差失衡时序差分累积校正(GAE 思路)
离策略评估行为策略与目标策略不一致,采样分布发生偏移重要性采样比率与自归一化加权

这三类并不是互相孤立的。你会发现真正常用的算法——无论是 PPO 里的 GAE 优势估计,还是 AlphaZero 类的 MCTS 回传修正,或者是离策略评价器——往往是三类校正手段的嵌套组合。理解了每一层的“为什么要加这一项”,再看一整条公式链时就不会觉得它们是拼凑出来的,而是每一步都在修补上一步留下的偏差。

2. 核心数学拆解:校正算式中每一种“修正项”的来历

2.1 值函数估计中的基线校正与偏差分解

我们从一个最朴素的目标开始:估计状态的价值,定义是

[ V^{\pi}(s_t) = \mathbb{E}{\tau \sim \pi} \left[ \sum{k=0}^{T-t} \gamma^k r_{t+k} \right] ]

这个期望的定义没有问题,问题在于估计它时,我们通常只有有限数量的 rollout 轨迹。用样本平均去算的话,蒙特卡洛估计量是无偏的,因为每条轨迹的回报就是对期望的一次独立采样。但它的方差非常大,尤其是在决策步数长、奖励稀疏的环境里。一条轨迹运气好拿满分,运气差拿零分,平均值抖动得很厉害。

这时候就产生了第一个校正需求:能不能把估计量里的“运气成分”抠掉一部分,只保留与状态相关的价值信号?

办法是引入一个只与状态有关的基线 ( b(s_t) )。注意,( b(s_t) ) 的期望校正作用来源于恒等式:

[ \mathbb{E}{a \sim \pi} \big[ \text{某个信号} \times b(s_t) \big] = b(s_t) \cdot \mathbb{E}{a \sim \pi}[\text{那个信号}] ]

如果某个信号的期望为零,乘上基线后期望仍然为零,不会改变整体估计的无偏性。这就给了我们一个自由度:随便挑一个不依赖于动作的基线,插入到估计量里,不破坏期望,但能改变方差。一个经典结论是最优基线应该接近回报与函数值的加权平均,实践中通常直接取 ( b(s_t) = V^{\pi}(s_t) ) 或它的估计,因为状态价值本身就是对回报合理程度的先验判断。

分解一下就知道它为什么能降方差。回报 ( G_t ) 可以拆成 ( V(s_t) + \delta_t ),其中 ( \delta_t ) 是这条轨迹在这个状态上的“运气偏差”。如果你直接用 ( G_t ) 去估计,等于把 ( V(s_t) ) 的波动和 ( \delta_t ) 的波动一起算进去了;而如果你先把 ( V(s_t) ) 这个确定部分减掉,再把回报加回来,就变成

[ \hat{V}(s_t) = G_t - V(s_t) + \bar{V}(s_t) ]

——后面那一项是基线本身的新估计,前面的残差只保留轨迹特有的随机波动。整体方差显著变小,而期望不变。这就是最原始、最基础的 rollout correction:用基线去吸收已知的结构化方差,让样本残差专注于真正需要学习的信息。

2.2 优势校正:从蒙特卡洛到 GAE 的累加设计

单看值函数估计还不够,因为做策略提升时,真正有用的是“这个动作比平均水平好多少”。这个量就是优势函数:

[ A^{\pi}(s_t, a_t) = Q^{\pi}(s_t, a_t) - V^{\pi}(s_t) ]

( Q ) 是执行动作后的期望回报,( V ) 是该状态下策略的平均回报。如果 ( A > 0 ),说明这个动作比平均好,应该加大概率;反之减小。问题依然出在估计上。你可以用 rollout 轨迹直接估计 ( Q ),再减去状态价值,也就是蒙特卡洛优势估计。无偏,但方差一样大。

你也可以用单步时序差分残差:

[ \delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) ]

TD 残差的方差小,但它是有偏的,因为 ( V(s_{t+1}) ) 本身是估计值,不是真值。这里就体现出校正数学的另一个典型操作:在有偏低方差和无偏高方差之间做插值。

GAE(广义优势估计)就是这种插值的标准答案。它定义:

[ \hat{A}t^{\text{GAE}} = \sum{l=0}^{T-t-1} (\gamma \lambda)^l \delta_{t+l} ]

把它展开来看:

[ \begin{aligned} \hat{A}t &= \delta_t + \gamma\lambda \delta{t+1} + (\gamma\lambda)^2 \delta_{t+2} + \cdots \ &= (r_t + \gamma V(s_{t+1}) - V(s_t)) \ &\quad + \gamma\lambda (r_{t+1} + \gamma V(s_{t+2}) - V(s_{t+1})) \ &\quad + (\gamma\lambda)^2 (r_{t+2} + \gamma V(s_{t+3}) - V(s_{t+2})) + \cdots \end{aligned} ]

把相邻项里的 ( V(s_{t+1}) ) 抵消掉,你会发现中间层的状态价值一项项被消去,最后只剩下:

[ \hat{A}t^{\text{GAE}} = -V(s_t) + \sum{l=0}^{T-t} (\gamma\lambda)^l r_{t+l} + (\gamma\lambda)^{T-t} V(s_T) ]

这就是为什么 GAE 的 lambda 参数那么重要:( \lambda=0 ) 时退化为一步 TD,偏差大方差小;( \lambda=1 ) 时退化为蒙特卡洛回报,无偏但方差大;介于中间时,它不是在“猜测”,而是在对轨迹中每一步的 TD 残差做带指数衰减的累加,相当于用时间方向上的加权平均修补单步估计的偏差。实际操作里,( \lambda ) 取 0.95 到 0.99 是非常稳的经验区间。

2.3 离策略校正:重要性采样比率的物理含义与 WIS

第三类校正解决的问题最直接:行为策略 ( b ) 产生的数据,凭什么能用来评估目标策略 ( \pi )?答案写在重要性采样恒等式里。对任意函数 ( f(x) ),我们有

[ \mathbb{E}{x \sim b} \left[ \frac{\pi(x)}{b(x)} f(x) \right] = \mathbb{E}{x \sim \pi} \left[ f(x) \right] ]

注意,这个式子之所以成立,只是因为两个期望在同一个支撑集上,比率 ( \pi(x)/b(x) ) 把采样分布从 ( b ) 拉回到了 ( \pi )。它像是数据分布的一个“找零操作”:多采的要折价,少采的要溢价。

在 rollout 轨迹上,比率变成整条路径的乘积。如果轨迹长度为 ( T ),序列似然之比是:

[ \rho_{t:T} = \prod_{k=t}^{T} \frac{\pi(a_k \mid s_k)}{b(a_k \mid s_k)} ]

理论上,你可以用 ( \rho \times G_t ) 作为无偏估计量。但实际中这个估计量的方差大得吓人——长序列的概率值经常是小数点后很多位,乘来乘去之后,少量轨迹占掉了绝大部分权重。解决办法是自归一化重要性采样(WIS,Weighted Importance Sampling):

[ \hat{V}^{\pi}{\text{WIS}} = \frac{\sum{i=1}^{N} \rho_i G_i}{\sum_{i=1}^{N} \rho_i} ]

它不像普通 IS 那样严格无偏,但偏差随样本量增大而减小,而方差表现要稳得多。很多离策略评估框架里默认用的是 WIS 而不是 IS,就是因为长 rollout 下 WIS 的数值稳定性比理论无偏性重要得多。这也是校正数学里一个值得记住的原则:如果一个无偏量在有限样本下方差爆炸,它的“无偏性”几乎没有实践价值;自归一化引入的小偏差,是换取可接受方差的基本代价。

3. 实操:实现一个 Rollout Correction Math 工具箱

3.1 设计接口与关键数据结构

说了这么多推导,最终要落到代码。我建议把校正逻辑封装成一个独立模块,不要和具体环境逻辑搅在一起。模块的职责很清晰:接收一批 rollout 轨迹(状态、动作、奖励、行为策略概率、目标策略概率),输出校正后的价值估计和优势估计。

数据结构上,我一般用字典列表或者简单的 NamedTuple 存轨迹。最小字段是:

  • states:状态序列
  • actions:动作序列
  • rewards:奖励序列
  • behavior_probs:行为策略在采样动作上的概率
  • target_probs:目标策略在相同动作上的概率

这里有个非常容易踩的细节:behavior_probs必须是在采样的那一刻记录下来的概率,而不是事后从行为策略模型重新计算的概率。如果行为策略是带随机性的,事后重算通常会因为参数更新产生细微偏差,短期不明显,长期迭代下来校正结果会漂移。

3.2 核心函数:价值校正、优势校正与离策略加权

下面是一个精炼版本,我逐段解释关键点:

import numpy as np def compute_is_ratio(behavior_probs, target_probs): # 转成 numpy 数组,防止维度问题 behavior_probs = np.asarray(behavior_probs, dtype=np.float64) target_probs = np.asarray(target_probs, dtype=np.float64) # 保护性 clip,避免除以零 behavior_probs = np.clip(behavior_probs, 1e-8, 1.0) return np.prod(target_probs / behavior_probs) def estimate_value_mc(trajectories, gamma=0.99): """ 基础蒙特卡洛估计,返回未校正的回报列表。 仅用于对比,展示校正前后的差异。 """ returns = [] for traj in trajectories: G = 0.0 for r in reversed(traj["rewards"]): G = r + gamma * G returns.append(G) return np.mean(returns) def estimate_value_wis(trajectories, gamma=0.99): """ 离策略校正后的加权价值估计 (WIS)。 """ weighted_sum = 0.0 weight_sum = 0.0 for traj in trajectories: rho = compute_is_ratio(traj["behavior_probs"], traj["target_probs"]) G = 0.0 for r in reversed(traj["rewards"]): G = r + gamma * G weighted_sum += rho * G weight_sum += rho return weighted_sum / (weight_sum + 1e-8) def compute_td_residuals(rewards, values, gamma=0.99): """ 计算 TD 残差序列,values 是状态价值函数的估计。 """ T = len(rewards) deltas = np.zeros(T) for t in range(T): next_value = values[t + 1] if t + 1 < T else 0.0 deltas[t] = rewards[t] + gamma * next_value - values[t] return deltas def estimate_advantage_gae(trajectory, values, gamma=0.99, lam=0.95): """ GAE 优势校正,对单条轨迹计算。 values 长度应为 T+1,最后一个是终止状态价值(通常为0)。 """ rewards = trajectory["rewards"] T = len(rewards) deltas = compute_td_residuals(rewards, values[:-1], gamma) adv = np.zeros(T) running = 0.0 # 反向累积,等价于逐项展开公式 for t in reversed(range(T)): running = deltas[t] + gamma * lam * running adv[t] = running return adv

几个值得展开说明的地方。

第一,compute_is_ratio里我用了float64而不是默认的float,这在大轨迹连乘时差别很大。float32在乘积超过一定量级后精度损失很明显,而重要性权重乘积经常跨越好几个数量级。

第二,estimate_value_wis的分子分母都做了防御处理。分母加1e-8是为了防止全零权重;更稳妥的做法是直接用np.sum(weights),然后判断是否太小。

第三,GAE 函数里values传的是整条轨迹的状态价值序列,最后一个元素是终止状态的价值。这里很多人会忘记:如果终止状态的价值可以设为零,那没问题;但如果你的环境有终止奖励,终止状态的价值不应是零,需要让价值网络输出一个终止状态估计,否则 TD 残差最后一步会有系统性偏差。

3.3 用一个小例子走一遍完整计算

再好的代码,不跑个小例子总是不够直观。我来构造一个超级简化的两决策轨迹:状态 ( s_0 ) 选择动作 ( a_0 ),转移至 ( s_1 );在 ( s_1 ) 选择动作 ( a_1 ),结束。奖励分别是 +0.5 和 +1.0,折扣 ( \gamma = 0.9 )。

行为策略在 ( s_0 ) 选择 ( a_0 ) 的概率是 0.3,目标策略是 0.7;在 ( s_1 ) 选择 ( a_1 ) 的行为策略概率是 0.4,目标策略是 0.6。那么整条轨迹的重要性比率为:

[ \rho = \frac{0.7}{0.3} \times \frac{0.6}{0.4} = \frac{0.42}{0.12} = 3.5 ]

轨迹折扣回报为:

[ G = 0.5 + 0.9 \times 1.0 = 1.4 ]

如果无视离策略差异,直接拿行为策略采样的回报做平均,你得到的是 1.4,而这个值实际上是行为策略视角下的期望回报,对目标策略来说是有偏的。用普通 IS 校正后得到 ( 3.5 \times 1.4 = 4.9 ),但这个数值本身不代表单一轨迹的“真实回报”,它只是在样本平均数意义下做了重新加权。

用 WIS 再看一组样本。假设采了三条轨迹,这三条轨迹的 ( \rho ) 分别是 0.2、1.0、5.0,对应回报分别是 0.8、1.2、2.0。普通平均是 ( (0.8+1.2+2.0)/3 \approx 1.33 )。WIS 则是:

[ \frac{0.2 \times 0.8 + 1.0 \times 1.2 + 5.0 \times 2.0}{0.2 + 1.0 + 5.0} = \frac{0.16 + 1.2 + 10.0}{6.2} \approx 1.83 ]

可以看到,权重大的轨迹显著拉高了估计值。如果目标策略确实更倾向于那些高回报轨迹,这个结果是有道理的;如果权重大的轨迹只是偶然回报高,WIS 的方差优势就会体现为结果更稳定。这个例子虽然简单,但足以说明 Rollout Correction Math 的核心逻辑:原来的平均是“每条轨迹一票”,而校正后的平均是“按目标策略下发生的概率重新投票”。

4. 实战中踩过的坑:数值、时间相关性与实现细节

4.1 校正系数爆炸与方差控制

离策略校正最著名的坑,就是重要性比率的方差爆炸。轨迹一旦超过十几步,概率乘积很容易变成 1e-5 或 1e4 这种量级,直接让加权平均失控。我在模拟项目 X 里试过用 50 步轨迹的 IS 估计,结果单条轨迹权重能占到总权重的 99.99%,剩下的样本对估计量几乎没有任何贡献。

解决思路有几个层次。最粗暴的是 clip:把 ( \rho ) 限制在 ( [1-\epsilon, 1+\epsilon] ) 或 ( [0, c] ) 区间,这是 PPO 等策略梯度算法里的常规做法。但 clip 不适合离策略评估,因为它改变期望,只能用在训练中对校正量做软约束的场景。评估时更推荐用 WIS,或者拿 ( \sqrt{\rho} ) 这种平滑变换去控制极端权重。

我这里还试过一种更实用的处理:对单步比率做归一化,而不是对整条轨迹的乘积做归一化。也就是把 ( \rho_{t:T} ) 拆成每个时间步的局部权重,按时间步逐步加权。这样即使某一步的概率比极端,前面累积的权重也能把它稀释一部分。效果比直接乘积稳定不少,偏差也相对可控。

4.2 时间相关性对校正的影响

另一类不显眼但很致命的坑,是 rollout 轨迹之间的时间相关性。如果你用同一个行为策略连续采集轨迹,相邻轨迹之间往往存在状态分布的时序依赖——尤其是环境本身存在自相关时,比如流量模拟或库存控制场景。用这些轨迹去做价值估计,样本之间并不是严格独立的,校正公式里假设“独立同分布采样”的前提被破坏,标准差会被低估,置信区间也会偏窄。

一个实用的做法是:在轨迹进入校正计算之前,先做一次“稀疏化”——每隔几步采样一条轨迹,或者在状态空间上做分桶去重。另一个做法是在价值估计的方差计算里加入 lag 相关修正,用自相关函数估计有效样本量。这个在统计学里叫 effective sample size,但对工程团队来说,最省事的还是多采几条独立轨迹,然后肉眼观察不同批次估计值的波动区间,比单次计算的数字可靠得多。

4.3 校正公式里最容易写错的符号与维度

写代码时最容易出错的,反而是那些不起眼的维度错位。

第一个坑:behavior_probs记录错位置。我之前在某图像处理 Demo 里做辅助决策模块时,因为行为策略是一个带 dropout 的网络,每次前传的概率都有微小差异,导致存下来的概率和实际采样概率对不上。后来改成了在采样动作的那一刻直接返回概率,而不是从模型重新推断,问题立刻消失。

第二个坑:GAE 的values序列长度。TD 残差里要用到 ( V(s_{t+1}) ),所以价值网络输出的长度必须是 ( T+1 ),而 reward 长度是 ( T )。一旦把values[:-1]传成values[:T],最后一个残差的计算就会错位,而且这种错误往往不影响代码报错,只影响数值,非常隐蔽。

第三个坑:乘法顺序。重要性比率是target_probs / behavior_probs,方向反了的话,校正后的估计值会指向行为策略而不是目标策略。我见过至少两次有人把分子分母写反,然后实验分数一塌糊涂——这种错误用单元测试都难发现,因为结果仍然是一个“合法的数字”,只是含义完全反了。最好的防御方法是在模块里加一个冒烟测试:当target_probs == behavior_probs时,校正结果必须严格等于未校正结果;当target_probs偏移时,估计值必须有合理的变化趋势。

4.4 校正公式在工程化中的性能优化

如果只做离线分析,校正计算的开销无所谓;但如果要把这套逻辑嵌入在线决策系统,比如每一帧都要对多条 rollout 做校正,那性能就得认真考虑了。

我实测下来的经验是三个优化点。第一,轨迹的奖励累加和重要性比率计算都可以向量化,不要用 Python 循环逐条处理。批量操作在 NumPy 下能快十倍以上。第二,GAE 的反向累加本质上是一个线性扫描,写成running = deltas + gamma * lam * np.concatenate((running[1:], [0]))这种形式可以压掉循环开销。第三,如果目标策略在 rollout 过程中是固定的,target_probs可以预计算,不用每条轨迹重复查表。

另外一个容易被忽略的优化:重要性比率乘积在数值上经常下溢或上溢,与其用原始概率相乘,不如在 log 域先算 log 和,再指数化。代码里可以写成:

log_rho = np.sum(np.log(target_probs) - np.log(behavior_probs)) rho = np.exp(np.clip(log_rho, -20, 20))

这样既防止下溢,又把极端权重控制在一定范围。这个技巧我在实际项目中屡试不爽。

5. 结尾的实战体会:校正数学是一套方法论,不是一个固定公式

做多了之后,我发现“Rollout Correction Math”其实不是一个固定的公式,而是一整套思考方式。它教你在面对任何“用错误分布采样去估计正确分布期望”的问题时,可以分三步走:第一步,确认偏差的来源是基线缺失、估计有偏还是分布偏移;第二步,选择对应的校正手段——值基线、优势累积插值或者重要性采样加权;第三步,在实现层面防护好数值稳定性和数据结构的一致性。

我个人在实际操作中的一个体会是,校正公式的推导不难,真正难的是判断“哪里需要校正”。很多时候,环境回报的噪声和策略本身的随机性混在一起,你会忍不住想加各种修正项,结果越改方差越大。这时候不妨先跑一版最简单的无校正 baseline,再看每条轨迹的权重分布和回报分布的形状。如果权重集中在少数样本上,说明离策略校正的压力大,应该优先考虑 WIS 或者缩短轨迹长度;如果权重分布均匀但回报抖动大,那更适合用 GAE 这类优势校正。这套判断流程比我当年拿着理论公式硬套要靠谱得多。

最后再分享一个小技巧:设计校正模块时,永远保留一个“关闭校正”的开关。我最开始没有这么做,每次对比实验都得改写调用逻辑,浪费了很多时间。现在我把 WIS、GAE、基线修正都做成可配置项,默认全开,但调试时能随时单独关掉某一项,看它对最终指标的贡献。毕竟校正数学的价值是帮我们看清估计量在不同条件下的行为,而不是盲目相信加了一堆修正项就一定更好。说到底,它能让你在一个充满噪声的世界里,把信号挖得更干净一点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询