Temporal Difference Learning • Part 2 of 3
📝Draft

Eligibility Traces and TD(λ)

Averaging all n-step returns at once with the trace-decay parameter λ

n-step TD left us with a nagging inefficiency: we measured that n=4n = 4 beats n=1n = 1 and n=8n = 8 on the random walk—but we only learned that by running all of them. Every n-step return is a legitimate estimate of the same quantity. Why should one update use exactly one of them and throw the rest away?

It doesn’t have to. This section develops the two central ideas:

  1. The forward view: define a target that averages all n-step returns at once, weighted geometrically by a parameter λ\lambda. This is the λ-return.
  2. The backward view: compute that averaging online, one cheap update per step, using a fading memory called an eligibility trace. This is the TD(λ) algorithm.

The punchline—kept precise throughout, and consistent with how we introduced it back in TD(0) Prediction: λ\lambda is a trace-decay parameter, not a step count. Setting λ=0\lambda = 0 recovers one-step TD; setting λ=1\lambda = 1 recovers (every-visit) Monte Carlo.

The Forward View: The λ-Return

Instead of betting on one n, place a spread of bets. The λ-return mixes the 1-step return, the 2-step return, the 3-step return, and so on—each weighted a factor λ\lambda less than the previous. With λ=0.9\lambda = 0.9, the 1-step return gets weight 0.10, the 2-step gets 0.09, the 3-step 0.081, and the weights keep shrinking geometrically.

One dial, λ[0,1]\lambda \in [0, 1], now controls the effective depth of the target. Small λ concentrates weight on shallow returns (TD-like); λ near 1 spreads weight far into the future (MC-like). And unlike n, which lurches from 1 to 2 to 3, λ moves the mixture smoothly.

Here are those weights, computed for λ=0.9\lambda = 0.9:

λ-return weights (1−λ)λⁿ⁻¹ for λ = 0.9
1-step
0.1000
2-step
0.0900
3-step
0.0810
4-step
0.0729
5-step
0.0656
6-step ⋯
weights keep decaying by ×0.9 …
Computed values: the first 5 weights sum to 0.4095; half of all weight falls on returns of depth 7 or less; depth beyond 20 still holds 0.1216 of the weight.
📖λ-Return

The λ-return GtλG_t^{\lambda} is the weighted average of all n-step returns from time tt, where the n-step return receives weight (1λ)λn1(1-\lambda)\lambda^{n-1}. The trace-decay parameter λ[0,1]\lambda \in [0, 1] sets how quickly the weights fade with depth.

Mathematical Details

The λ-return:

Gtλ=(1λ)n=1λn1Gt:t+nG_t^{\lambda} = (1-\lambda) \sum_{n=1}^{\infty} \lambda^{n-1} G_{t:t+n}

The prefactor (1λ)(1-\lambda) exists to make the weights sum to one—it normalizes the geometric series:

(1λ)n=1λn1=(1λ)11λ=1(1-\lambda) \sum_{n=1}^{\infty} \lambda^{n-1} = (1-\lambda) \cdot \frac{1}{1-\lambda} = 1

So GtλG_t^{\lambda} is a proper weighted average of legitimate targets, hence itself a legitimate target.

In an episodic task, every n-step return with t+nTt + n \geq T equals the full return GtG_t. Collecting all that tail weight into one term gives the practical form:

Gtλ=(1λ)n=1Tt1λn1Gt:t+n  +  λTt1GtG_t^{\lambda} = (1-\lambda) \sum_{n=1}^{T-t-1} \lambda^{n-1} G_{t:t+n} \;+\; \lambda^{T-t-1} G_t

The boundary cases now follow by inspection:

  • λ = 0: only the n=1n = 1 term survives (using 00=10^0 = 1), so Gt0=Gt:t+1=Rt+1+γV(St+1)G_t^{0} = G_{t:t+1} = R_{t+1} + \gamma V(S_{t+1}) — the TD(0) target. That is why the algorithm is called TD(0).
  • λ = 1: every finite-n weight (1λ)λn1(1-\lambda)\lambda^{n-1} vanishes and the tail weight λTt1\lambda^{T-t-1} becomes 1, so Gt1=GtG_t^{1} = G_t — the full Monte Carlo return.

The forward-view algorithm (“offline λ-return algorithm”) updates every visited state toward its λ-return at episode end:

