TAI BUI
Back to curriculum
BuildPython

Deep Q-Networks (DQN)

2013: Mnih trained a single Q-learning network on raw pixels and beat every classical RL agent on seven Atari games. 2015: the same idea scaled to 49 games, landed in Nature, and kicked off the deep-RL era. DQN is just Q-learning plus three tricks that make function approximation hold together.

TIME: ~75 minutesPREREQUISITES: Phase 3 · 03 (Backpropagation), Phase 9 · 04 (Q-learning, SARSA)

Learning Objectives

  • Explain why tabular Q-learning cannot scale past a few thousand states, and how a neural Q-function Q(s, a; θ) replaces the lookup table.
  • Name DQN's three stabilization tricks (experience replay, target network, reward clipping) and the deadly-triad failure mode each one counters.
  • Implement a full DQN training loop from scratch in stdlib Python: replay buffer, Q-network, ε-greedy action selection, and periodic target-network sync.
  • Apply Double DQN's action-selection/evaluation split to fix maximization bias, and place DQN correctly against SAC/TD3, PPO, and offline RL for a given task.

The Problem

Tabular Q-learning needs a separate Q-value for every (state, action) pair. A chess board has roughly 10⁴³ states. A single Atari frame is 210×160×3 = 100,800 features. Tabular RL dies at thousands of states, let alone billions.

The fix looks obvious in hindsight: swap the Q-table for a neural network, Q(s, a; θ). Obvious in hindsight took decades to actually work, because naive function approximation with Q-learning diverges under the "deadly triad": function approximation plus bootstrapping plus off-policy learning. Mnih et al. (2013, 2015) found three engineering tricks that stabilize the whole thing:

  1. Experience replay decorrelates transitions.
  2. Target network freezes the bootstrap target.
  3. Reward clipping normalizes gradient magnitudes.

DQN on Atari was the first time a single architecture with a single hyperparameter set solved dozens of control problems straight from raw pixels. Everything "deep-RL" built since (DDQN, Rainbow, Dueling, Distributional, R2D2, Agent57) stacks on top of this three-trick base.

Warm-up0/3
1. The previous lesson's tabular Q-learning stores one value for every (state, action) pair in a table you fill in as you go. If the number of possible states runs into the billions instead of a few hundred, what would you expect to happen to that approach?Warm-up
Two small copies of a Q-table with columns state, action and Q and rows s1 a1, s1 a2, s2 a1 and an ellipsis: the left one is for a few hundred states and the right one for billions of states, each captioned with its row count.
2. Backpropagation nudges a network's weights toward a target value. If that target is itself computed by the same network and shifts every time the weights update, what would you expect for training stability compared to training toward a fixed target?Warm-up
Two training loops side by side. In the first a network's prediction and a fixed target number feed a squared-error loss that updates the weights w. In the second the target is computed by the same network, and a dashed line leads from the weight update back to the target.
3. You train a neural network with minibatch gradient descent on data that arrives in time order, like video frames. Why do practitioners usually shuffle samples into random minibatches instead of using them in arrival order?Warm-up
A strip of 12 frames t1 to t12 in arrival order, each a small game frame with a ball, then two rows of three minibatches of four: one taken in arrival order (t1 to t4, t5 to t8, t9 to t12) and one drawn at random from the 12 frames.
Frame positions are illustrative.
Check: The Problem0/2
1. What is the 'deadly triad' this lesson says destabilizes naive Q-learning with function approximation?Define
Line chart of the 8 weights of a linear value function on Baird's 7-state example over 2000 update sweeps: w1 to w6 (identical) and w8 climb past 1,000 and keep accelerating, while w7 drifts below zero.
Baird's counterexample is not from the lesson. Computed here with numpy: expected semi-gradient TD(0) sweeps over all 7 states, reward 0, gamma 0.99, step size 0.01, start weights 1,1,1,1,1,1,10,1.
2. How does a neural Q-function Q(s, a; θ) get around the scaling problem that stops tabular Q-learning on a 100,800-feature Atari frame?Explain
A small grid standing for one Atari frame of 210 by 160 by 3 equals 100,800 features, with chess at roughly 10 to the 43 states, then a Q-table with rows s1 a1, s2 a1 and an ellipsis beside a box Q(s, a; theta) with weights theta producing Q-values for one frame.

