Reinforcement Learning

Q-learning

Q-learning builds a table of "how good is this move from this spot" by trial and error, and it works without ever being told the rules of the world.

Read these first

On this page 10
  1. The short answer
  2. The analogy you have already lived
  3. What makes it different from the last few lessons
  4. The one line that matters
  5. Watching it spread
  6. Why it can learn from its own mistakes
  7. Where you have already seen it
  8. What is honestly hard here
  9. Remember this
  10. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

The short answer

Q-learning keeps a scorecard of every move from every spot, and improves it a little after each thing that happens.

The score is not "was that fun". It is "how well will the rest of my life go if I do this now".

The analogy you have already lived

Think about learning your way around a new neighbourhood without a map.

The first week you take wrong turns constantly. But you build up a feeling. From the temple, going left leads somewhere useful. Going right leads to that dead end near the drain.

You did not memorise the map. You attached a rough score to each turn from each landmark. When a route turns out badly, you quietly downgrade the turn that started it. When it works, you upgrade it.

That collection of feelings is a Q-table. Q is for quality.

What makes it different from the last few lessons

In the MDP lesson, you computed the perfect answer — but you needed the complete rulebook first. You had to know every probability of every outcome.

Q-learning throws that requirement away. It never asks for the rules. It watches what happens and updates.

That single change is what makes reinforcement learning usable in the real world, where nobody hands you a table of probabilities for a warehouse floor.

The one line that matters

After every step, one number in the table gets nudged:

   new score  =  old score  +  a small step toward:
                              (reward I got now)
                            + (how good the next spot looks)

Read the second half again, because it is the clever part.

The agent does not wait until the end of the episode to learn. It updates immediately, using its own current guess about the next spot. It is correcting a guess using another guess.

That sounds like it should not work. It is called bootstrapping, and it is the central idea in this half of the field.

This is confusing for almost everyone the first time. Read it twice — that is normal. The reason it works is that one of the guesses is anchored to something real. The square next to the goal touches an actual reward. Its neighbour learns from it. Their neighbour learns from them. Truth spreads outward from the reward, one step per visit.

Watching it spread

Every square starts at zero. Nobody knows anything.

   attempt 1:   the agent stumbles into the goal by accident.
                the last square before the goal gets a real score.

   attempt 5:   the square before that one picks up some score
                from its now-valuable neighbour.

   attempt 50:  a trail of increasing scores runs all the way back
                to the start. following it uphill is the best route.

Nobody drew the route. It grew backwards from the reward, one attempt at a time.

Why it can learn from its own mistakes

Q-learning has an unusual property. While it is exploring — deliberately taking random, often silly actions — it still learns as if it had played well.

The trick is that when it updates, it uses the best action available in the next spot, not the random one it is about to take. So a bad move teaches it a good lesson.

This is called being off-policy: learning about one way of behaving while actually behaving another way. It is why an agent can learn from old recordings, from another agent's games, or from a human's demonstrations. That flexibility is why Q-learning underpins so much of what came later.

Where you have already seen it

  • Elevator scheduling and traffic-signal timing in some cities.
  • Robot vacuums working out which room order covers the flat fastest.
  • Game AI in older bots that learned by playing thousands of matches.
  • Network routing that adapts to congested links.

What is honestly hard here

The table has one row per situation. Chess has more positions than there are atoms in the observable universe. You cannot keep a row for each. Q-learning in this pure form works only for small, countable problems.

That limit is real. The next lesson is about the repair: replacing the table with a neural network that can score situations it has never seen.

It needs to visit everything, repeatedly. A square the agent never enters keeps its score of zero forever, and a zero score means "no opinion", not "bad". Confusing those two is a common source of bugs.

Remember this

  • Q-learning learns a score for each move from each spot, without knowing the rules.
  • It updates immediately using its own guess about the next spot — bootstrapping.
  • It is off-policy: it can behave randomly and still learn the best behaviour.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install gymnasium numpy

