Forward View of TD(lambda)
Instead of fixing one lookahead, mix every lookahead at once: mostly the one-step return, a little less of the two-step, fading geometrically out to the full episode. One knob slides from TD to Monte Carlo.
Why Does This Exist?
One-step TD and Monte Carlo sit at opposite ends of a real dilemma. Looking one transition ahead gives low-variance targets and updates after every step, but bootstraps on young, biased guesses. Waiting for the full episode removes that bias and replaces it with the compounded noise of the whole trajectory. Multi-step TD compromises with a fixed horizon , covered at n-step TD prediction, yet any single is arbitrary: short horizons win in some states, long ones in others.
The forward view refuses to choose. It defines the -return : a weighted average of all -step returns with geometrically fading weights , in . Short low-variance horizons get the most weight, long low-bias horizons get progressively less, and the weights always sum to one. The geometric shape is load-bearing, not decorative: its memoryless property is exactly what lets the online backward view with eligibility traces reproduce this target causally, step by step.
The one idea you need first.
An -step return adds up real rewards then bootstraps: , where in is the discount factor and is the current value guess. At this is the TD target; at the episode end it is the Monte Carlo return. Everything below mixes these objects.
Think of It Like This
A concert hall blending direct sound with echoes
A violin note reaches you twice: first straight from the strings, sharp and exact, then as early reflections off the near walls, then as a long fading tail from the far balconies. You never hear the dry pluck alone or the wash of echoes alone; your ear blends the crisp attack with the decaying tail into one warm percept.
Point for point: the direct sound is the 1-step return with weight , the early reflections are the 2-step and 3-step returns with weights and , the tail is the distant horizons and the Monte Carlo remainder, and the blend you perceive is . Where it stops: the hall's decay is fixed by architecture, while is a knob you turn deliberately. And real echoes arrive after the note, whereas the forward view's echoes are future rewards that have not happened yet at time , which is why this view is theory, not an online algorithm.
How It Actually Works
The lambda return
In continuing tasks the definition runs over every horizon:
The weights form a valid distribution because . In an episode ending at , every -step return past equals the full return , so the infinite tail collapses into one remainder weight :
The finite weights sum to and the tail supplies the rest, so the total is exactly . The value update aims at this composite target with step size in : .
The two endpoints fall out for free
At every weight except the first vanishes and , pure one-step TD. At the finite weights vanish and the tail weight is , so , pure Monte Carlo. Between them the target slides smoothly from low-variance bootstrapping to unbiased full rollouts, which is why reads as a bias-variance knob.
Worked example
Three transitions: with reward , with reward , with reward , then termination. Discount , blend , step size . Current guesses , , , terminal . Target at , where .
The three -step returns from :
The weights: , , tail , summing to . The blend: . Against the endpoints, TD alone aims at and Monte Carlo at ; the blend lands at between them. The update moves from to .
Code
The -step returns, the blended target, the full spectrum, and the offline update, using only the standard library.
from typing import Dict, List
def n_step(rewards: List[float], start: int, n: int, gamma: float, boot: float) -> float: """n-step return from index start: n real rewards, then bootstrap value.""" total = 0.0 discount = 1.0 for k in range(start, min(start + n, len(rewards))): total += discount * rewards[k] discount *= gamma if start + n < len(rewards): total += discount * boot return total
def lambda_return(rewards: List[float], start: int, gamma: float, lam: float, values: List[float]) -> float: """Episodic forward lambda return at index start.""" remaining = len(rewards) - start targets = [n_step(rewards, start, n, gamma, values[start + n]) for n in range(1, remaining + 1)] blended = sum((1.0 - lam) * lam ** (n - 1) * targets[n - 1] for n in range(1, remaining)) return blended + lam ** (remaining - 1) * targets[-1]
rewards = [2.0, 1.0, 4.0]values = [1.0, 3.0, 5.0, 0.0] # last entry is the terminal value V = 0.0gamma, lam, alpha = 0.9, 0.7, 0.1print(f"{n_step(rewards, 0, 1, gamma, values[1]):.4f}") # -> 4.7000print(f"{n_step(rewards, 0, 2, gamma, values[2]):.4f}") # -> 6.9500print(f"{n_step(rewards, 0, 3, gamma, 0.0):.4f}") # -> 6.1400print(f"{lambda_return(rewards, 0, gamma, lam, values):.4f}") # -> 5.8781for test in [0.0, 0.5, 0.7, 1.0]: print(f"{test:.1f} {lambda_return(rewards, 0, gamma, test, values):.4f}") # -> 0.0 4.7000, 0.5 5.6225, 0.7 5.8781, 1.0 6.1400print(f"{values[0] + alpha * (lambda_return(rewards, 0, gamma, lam, values) - values[0]):.4f}") # -> 1.4878Every figure matches the hand trace, including the midpoint of the spectrum at .
Watch Out For
Shipping the forward view as an online algorithm
The definition peeks at rewards that have not arrived yet: at step the target needs through . Code that computes it directly cannot update until the episode ends, grows unbounded buffers on continuing tasks, and reintroduces Monte Carlo latency into what was supposed to be online learning.
The symptom is an agent that learns in end-of-episode jerks or an implementation that never finalizes a target on non-terminating streams. The fix is the division of labor the theory intends: keep the forward view as the specification, and run the backward view with eligibility traces for the causal updates.
Parking lambda at 1 on long noisy episodes
With the target is the full Monte Carlo return, so every bit of trajectory noise lands in the update. On episodes hundreds of steps long with stochastic transitions, targets swing so widely that learning stalls despite being unbiased.
The symptom is high-variance updates and a value curve that thrashes while short-horizon runs on the same task settle. The fix is an intermediate around to : keep most of the low bias while the geometric tail damps the far noise. Reserve exactly for short deterministic episodes where the variance has nowhere to hide.
The Quick Version
- The -return blends every -step return with geometric weights plus a tail weight on the full return, always summing to one.
- At it is one-step TD and at it is Monte Carlo; between them it trades bootstrap bias against rollout variance.
- In the worked episode the weights , , blend , , into , moving from to .
- The forward view needs future rewards, so it cannot run online; it is the specification that eligibility traces implement causally.
- On long noisy episodes prefer an intermediate lambda near to over exactly .