The Concept

DQN training loop: env, replay buffer, online net, target net, Bellman TD loss

The objective. DQN minimizes the one-step TD loss on a neural Q-function:

L(θ) = E_{(s,a,r,s')~D} [ (r + γ max_{a'} Q(s', a'; θ^-) - Q(s, a; θ))² ]

θ is the online network, updated every step by gradient descent. θ^- is the target network, periodically copied from θ (every ~10,000 steps). D is the replay buffer of past transitions.

The three tricks, in order of importance:

Experience replay. A ring buffer of ~10⁶ transitions. Each training step samples a minibatch uniformly at random. This breaks temporal correlation (successive frames are nearly identical), lets the network learn from rare rewarding transitions many times, and decorrelates consecutive gradient updates. Without it, on-policy TD with a neural net diverges on Atari.

Target network. A second copy of the Q-network, Q(·; θ^-), with its weights frozen and periodically copied from the online network: θ → θ^- every C steps. It exists because using the same network Q(·; θ) on both sides of the Bellman equation makes the target move every update, so the network ends up chasing its own tail. The frozen copy stabilizes the regression target for thousands of gradient steps at a stretch. Soft updates, θ^- ← τ θ + (1-τ) θ^- (used in DDPG, SAC), are a smoother variant.

Reward clipping. Clip every reward to {-1, 0, +1}. Atari reward magnitudes vary from 1 to 1000+, so without clipping, one game's scale would dominate the gradient. Wrong when reward magnitude matters; fine for Atari, where only sign matters.

Double DQN. Hasselt (2016) fixes maximization bias: use the online net to select the action, the target net to evaluate it.

target = r + γ Q(s', argmax_{a'} Q(s', a'; θ); θ^-)

Drop-in replacement, consistently better. Use it by default.

Other improvements (Rainbow, 2017): prioritized replay (sample high-TD-error transitions more), dueling architecture (separate V(s) and advantage heads), noisy networks (learned exploration), n-step returns, distributional Q (C51/QR-DQN), multi-step bootstrapping. Each adds a few percent; the gains are roughly additive.

