Skip to content
AI360Xpert
Beta

SARSA(lambda)

SARSA(lambda) attaches a decaying memory trace to every state-action choice you make, so when a reward arrives, praise or blame broadcasts instantly backwards across the entire sequence that caused it.

SARSA(lambda) maintains an action-value eligibility trace matrix that broadcasts each on-policy temporal difference error backwards across all recently chosen actions.
SARSA(lambda) maintains an action-value eligibility trace matrix that broadcasts each on-policy temporal difference error backwards across all recently chosen actions.

Why Does This Exist?

In reinforcement learning control, an agent must discover which actions maximize cumulative reward. Standard 1-step SARSA (equivalent to λ=0\lambda = 0) updates only the single most recent state-action pair (St,At)(S_t, A_t) using the immediate transition reward and the value of the next chosen action Q(St+1,At+1)Q(S_{t+1}, A_{t+1}).

While computationally simple, 1-step SARSA suffers from a severe information bottleneck:

  • Glacial credit propagation: If an agent navigates a 20-step corridor to reach a +10+10 goal, 1-step SARSA only updates the final transition (S19,A19)(S_{19}, A_{19}) in the first episode. The agent must successfully navigate the corridor 20 separate times for that goal reward to propagate back to the starting state (S0,A0)(S_0, A_0).
  • Monte Carlo control is too volatile: Pure Monte Carlo methods update the entire trajectory at once, but they require waiting for complete episode termination, cannot learn online, and suffer from high sample variance across long, stochastic rollouts.
  • nn-step SARSA introduces latency: While nn-step methods propagate rewards across nn transitions, they require maintaining explicit trajectory buffers and delay updates by nn steps.

SARSA(λ\lambda) exists to resolve this fundamental trade-off. By extending backward-view eligibility traces from state values V(s)V(s) to state-action values Q(s,a)Q(s, a), SARSA(λ\lambda) gives every state-action pair an internal "memory trace" z(s,a)z(s, a). Whenever a non-zero temporal difference error δt\delta_t occurs, it is broadcast backward to all recently chosen actions simultaneously in real time. Credit propagates across long sequences in a single episode without storing trajectories or waiting for the episode to finish.

Think of It Like This

Film Room Review: Praising the Full Soccer Sequence Instead of Just the Tap-In

Imagine a soccer coaching staff analyzing a match-winning goal:

  • 1-Step SARSA (λ=0\lambda = 0): The coach only praises the striker who tapped the ball into the net from two yards out (S2,A2S_2, A_2). The midfielder who intercepted an attack in their own defensive box (S0,A0S_0, A_0) and the winger who sprinted 60 yards and delivered a cross (S1,A1S_1, A_1) receive zero credit. It will take dozens of future games for the coach to realize that the initial defensive interception was what created the scoring opportunity.
  • Monte Carlo (λ=1\lambda = 1): The coach refuses to evaluate any individual play until the entire 90-minute match has ended. Because every single minute is averaged together, the brilliance of that one attacking sequence is diluted by 80 minutes of unrelated throw-ins, fouls, and midfield scuffles, introducing high variance.
  • SARSA(λ\lambda) (Intermediate λ≈0.7\lambda \approx 0.7): The coach watches the goal on film and immediately praises everyone involved proportional to their recent contribution. The striker who scored gets 100% praise (z=1.0z = 1.0), the winger who delivered the cross gets 63% praise (z=0.63z = 0.63), and the midfielder who launched the counter-attack gets 40% praise (z=0.40z = 0.40). Players sitting on the bench receive 0% credit (z=0.0z = 0.0).

Where the analogy stops: In reinforcement learning, the trace decay factor is an exact geometric multiplier γλ\gamma \lambda, updates directly adjust a numerical Q-table, and the next action At+1A_{t+1} is sampled strictly on-policy from the agent's exploratory policy (e.g., ε\varepsilon-greedy).

How It Actually Works

Action-Value Eligibility Traces and On-Policy TD Updates

Consider an environment with discrete state space S\mathcal{S} and action space A\mathcal{A}. The discount factor is γ∈[0,1]\gamma \in [0, 1], the trace decay parameter is λ∈[0,1]\lambda \in [0, 1], and the step-size learning rate is α∈(0,1]\alpha \in (0, 1].

