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.
Why Does This Exist?
In reinforcement learning control, an agent must discover which actions maximize cumulative reward. Standard 1-step SARSA (equivalent to ) updates only the single most recent state-action pair using the immediate transition reward and the value of the next chosen action .
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 goal, 1-step SARSA only updates the final transition 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 .
- 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.
- -step SARSA introduces latency: While -step methods propagate rewards across transitions, they require maintaining explicit trajectory buffers and delay updates by steps.
SARSA() exists to resolve this fundamental trade-off. By extending backward-view eligibility traces from state values to state-action values , SARSA() gives every state-action pair an internal "memory trace" . Whenever a non-zero temporal difference error 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 (): The coach only praises the striker who tapped the ball into the net from two yards out (). The midfielder who intercepted an attack in their own defensive box () and the winger who sprinted 60 yards and delivered a cross () 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 (): 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() (Intermediate ): The coach watches the goal on film and immediately praises everyone involved proportional to their recent contribution. The striker who scored gets 100% praise (), the winger who delivered the cross gets 63% praise (), and the midfielder who launched the counter-attack gets 40% praise (). Players sitting on the bench receive 0% credit ().
Where the analogy stops: In reinforcement learning, the trace decay factor is an exact geometric multiplier , updates directly adjust a numerical Q-table, and the next action is sampled strictly on-policy from the agent's exploratory policy (e.g., -greedy).
How It Actually Works
Action-Value Eligibility Traces and On-Policy TD Updates
Consider an environment with discrete state space and action space . The discount factor is , the trace decay parameter is , and the step-size learning rate is .
1. The Action-Value Eligibility Trace Matrix
SARSA() maintains a short-term memory matrix alongside its Q-table. At the start of each episode, all traces are initialized to zero:
At each time step , the agent visits state and executes action . The eligibility trace for can be updated using one of two strategies:
-
Accumulating Traces: Increments the trace upon each visit: 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: 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 in state , the agent receives reward and arrives in next state . Crucially, the agent chooses its next action according to its behavior policy (such as -greedy with respect to current Q-values).
The temporal difference error is computed on-policy:
If is a terminal state, is treated as 0.
3. Global Broadcast Update
Instead of updating only , the scalar error updates every state-action pair simultaneously, scaled by its eligibility trace:
Finally, all traces decay by factor in preparation for the next time step:
Notice the spectrum of :
- When : Only the current pair has while all past traces immediately decay to 0, reducing the algorithm exactly to standard 1-step SARSA.
- When : Traces decay strictly by the discount factor , causing updates to approximate online Monte Carlo control.
- When : 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() rewards past actions in a single episode:
Parameters:
- Discount factor
- Trace decay
- Learning rate
- Replacing traces
- Initial and for all pairs
Step 0 (): Transition with
- Trace update:
- TD error:
- Q update: remains .
- Trace decay:
Step 1 (): Transition with
- Trace update:
- TD error:
- Q update: remains .
- Trace decay:
Step 2 (): Transition with (Goal Reached)
- Trace update:
- On-policy TD error (Terminal, so ):
- Global broadcast update ():
- For :
- For :
- For :
- For all unvisited pairs:
Comparison with 1-Step SARSA
| State-Action Pair | 1-Step SARSA () | SARSA() () | Causal Credit |
|---|---|---|---|
| Immediate transition into goal | |||
| Set up the goal cross | |||
| Initiated the winning counter-attack |
In standard 1-step SARSA, neither nor learned anything from this goal. In SARSA(), both actions immediately acquired positive value estimates in that very same episode.
Code
The following self-contained Python implementation trains a tabular SARSA() 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 into the SARSA() TD error formula while keeping standard eligibility traces running.
The Failure Mode: SARSA is strictly an on-policy algorithm: its TD error must evaluate the action actually sampled by the behavior policy. If you plug in (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:
- Trace Leakage Across Episodes: Failing to reset at the start of a new episode allows lingering traces from a previous episode to receive credit for transitions in the new episode.
- Accumulating Trace Explosions: In environments with cycles or tight loops, accumulating traces () can exceed or , causing learning rates to blow up. Use replacing traces () for robust tabular control.
The Fix:
- Strictly use the actual sampled action in .
- Reset for all at the start of each episode.
- Use replacing traces in control problems to bound maximum eligibility to .
The Quick Version
- Action-value traces: SARSA() extends eligibility traces from states to state-action pairs by maintaining a trace matrix .
- Backward broadcast: Whenever an on-policy transition produces a TD error , that single error updates all recently visited pairs simultaneously, weighted by .
- 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 using the action actually selected by the exploratory policy, preserving convergence guarantees without complex trace-cutting logic.