Module 26: Reinforcement learning 45 min13 min quick readDeep learning
Q-learning and SARSA
Learn action values from experience with one-step temporal-difference updates, compute Q-learning and SARSA targets by hand, and explain why the two take different paths along a cliff.
Before this: MDPs, states, actions, and rewards
Quick read: the intuition, interactive visualization, summary, and interview questions. The math, implementation, engineering, and knowledge-check sections are hidden.
Mental model. Keep a scorecard with one number for every (place, move) pair: "how much total reward do I expect if I make this move here?" After every move, compare what the card predicted with what just happened (the reward, plus what the card says about where you landed) and nudge the entry a little toward that. Q-learning fills in "where you landed" assuming you'll play perfectly from there. SARSA fills it in with the move you will actually make, mistakes included.
You will learn to
- Write the temporal-difference (TD) update and compute a Q-learning and a SARSA update by hand.
- Explain ε-greedy exploration and why optimistic initial values also explore.
- Distinguish on-policy (SARSA) from off-policy (Q-learning) learning, and predict which takes the safer route on a cliff.
- Tune the learning rate α, discount γ, and exploration rate ε, and read a learning curve.
- Recognize the limits of a table: unvisited states, maximization bias, and state spaces too big to enumerate.
Why it matters. The previous lesson solved an MDP when we knew the rules. Usually you don't: you can only act and observe. TD learning updates after every single step, with no model and no need to wait for the episode to end. The same one-line update, with a neural network in place of the table, is DQN; the same "target minus estimate" error drives the critic in PPO. Interviewers ask about the cliff-walking difference because it tests whether you understand on-policy versus off-policy, not just the formula.
1. Intuition
Our world is a 5×5 grid. The agent starts in the bottom-left corner and wants the bottom-right corner. The three cells between them are pits: falling in costs −100 and ends the episode. Every other move costs −1. Two walls sit in the second row.
The agent keeps a Q-table: 25 cells × 4 moves = 100 numbers, all starting at 0. Each number is a guess of the total reward from making move in cell and then carrying on.
After each move it sees one reward and the cell it landed in, . It builds a better guess for , called the target:
target = reward now + γ × (value of being in )
and moves a fraction α of the way toward it. This is a temporal-difference update: it learns from the difference between consecutive guesses, so values spread backward from the goal one step at a time. Nothing needs the episode to finish first.
The two algorithms disagree on one thing: the value of being in .
- Q-learning uses the best move in : . It learns how good things would be if the agent played greedily from here on.
- SARSA uses the move the agent will actually make next, with already chosen. Its name lists what one update uses: .
Exploration. If the agent always took its best-looking move, it would never discover anything better. ε-greedy takes a random move with probability ε (say 10%) and the best-looking move otherwise.
The cliff. Here the difference becomes visible. The shortest route runs along the row just above the pits: 6 moves. But with ε = 0.1, one move in ten is random, and a random "down" next to a pit costs 100. Q-learning ignores its own randomness when it computes targets, so it learns that the edge path is best and keeps walking it, falling in now and then. SARSA's targets include those random moves, so the cells beside the pits look bad, and it learns a path one row further up: 8 moves, but fewer falls.
Key distinctionOff-policy vs on-policy. Q-learning is off-policy: it learns the value of the greedy policy while behaving with a different, exploring one. SARSA is on-policy: it learns the value of the policy it is actually running. If exploration will continue when the agent is deployed, SARSA's answer is the more honest one. If you will switch exploration off, Q-learning's is the better plan.
2. Visualization
Q-learning on a cliff grid
A synthetic 5×5 world: −1 per move, −100 for a pit. Watch the Q-table fill in, one temporal-difference update at a time.
In the lab, each cell shows four arrows, one per move, sized by how good that move looks relative to the others; the teal arrow is the greedy choice. The dashed orange line is the greedy path from the start. The panel under the curve writes out the last update with real numbers.
Try it (predict first, then check):
- Press Step a few times from a fresh start. Before each, predict which single Q-value will change and whether it goes up or down. Why is the first update always or ?
- Run 300 episodes with Q-learning, then switch to SARSA and run 300 again. Predict the length of each greedy path, and which one has the higher mean return.
- Set ε to 0 and train from scratch. Predict whether the agent still finds the goal. (Hint: every untried move still has its initial value of 0, while every tried move costs at least −1.)
- Set α to 1. Predict what happens to the return curve when the agent occasionally falls into a pit.
3. The math
Symbols
| Symbol | Meaning | Shape / type |
|---|---|---|
| current estimate of the action value | table, 25 × 4 here | |
| state, action, reward, next state of one transition | cell, move, scalar, cell | |
| next action: argmax for Q-learning, the sampled action for SARSA | move | |
| learning rate (step size) | scalar in | |
| discount | scalar in | |
| exploration probability in ε-greedy | scalar in | |
| TD target | scalar | |
| TD error, | scalar |
The updates
Both move the estimate toward a one-step target:
If is terminal, the target is just : there is no future to bootstrap from. Bootstrapping means the target uses our own current estimate . That makes the target biased (the estimate may be wrong) but low-variance (it depends on one random step, not a whole episode), which is the trade TD makes against Monte Carlo methods that wait for the full return.
The Q-learning target is a sampled version of the Bellman optimality equation from the previous lesson, so Q-learning converges to , given that every state-action pair keeps being visited and α shrinks appropriately (formally , ). SARSA's target is a sample of the Bellman expectation equation for the ε-greedy policy itself, so it converges to that policy's values, and to only if ε decays to 0 over time.
Expected SARSA replaces the sampled by its average under the policy, : same target in expectation as SARSA, less noise.
Worked example: one update, two rules
The agent is in cell with . It moves right, gets , and lands in where the table holds:
| up | right | down | left | |
|---|---|---|---|---|
| −3.0 | −1.5 | −4.0 | −2.0 |
Take , . Suppose the ε-greedy draw at came up "explore" and picked down.
Q-learning:
- Best next value: .
- Target: .
- TD error: .
- New value: .
SARSA:
- Next action's value: .
- Target: .
- TD error: .
- New value: .
Same transition, very different updates. SARSA was charged for the exploratory "down" it is about to take; Q-learning assumed it would take the best move. Near a cliff, that exploratory move is the one that falls in, which is exactly why SARSA learns to keep its distance.
Ground truth check. On this grid the shortest path is 6 moves, so with , . After 500 episodes the script's Q-learning table has and matches exactly on every cell of its greedy path. Yet the largest error over all 100 entries is 34.87: pairs the agent rarely tries, like walking into the pits from the top rows, still hold rough guesses. A tabular agent is only as good as its coverage.
4. Implementation
The full script is code/26-reinforcement-learning/q_learning.py (standard library plus matplotlib for the figure). It trains both algorithms on the same grid as the lab, over 10 seeds, and checks against value iteration.
Build it from scratch
Step 1: the environment. A string map keeps the world readable and easy to edit. The step function returns the next cell, the reward, and whether the episode ended; walls and edges leave the agent where it was.
LAYOUT = [
".....",
".#.#.",
".....",
".....",
"SPPPG",
]
def step(state, action):
r, c = state
dr, dc = MOVES[action]
nr, nc = r + dr, c + dc
if not (0 <= nr < ROWS and 0 <= nc < COLS) or LAYOUT[nr][nc] == "#":
nr, nc = r, c
cell = LAYOUT[nr][nc]
if cell == "P":
return (nr, nc), PIT_REWARD, True
return (nr, nc), STEP_REWARD, cell == "G"Predict: from the start , what does moving right return?
Step 2: ε-greedy with random tie-breaking. At the start every Q-value is 0, so every move ties. Always picking the first tied action (say "up") would bias the agent; breaking ties at random avoids it.
def greedy(q, rng):
best = max(q.values())
return rng.choice([a for a in ACTIONS if q[a] == best]) # break ties at random
def epsilon_greedy(Q, s, eps, rng):
if rng.random() < eps:
return rng.choice(ACTIONS) # explore
return greedy(Q[s], rng) # exploitPredict: with ε = 0.1 and four actions, what is the probability the agent takes the greedy action (assuming no ties)? (Not 0.9.)
Step 3: Q-learning. Choose, act, update toward the max over next actions. The if done branch stops bootstrapping from a terminal state. Note that max_steps cuts off long episodes without treating the cutoff as terminal.
def q_learning(episodes, alpha=0.1, gamma=1.0, eps=0.1, seed=0, max_steps=100):
rng, Q, returns = random.Random(seed), new_table(), []
for _ in range(episodes):
s, total = START, 0.0
for _ in range(max_steps):
a = epsilon_greedy(Q, s, eps, rng)
s2, r, done = step(s, a)
target = r if done else r + gamma * max(Q[s2].values())
Q[s][a] += alpha * (target - Q[s][a])
s, total = s2, total + r
if done:
break
returns.append(total)
return Q, returnsPredict: if you deleted the r if done else part, what would the target be after falling into a pit, and would it matter here? (Pit and goal cells' Q-values are never updated, so they stay 0.)
Step 4: SARSA. The only real change: choose the next action before the update and use its value in the target, then actually take that action on the next step.
def sarsa(episodes, alpha=0.1, gamma=1.0, eps=0.1, seed=0, max_steps=100):
rng, Q, returns = random.Random(seed), new_table(), []
for _ in range(episodes):
s, total = START, 0.0
a = epsilon_greedy(Q, s, eps, rng)
for _ in range(max_steps):
s2, r, done = step(s, a)
a2 = epsilon_greedy(Q, s2, eps, rng)
target = r if done else r + gamma * Q[s2][a2]
Q[s][a] += alpha * (target - Q[s][a])
s, a, total = s2, a2, total + r
if done:
break
returns.append(total)
return Q, returnsPredict: if you set ε = 0, are Q-learning and SARSA the same algorithm? (Look at what a2 becomes.)
Use the library
Tabular methods are short enough that most people write them by hand, as above, on top of a Gymnasium environment. Gymnasium ships the classic cliff world as CliffWalking-v1 (-v0 in releases before 1.1; 4×12, a cliff row costing −100 that sends you back to the start), with integer states you can index into a NumPy table:
import gymnasium as gym
import numpy as np
env = gym.make("CliffWalking-v1") # "CliffWalking-v0" on older Gymnasium
Q = np.zeros((env.observation_space.n, env.action_space.n))
s, _ = env.reset(seed=0)
a = env.action_space.sample()
s2, r, terminated, truncated, _ = env.step(a)
Q[s, a] += 0.1 * (r + (0 if terminated else Q[s2].max()) - Q[s, a]) # one Q-learning updateThe environment adds seeded resets and the terminated versus truncated split. Once the state is an image or a vector of floats, the table stops working and you need a function approximator; deep RL libraries such as Stable-Baselines3 start there, with DQN.
Run it
uv run code/26-reinforcement-learning/q_learning.pyQ-learning target = -1 + 0.9 * -1.5 = -2.35; new Q = -2.0 + 0.5 * (-2.35 - (-2.0)) = -2.175
SARSA target = -1 + 0.9 * -4.0 = -4.60; new Q = -2.0 + 0.5 * (-4.60 - (-2.0)) = -3.300
=== 5x5 cliff grid: 500 episodes, alpha=0.1, gamma=1.0, epsilon=0.1, 10 seeds ===
optimal return (6 moves along the pits) = -6
Q-learning greedy path length per seed = [6, 6, 6, 6, 6, 6, 6, 6, 6, 6] mean return while exploring (last 100 eps) = -16.12
SARSA greedy path length per seed = [8, 8, 8, 8, 8, 8, 8, 8, 8, 8] mean return while exploring (last 100 eps) = -12.79
Q-learning, seed 0: max Q(start) = -6.00 (true -6); max |V - V*| on the greedy path = 0.00; max |Q - Q*| over all pairs = 34.87
saved figures/q-learning-sarsa.png5. Engineering
Where tabular TD is still used. Small discrete problems: toy control, simple games, inventory with a few stock levels, teaching, and as a sanity check for a deep RL pipeline (if your DQN can't solve a grid that a table solves in a second, the bug is in your code). Beyond about a million state-action pairs, or with continuous states, you need function approximation.
Choosing which. Use Q-learning when you can explore freely in training (a simulator) and will act greedily when deployed. Prefer SARSA or Expected SARSA when exploration is costly or will continue in deployment, since it accounts for its own mistakes. Q-learning's off-policy nature has a second benefit: it can learn from data generated by any policy, which is what makes experience replay in DQN possible.
Hyperparameters.
- α: constant 0.05 to 0.3 is common for tabular problems. Too high and a single unlucky pit fall swings a value by a lot (try α = 1 in the lab); too low and learning crawls.
- ε: start high (often 1.0) and decay to a small floor like 0.05. Theory needs "greedy in the limit with infinite exploration" (GLIE) for convergence to the optimal policy.
- γ: set from the horizon you care about, as in the previous lesson.
- Initial values: with negative step rewards, initializing at 0 is optimistic: every untried move looks better than every tried one, so the agent explores even with ε = 0. Pessimistic initial values can lock in the first path found.
Maximization bias. over noisy estimates is biased upward: the max picks whichever estimate happens to be too high. With noisy rewards Q-learning overestimates values. Double Q-learning keeps two tables, picks the best action with one, and evaluates it with the other; Double DQN is the deep version.
Watch outFailure modes. Never-visited states keep their initial values, so the greedy policy is meaningless there (see the top rows of the trained Q-learning table). Treating a time-limit cutoff as terminal teaches the agent that the world ends at step 100. Rewards on wildly different scales (−100 vs −1) make large α unstable. Using a list or unrounded float vector as a dictionary key silently creates duplicate states.
Common mistakes
- Bootstrapping from a terminal state (the target after a terminal transition is just ).
- In SARSA, choosing for the update and then choosing a different action to execute. The update and the behavior must use the same .
- Always breaking ties toward the first action, which biases early exploration.
- Judging Q-learning by its return while exploring and concluding it is worse than SARSA. Evaluate both greedily (ε = 0) if the deployed agent will be greedy.
- Leaving ε fixed at 1.0 (pure random) and reading the learning curve as "not learning".
6. Knowledge check
Quiz: Q-learning and SARSA
, reward , next-state values , , . What is the new after one Q-learning update?
Practice exercise. . The agent moves up, gets , and lands in with (up, right, down, left). The next action chosen by ε-greedy is "left". With and , compute the new under Q-learning and under SARSA.
Solution
Q-learning: ; ; new .
SARSA: ; ; new .
The same transition raises the estimate under Q-learning and lowers it under SARSA, because the exploratory "left" is worse than the greedy "right".
Implementation challenge. Add Expected SARSA to q_learning.py. Under ε-greedy, the expected next value is (ignoring ties). Which path length does it learn on the cliff grid with ε = 0.1, and how does its mean return while exploring compare with SARSA's?
Solution sketch
Copy q_learning and replace the target with r + gamma * ((1 - eps) * max(q2.values()) + eps * sum(q2.values()) / 4) where q2 = Q[s2]. It is on-policy like SARSA, so it should also avoid the pit edge, but its targets have less noise (no sampled ), so its learning curve is smoother and usually at least as high as SARSA's. Verify by running 10 seeds as the script does.
Summary
- TD learning updates after every step toward a one-step target, reward plus discounted estimate of the next state: bootstrapped, biased, low-variance, and model-free.
- Q-learning's target uses (off-policy, learns ); SARSA's uses the next action actually taken (on-policy, learns the values of the exploring policy).
- On a cliff, Q-learning learns the shortest path along the edge and falls in while exploring; SARSA learns a safer, longer path and earns more while exploring.
- ε-greedy and optimistic initial values drive exploration; α, γ, ε need tuning, and unvisited states stay wrong.
- The max in Q-learning overestimates under noise (fix: Double Q-learning), and tables don't scale, which motivates DQN.
Next: Deep Q-networks (DQN)
Related: MDPs, states, actions, and rewards · Policy gradients and REINFORCE · PPO
Go deeper, code-first (AI Engineering from Scratch): Monte Carlo Methods — Learning from Complete Episodes
Parts of this lesson are adapted from AI Engineering from Scratch by Rohit Ghumare (MIT license, notice): Temporal Difference — Q-Learning & SARSA.
Interview angle
What is the difference between Q-learning and SARSA, and which is on-policy?
Both are one-step temporal-difference methods that update Q(s, a) toward a target of reward plus the discounted value of the next state. The difference is which next value they use. Q-learning uses the max over next actions, so its target describes the greedy policy no matter how the agent actually behaves; that makes it off-policy, and it converges to Q*. SARSA uses the value of the action the agent will actually take next, sampled from its ε-greedy policy, so it learns the value of the policy it is running; that is on-policy. The practical consequence shows up when exploration is risky: on a cliff, Q-learning learns the shortest path along the edge and keeps falling in while exploring, while SARSA learns a safer path because its targets include its own random moves.
When would you choose SARSA over Q-learning, or vice versa?
I choose based on whether exploration continues in deployment and how costly mistakes are. If I train in a simulator and will deploy a greedy policy, Q-learning gives the better plan, because it learns optimal values regardless of the exploration noise. If the agent keeps exploring in the real system, or mistakes during learning are expensive, SARSA or Expected SARSA is safer, because it optimizes the return of the policy I'm actually running. Off-policy learning also has a data benefit: Q-learning can reuse transitions from any policy, such as a replay buffer or logged data, while SARSA needs fresh on-policy data. A practical middle ground is Expected SARSA, which averages over the next action instead of sampling it, reducing variance while staying on-policy.
Your tabular Q-learning agent's learning curve is flat after thousands of episodes. What do you check?
First, whether the agent ever reaches a reward signal: if the goal is never found, nothing propagates back, so I'd log how many episodes reach it. Then exploration: ε fixed at 1 means the behavior is random no matter what the table says, and ε at 0 with pessimistic initial values can lock in the first path. I'd check the update for the classic bugs: bootstrapping from terminal states, treating time-limit cutoffs as terminal, or a state key that isn't hashable consistently, so updates go to duplicate entries. Then α: too small learns imperceptibly, too large with big negative rewards oscillates. Finally, I'd evaluate the greedy policy separately from the exploring one, since the curve while exploring can stay flat even after the greedy policy is optimal.
Q(s, a) is −2, reward −1, next-state values are −3, −1.5, −4, −2, and the agent will explore to the −4 action. With α 0.5 and γ 0.9, what are the two updates?
Q-learning takes the max next value, −1.5. The target is −1 + 0.9 × (−1.5) = −2.35, the TD error is −2.35 − (−2) = −0.35, and the new value is −2 + 0.5 × (−0.35) = −2.175. SARSA uses the value of the action actually taken next, −4. The target is −1 + 0.9 × (−4) = −4.6, the TD error is −2.6, and the new value is −2 + 0.5 × (−2.6) = −3.3. So one exploratory step pulls SARSA's estimate down by 1.3 while Q-learning moves only 0.175. Repeated near a cliff, that difference is what makes SARSA treat the edge as dangerous and route around it, while Q-learning keeps valuing the edge path as if it would never slip.