FrozenLake is a four-by-four grid. You start top-left, the goal is bottom-right, and four squares are holes that end the episode with nothing. We switch slipping off with is_slippery=False, so the world is deterministic and the learning is easy to read.

This trains in well under a second on any laptop.

The whole algorithm

q_learning.py
import numpy as np
import gymnasium as gym

env = gym.make("FrozenLake-v1", map_name="4x4", is_slippery=False)
rng = np.random.default_rng(0)

Q = np.zeros((env.observation_space.n, env.action_space.n))   # 16 squares x 4 moves
ALPHA, GAMMA, EPISODES = 0.8, 0.95, 2000

wins = []
for ep in range(EPISODES):
    state, _ = env.reset(seed=ep)
    epsilon = max(0.10, 1.0 - ep / 1000)      # curious early, decisive later
    done, got = False, 0.0
    while not done:
        if rng.random() < epsilon:
            action = int(rng.integers(env.action_space.n))
        else:
            action = int(np.argmax(Q[state]))
        nxt, reward, terminated, truncated, _ = env.step(action)
        # nudge Q toward "reward now + the best I could do from the next square"
        target = reward + GAMMA * (0.0 if terminated else Q[nxt].max())
        Q[state, action] += ALPHA * (target - Q[state, action])
        state, done, got = nxt, terminated or truncated, got + reward
    wins.append(got)

for lo in (0, 500, 1000, 1500):
    print(f"episodes {lo:4d}-{lo + 499}: win rate {np.mean(wins[lo:lo + 500]):.2f}")
print()

ARROW = ["<", "v", ">", "^"]
MAP = "SFFFFHFHFFFHHFFG"
print("learned policy (H = hole, G = goal):")
for r in range(4):
    print("  " + " ".join(MAP[r * 4 + c] if MAP[r * 4 + c] in "HG"
                          else ARROW[int(np.argmax(Q[r * 4 + c]))] for c in range(4)))

print("\nQ values for the start square [left, down, right, up]:", np.round(Q[0], 3).tolist())

state, _ = env.reset(seed=123)
path, reward = [state], 0.0
for _ in range(20):
    state, reward, terminated, truncated, _ = env.step(int(np.argmax(Q[state])))
    path.append(state)
    if terminated or truncated:
        break
print("greedy path:", path, "-> reward", reward)
env.close()
Output
episodes    0-499: win rate 0.10
episodes  500-999: win rate 0.70
episodes 1000-1499: win rate 0.87
episodes 1500-1999: win rate 0.91

learned policy (H = hole, G = goal):
  v < < <
  v H v H
  > v v H
  H > > G

Q values for the start square [left, down, right, up]: [0.735, 0.774, 0.698, 0.735]
greedy path: [0, 4, 8, 9, 13, 14, 15] -> reward 1

Reading that output properly

The win rate never reaches 1.00, and that is correct. It plateaus at 0.91 because ten percent of actions are still random by design. The greedy path at the bottom — six moves, straight to the goal, reward 1 — is the honest measure of what it learned. Always evaluate greedily, separately from training.

The Q values at the start square decay backwards from the goal. The best action scores 0.774. The goal is six steps away and the discount is 0.95, so the theoretical best is 0.95**5 = 0.774. The table matched the exact optimal value. That is not luck; it is what convergence looks like in a deterministic world.

The arrows in the top-right point left, into nowhere useful. Those squares are almost never visited, so their rows are near zero and argmax returns whatever wins the tie. An unvisited state has no opinion, not a bad one. Never read a policy in states the agent has not been to.

Line by line

target = reward + GAMMA * (0.0 if terminated else Q[nxt].max()) — the entire algorithm. Q[nxt].max() is the off-policy part: it uses the best next action, not the one the exploring agent will actually take. And 0.0 if terminated is essential — a terminal state has no future, and bootstrapping past it makes the values grow without limit.

