@d2l.add_to_class(d2l.Batch)
def td_target(self, bootstrap, gamma):
"""r_t + gamma (1 - terminated) V(s'), by a numpy bootstrap."""
return self.rew + gamma * (1 - self.term) * bootstrap(self.next_obs)Dive into Deep Learning · §15.1
Actor-critic and the credit-assignment dial
bootstrapped advantage estimates · coupled actor and critic updates · generalized advantage estimation · measured bias and variance
The Monte Carlo weight waits for the episode to end. But \hat G_t = r_t + \gamma \hat G_{t+1}, and \hat V(s_{t+1}) is trained to predict exactly what \hat G_{t+1} samples. Substitute:
\delta_t = r_t + \gamma\, \hat V(s_{t+1}) - \hat V(s_t)
:eqref:eq_td_error’s scalar, with the max replaced by the policy’s own continuation.
Because a NumPy array carries no gradient graph, the target is treated as data by construction. No explicit detach operation is needed.
If the critic were exact, \hat V = V^\pi:
E[\delta_t \mid s_t, a_t] = Q^\pi(s_t, a_t) - V^\pi(s_t).
A single transition therefore provides an advantage estimate, whereas the Monte Carlo estimate requires the rest of the trajectory.
During training, however, \hat V generally differs from V^\pi, so this estimate is biased. Actor–critic methods accept this bias in exchange for lower variance.
w \leftarrow w + \alpha_w\, \delta_t\, \nabla_w \hat V_w(s_t), \qquad \theta \leftarrow \theta + \alpha_\theta\, \delta_t\, \nabla_\theta \log \pi_\theta(a_t \mid s_t)
The following implementation collects a batch of episodes and takes one actor step per batch. It is a single-environment teaching analogue of A2C :cite:Mnih.Badia.Mirza.ea.2016. Relative to the loop in :numref:sec_deeprl, only the final update changes:
def train_ac(seed, ac, num_updates=num_updates):
"""The same loop with the sampled tail replaced by the bootstrap."""
rng, env = np.random.default_rng(seed), gym.make('CartPole-v1')
env.reset(seed=seed)
for _ in range(num_updates):
batch = d2l.rollout(env, ac.act, batch_episodes, rng)
for _ in range(critic_steps): # fresh target, one pass, repeat
fit_value(ac, batch.obs, batch.td_target(ac.value_np, gamma))
delta = batch.td_target(ac.value_np, gamma) - ac.value_np(batch.obs)
gnorm = policy_step_clip(ac, batch, d2l.normalize(delta))
yield float(batch.episode_returns().mean()), gnormThe critic uses td_target instead of G, takes critic_steps passes before the actor update, and supplies the normalized TD error \delta_t as the actor’s weight.
Actor–critic learns more slowly at first while its critic is inaccurate. Later, both methods reach the 500-step ceiling, but only actor–critic remains there for long consecutive intervals in these runs.
REINFORCE + baseline: longest stretch of perfect batches, per seed: [6, 7, 4]
actor-critic: longest stretch of perfect batches, per seed: [36, 26, 11]
A perfect batch contains eight episodes of length 500. Only actor–critic produces long sequences of such batches in these runs. This observation is consistent with the greater sampling variance of Monte Carlo returns, though three seeds do not establish a general stability result.
REINFORCE + baseline: median pre-clip gradient norm 0.04, clip binds on 0% of updates
actor-critic: median pre-clip gradient norm 0.40, clip binds on 35% of updates
Although both weights are normalized to unit variance, the actor–critic gradient norms are an order of magnitude larger in this experiment. A state-dependent component of the weight contributes variance but cancels in expectation, as shown in :numref:sec_baselines. The result is therefore consistent with the TD error concentrating more of its variation in action-dependent signal. :numref:sec_ppo develops a more accurate advantage estimator.
Rerun the loop, measuring both candidate weights per batch:
Fresh on-policy data keeps the critic’s training distribution close to the current policy’s state distribution, reducing one source of mismatch. This is not a convergence guarantee: nonlinear TD can diverge even on-policy :cite:Tsitsiklis.VanRoy.1997. Our critic recomputes its target on every fitted-TD pass. In the off-policy setting of :numref:sec_dqn, a frozen target network is used instead.
\hat G^{(n)}_t = r_t + \cdots + \gamma^{n-1} r_{t+n-1} + \gamma^n \hat V(s_{t+n}), \qquad \hat A^{\textrm{GAE}}_t = (1-\lambda) \sum_{n \ge 1} \lambda^{n-1} \big(\hat G^{(n)}_t - \hat V(s_t)\big)
Telescoping identity :cite:Schulman.Moritz.Levine.ea.2016:
\hat A^{\textrm{GAE}}_t = \sum_{l \ge 0} (\gamma\lambda)^l\, \delta_{t+l}
Expanding each n-step return into TD errors and exchanging the sums leaves the geometric coefficient \lambda^l. TD(\lambda) computes the analogous quantity online with eligibility traces, an approach that remains useful in streaming settings :cite:Elsayed.Vasan.Mahmood.2024.
The same backward scan used for reward-to-go in :numref:sec_baselines can be applied to TD errors.
lambda = 0 is the TD error; lambda = 1 is the Monte Carlo advantage
The tests verify both endpoint identities.
lambda = 0.0: best 20-update window, median 222.5, seeds [138. 198. 223. 283. 328.]
lambda = 0.5: best 20-update window, median 203.3, seeds [175. 179. 203. 218. 321.]
lambda = 0.9: best 20-update window, median 419.5, seeds [387. 400. 420. 454. 486.]
lambda = 0.95: best 20-update window, median 406.9, seeds [327. 367. 407. 444. 482.]
lambda = 1.0: best 20-update window, median 449.7, seeds [293. 385. 450. 464. 479.]
With five seeds per value of \lambda and 50 updates on small batches, low values of \lambda learn substantially more slowly. Values from 0.9 to 1 perform similarly. Because the critic is initially inaccurate, this short experiment favors estimators that rely more heavily on Monte Carlo returns.
lambda = 0.0: relative bias 1.05, relative variance 0.1, one-draw error 1.2
lambda = 0.5: relative bias 1.06, relative variance 0.1, one-draw error 1.2
lambda = 0.9: relative bias 0.95, relative variance 0.2, one-draw error 1.1
lambda = 0.95: relative bias 0.83, relative variance 0.3, one-draw error 0.9
lambda = 1.0: relative bias 0.00, relative variance 1.6, one-draw error 1.6
The mean-squared error is shallow and U-shaped. As \lambda decreases from one, variance initially falls faster than squared bias grows; for still smaller values, bias dominates. The minimum therefore occurs between the two endpoints. PPO implementations commonly use \lambda between 0.9 and 0.97 (:numref:sec_ppo).
sec_ppo reuses each batch while controlling the policy update, and :numref:sec_dqn addresses off-policy learning with replayed data.