21. Generalized Advantage Estimation (GAE)
Problem
Actor-critic methods need an advantage estimate to tell the policy gradient “how much better than average was this action,” but the two obvious choices both have a flaw: the one-step TD error has low variance but high bias, while the full Monte Carlo return has high variance but low bias. GAE interpolates between these two extremes with a single parameter, lambda, via an exponentially-weighted sum of k-step TD errors. It's the advantage estimator underlying most modern actor-critic training, including the advantage-based updates in DeepMind's own A3C/IMPALA line of actor-critic architectures.
Given per-timestep rewards, a value function's estimates (including one extra bootstrap value for the state just after the last reward), and done flags marking episode boundaries, compute the GAE advantage at every timestep via the backward recursion:delta_t = r_t + gamma * V(s_{t+1}) * (1 - done_t) - V(s_t)A_t = delta_t + gamma * lambda * (1 - done_t) * A_{t+1}
with A_T (past the last real timestep) implicitly zero.
Source: src/21_gae_advantage.py
def compute_gae(
rewards: torch.Tensor, values: torch.Tensor, dones: torch.Tensor,
gamma: float, lam: float,
) -> torch.Tensor: ...
>>> rewards = torch.tensor([[1.0, 1.0, 1.0]])
>>> values = torch.tensor([[0.5, 0.5, 0.5, 0.0]])
>>> dones = torch.tensor([[0.0, 0.0, 1.0]])
>>> compute_gae(rewards, values, dones, gamma=0.99, lam=0.95).shape
torch.Size([1, 3])
Step-by-Step Approach
- Note the shape mismatch by design:
valueshas one more timestep thanrewardsanddones, holding the bootstrap value for the state right after the final reward. - Initialize a running
gaeaccumulator to zero (batch,) — this representsA_{t+1}as the loop walks backward, and there is nothing past the last timestep to bootstrap from. - Loop over timesteps in reverse. At each step compute the one-step TD error
delta_t, usingvalues[:, t+1]as the bootstrap and masking it by(1 - dones[:, t]). - Fold
delta_tinto the running GAE accumulator:gae = delta_t + gamma * lam * (1 - dones[:, t]) * gae— the same done-mask also has to zero the recursive term, not just the bootstrap value. - Store
gaeinto the output advantages tensor at positiontbefore moving tot - 1. - Sanity-check the
lam = 0edge case: the recursive term vanishes entirely and each advantage should equal exactly its own one-step TD error, nothing from later timesteps.
GAE is really just one accumulator variable threaded backward through time, reused for two purposes at once: it both stores “the advantage so far” and carries “the discounted future correction” into the next (earlier) step's calculation — get the mask on the recursive term wrong and the algorithm silently bootstraps advantage across an episode boundary that shouldn't exist.
Reference solution
import torch
def compute_gae(
rewards: torch.Tensor, values: torch.Tensor, dones: torch.Tensor,
gamma: float, lam: float,
) -> torch.Tensor:
batch, T = rewards.shape
advantages = torch.zeros_like(rewards)
# gae carries A_{t+1} backward into the computation of A_t -- it starts
# at 0 because there is nothing past the last real timestep to bootstrap
gae = torch.zeros(batch)
for t in reversed(range(T)):
# (1 - done_t) zeroes out both the value bootstrap and the recursive
# term whenever the episode ended at t: no state t+1 exists to look at
not_done = 1.0 - dones[:, t]
delta = rewards[:, t] + gamma * values[:, t + 1] * not_done - values[:, t]
gae = delta + gamma * lam * not_done * gae
advantages[:, t] = gae
return advantages
Key Functions & Tricks
torch.zeros_like(rewards)— allocates the output advantages tensor with the same shape and dtype as the rewards, before filling it in via the backward loopreversed(range(T))— walks timesteps back-to-front so each step's advantage can reuse the already-computedA_{t+1}values[:, t + 1]— the one-step-ahead bootstrap value, whyvaluesis deliberately one timestep longer thanrewards(1.0 - dones[:, t])— applied twice per step — once to the value bootstrap, once to the recursive GAE term — since a terminal transition invalidates both
How to Recognize This Pattern
Recognize this pattern whenever a problem asks for an advantage or return estimate computed backward through a trajectory with a value function and a mixing parameter interpolating between one-step and full-horizon estimates — that backward accumulator threading a decay through time is the GAE signature. A common variation asks for it expressed via the equivalent forward-looking exponentially weighted sum of k-step returns, which is mathematically identical but easy to mis-implement without the backward recursion's built-in reuse of prior work. The most common pitfall is forgetting that the done mask must zero out both the bootstrap value and the recursive term at an episode boundary, not just one of the two.