V(St)V(St)+α[GtλV(St)]V(S_t) \leftarrow V(S_t) + \alpha \left[ G_t^{\lambda} - V(S_t) \right]

ℹ️Note

Keep the two dials distinct: n is an integer that selects one backup depth; λ is a decay rate that mixes all depths. TD(0) still uses one real reward—the “0” refers to λ, not to how many rewards are collected. This is exactly the framing we flagged in TD(0) Prediction, now with the machinery to back it up.

There’s an obvious practical problem, though. The λ-return for time tt depends on n-step returns of every depth—which means it isn’t fully known until the episode ends. Written this way, the λ-return algorithm is Monte Carlo-like in the worst way: theoretically clean, but offline and acausal. We seem to have lost TD’s best feature.

The Backward View: Eligibility Traces

The fix is one of the most elegant ideas in RL: flip the direction of time.

The forward view stands at state StS_t looking ahead: “which future returns should update me?” The backward view stands at the current TD error looking behind: “which past states should I update?”

Picture every state carrying a glowing ember. When the agent visits a state, its ember flares up. Every step afterward, all embers fade by a factor γλ\gamma\lambda. Now, whenever a TD error δt\delta_t occurs—a moment of surprise—every state gets updated in proportion to how brightly its ember is still glowing.

The ember is the eligibility trace. It implements two credit-assignment heuristics at once:

  • Recency: states visited recently glow brighter, so they absorb more of the blame or credit for the current surprise.
  • Frequency: states visited repeatedly have had their embers bumped multiple times, so they’re extra eligible.

The magic (made precise below): fading embers by γλ\gamma\lambda per step distributes each TD error backward in exactly the pattern needed to reproduce λ-return learning.

📖Eligibility Trace (Accumulating)

The eligibility trace et(s)e_t(s) is a per-state memory that decays by γλ\gamma\lambda every step and increments by 1 whenever ss is visited: et(s)=γλet1(s)+1[St=s]e_t(s) = \gamma \lambda \, e_{t-1}(s) + \mathbf{1}[S_t = s], where 1[]\mathbf{1}[\cdot] is 1 if the condition holds and 0 otherwise. Traces start at zero at the beginning of each episode.

Mathematical Details

TD(λ) with accumulating traces. At each step, compute the ordinary one-step TD error:

δt=Rt+1+γV(St+1)V(St)\delta_t = R_{t+1} + \gamma V(S_{t+1}) - V(S_t)

then update all states at once, each in proportion to its trace:

V(s)V(s)+αδtet(s)for all sV(s) \leftarrow V(s) + \alpha \, \delta_t \, e_t(s) \quad \text{for all } s

Per step this costs one TD error plus one sweep over the trace vector—O(number of states), independent of episode length and of λ. No stored rewards, no n-step buffer, no waiting: TD(λ) is exactly as online as TD(0).

Algorithm: Tabular TD(λ), accumulating traces
─────────────────────────────────────────────
Input: policy π, step size α, discount γ, trace decay λ
Initialize V(s) arbitrarily

Loop for each episode:
    e(s) ← 0 for all s
    Initialize S
    Loop for each step:
        Take action from π, observe R, S'
        δ ← R + γV(S') − V(S)          (V(S') = 0 if S' terminal)
        e(s) ← γλ e(s) for all s       (all traces fade)
        e(S) ← e(S) + 1                (current state flares up)
        V(s) ← V(s) + α δ e(s) for all s
        S ← S'
    until S is terminal
</>Implementation

The full implementation, and the one used for the measurements below:

import numpy as np

# Same 19-state random walk as the n-step experiment:
# states 1..19, terminals 0 and 20, start at 10, rewards -1/+1 at the ends.
N, START, GAMMA = 19, 10, 1.0
TRUE_V = np.arange(-20, 22, 2) / 20.0
TRUE_V[0] = TRUE_V[20] = 0.0

def gen_episode(rng):
    states, rewards, s = [START], [], START
    while 0 < s < 20:
        s2 = s + (1 if rng.random() < 0.5 else -1)
        rewards.append(1.0 if s2 == 20 else (-1.0 if s2 == 0 else 0.0))
        states.append(s2)
        s = s2
    return states, rewards