ALPHA = 0.8 — a very large learning rate, which is fine here because the environment is deterministic. Turn slipping on and this must come down to about 0.1, or the estimates thrash against the randomness.

GAMMA = 0.95 — with the reward only at the goal, a discount below 1 is what makes shorter routes score higher. Set GAMMA = 1.0 and every path that reaches the goal scores the same, so the agent has no reason to prefer the quick one.

epsilon = max(0.10, 1.0 - ep / 1000) — the floor of 0.10 is doing more work than it looks like. Try max(0.0, ...): once epsilon hits zero, an agent that has not yet found the goal has an all-zero table, argmax returns action 0 (left), it walks into the wall for 100 steps, and it never escapes. Run the same script with 400 episodes and a floor of 0.05 and the win rate stays at exactly 0.00 forever. Nothing errors. That is the failure mode to recognise.

Turning the ice back on

Change one flag:

python
env = gym.make("FrozenLake-v1", map_name="4x4", is_slippery=True)

Now each move goes where you intended one third of the time and slides sideways the other two thirds. The same script will do badly. Fixing it needs ALPHA around 0.1, EPISODES around 20000, and patience. A win rate near 0.75 is roughly the ceiling for this map, because the ice sometimes pushes you into a hole no matter how well you play.

That is worth doing once. It is the difference between an algorithm that works and an algorithm you have tuned.

Common mistakes

Bootstrapping through a terminal state. Omitting the 0.0 if terminated check is the single most common Q-learning bug. Symptom: values climb steadily and never settle.

Confusing truncated with terminated for the target. A time-limit cutoff is not an ending. Bootstrap from the last observation when truncated is true.

Choosing the action with Q[state].argmax() while the table is all zeros. It returns index 0 every time. Your "greedy" agent is a constant function pretending to be a policy.

A learning rate that is too high for a stochastic environment. With ALPHA = 0.8 on slippery ice, each update almost throws away the old estimate, so the table follows the last thing that happened rather than the average.

Try it yourself

Set GAMMA = 1.0 and re-run. The win rate stays high, but print the greedy path — it may wander before finding the goal, because a longer route now scores the same as a short one. Then set GAMMA = 0.5 and watch the start-square values collapse toward zero, because a reward six steps away is worth almost nothing at that discount. Those two experiments teach more about discounting than any amount of reading.

What to learn next

Researcher — Mathematics and papers.

The update

Q-learning (Watkins, 1989) is an off-policy temporal-difference control method:

$$ Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha \Big[ \underbrace{r_{t+1} + \gamma \max_{a'} Q(s_{t+1}, a') - Q(s_t, a_t)}_{\text{TD error } \delta_t} \Big] $$

Symbols: $\alpha \in (0,1]$ the step size, $\gamma$ the discount, $r_{t+1}$ the observed reward, and $\delta_t$ the temporal-difference error. The target $r_{t+1} + \gamma \max_{a'} Q(s_{t+1},a')$ is a one-sample estimate of the Bellman optimality backup, with $P$ replaced by the single transition actually observed.

Off-policy, precisely