1. The Action-Value Eligibility Trace Matrix

SARSA(λ\lambda) maintains a short-term memory matrix zt∈R∣S∣×∣A∣z_t \in \mathbb{R}^{|\mathcal{S}| \times |\mathcal{A}|} alongside its Q-table. At the start of each episode, all traces are initialized to zero:

z0(s,a)=0∀s∈S,a∈Az_0(s, a) = 0 \quad \forall s \in \mathcal{S}, a \in \mathcal{A}

At each time step tt, the agent visits state StS_t and executes action AtA_t. The eligibility trace for (St,At)(S_t, A_t) can be updated using one of two strategies:

  • Accumulating Traces: Increments the trace upon each visit: zt(s,a)={γλzt−1(s,a)+1if s=St and a=Atγλzt−1(s,a)otherwisez_t(s, a) = \begin{cases} \gamma \lambda z_{t-1}(s, a) + 1 & \text{if } s = S_t \text{ and } a = A_t \\ \gamma \lambda z_{t-1}(s, a) & \text{otherwise} \end{cases} If an agent loops repeatedly through the same state-action pair, accumulating traces can grow larger than 1, which sometimes causes value overestimation.

  • Replacing Traces: Resets the trace of the chosen action to exactly 1: zt(s,a)={1if s=St and a=At0if s=St and a≠Atγλzt−1(s,a)if s≠Stz_t(s, a) = \begin{cases} 1 & \text{if } s = S_t \text{ and } a = A_t \\ 0 & \text{if } s = S_t \text{ and } a \ne A_t \\ \gamma \lambda z_{t-1}(s, a) & \text{if } s \ne S_t \end{cases} Replacing traces clip maximum eligibility at 1.0 and zero out competing actions in the same state, providing significantly greater stability and faster convergence in control tasks.

2. The On-Policy TD Error

After executing AtA_t in state StS_t, the agent receives reward Rt+1R_{t+1} and arrives in next state St+1S_{t+1}. Crucially, the agent chooses its next action At+1A_{t+1} according to its behavior policy π\pi (such as ε\varepsilon-greedy with respect to current Q-values).

The temporal difference error δt\delta_t is computed on-policy:

δt=Rt+1+γQt(St+1,At+1)−Qt(St,At)\delta_t = R_{t+1} + \gamma Q_t(S_{t+1}, A_{t+1}) - Q_t(S_t, A_t)

If St+1S_{t+1} is a terminal state, Qt(St+1,At+1)Q_t(S_{t+1}, A_{t+1}) is treated as 0.

3. Global Broadcast Update

Instead of updating only (St,At)(S_t, A_t), the scalar error δt\delta_t updates every state-action pair simultaneously, scaled by its eligibility trace:

Qt+1(s,a)=Qt(s,a)+αδtzt(s,a)∀s∈S,a∈AQ_{t+1}(s, a) = Q_t(s, a) + \alpha \delta_t z_t(s, a) \quad \forall s \in \mathcal{S}, a \in \mathcal{A}

Finally, all traces decay by factor γλ\gamma \lambda in preparation for the next time step:

zt+1(s,a)=γλzt(s,a)∀s∈S,a∈Az_{t+1}(s, a) = \gamma \lambda z_t(s, a) \quad \forall s \in \mathcal{S}, a \in \mathcal{A}

Notice the spectrum of λ\lambda:

  • When λ=0\lambda = 0: Only the current pair has z(St,At)=1z(S_t, A_t) = 1 while all past traces immediately decay to 0, reducing the algorithm exactly to standard 1-step SARSA.
  • When λ=1\lambda = 1: Traces decay strictly by the discount factor γ\gamma, causing updates to approximate online Monte Carlo control.
  • When 0<λ<10 < \lambda < 1: The agent achieves the optimal trade-off between fast credit assignment and low variance.

Worked numerical example

Let us trace a 3-step navigation sequence through a discrete grid world to see how SARSA(λ\lambda) rewards past actions in a single episode:

Trajectory: (S0,right)→R1=0.0(S1,up)→R2=0.0(S2,right)→R3=10.0(S3,Terminal)\text{Trajectory: } (S_0, \text{right}) \xrightarrow{R_1 = 0.0} (S_1, \text{up}) \xrightarrow{R_2 = 0.0} (S_2, \text{right}) \xrightarrow{R_3 = 10.0} (S_3, \text{Terminal})

Parameters:

  • Discount factor γ=0.9\gamma = 0.9
  • Trace decay λ=0.7  ⟹  γλ=0.9×0.7=0.6300\lambda = 0.7 \implies \gamma \lambda = 0.9 \times 0.7 = 0.6300
  • Learning rate α=0.1\alpha = 0.1
  • Replacing traces
  • Initial Q(s,a)=0.0Q(s, a) = 0.0 and z(s,a)=0.0z(s, a) = 0.0 for all pairs

Step 0 (t=0t=0): Transition (S0,right)→(S1,up)(S_0, \text{right}) \to (S_1, \text{up}) with R1=0.0R_1 = 0.0

  1. Trace update: z(S0,right)=1.0000,all other z=0.0z(S_0, \text{right}) = 1.0000, \quad \text{all other } z = 0.0
  2. TD error: δ0=R1+γQ(S1,up)−Q(S0,right)=0.0+0.9(0.0)−0.0=0.0000\delta_0 = R_1 + \gamma Q(S_1, \text{up}) - Q(S_0, \text{right}) = 0.0 + 0.9(0.0) - 0.0 = 0.0000
  3. Q update: δ0=0  ⟹  Q(s,a)\delta_0 = 0 \implies Q(s, a) remains 0.00.0.
  4. Trace decay: z(S0,right)←0.63×1.0000=0.6300z(S_0, \text{right}) \leftarrow 0.63 \times 1.0000 = 0.6300

Step 1 (t=1t=1): Transition (S1,up)→(S2,right)(S_1, \text{up}) \to (S_2, \text{right}) with R2=0.0R_2 = 0.0

  1. Trace update: z(S1,up)=1.0000,z(S0,right)=0.6300z(S_1, \text{up}) = 1.0000, \quad z(S_0, \text{right}) = 0.6300
  2. TD error: δ1=R2+γQ(S2,right)−Q(S1,up)=0.0+0.9(0.0)−0.0=0.0000\delta_1 = R_2 + \gamma Q(S_2, \text{right}) - Q(S_1, \text{up}) = 0.0 + 0.9(0.0) - 0.0 = 0.0000
  3. Q update: δ1=0  ⟹  Q(s,a)\delta_1 = 0 \implies Q(s, a) remains 0.00.0.
  4. Trace decay: z(S1,up)←0.63×1.0000=0.6300z(S_1, \text{up}) \leftarrow 0.63 \times 1.0000 = 0.6300 z(S0,right)←0.63×0.6300=0.3969z(S_0, \text{right}) \leftarrow 0.63 \times 0.6300 = 0.3969

Step 2 (t=2t=2): Transition (S2,right)→S3(S_2, \text{right}) \to S_3 with R3=10.0R_3 = 10.0 (Goal Reached)

  1. Trace update: z(S2,right)=1.0000,z(S1,up)=0.6300,z(S0,right)=0.3969z(S_2, \text{right}) = 1.0000, \quad z(S_1, \text{up}) = 0.6300, \quad z(S_0, \text{right}) = 0.3969
  2. On-policy TD error (Terminal, so Q(S3)=0Q(S_3) = 0): δ2=R3+γ(0.0)−Q(S2,right)=10.0+0.0−0.0=+10.0000\delta_2 = R_3 + \gamma(0.0) - Q(S_2, \text{right}) = 10.0 + 0.0 - 0.0 = \mathbf{+10.0000}
  3. Global broadcast update (αδ2=0.1×10.0=1.0000\alpha \delta_2 = 0.1 \times 10.0 = 1.0000):
    • For (S2,right)(S_2, \text{right}): Q(S2,right)←0.0+1.0000×1.0000=+1.0000Q(S_2, \text{right}) \leftarrow 0.0 + 1.0000 \times 1.0000 = \mathbf{+1.0000}
    • For (S1,up)(S_1, \text{up}): Q(S1,up)←0.0+1.0000×0.6300=+0.6300Q(S_1, \text{up}) \leftarrow 0.0 + 1.0000 \times 0.6300 = \mathbf{+0.6300}
    • For (S0,right)(S_0, \text{right}): Q(S0,right)←0.0+1.0000×0.3969=+0.3969Q(S_0, \text{right}) \leftarrow 0.0 + 1.0000 \times 0.3969 = \mathbf{+0.3969}
    • For all unvisited pairs: Q(s,a)←0.0+1.0000×0.0000=0.0000Q(s, a) \leftarrow 0.0 + 1.0000 \times 0.0000 = \mathbf{0.0000}

Comparison with 1-Step SARSA

State-Action Pair1-Step SARSA (λ=0\lambda = 0)SARSA(λ\lambda) (λ=0.7\lambda = 0.7)Causal Credit
(S2,right)(S_2, \text{right})+1.0000+1.0000+1.0000+1.0000Immediate transition into goal
(S1,up)(S_1, \text{up})0.00000.0000+0.6300+0.6300Set up the goal cross
(S0,right)(S_0, \text{right})0.00000.0000+0.3969+0.3969Initiated the winning counter-attack

In standard 1-step SARSA, neither (S1,up)(S_1, \text{up}) nor (S0,right)(S_0, \text{right}) learned anything from this goal. In SARSA(λ\lambda), both actions immediately acquired positive value estimates in that very same episode.

Code

The following self-contained Python implementation trains a tabular SARSA(λ\lambda) agent on a linear navigation grid, demonstrating eligibility trace accumulation, on-policy error broadcasting, and automated assertions.

from typing import Dict, List, Tuple

class MiniGridSARSA:    """Tabular SARSA(lambda) control on a discrete linear gridworld.
    Layout: S0 <-> S1 <-> S2 <-> S3 (Terminal Goal, Reward = +10.0)    Actions: 0 = 'left', 1 = 'right'    """
    def __init__(        self,        num_states: int = 4,        gamma: float = 0.9,        lam: float = 0.7,        alpha: float = 0.1,        trace_type: str = "replacing",    ) -> None:        self.num_states = num_states        self.goal_state = num_states - 1        self.actions = [0, 1]  # 0: left, 1: right        self.gamma = gamma        self.lam = lam        self.alpha = alpha        self.trace_type = trace_type
        # Initialize Q-table and eligibility trace matrix to 0.0        self.q_table: Dict[Tuple[int, int], float] = {            (s, a): 0.0 for s in range(num_states) for a in self.actions        }        self.z_traces: Dict[Tuple[int, int], float] = {            (s, a): 0.0 for s in range(num_states) for a in self.actions        }
    def choose_action(self, state: int) -> int:        """Deterministic policy that prefers 'right' (1) on value ties."""        q_left = self.q_table[(state, 0)]        q_right = self.q_table[(state, 1)]        return 1 if q_right >= q_left else 0
    def step_environment(        self, state: int, action: int    ) -> Tuple[int, float, bool]:        """Executes action in environment. Moving right advances state."""        if action == 1:            next_state = min(state + 1, self.goal_state)        else:            next_state = max(state - 1, 0)
        done = next_state == self.goal_state        reward = 10.0 if done else 0.0        return next_state, reward, done
    def train_episode(self) -> int:        """Executes a single on-policy episode of SARSA(lambda)."""        # Traces must be zeroed out at the start of every episode        for key in self.z_traces:            self.z_traces[key] = 0.0
        state = 0        action = self.choose_action(state)        step_count = 0
        while state != self.goal_state and step_count < 50:            next_state, reward, done = self.step_environment(state, action)            next_action = self.choose_action(next_state) if not done else 0
            # 1. Update eligibility trace for current state-action pair            if self.trace_type == "replacing":                # Replacing trace: set chosen action to 1.0, other actions in state to 0.0                for a in self.actions:                    self.z_traces[(state, a)] = 1.0 if a == action else 0.0            else:                # Accumulating trace: add 1.0 to visited pair                self.z_traces[(state, action)] += 1.0
            # 2. Compute on-policy TD error delta_t            q_next = (                self.q_table[(next_state, next_action)] if not done else 0.0            )            td_error = (                reward + self.gamma * q_next - self.q_table[(state, action)]            )
            # 3. Global broadcast update across all state-action pairs            for pair in self.q_table:                self.q_table[pair] += (                    self.alpha * td_error * self.z_traces[pair]                )                # 4. Decay traces by gamma * lambda                self.z_traces[pair] *= self.gamma * self.lam
            state = next_state            action = next_action            step_count += 1
        return step_count

# Instantiate and train agent on episode 1agent = MiniGridSARSA(    num_states=4, gamma=0.9, lam=0.7, alpha=0.1, trace_type="replacing")steps_taken = agent.train_episode()
print(f"Episode completed in {steps_taken} steps.")print(f"Q(S0, right): {agent.q_table[(0, 1)]:.4f}")print(f"Q(S1, right): {agent.q_table[(1, 1)]:.4f}")print(f"Q(S2, right): {agent.q_table[(2, 1)]:.4f}")
# Validate multi-step credit assignment with assertionsassert steps_taken == 3, f"Expected 3 steps, got {steps_taken}"assert round(agent.q_table[(2, 1)], 4) == 1.0000, "S2 Q-value incorrect"assert round(agent.q_table[(1, 1)], 4) == 0.6300, "S1 Q-value incorrect"assert round(agent.q_table[(0, 1)], 4) == 0.3969, "S0 Q-value incorrect"print("Assertions passed: Multi-step credit propagated in a single episode!")
# -> Expected output:# -> Episode completed in 3 steps.# -> Q(S0, right): 0.3969# -> Q(S1, right): 0.6300# -> Q(S2, right): 1.0000# -> Assertions passed: Multi-step credit propagated in a single episode!

Watch Out For

Confusing On-Policy SARSA(lambda) with Off-Policy Watkins' Q(lambda)

A subtle but damaging error is substituting the off-policy maximum operator max⁡aQ(St+1,a)\max_a Q(S_{t+1}, a) into the SARSA(λ\lambda) TD error formula while keeping standard eligibility traces running.

The Failure Mode: SARSA is strictly an on-policy algorithm: its TD error δt=Rt+1+γQ(St+1,At+1)−Q(St,At)\delta_t = R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) - Q(S_t, A_t) must evaluate the action At+1A_{t+1} actually sampled by the behavior policy. If you plug in max⁡aQ(St+1,a)\max_a Q(S_{t+1}, a) (as in Q-learning) without cutting traces when exploratory actions are taken, eligibility traces mistakenly reward past actions for an exploratory deviation they never committed to. This causes Q-values to diverge or oscillate wildly.

Secondary Traps:

  1. Trace Leakage Across Episodes: Failing to reset z(s,a)←0z(s, a) \leftarrow 0 at the start of a new episode allows lingering traces from a previous episode to receive credit for transitions in the new episode.
  2. Accumulating Trace Explosions: In environments with cycles or tight loops, accumulating traces (z←z+1z \leftarrow z + 1) can exceed 1010 or 100100, causing learning rates to blow up. Use replacing traces (z←1z \leftarrow 1) for robust tabular control.

The Fix:

  • Strictly use the actual sampled action At+1A_{t+1} in δt\delta_t.
  • Reset z(s,a)=0.0z(s, a) = 0.0 for all (s,a)(s, a) at the start of each episode.
  • Use replacing traces in control problems to bound maximum eligibility to 1.01.0.

The Quick Version

  • Action-value traces: SARSA(λ\lambda) extends eligibility traces from states V(s)V(s) to state-action pairs Q(s,a)Q(s, a) by maintaining a trace matrix z(s,a)∈R∣S∣×∣A∣z(s, a) \in \mathbb{R}^{|\mathcal{S}| \times |\mathcal{A}|}.
  • Backward broadcast: Whenever an on-policy transition produces a TD error δt\delta_t, that single error updates all recently visited (s,a)(s, a) pairs simultaneously, weighted by z(s,a)z(s, a).
  • Single-episode propagation: Downstream rewards propagate backward across multi-step trajectories in a single episode, eliminating the one-step propagation bottleneck of standard SARSA.
  • Strictly on-policy: The target evaluates Q(St+1,At+1)Q(S_{t+1}, A_{t+1}) using the action actually selected by the exploratory policy, preserving convergence guarantees without complex trace-cutting logic.