def td_lambda_run(lam, alpha, episodes, rng):
    """Online TD(lambda), accumulating traces. RMS error after each episode."""
    V = np.zeros(21)
    errs = []
    for _ in range(episodes):
        states, rewards = gen_episode(rng)
        e = np.zeros(21)                      # traces reset each episode
        for t in range(len(rewards)):
            s, s2, r = states[t], states[t + 1], rewards[t]
            delta = r + GAMMA * V[s2] - V[s]  # one ordinary TD error
            e *= GAMMA * lam                  # all traces fade
            e[s] += 1.0                       # current state becomes eligible
            V += alpha * delta * e            # one error updates many states
        errs.append(np.sqrt(np.mean((V[1:20] - TRUE_V[1:20]) ** 2)))
    return errs

Note what the inner loop does not contain: no n-step returns, no lookahead, no stored episode. Four lines convert TD(0) into the entire TD-to-MC spectrum.

Measured: TD(λ) on the Random Walk

Same protocol as the n-step experiment—average RMS error over the 19 states and the first 10 episodes, averaged over 100 runs, sweeping α\alpha from 0.02 to 1.0 for each λ. The measured results:

λBest αMin avg RMS error
0.00.820.348
0.40.680.276
0.80.320.266
0.90.200.294
0.950.120.334
1.00.020.508

The same U-shape as the n sweep, for the same bias-variance reason—and the numbers line up beautifully with the n-step results:

  • λ = 0 exactly reproduces 1-step TD: best α of 0.82 and RMS 0.348, identical to the n=1n = 1 row of the n-step sweep. That’s not a coincidence; it’s the boundary case, confirmed numerically.
  • The winner, λ = 0.8 (RMS 0.266), matches the best n-step result (n=4n = 4, RMS 0.267). With λ=0.8\lambda = 0.8, over half the λ-return’s weight falls on depths 4 and shallower, with the rest spreading deeper—an averaged version of the same intermediate depth.
  • λ = 1 is the worst setting tested (RMS 0.508 at a tiny best α of 0.02), and it’s fragile: with accumulating traces it diverged outright for every α of 0.32 and above in this sweep—value estimates ran away to floating-point overflow—and even at α = 0.1 the average error exploded to about 167. Monte Carlo-like variance plus traces that can grow past 1 on revisits is an explosive combination. λ = 1 is a conceptual endpoint, not a practical setting.
💡Tip

The pattern generalizes: λ in the 0.8–0.95 range is a strong default across many tabular problems, with smaller best step sizes as λ grows (same variance logic as with growing n). And as with n, the exact winner depends on the problem and horizon—the robust fact is that intermediate λ beats both endpoints.

Why the Two Views Agree

We introduced traces with a metaphor. Here’s the actual claim, and a numerical verification of it.

Follow one visit to a state ss at time tt. Its trace starts at 1 and fades: γλ\gamma\lambda after one step, (γλ)2(\gamma\lambda)^2 after two, and so on. So over the rest of the episode, the backward view gives ss a total update proportional to δt+(γλ)δt+1+(γλ)2δt+2+\delta_t + (\gamma\lambda)\delta_{t+1} + (\gamma\lambda)^2\delta_{t+2} + \cdots — every future surprise, discounted by how much the trace has faded.

Meanwhile, the forward view wants to move V(s)V(s) by GtλV(St)G_t^{\lambda} - V(S_t). Expand that error and it telescopes into exactly the same discounted sum of TD errors. Each successive TD error is precisely the correction that upgrades an n-step return to an (n+1)-step return, and the geometric λ-weights of the forward view become the geometric fading of the trace. Same quantity, computed in opposite time directions.

Mathematical Details

The key identity: if VV is held fixed during the episode, the λ-return error decomposes as

GtλV(St)=k=tT1(γλ)ktδkG_t^{\lambda} - V(S_t) = \sum_{k=t}^{T-1} (\gamma\lambda)^{k-t} \, \delta_k

Sketch for the λ = 1 case (the general case just carries λ through): repeatedly apply GtV(St)=δt+γ(Gt+1V(St+1))G_t - V(S_t) = \delta_t + \gamma\left(G_{t+1} - V(S_{t+1})\right), which follows from adding and subtracting γV(St+1)\gamma V(S_{t+1}) inside Gt=Rt+1+γGt+1G_t = R_{t+1} + \gamma G_{t+1}. Unrolling to the end of the episode telescopes the MC error into kγktδk\sum_k \gamma^{k-t}\delta_k.

Summing these per-state errors over all visits and exchanging the order of summation shows: the total increments produced by the forward view over an episode equal the total increments produced by the backward view—provided updates are applied offline (accumulated during the episode, applied at the end, so that VV stays fixed while the deltas are computed).