Check: The Concept0/13
1. What problem does the target network Q(·; θ^-) solve in DQN, per this lesson?Define
The lesson's train_step code with the target.forward line highlighted, above two boxes: the online net Q(·; θ) that gives q[a] and receives the gradient, and the target net Q(·; θ^-) that gives max(q_next) inside y.
2. What does Double DQN change about how the Bellman target is computed, per the lesson's formula?Define
A table with the online net's values 2, 5, 3 and the target net's values 4, 1, 6 for three actions a0, a1, a2 of a next state s', the plain DQN target r + gamma max Q(s', a'; theta minus) = r + gamma times 6, and a Double DQN target r + gamma times a question mark.
Values are illustrative, not from the lesson.
3. What does the dueling architecture do to the Q-network?Define
A Q-network drawn as state s going through shared hidden layers into one output per action: Q(s, a1) = 4.0, Q(s, a2) = 5.0, Q(s, a3) = 3.0.
Output values are illustrative.
4. What is prioritized experience replay (PER)?Define
Bars of the absolute TD error for 8 transitions in a replay buffer, #1 to #8: 0.1, 0.2, 0.1, 3.0, 0.3, 0.1, 1.5, 0.2, with the probability of picking transition #4 left as a question mark.
TD errors are illustrative.
5. Which set of improvements does the lesson attribute to Rainbow (2017)?Define
A timeline of five papers from the lesson's reference list: Mnih et al. 2013, Mnih et al. 2015, van Hasselt et al. 2016 on Double Q-learning, Wang et al. 2016 on dueling architectures, and Hessel et al. 2018, Rainbow: Combining Improvements in Deep RL.
Years as given in the lesson's reference list. The Rainbow link there is arXiv 1710.02298, an October 2017 identifier.
6. What is DQN?Define
A 4x4 GridWorld with the agent at cell 5 and the goal in the opposite corner, its 16-dim one-hot state vector, a network box Q(s, a; theta) with layer sizes 16, 32, 4, and four output values: up -4.65, down -3.43, left -4.67, right -3.44.
Output values come from a numpy re-implementation of the lesson's DQN loop (400 episodes, reward -1 per step, gamma 0.9), not from main.py itself.
7. What is experience replay?Define
A list of 8 stored transitions #0 to #7 as (s, a, r, s_next, done) tuples from a 4x4 GridWorld with reward -1 per step, above a call buf.sample(4, rng) whose result is left as a question.
Transitions come from a random-policy walk in numpy.
8. Why does the lesson recommend clipping Atari rewards to {-1, 0, +1}?Explain
A table mapping five example raw rewards -50, 0, 1, 7 and 1000 to clipped values -1, 0, +1, +1, +1, beside a step plot of clipped reward against raw reward that is -1 below zero, 0 at zero and +1 above zero.
9. What specific problem does experience replay solve, as distinct from the target network?Explain
A diagram of the DQN training loop: the environment feeds a reward-clipping step and then the replay buffer, a minibatch from the buffer goes to the TD loss, the online net Q(·; θ) gives Q(s, a; θ) and receives the update, the target net Q(·; θ^-) supplies y to the loss, a dashed arrow copies θ to the target every C steps, and the online net sends epsilon-greedy actions to the environment.
10. The target r + γ max Q(s', a') takes a max over noisy Q estimates. Why does that make the target biased?Explain
Four bars for the learned estimates of Q(s', a') for actions a1 to a4, scattered above and below a dashed line marking the true value 0 shared by all four, with the target r + gamma max Q(s', a') written beneath.
Estimates are illustrative draws of true value plus noise.
11. For a next state s', the online net gives Q = [1, 4, 2] and the target net gives Q = [3, 2, 5] over three actions. With r = 1 and gamma = 0.9, what is the Double DQN target?Apply
A table of Q-values for three actions of next state s': online net 1, 4, 2 and target net 3, 2, 5, with r = 1, gamma = 0.9, the Double DQN target formula and the target left as a question mark.
12. You train a DQN but skip the target network, using the same online network Q(s, a; θ) on both sides of the Bellman update. What would you expect during training, based on this lesson?Analyze
A Q-network with weights theta feeding both the prediction Q(s, a; theta) and the target r + gamma max Q(s', a'; theta) into one loss, with an arrow from the loss back to the same weights theta.
13. A colleague's DQN trains on each transition immediately as it happens, with no buffer. Updates swing toward whatever the last few seconds of near-identical frames looked like, and returns collapse. Which ingredient is missing?Analyze
Eight consecutive game frames t1 to t8 with the ball moving only slightly between them, each followed by an update pill, meaning one gradient step on that transition alone, with a note that no transition is stored for later.
Ball positions are illustrative.

Build It

The code here is stdlib-only, no NumPy: a hand-rolled single-hidden-layer MLP running on a tiny continuous GridWorld, so every training step takes microseconds. The algorithm is identical to Atari DQN, just at a much smaller scale.

Step 1: replay buffer

python
class ReplayBuffer:
    def __init__(self, capacity):
        self.buf = []
        self.capacity = capacity
    def push(self, s, a, r, s_next, done):
        if len(self.buf) == self.capacity:
            self.buf.pop(0)
        self.buf.append((s, a, r, s_next, done))
    def sample(self, batch, rng):
        return rng.sample(self.buf, batch)

~50,000 capacity for Atari; 5,000 suffices for this toy env.

Step 2: a tiny Q-network (manual MLP)

python
class QNet:
    def __init__(self, n_in, n_hidden, n_actions, rng):
        self.W1 = [[rng.gauss(0, 0.3) for _ in range(n_in)] for _ in range(n_hidden)]
        self.b1 = [0.0] * n_hidden
        self.W2 = [[rng.gauss(0, 0.3) for _ in range(n_hidden)] for _ in range(n_actions)]
        self.b2 = [0.0] * n_actions
    def forward(self, x):
        h = [max(0.0, sum(w * xi for w, xi in zip(row, x)) + b) for row, b in zip(self.W1, self.b1)]
        q = [sum(w * hi for w, hi in zip(row, h)) + b for row, b in zip(self.W2, self.b2)]
        return q, h