The behaviour policy $\mu$ generates the data; the target policy is greedy with respect to $Q$. Because the target uses $\max_{a'}$ rather than the action $\mu$ will choose next, the fixed point is $Q^*$ regardless of $\mu$ — provided $\mu$ has adequate coverage.

Contrast SARSA, the on-policy sibling: $$ Q(s_t,a_t) \leftarrow Q(s_t,a_t) + \alpha\left[ r_{t+1} + \gamma Q(s_{t+1}, a_{t+1}) - Q(s_t,a_t) \right] $$ using the action actually taken. SARSA converges to $Q^\pi$ for the $\epsilon$-greedy policy being followed, so it learns a safe policy that accounts for its own exploration. On the classic cliff-walking task, Q-learning learns the optimal path along the cliff edge and falls off during training; SARSA learns a longer, safer path and scores better online. Neither is uniformly correct — they answer different questions.

Convergence

Watkins and Dayan (1992) proved almost-sure convergence of tabular Q-learning to $Q^*$ under:

  1. Every $(s,a)$ is visited infinitely often.
  2. Robbins-Monro step sizes: $\sum_t \alpha_t(s,a) = \infty$ and $\sum_t \alpha_t^2(s,a) < \infty$.
  3. Bounded rewards.

The proof treats the update as stochastic approximation of a contraction mapping and applies the Jaakkola, Jordan and Singh (1994) framework. Note that condition (2) rules out the constant $\alpha$ that essentially all practical implementations use — constant step sizes converge to a bounded neighbourhood of $Q^*$ rather than to the point, which is the correct trade for a non-stationary problem.

Maximisation bias

$\mathbb{E}[\max_a \hat{Q}(s,a)] \ge \max_a \mathbb{E}[\hat{Q}(s,a)]$ by Jensen's inequality. Taking a max over noisy estimates systematically overestimates, and the bias compounds through bootstrapping.

Double Q-learning (Hasselt, 2010) decouples selection from evaluation with two tables: $$ Q_A(s,a) \leftarrow Q_A(s,a) + \alpha\left[ r + \gamma\, Q_B!\left(s', \arg\max_{a'} Q_A(s',a')\right) - Q_A(s,a) \right] $$ updating $A$ or $B$ at random each step. $Q_A$ picks the action, $Q_B$ scores it, so the noise in the two is independent and the bias largely cancels. This carries directly into Double DQN.

Sample complexity

For a $\gamma$-discounted MDP with a generative model, synchronous Q-learning needs $\tilde{\Theta}!\left( \frac{|\mathcal{S}||\mathcal{A}|}{(1-\gamma)^4 \varepsilon^2} \right)$ samples for an $\varepsilon$-accurate $Q$ (Li et al., 2020, with a matching lower bound). Note $(1-\gamma)^{-4}$ — worse than the $(1-\gamma)^{-3}$ achievable by model-based value iteration on the same data. Model-based methods are provably more sample-efficient here, a result that is often overlooked.

Variants worth knowing

  • Q($\lambda$) — eligibility traces spread credit back over many steps, interpolating between one-step TD and Monte Carlo. Watkins's version cuts the trace at a non-greedy action; Peng's does not, trading a small bias for lower variance.
  • Expected SARSA — replaces $Q(s_{t+1}, a_{t+1})$ with $\sum_{a'}\pi(a' \mid s_{t+1})Q(s_{t+1},a')$, removing the variance from sampling the next action. Usually dominates SARSA at equal cost.
  • Speedy Q-learning (Azar et al., 2011) — attains the improved $(1-\gamma)^{-3}$ rate by using both the current and previous $Q$ in the target.

The limitation that ends the tabular story

The table has $|\mathcal{S}| \times |\mathcal{A}|$ entries and needs each visited many times. Function approximation is unavoidable for anything real, and it breaks the convergence guarantee: off-policy learning, bootstrapping and function approximation together form the deadly triad (Sutton and Barto, chapter 11), and their combination can diverge. Baird's counterexample (1995) shows linear Q-learning diverging on a seven-state MDP. Everything in the DQN lesson is machinery for making the triad behave in practice, without ever making it safe in theory.

References

  • Watkins (1989), Learning from Delayed Rewards, PhD thesis, Cambridge.
  • Watkins and Dayan (1992), Q-learning, Machine Learning 8:279-292.
  • Hasselt (2010), Double Q-learning, NeurIPS.
  • Baird (1995), Residual Algorithms: Reinforcement Learning with Function Approximation.
  • Li et al. (2020), Sample Complexity of Asynchronous Q-Learning — arxiv.org/abs/2006.03041.
  • Sutton and Barto (2018), chapters 6 and 11.

What to learn next