</>Implementation

Don’t take the algebra’s word for it. This check computes both views’ summed increments on the same episodes with the same frozen VV, and compares:

def offline_forward_increments(states, rewards, V, lam, alpha):
    """Summed lambda-return updates for one episode (V frozen)."""
    T, inc = len(rewards), np.zeros(21)
    for t in range(T):
        G_lam = 0.0
        for n in range(1, T - t):                       # truncated n-step returns
            G = sum(GAMMA ** k * rewards[t + k] for k in range(n))
            G += GAMMA ** n * V[states[t + n]]
            G_lam += (1 - lam) * lam ** (n - 1) * G
        G_full = sum(GAMMA ** k * rewards[t + k] for k in range(T - t))
        G_lam += lam ** (T - t - 1) * G_full            # tail weight on full return
        inc[states[t]] += alpha * (G_lam - V[states[t]])
    return inc

def offline_backward_increments(states, rewards, V, lam, alpha):
    """Summed eligibility-trace updates for one episode (V frozen)."""
    inc, e = np.zeros(21), np.zeros(21)
    for t in range(len(rewards)):
        s, s2, r = states[t], states[t + 1], rewards[t]
        delta = r + GAMMA * V[s2] - V[s]
        e *= GAMMA * lam
        e[s] += 1.0
        inc += alpha * delta * e
    return inc

Running both on 50 random-walk episodes with randomized value functions, for each λ{0,0.5,0.9,1.0}\lambda \in \{0, 0.5, 0.9, 1.0\}, the measured maximum absolute difference between the two increment vectors is 5.3 × 10⁻¹⁵—zero, up to floating-point round-off. The equivalence is exact.

The Endpoints, Precisely

We can now state the family relationships exactly—these are the claims previewed in TD(0) Prediction and TD vs Monte Carlo, now proven and measured:

TD(0) = one-step TD
With λ = 0, traces vanish immediately after one step: e(s) is 1 on the visited state and 0 elsewhere. The update reduces to the familiar V(S) ← V(S) + αδ — plain one-step TD, still using one real reward.
TD(1) = every-visit MC
With λ = 1, offline TD(1)‘s summed increments equal every-visit Monte Carlo’s, exactly (measured max difference: 1.1 × 10⁻¹⁴). Every visit, because an accumulating trace bumps by 1 on each revisit — each visit earns its own full-return update.

Note the fine print on the MC side: it’s every-visit MC (each visit to a state gets an update), not first-visit—and the equivalence is for offline updating. Online TD(1) is a slightly different, and as we measured, fragile algorithm.

🔍Deep Dive

Trace variants. The accumulating trace is the classic, but not the only choice:

  • Replacing traces reset the trace to 1 on a visit (e(s)1e(s) \leftarrow 1) instead of adding 1. This caps traces at 1, tames the divergence we measured at high λ with large α, and historically often worked better on problems with frequent revisits.
  • Dutch traces (e(s)γλe(s)+1αγλe(s)e(s) \leftarrow \gamma\lambda e(s) + 1 - \alpha \gamma\lambda e(s) for the visited state) arise naturally in the derivation of true online TD(λ) and are the theoretically preferred modern choice.

For linear function approximation, the trace becomes a vector over parameters rather than states—eγλe+V^(St;w)\mathbf{e} \leftarrow \gamma\lambda\mathbf{e} + \nabla \hat{V}(S_t; \mathbf{w})—the same fading-memory construction addressed to whatever is doing the predicting. That generalization is where function approximation will pick up the thread.

Summary

The λ-return averages all n-step returns with geometric weights (1λ)λn1(1-\lambda)\lambda^{n-1}—the forward view. Eligibility traces compute the same learning backward in time: traces fade by γλ\gamma\lambda, flare on visits, and each one-step TD error updates all traced states at once—online, O(states) per step, no waiting. The two views produce identical total updates offline (verified to 10⁻¹⁵ here), and the endpoints are exact: λ = 0 is one-step TD, offline λ = 1 is every-visit Monte Carlo. Measured on the random walk, intermediate λ wins—λ = 0.8 hit RMS 0.266, matching the best n-step method, while λ = 1 was both worst and prone to divergence.

You’ve now seen the complete bridge between TD and Monte Carlo, built twice—once with n, once with λ. The final section zooms out: one map for every method so far, and the direct line from this chapter to Rainbow and GAE.