Forward pass: linear → ReLU → linear. That is the entire net.

Step 3: the DQN update

python
def train_step(online, target, batch, gamma, lr):
    grads = zeros_like(online)
    for s, a, r, s_next, done in batch:
        q, h = online.forward(s)
        if done:
            y = r
        else:
            q_next, _ = target.forward(s_next)
            y = r + gamma * max(q_next)
        td_error = q[a] - y
        accumulate_grads(grads, online, s, h, a, td_error)
    apply_sgd(online, grads, lr / len(batch))

The shape is Q-learning from Lesson 04 with two differences: you backprop through a differentiable Q(·; θ) instead of indexing a table, and the target uses Q(·; θ^-).

Step 4: the outer loop

For each episode, act ε-greedy on Q(·; θ), push transitions into the buffer, sample a minibatch, take a gradient step, periodically sync θ^- ← θ. The pattern:

python
for episode in range(N):
    s = env.reset()
    while not done:
        a = epsilon_greedy(online, s, epsilon)
        s_next, r, done = env.step(s, a)
        buffer.push(s, a, r, s_next, done)
        if len(buffer) >= batch:
            train_step(online, target, buffer.sample(batch), gamma, lr)
        if steps % sync_every == 0:
            target = copy(online)
        s = s_next

On this tiny GridWorld with a 16-dim one-hot state, the agent learns a near-optimal policy in about 500 episodes. On Atari, scale this up to 200M frames and add a CNN feature extractor.

Check: Build It0/6
1. The Build It GridWorld uses a stdlib-only MLP and a 16-dim one-hot state instead of Atari. How does that toy DQN relate to Atari DQN?Explain
A comparison table of Atari DQN and the Build It GridWorld DQN: state (210 by 160 by 3 frame versus 16-dim one-hot), network (CNN features versus stdlib MLP with one hidden layer), replay capacity (about 50,000 versus 5,000), training (200M frames versus about 500 episodes), and an algorithm row left as question marks.
2. In train_step, a sampled transition has done=True, r=0, and the target network gives max(q_next) = 50 for the stored next state, with gamma = 0.99. What is the target y?Apply
The lesson's train_step code and a sampled transition with done = True, r = 0, gamma = 0.99, where the target network would give max(q_next) = 50, and y left as a question mark.
3. In train_step, q[a] = 2.0 for the sampled action, r = 1, gamma = 0.9, done is False, and max(q_next) from the target network is 3. What td_error does the code compute?Apply
The lesson's train_step code and one sampled transition with q[a] = 2.0, r = 1, gamma = 0.9, done = False and max(q_next) = 3, with td_error left as a question mark.
4. A ReplayBuffer has capacity 3 and receives pushes of transitions A, B, C, D in that order. What does buf hold afterwards?Apply
The lesson's ReplayBuffer.push code and a buffer of capacity 3 holding transitions A, B, C in that order, followed by a push of D, with the contents afterward left as a question mark.
5. A QNet is built with n_in=16, n_hidden=32, n_actions=4. What does forward(x) return for a 16-dim one-hot state x?Apply
The lesson's QNet constructor and forward code, and a call QNet(n_in=16, n_hidden=32, n_actions=4) followed by forward on a 16-dim one-hot state, with the returned q and h left as question marks.
6. An engineer sets sync_every = 1 in the outer loop, so target = copy(online) runs after every environment step. What does this give them?Analyze
The lesson's outer-loop excerpt with sync_every = 1, if steps % sync_every == 0 and target = copy(online), above environment steps 1 to 8 each marked True for the condition steps % sync_every == 0.

