Skip to content
AI360Xpert
Beta

Eligibility Traces

Eligibility traces act as a fading short-term memory that records which states were recently visited, allowing a single reward to immediately credit past decisions without waiting for the episode to end.

Eligibility traces decay exponentially across time steps to broadcast credit assignment backward to all active states simultaneously.
Eligibility traces decay exponentially across time steps to broadcast credit assignment backward to all active states simultaneously.

Why Does This Exist?

In multi-step decision problems, reinforcement learning faces the fundamental temporal credit assignment problem: when an agent receives a delayed reward after dozens of choices, which earlier actions actually caused the success?

Standard one-step temporal difference learning, TD(0)\text{TD}(0), updates solely the single immediately preceding state St−1S_{t-1}. If a crucial choice was made ten steps before reaching a rewarding terminal state, information requires ten distinct, full traversals through that trajectory to trickle back to the source. Conversely, pure Monte Carlo methods update the entire sequence of visited states at once, but they require waiting until the episode terminates, cannot function in non-terminating continuing environments, and exhibit notoriously high variance.

Eligibility traces exist to bridge this gap. They provide an incremental, online backward-view mechanism that unifies one-step bootstrapping and multi-step Monte Carlo learning. By maintaining a decaying vector of state eligibilities zt∈R∣S∣z_t \in \mathbb{R}^{|\mathcal{S}|}, a single one-step prediction error δt\delta_t is instantly broadcast backward across every state that contributed to the current situation—achieving multi-step credit assignment on every single time step with minimal computational cost.

Think of It Like This

Footprints in the Sand and the Ocean Tide

Imagine walking across a sandy beach hunting for a buried treasure chest nestled among coastal dunes.

Every time you place a foot on the sand, you press a deep, fresh imprint into the shoreline. As the minutes tick by, ocean breezes and rising tides gently wash across the beach: your older footprints gradually soften and erode, while your most recent steps remain sharp and deep. If you circle back and step in the exact same spot twice, the indentation becomes even deeper.

The instant you unearth the gold chest, an aerial drone does not merely drop a celebratory reward on your final resting spot. Instead, it looks down at the entire coastline and scatters gold dust along your trail:

  • The freshest, deepest footprints right next to the chest receive the largest share of gold dust.
  • Footprints from five minutes earlier receive a moderate sprinkle.
  • Faint indentations from the start of your journey receive a small dusting.
  • Unvisited stretches of sand receive nothing at all.

Where the analogy breaks down: Real ocean tides erode footprints non-linearly depending on wind gusts and shoreline moisture. In reinforcement learning, eligibility traces decay strictly exponentially by a deterministic factor (γλ)(\gamma \lambda) on each discrete clock tick. Furthermore, tabular eligibility traces store a single scalar memory bucket per discrete state label rather than distinct spatial coordinates across continuous physical terrain.

How It Actually Works

The Eligibility Trace Vector and Decay Dynamics

An eligibility trace is an additional short-term memory variable associated with each state (or feature weight). We denote the eligibility trace vector at time step tt by zt∈R∣S∣z_t \in \mathbb{R}^{|\mathcal{S}|}, initialized to zero at the beginning of an episode:

z0(s)=0∀s∈Sz_0(s) = 0 \quad \forall s \in \mathcal{S}

On every time step tt, two mathematical operations modify the trace vector:

  1. Decay: All existing state traces decay exponentially by the product of the environmental discount factor γ∈[0,1]\gamma \in [0, 1] and the trace-decay parameter λ∈[0,1]\lambda \in [0, 1]: decay factor=γλ\text{decay factor} = \gamma \lambda
  2. Visit Activation: The state visited at time tt, StS_t, receives an eligibility bump indicating its recency.

Practitioners commonly distinguish two classic formulation styles for tabular traces:

1. Accumulating Traces

In accumulating traces, every visit to state ss adds +1+1 directly to the trace without bound:

zt(s)={γλzt−1(s)+1if s=Stγλzt−1(s)if s≠Stz_t(s) = \begin{cases} \gamma \lambda z_{t-1}(s) + 1 & \text{if } s = S_t \\ \gamma \lambda z_{t-1}(s) & \text{if } s \neq S_t \end{cases}

When an agent loops through the same state repeatedly within a short window, the trace sums up geometrically:

zt(s)=∑k=0t(γλ)t−k1s=Skz_t(s) = \sum_{k=0}^t (\gamma \lambda)^{t-k} \mathbf{1}_{s = S_k}

2. Replacing Traces

In replacing traces (introduced by Singh and Sutton, 1996), visiting a state resets its eligibility to 1.01.0 rather than adding to whatever residual trace remained:

zt(s)={1if s=Stγλzt−1(s)if s≠Stz_t(s) = \begin{cases} 1 & \text{if } s = S_t \\ \gamma \lambda z_{t-1}(s) & \text{if } s \neq S_t \end{cases}

Replacing traces prevent cyclic loops and high-frequency state visitations from inflating credit disproportionately, delivering substantially more stable performance in tasks with dense revisiting.

Backward Broadcast of the TD Error

Once the transition from StS_t to St+1S_{t+1} yields immediate reward Rt+1R_{t+1}, the agent computes the standard one-step Temporal Difference error:

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

Instead of updating only the value of StS_t, this prediction error δt\delta_t is broadcast across all states simultaneously, weighted by their active eligibility zt(s)z_t(s) and learning rate α\alpha:

V(s)←V(s)+αδtzt(s)∀s∈SV(s) \leftarrow V(s) + \alpha \delta_t z_t(s) \quad \forall s \in \mathcal{S}

The parameter λ∈[0,1]\lambda \in [0, 1] governs the time horizon of credit assignment:

  • λ=0\lambda = 0 (TD(0)\text{TD}(0)): zt(s)=1s=Stz_t(s) = \mathbf{1}_{s=S_t}. The trace persists for only one step, reducing the algorithm strictly to standard one-step bootstrapping.
  • λ=1\lambda = 1 (TD(1)\text{TD}(1)): Traces decay only by discount factor γ\gamma. Over the course of an entire episode, the cumulative updates match those of full Monte Carlo returns, yet are computed incrementally on every transition.
  • 0<λ<10 < \lambda < 1 (TD(λ)\text{TD}(\lambda)): Smoothly blends short-term bootstrapping with long-horizon rollouts, achieving lower variance than Monte Carlo and lower asymptotic bias than one-step TD.

Worked numerical example

Consider an environment with three non-terminal states S={SA,SB,SC}\mathcal{S} = \{S_A, S_B, S_C\} and terminal state STS_T.

  • Initial values: V(SA)=0.0V(S_A) = 0.0, V(SB)=0.0V(S_B) = 0.0, V(SC)=0.0V(S_C) = 0.0.
  • Hyperparameters: learning rate α=0.5\alpha = 0.5, discount factor γ=0.9\gamma = 0.9, trace decay λ=0.8\lambda = 0.8.
  • Effective trace decay: γλ=0.9×0.8=0.72\gamma \lambda = 0.9 \times 0.8 = 0.72.

An agent executes the four-step sequence: SA→R1=0SB→R2=0SA→R3=0SC→R4=10STS_A \xrightarrow{R_1=0} S_B \xrightarrow{R_2=0} S_A \xrightarrow{R_3=0} S_C \xrightarrow{R_4=10} S_T.

Step 0: Visit SAS_A, transition to SBS_B with reward R1=0R_1 = 0

  • Traces updated:
    • Accumulating: z0(SA)=0.72(0)+1=1.0z_0(S_A) = 0.72(0) + 1 = 1.0, z0(SB)=0z_0(S_B) = 0, z0(SC)=0z_0(S_C) = 0.
    • Replacing: z0(SA)=1.0z_0(S_A) = 1.0, z0(SB)=0z_0(S_B) = 0, z0(SC)=0z_0(S_C) = 0.
  • TD error: δ0=0+0.9V(SB)−V(SA)=0+0−0=0.0\delta_0 = 0 + 0.9 V(S_B) - V(S_A) = 0 + 0 - 0 = 0.0.
  • Value update: ΔV(s)=0.5×0.0×z0(s)=0.0\Delta V(s) = 0.5 \times 0.0 \times z_0(s) = 0.0. Values remain all 0.00.0.

Step 1: In SBS_B, transition back to SAS_A with reward R2=0R_2 = 0

  • Traces updated:
    • Both methods decay previous traces by 0.720.72: z1(SA)=0.72×1.0=0.72z_1(S_A) = 0.72 \times 1.0 = 0.72.
    • State SBS_B visited: z1(SB)=1.0z_1(S_B) = 1.0, z1(SC)=0.0z_1(S_C) = 0.0.
  • TD error: δ1=0+0.9V(SA)−V(SB)=0.0\delta_1 = 0 + 0.9 V(S_A) - V(S_B) = 0.0.
  • Value update: All values remain 0.00.0.

Step 2: Revisit SAS_A, transition to SCS_C with reward R3=0R_3 = 0

  • Trace decay prior to visit bump:
    • SAS_A had 0.720.72, decaying to 0.72×0.72=0.51840.72 \times 0.72 = 0.5184.
    • SBS_B had 1.01.0, decaying to 0.72×1.0=0.720.72 \times 1.0 = 0.72.
  • Visit SAS_A:
    • Accumulating trace: z2(SA)=0.5184+1.0=1.5184z_2(S_A) = 0.5184 + 1.0 = 1.5184.
    • Replacing trace: z2(SA)=1.0z_2(S_A) = 1.0 (reset, suppressing the loop buildup).
  • TD error: δ2=0+0.9V(SC)−V(SA)=0.0\delta_2 = 0 + 0.9 V(S_C) - V(S_A) = 0.0. Values remain 0.00.0.

Step 3: In SCS_C, terminal transition with reward R4=+10.0R_4 = +10.0

  • Trace decay and bump for SCS_C:
    • Accumulating trace:
      • z3(SA)=0.72×1.5184=1.0932z_3(S_A) = 0.72 \times 1.5184 = 1.0932
      • z3(SB)=0.72×0.72=0.5184z_3(S_B) = 0.72 \times 0.72 = 0.5184
      • z3(SC)=0.72(0)+1.0=1.0z_3(S_C) = 0.72(0) + 1.0 = 1.0
    • Replacing trace:
      • z3(SA)=0.72×1.0=0.72z_3(S_A) = 0.72 \times 1.0 = 0.72
      • z3(SB)=0.72×0.72=0.5184z_3(S_B) = 0.72 \times 0.72 = 0.5184
      • z3(SC)=1.0z_3(S_C) = 1.0
  • TD error at terminal transition (V(ST)=0V(S_T) = 0): δ3=R4+γV(ST)−V(SC)=10.0+0.9(0)−0.0=+10.0\delta_3 = R_4 + \gamma V(S_T) - V(S_C) = 10.0 + 0.9(0) - 0.0 = +10.0
  • Broadcast update: ΔV(s)=αδ3z3(s)=0.5×10.0×z3(s)=5.0×z3(s)\Delta V(s) = \alpha \delta_3 z_3(s) = 0.5 \times 10.0 \times z_3(s) = 5.0 \times z_3(s).
StateAccumulating Trace z3z_3Updated VaccumV_{\text{accum}}Replacing Trace z3z_3Updated VreplV_{\text{repl}}
SCS_C1.00001.00000.0+5.0×1.0=5.0000.0 + 5.0 \times 1.0 = \mathbf{5.000}1.00001.00000.0+5.0×1.0=5.0000.0 + 5.0 \times 1.0 = \mathbf{5.000}
SBS_B0.51840.51840.0+5.0×0.5184=2.5920.0 + 5.0 \times 0.5184 = \mathbf{2.592}0.51840.51840.0+5.0×0.5184=2.5920.0 + 5.0 \times 0.5184 = \mathbf{2.592}
SAS_A1.09321.09320.0+5.0×1.0932=5.4660.0 + 5.0 \times 1.0932 = \mathbf{5.466}0.72000.72000.0+5.0×0.72=3.6000.0 + 5.0 \times 0.72 = \mathbf{3.600}

In a single episode, the reward received in SCS_C immediately backward-propagated to both SBS_B and SAS_A. Under accumulating traces, SAS_A earned extra credit because it was visited twice; under replacing traces, its credit was bounded cleanly.

Code

from typing import Dict, List, Literal, Tuple

def simulate_eligibility_traces(    trajectory: List[Tuple[str, float]],    gamma: float = 0.9,    lam: float = 0.8,    alpha: float = 0.5,    trace_mode: Literal["accumulating", "replacing"] = "accumulating",) -> Tuple[Dict[str, float], Dict[str, float]]:    """Simulate online backward-view TD(lambda) updates over an episode.
    Args:        trajectory: Sequence of (state, incoming_reward) steps terminating in          'terminal'.        gamma: Environmental discount factor in [0, 1].        lam: Trace-decay parameter lambda in [0, 1].        alpha: Step-size learning rate in (0, 1].        trace_mode: Either 'accumulating' or 'replacing'.
    Returns:        A tuple of (final_state_values, final_eligibility_traces).    """    non_terminal_states = sorted(        list({state for state, _ in trajectory if state != "terminal"}    ))    values: Dict[str, float] = {s: 0.0 for s in non_terminal_states}    traces: Dict[str, float] = {s: 0.0 for s in non_terminal_states}    decay = gamma * lam
    print(f"=== {trace_mode.upper()} TRACES (gamma*lambda = {decay:.2f}) ===")
    for t in range(len(trajectory) - 1):        state_t, _ = trajectory[t]        next_state, reward_t1 = trajectory[t + 1]
        # 1. Decay all active traces        for s in traces:            traces[s] *= decay
        # 2. Update eligibility for the current visited state        if trace_mode == "accumulating":            traces[state_t] += 1.0        elif trace_mode == "replacing":            traces[state_t] = 1.0        else:            raise ValueError(f"Unknown trace mode: {trace_mode}")
        # 3. Compute 1-step TD prediction error        next_v = 0.0 if next_state == "terminal" else values[next_state]        delta_t = reward_t1 + gamma * next_v - values[state_t]
        # 4. Broadcast error backward to all eligible states        for s in values:            values[s] += alpha * delta_t * traces[s]
        trace_repr = {s: f"{traces[s]:.4f}" for s in traces}        value_repr = {s: f"{values[s]:.3f}" for s in values}        print(            f"Step {t} ({state_t} -> {next_state}, R={reward_t1:.1f}) | "            f"delta={delta_t:+.1f} | Traces: {trace_repr} | Values: {value_repr}"        )
    return values, traces

if __name__ == "__main__":    # Trajectory: S_A -> S_B -> S_A -> S_C -> terminal with reward +10.0    episode: List[Tuple[str, float]] = [        ("S_A", 0.0),        ("S_B", 0.0),        ("S_A", 0.0),        ("S_C", 0.0),        ("terminal", 10.0),    ]
    vals_accum, _ = simulate_eligibility_traces(        episode, gamma=0.9, lam=0.8, alpha=0.5, trace_mode="accumulating"    )    print()    vals_repl, _ = simulate_eligibility_traces(        episode, gamma=0.9, lam=0.8, alpha=0.5, trace_mode="replacing"    )
    print("\n--- Final Value Comparison ---")    for s in sorted(vals_accum.keys()):        print(            f"State {s}: Accumulating = {vals_accum[s]:.3f} | "            f"Replacing = {vals_repl[s]:.3f}"        )
# Expected Output:# === ACCUMULATING TRACES (gamma*lambda = 0.72) ===# Step 0 (S_A -> S_B, R=0.0) | delta=+0.0 | Traces: {'S_A': '1.0000', 'S_B': '0.0000', 'S_C': '0.0000'} | Values: {'S_A': '0.000', 'S_B': '0.000', 'S_C': '0.000'}# Step 1 (S_B -> S_A, R=0.0) | delta=+0.0 | Traces: {'S_A': '0.7200', 'S_B': '1.0000', 'S_C': '0.0000'} | Values: {'S_A': '0.000', 'S_B': '0.000', 'S_C': '0.000'}# Step 2 (S_A -> S_C, R=0.0) | delta=+0.0 | Traces: {'S_A': '1.5184', 'S_B': '0.7200', 'S_C': '0.0000'} | Values: {'S_A': '0.000', 'S_B': '0.000', 'S_C': '0.000'}# Step 3 (S_C -> terminal, R=10.0) | delta=+10.0 | Traces: {'S_A': '1.0932', 'S_B': '0.5184', 'S_C': '1.0000'} | Values: {'S_A': '5.466', 'S_B': '2.592', 'S_C': '5.000'}## === REPLACING TRACES (gamma*lambda = 0.72) ===# Step 0 (S_A -> S_B, R=0.0) | delta=+0.0 | Traces: {'S_A': '1.0000', 'S_B': '0.0000', 'S_C': '0.0000'} | Values: {'S_A': '0.000', 'S_B': '0.000', 'S_C': '0.000'}# Step 1 (S_B -> S_A, R=0.0) | delta=+0.0 | Traces: {'S_A': '0.7200', 'S_B': '1.0000', 'S_C': '0.0000'} | Values: {'S_A': '0.000', 'S_B': '0.000', 'S_C': '0.000'}# Step 2 (S_A -> S_C, R=0.0) | delta=+0.0 | Traces: {'S_A': '1.0000', 'S_B': '0.7200', 'S_C': '0.0000'} | Values: {'S_A': '0.000', 'S_B': '0.000', 'S_C': '0.000'}# Step 3 (S_C -> terminal, R=10.0) | delta=+10.0 | Traces: {'S_A': '0.7200', 'S_B': '0.5184', 'S_C': '1.0000'} | Values: {'S_A': '3.600', 'S_B': '2.592', 'S_C': '5.000'}## --- Final Value Comparison ---# State S_A: Accumulating = 5.466 | Replacing = 3.600# State S_B: Accumulating = 2.592 | Replacing = 2.592# State S_C: Accumulating = 5.000 | Replacing = 5.000

Watch Out For

Runaway Trace Accumulation in Continuing Tasks with λ=1

In episodic problems with terminal resets, setting λ=1.0\lambda = 1.0 neatly mirrors Monte Carlo returns. However, in continuing tasks (environments without terminal states, like balance control or industrial robotics) or Markov Decision Processes with dense cyclical orbits, setting λ=1.0\lambda = 1.0 alongside an undiscounted or weakly discounted objective (γ≈1.0\gamma \approx 1.0) causes accumulating traces to explode.

Because accumulating traces append +1+1 on every revisit without decaying to zero, the sum diverges toward infinity: ∑k=0∞(γλ)k→∞\sum_{k=0}^\infty (\gamma \lambda)^k \to \infty When an unexpected non-zero TD error δt\delta_t eventually arrives, multiplying it by an inflated trace vector produces an enormous weight jump (ΔV=αδtzt\Delta V = \alpha \delta_t z_t), instantly blowing up value estimates, destabilizing the policy, and causing numerical NaN gradient crashes.

Concrete Fixes:

  1. Switch to Replacing or Dutch Traces: Replacing traces cap maximum state eligibility at 1.01.0, preventing revisits from amplifying values out of proportion.
  2. Guarantee Exponential Decay: Ensure the compound decay factor satisfies γλ<1.0\gamma \lambda < 1.0 strictly, bounding the maximum asymptotic trace amplitude to 11−γλ\frac{1}{1 - \gamma \lambda}.
  3. Reset at Episode Horizons: In episodic settings, unconditionally zero the trace vector (z←0z \leftarrow \mathbf{0}) immediately when an episode concludes.

The Quick Version

  • Fading short-term memory: Eligibility traces maintain a vector zt(s)z_t(s) tracking state visit recency and frequency, decaying exponentially by γλ\gamma \lambda on each transition.
  • Backward credit assignment: Instead of waiting for episode termination, any TD error δt\delta_t is instantly broadcast backward across all states via ΔV(s)=αδtzt(s)\Delta V(s) = \alpha \delta_t z_t(s).
  • Unifying TD and Monte Carlo: Setting λ=0\lambda = 0 reduces to standard 1-step TD(0)\text{TD}(0), while setting λ=1\lambda = 1 replicates the credit distribution of full Monte Carlo rollouts online.
  • Accumulating vs. replacing: Accumulating traces add eligibility upon revisits (z(s)←γλz(s)+1z(s) \leftarrow \gamma \lambda z(s) + 1), which risks divergence in cycles; replacing traces reset eligibility to 1.01.0, ensuring numerical stability.