多步回报、资格迹与 GAE¶
一步 TD 每看到一个 transition 就 bootstrap,Monte Carlo 则等待完整回报。多步方法在两者之间连续插值:向前多看几步可减少对当前 value estimate 的依赖,却也会引入更多采样噪声。理解这条轴,才能分清 n-step return、TD(\(\lambda\))、eligibility trace 与 GAE 各自在做什么。
本文沿用 transition \((s_t,a_t,r_t,s_{t+1})\),重点保留 n-step、\(\lambda\)-return 与 eligibility trace 的历史连接。GAE 的完整推导、双 mask、actor/critic target 与语言模型时间轴已独立到 Advantage 估计与 GAE。先读价值函数与 Bellman 递推和 Monte Carlo、TD 与控制,会更容易看清 bootstrap target 从何而来。
从一步到 n 步¶
忽略中途边界时,\(n\)-step return 为
\(n=1\) 是 TD target;若 \(n\) 一直延伸到真正 terminal,最后一项消失,就得到 Monte Carlo return。增加 \(n\) 通常降低 bootstrap bias、提高采样 variance,但这不是绝对排序:value 是否准确、reward 噪声、horizon 与 mixing speed 都会改变权衡。
终止语义必须进入定义。令
terminated表示任务动力学真正结束,target 不再 bootstrap;truncated表示采样窗口或时间上限结束,通常仍从该 transition 的真实 final observation bootstrap;- 二者都是轨迹边界,因此 trace 不能串进下一条 episode;
- 基础设施故障不是普通 truncation,应排除样本或单独建模。
若第 \(j\) 步遇到 truncation,应在该步使用 \(V(s_{t+j+1})\) 后停止展开,而不是读取 batch 中物理相邻的下一条轨迹。
下面直接消费每个 transition 自己的 next_value。真正 terminal 丢弃它;truncation 或达到 \(n\) 步时用它 bootstrap,因此实现不需要读取可能属于下一条轨迹的 value[t + 1]。
import torch
@torch.no_grad()
def n_step_return(reward, next_value, terminated, boundary, start, steps, gamma=.99):
if not all(x.shape == reward.shape for x in (next_value, terminated, boundary)):
raise ValueError("transition tensors must align")
if not 0 <= start < reward.numel() or steps <= 0:
raise ValueError("invalid start or horizon")
total = reward.new_zeros(())
discount = 1.
for offset, t in enumerate(range(start, min(start + steps, reward.numel()))):
total += discount * reward[t]
discount *= gamma
if terminated[t]:
return total
if boundary[t] or offset + 1 == steps or t + 1 == reward.numel():
return total + discount * next_value[t]
raise RuntimeError("unreachable horizon")
r, nv = torch.tensor([1., 2., 10.]), torch.tensor([3., 4., float("nan")])
terminated = torch.tensor([False, False, True])
boundary = torch.tensor([False, True, True])
torch.testing.assert_close(n_step_return(r, nv, terminated, boundary, 0, 1, .5), torch.tensor(2.5))
torch.testing.assert_close(n_step_return(r, nv, terminated, boundary, 0, 3, .5), torch.tensor(3.))
assert n_step_return(r, nv, terminated, boundary, 2, 2, .5) == 10
这段函数返回单个 state 的 target,不负责 batch padding、value snapshot 或 loss reduction。完整 n-step 与 trace 对照见强化学习手撕实现;生产数据若没有真实 truncation final observation,应标为缺失,而不是把相邻样本 value 当作替代。
Forward view:\(\lambda\)-return¶
设从 \(t\) 到边界还有 \(H\) 个 transition。有限轨迹的 forward \(\lambda\)-return 为
最后一个 return 承接剩余权重,所以权重和恰为 \(1\)。两个端点很重要:
这里的 \(H\) 由 terminal 或 truncation 决定。对 truncation,\(G_t^{(H)}\) 仍包含边界 final observation 的 bootstrap;对 terminal 则不包含。
定义一步 TD residual
在一条固定 value snapshot 上,forward view 可等价写成
展开就是直到边界为止的加权 TD residual。这个等式也是 batch GAE 的来源。
Backward view:eligibility trace¶
forward view 看“未来哪些 return 应更新当前状态”;backward view 反过来维护“当前 TD error 应归因给哪些过去特征”。对可微 value function,经典 accumulating trace 为
在 episode 边界后必须令 \(e=0\)。Replacing trace 会把重复激活特征截到固定水平,主要用于稀疏二值特征;它不是 accumulating trace 的普遍等价替换。
资格迹是采样时随参数更新的状态,而不是事后倒序计算一组固定 target。对线性 \(V_w(s)=w^\top x(s)\),下面一步同时更新 TD residual、accumulating trace 与权重;terminated 只关闭 bootstrap,boundary 在本次更新后重置 trace。
import torch
@torch.no_grad()
def td_lambda_step(weight, trace, feature, reward, next_feature,
terminated, boundary, gamma=.99, lam=.95, alpha=.1):
if not (weight.shape == trace.shape == feature.shape == next_feature.shape):
raise ValueError("linear value tensors must align")
value = torch.dot(weight, feature)
next_value = value.new_zeros(()) if terminated else torch.dot(weight, next_feature)
delta = reward + gamma * next_value - value
trace = gamma * lam * trace + feature
weight = weight + alpha * delta * trace
if boundary:
trace.zero_()
return weight, trace, delta
w, e = torch.zeros(2), torch.zeros(2)
w, e, d0 = td_lambda_step(w, e, torch.tensor([1., 0.]), 0.,
torch.tensor([0., 1.]), False, False, .9, 1., .1)
w, e, d1 = td_lambda_step(w, e, torch.tensor([0., 1.]), 1.,
torch.tensor([float("nan"), float("nan")]), True, True, .9, 1., .1)
torch.testing.assert_close(w, torch.tensor([.09, .1]))
torch.testing.assert_close(e, torch.zeros(2))
assert d0 == 0 and d1 == 1
最终第一维得到更新,正是第二步 TD error 沿 trace 回传到早期特征;边界后 trace 已清零。非线性 critic 需要用 autograd 得到 \(\nabla_wV_w(s)\),true-online、replacing 与 off-policy trace 还会改变递推,不能只复用这个 accumulating 版本。
等价边界¶
“forward view 与 backward view 等价”必须说明是哪一种等价:
- 固定整条轨迹中的参数、在末尾累计更新时,传统 TD(\(\lambda\)) 与离线 forward view 精确对应;
- 传统在线 TD(\(\lambda\)) 每步都会改变参数,后续 target 也随之变化,因此一般只在小步长极限下近似原始 forward view;
- true-online TD(\(\lambda\)) 定义了随时间截断的 online forward view,并以 Dutch trace 和额外修正项保持逐时刻精确等价;该结论首先针对线性函数逼近,不应无条件外推到任意神经网络。
线性 \(V_w(s)=w^\top x(s)\) 的常见 true-online 写法为
episode 开始时 \(e=0,V_{\mathrm{old}}=0\);每步先用更新前的参数缓存 \(V(s_{t+1})\),完成权重更新后再赋给 \(V_{\mathrm{old}}\)。这不是在普通 trace 上换一个名字。
下面的反向实现计算有限片段的 \(\lambda\)-return。bootstrap[t] 决定 \(G_t^{(1)}\) 是否包含下一状态价值,boundary[t] 决定更长 return 是否跨过该 transition;两者分开后,time-limit truncation 可以 bootstrap,却不会串入下一条 packed 轨迹。
import torch
@torch.no_grad()
def lambda_return(reward, next_value, bootstrap, boundary, gamma=.99, lam=.95):
if not all(x.shape == reward.shape for x in (next_value, bootstrap, boundary)):
raise ValueError("transition tensors must align")
if reward.ndim != 1 or reward.numel() == 0 or not boundary[-1]:
raise ValueError("a finite fragment must be non-empty, one-dimensional and boundary-closed")
if bootstrap.dtype != torch.bool or boundary.dtype != torch.bool:
raise ValueError("bootstrap and boundary must be boolean")
if torch.any(~bootstrap & ~boundary):
raise ValueError("a transition without bootstrap must close its trajectory")
out = torch.empty_like(reward)
carry = reward.new_zeros(())
for t in range(reward.numel() - 1, -1, -1):
one_step = reward[t] + gamma * next_value[t] if bootstrap[t] else reward[t]
carry = one_step if boundary[t] else one_step + gamma * lam * (carry - next_value[t])
out[t] = carry
return out
r = torch.tensor([1., 2., 10.])
nv = torch.tensor([3., 4., float("nan")])
boot = torch.tensor([True, True, False])
boundary = torch.tensor([False, True, True])
ret = lambda_return(r, nv, boot, boundary, gamma=.5, lam=.8)
torch.testing.assert_close(ret[0], torch.tensor(1 + .5 * ((1 - .8) * 3 + .8 * 4)))
assert ret[1] == 4 and ret[2] == 10 and not ret.requires_grad
for bad_bootstrap, bad_boundary in [
(torch.tensor([True, False, True]), torch.tensor([False, False, True])),
(torch.ones_like(boot), torch.tensor([False, True, False])),
(boot.long(), boundary.long()),
]:
try:
lambda_return(r, nv, bad_bootstrap, bad_boundary)
except ValueError:
continue
raise AssertionError("invalid return boundaries must be rejected")
输入只包含真实 transition,不含 padding;若使用二维 batch,应沿每条轨迹独立递推或先按 trajectory ID 分段。这里返回的是 value target,不是 policy advantage;后者还需减去同一冻结 snapshot 的当前 value。
与 GAE 的接口¶
GAE 把同一 \(\lambda\)-return 结构用于 policy-gradient advantage:
本页到这里为止只建立历史与代数接口:\(\lambda\)-return 如何连接 forward view 与 eligibility trace。GAE 的 advantage 语义、bootstrap/trace 双边界、actor/critic target,以及 token、turn、segment 三条时间轴见 Advantage 估计与 GAE;packed tensor 实现与断言见手撕 LLM 策略优化。
常见误区¶
- \(\lambda=1\) 永远等于无偏 Monte Carlo。只有展开到真正 terminal 才成立;在 truncation 上仍会依赖 bootstrap value。
- GAE 越长越准确。critic bias 会随 residual 传播,采样 variance 与非平稳性也会累积。
- 反向循环就是 eligibility trace。batch reverse GAE 只是在固定数据上算 target;在线 trace 还会在采样期间更新参数。
- 传统 TD(\(\lambda\)) 在线时也与 forward view 严格等价。一般只有离线版本精确;true-online 方法专门修复了这个边界。
done一个布尔量足够。合并 terminal 与 time-limit truncation 会分别丢失 bootstrap 和 trace-reset 语义。
Reference¶
- Sutton, Learning to Predict by the Methods of Temporal Differences
- Sutton and Barto, Reinforcement Learning: An Introduction, Second Edition
- van Seijen and Sutton, True Online TD(\(\lambda\))
- van Seijen et al., True Online Temporal-Difference Learning
- Schulman et al., High-Dimensional Continuous Control Using Generalized Advantage Estimation
- Farama Foundation, Handling Time Limits in Gymnasium