Pitfalls

  • Deadly triad. Function approximation + off-policy + bootstrapping can diverge. DQN mitigates with target net + replay; do not remove either.
  • Exploration. ε must decay, typically from 1.0 to 0.01 over the first ~10% of training. Without enough early exploration the Q-net converges to a local basin.
  • Overestimation. max over noisy Q is upward-biased. Always use Double DQN in production.
  • Reward scale. Clip or normalize rewards; the gradient magnitude is proportional to reward magnitude.
  • Replay buffer coldstart. Don't train until the buffer has a few thousand transitions. Early gradients on ~20 samples overfit.
  • Target sync frequency. Too frequent ≈ no target net; too infrequent ≈ stale targets. Atari DQN uses 10,000 env steps. Rule of thumb: sync every ~1/100 of training horizon.
  • Observation preprocessing. Atari DQN stacks 4 frames to make state Markov. Any env with velocity info needs frame-stacking or recurrent state.
Check: Pitfalls0/9
1. What goes wrong if the target network is synced far too infrequently?Explain
Two timelines over a 1,000,000-step run with tick marks for target syncs: 100 evenly spaced ticks for the Atari DQN setting of every 10,000 steps, and 2 ticks, at 500,000 and 1,000,000, for a run that syncs every 500,000 steps.
The 500,000-step interval is an illustration of far too infrequent.
2. Why should ε start near 1.0 and decay toward 0.01 instead of starting small?Explain
Two epsilon schedules over a 1,000,000-step run: one starts at 1.0 and falls to 0.01 by step 100,000 then stays, the other starts small, drawn as 0.01 from the first step.
The linear shape of the decay and the value 0.01 for starting small are illustrative; the lesson gives only start, end and the first 10% of training.
3. According to the pitfalls section, what should you avoid doing with a freshly initialized replay buffer?Explain
The lesson's Step 4 loop code with the condition if len(buffer) >= batch before train_step, and a strip of 50 cells standing for the 5,000 slots of the buffer, with only the first cell marked after the first push.
4. Why does the lesson say Atari DQN stacks 4 frames as input instead of using a single frame?Explain
Two network inputs for a game: a single frame t of 210 by 160 by 3 with a ball and a paddle, and a stack of four frames t-3, t-2, t-1 and t of the same size.
Drawn as two input shapes; the ball positions in the stack are not meant to differ.
5. You plan a DQN run of 1,000,000 environment steps. Using the lesson's rule of thumb, roughly how often should you sync the target network?Apply
A bar spanning a DQN run of 1,000,000 environment steps labelled at 0, 250,000, 500,000, 750,000 and 1,000,000, and the line target = copy(online) every C steps with C left as a question mark.
6. You train for 1,000,000 steps using the lesson's typical ε schedule. Roughly when should ε reach 0.01?Apply
Axes of epsilon against training step over 1,000,000 steps, with a point at epsilon 1.0 at step 0 and a dashed horizontal line at 0.01 labelled with the step where it is reached left as a question mark.
7. On a noisy-reward GridWorld, your DQN's Q(s_0, best_a) climbs far above the true V*(s_0) as training proceeds. Which change targets this symptom?Analyze
Line chart over 1,000 training episodes, on a signed log scale, of max over actions of Q(s_0, a) for 6 seeds of a DQN on a noisy-reward 4x4 GridWorld, against a dashed line at the true V*(s_0) of -5.85. In 4 of the 6 seeds the estimate grows far above that line, to between a few hundred and over ten billion by the end, while one seed stays near it and one falls below it.
Runs of a numpy re-implementation of the lesson's loop (TD error clipped to 5, batch 16, sync every 100 steps, lr 0.05): reward -1 plus N(0, 5²) noise per step, gamma 0.99, 1,000 episodes, 6 seeds.
8. A DQN agent trained on single frames of a game plays poorly because it cannot tell which way the ball is moving, though the Q-network itself is fine. What is the right fix?Analyze
Two identical single game frames, labelled game A and game B, each with the ball at the same place above a paddle, with a note asking where the ball goes next.
9. A discrete-action task gives most steps a reward near 1 but a rare goal reward of 500, and the difference in size matters to what you want. You use DQN. What is the best way to handle reward scale?Design
Bars of the reward at each of 10 steps of one episode: nine steps with reward 1 and the last step with reward 500.
Episode length is illustrative.

Use It

In 2026, DQN is rarely state-of-the-art but remains the reference off-policy algorithm:

TaskMethod of choiceWhy not DQN?
Discrete-action Atari-likeRainbow DQN or MuesliSame framework, more tricks.
Continuous controlSAC / TD3 (Phase 9 · 07)DQN has no policy network.
On-policy / high-throughputPPO (Phase 9 · 08)No replay buffer; easier to scale.
Offline RLCQL / IQL / Decision TransformerConservative Q targets, no bootstrapping blowups.
Large discrete action spaces (recommender)DQN with action embedding, or IMPALAFine; decoration matters.
LLM RLPPO / GRPOSequence-level, not step-level; different loss.

The lessons still travel. Replay and target networks appear in SAC, TD3, DDPG, SAC-X, AlphaZero's self-play buffer, and every offline RL method. Reward clipping lives on as advantage normalization in PPO. The architecture is the blueprint.

Check: Use It0/3
1. You must train an agent whose actions are continuous joint torques for a robot arm. Which method does the Use It table point to, and why?Apply
A drawing of a three-joint robot arm with a torque label tau1, tau2 and tau3 at each joint, beside a card saying an action is the triple (tau1, tau2, tau3) with each torque a real number in a range such as -1.0 to 1.0.
Number of joints and the range are illustrative.
2. You have a fixed dataset of logged decisions and cannot interact with the environment while training. Which approach does the lesson point to, and why?Design
A fixed table of logged decisions with columns state, action, reward and next state, and a dashed greyed Environment box marked no access during training.
Rows are illustrative.
3. You want to fine-tune a language model with rewards defined on whole generated sequences. Which choice fits the lesson, and why?Design
A prompt followed by eight generated tokens y1 to y8, with one bracket spanning all of them labelled one reward for the whole sequence, R(y1 ... y8) = 0.7.
The reward value is illustrative.

Ship It

Save as outputs/skill-dqn-trainer.md:

markdown
---
name: dqn-trainer
description: Produce a DQN training config (buffer, target sync, ε schedule, reward clipping) for a discrete-action RL task.
version: 1.0.0
phase: 9
lesson: 5
tags: [rl, dqn, deep-rl]
---
 
Given a discrete-action environment (observation shape, action count, horizon, reward scale), output:
 
1. Network. Architecture (MLP / CNN / Transformer), feature dim, depth.
2. Replay buffer. Capacity, minibatch size, warmup size.
3. Target network. Sync strategy (hard every C steps or soft τ).
4. Exploration. ε start / end / schedule length.
5. Loss. Huber vs MSE, gradient clip value, reward clipping rule.
6. Double DQN. On by default unless explicit reason to disable.
 
Refuse to ship a DQN with no target network, no replay buffer, or ε held at 1. Refuse continuous-action tasks (route to SAC / TD3). Flag any reward range > 10× per-step mean as needing clipping or scale normalization.

Exercises

  1. Easy. Run code/main.py. Plot the per-episode return curve. How many episodes until the running mean exceeds -10?
  2. Medium. Disable the target network (use the online net for both sides of the Bellman target). Measure training instability: does return oscillate or diverge?
  3. Hard. Add Double DQN: use the online net to pick argmax a', target net to evaluate. Compare bias of Q(s_0, best_a) vs true V*(s_0) after 1,000 episodes with vs without Double DQN on a noisy-reward GridWorld.

Key Terms

TermWhat people sayWhat it actually means
DQN"Deep Q-learning"Q-learning with a neural Q-function, replay buffer, and target network.
Experience replay"Shuffled transitions"Ring buffer sampled uniformly each gradient step; decorrelates data.
Target network"Frozen bootstrap"Periodic copy of Q used in the Bellman target; stabilizes training.
Deadly triad"Why RL diverges"Function approximation + bootstrapping + off-policy = no convergence guarantee.
Double DQN"Fix for maximization bias"Online net selects action, target net evaluates it.
Dueling DQN"V and A heads"Decompose Q = V + A - mean(A); same output, better gradient flow.
Rainbow"All the tricks"DDQN + PER + dueling + n-step + noisy + distributional in one.
PER"Prioritized Replay"Sample transitions proportional to TD-error magnitude.

Further Reading

Code

Ships With