The Three Innovations of DQN — Experimental Visualizations#
Generates the figures corresponding to the three core innovations of DQN in dqn.tex:
Neural approximation of Q values — motivation: the curse of dimensionality;
Experience replay — training curves with / without;
Target network — training stability with / without.
Output figures:
fig1_qtable_motivation.pdffig2_replay_comparison.pdffig3_target_comparison.pdf
Environment: CartPole-v1 | Estimated runtime: 5–10 minutes.
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib as mpl
import matplotlib.pyplot as plt
from collections import deque
import random
# ── Academic style ──────────────────────────────────────────────
mpl.rcParams.update({
'font.family': 'serif',
'font.serif': ['Times New Roman', 'DejaVu Serif', 'serif'],
'font.size': 11,
'axes.labelsize': 12,
'axes.titlesize': 13,
'axes.titleweight': 'bold',
'axes.titlepad': 10,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'legend.fontsize': 10,
'legend.framealpha': 0.9,
'legend.edgecolor': '#CCCCCC',
'figure.dpi': 150,
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'grid.color': '#DDDDDD',
'grid.linestyle': '--',
'grid.linewidth': 0.6,
'lines.linewidth': 2.0,
'savefig.bbox': 'tight',
})
# Color palette
C_BLUE = '#2166AC'
C_RED = '#D6604D'
C_PURPLE = '#762A83'
C_GREEN = '#4DAC26'
C_GRAY = '#888888'
# ── gymnasium / gym compatibility ───────────────────────────────
try:
import gymnasium as gym
def make_env(name, seed=None):
env = gym.make(name)
if seed is not None and hasattr(env.action_space, 'seed'):
env.action_space.seed(seed)
return env
def reset_env(env, seed=None):
obs, _ = env.reset(seed=seed); return obs
def step_env(env, action):
obs, r, ter, tru, _ = env.step(action); return obs, r, ter, ter or tru
print('Using gymnasium')
except ImportError:
import gym
def make_env(name, seed=None):
env = gym.make(name)
if seed is not None and hasattr(env.action_space, 'seed'):
env.action_space.seed(seed)
return env
def reset_env(env, seed=None):
if seed is not None: env.seed(seed)
return env.reset()
def step_env(env, action):
obs, r, done, _ = env.step(action); return obs, r, done, done
print('Using gym')
print('Setup complete.')
Using gymnasium
Setup complete.
Figure 1 — The curse of dimensionality: why a neural network#
Q-table size explodes exponentially with state dimension and discretization resolution, far exceeding the parameter count of a small neural network — tabular methods are infeasible for continuous / high-dimensional state spaces; function approximation is inevitable.
fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
# ── Left: Q-table size vs. bins, different state dims ───────────
ax = axes[0]
bins = np.arange(2, 31)
n_actions = 2
dims = [1, 2, 4, 8]
dim_colors = ['#BDBDBD', '#90CAF9', C_RED, '#8B0000']
dim_labels = ['1-D state', '2-D state', '4-D state (CartPole)', '8-D state']
for d, color, label in zip(dims, dim_colors, dim_labels):
sizes = (bins ** d) * n_actions
lw = 2.5 if d == 4 else 1.5
ax.semilogy(bins, sizes, color=color, linewidth=lw, label=label)
# Neural network parameter count as reference (4 -> 128 -> 128 -> 2)
nn_params = (4*128 + 128) + (128*128 + 128) + (128*2 + 2) # 17,282
ax.axhline(y=nn_params, color=C_BLUE, linestyle='--', linewidth=2.0,
label=f'DQN network params (~{nn_params//1000}K)')
b10_size = 10**4 * 2 # CartPole, b=10
ax.annotate(
f'CartPole, b=10\n{b10_size:,} entries',
xy=(10, b10_size), xytext=(17, b10_size * 10),
arrowprops=dict(arrowstyle='->', color=C_RED, lw=1.5),
fontsize=9, color=C_RED
)
ax.set_xlabel('Discretization Bins per Dimension')
ax.set_ylabel('Q-Table Entries (log scale)')
ax.set_title('Curse of Dimensionality:\nQ-Table Size Explosion')
ax.legend(loc='upper left')
ax.grid(True, which='both')
# ── Right: Q-table size across environments ──────────────────────
ax2 = axes[1]
env_names = ['GridWorld\n(5×5)', 'Mountain Car\n(2-D, b=20)',
'CartPole\n(4-D, b=10)', 'Atari Pong\n(image input)']
env_sizes = [5*5*4, 20**2*3, 10**4*2, int(1e15)]
env_colors = ['#4CAF50', '#FFC107', C_RED, '#B71C1C']
bars = ax2.bar(env_names, env_sizes, color=env_colors,
alpha=0.80, edgecolor='white', linewidth=1.5, width=0.5)
ax2.set_yscale('log')
ax2.axhline(y=nn_params, color=C_BLUE, linestyle='--', linewidth=2.0,
label=f'DQN params (~{nn_params//1000}K)')
bar_labels = ['400', '1,200', '20,000', r'$\sim 10^{67888}$']
for bar, lbl in zip(bars, bar_labels):
ax2.text(bar.get_x() + bar.get_width() / 2, bar.get_height() * 3,
lbl, ha='center', va='bottom', fontsize=9, fontweight='bold')
ax2.text(0.98, 0.90, 'Atari bar truncated for display\n(true count = 256$^{84\times84\times4}$)',
transform=ax2.transAxes, ha='right', fontsize=8, color='#B71C1C')
ax2.set_ylabel('Q-Table Entries (log scale)')
ax2.set_title('Q-Table Size Across Environments')
ax2.legend()
ax2.grid(True, which='both', axis='y')
plt.tight_layout(pad=2.5)
plt.savefig('fig1_qtable_motivation.pdf')
plt.show()
print('Saved: fig1_qtable_motivation.pdf')
Saved: fig1_qtable_motivation.pdf
Figure 2 — Experience replay: breaking sample correlation#
Ablation (CartPole-v1, 3 random seeds, shading \(\pm 1\sigma\)): with replay the run converges faster with less variance. The buffer scatters temporally adjacent, strongly correlated samples into approximately i.i.d. mini-batches while reusing old experience.
print('=== Experiment 1: Experience Replay ===')
print('\n[A] With replay buffer (capacity=10000, batch=64)')
res_with_replay = run_seeds(n_seeds=3, use_replay=True, use_target=True, n_episodes=400)
print('\n[B] Without replay buffer (online single-step update)')
res_no_replay = run_seeds(n_seeds=3, use_replay=False, use_target=True, n_episodes=400)
=== Experiment 1: Experience Replay ===
[A] With replay buffer (capacity=10000, batch=64)
seed=42 ...
last-50 avg: 331.1
seed=142 ...
last-50 avg: 102.8
seed=242 ...
last-50 avg: 363.5
[B] Without replay buffer (online single-step update)
seed=42 ...
last-50 avg: 10.1
seed=142 ...
last-50 avg: 10.2
seed=242 ...
last-50 avg: 10.6
fig2 = plot_comparison(
res_with_replay, res_no_replay,
label_a='With Experience Replay',
label_b='Without Experience Replay',
title='Effect of Experience Replay\nCartPole-v1 (shading = $\\pm 1\\sigma$)',
filename='fig2_replay_comparison.pdf'
)
Saved: fig2_replay_comparison.pdf
Figure 3 — Target network: stabilizing the bootstrap target#
Ablation (same setup): without a target network the TD target drifts with the online network every step — “chasing its own tail” — and training is less stable with higher variance across seeds; a target frozen for a while keeps the regression target nearly fixed.
print('=== Experiment 2: Target Network ===')
print('\n[A] With target network (sync every 100 steps)')
res_with_target = run_seeds(n_seeds=3, use_replay=True, use_target=True, n_episodes=400)
print('\n[B] Without target network (q_net used as its own target)')
res_no_target = run_seeds(n_seeds=3, use_replay=True, use_target=False, n_episodes=400)
=== Experiment 2: Target Network ===
[A] With target network (sync every 100 steps)
seed=42 ...
last-50 avg: 217.4
seed=142 ...
last-50 avg: 391.3
seed=242 ...
last-50 avg: 201.2
[B] Without target network (q_net used as its own target)
seed=42 ...
last-50 avg: 88.4
seed=142 ...
last-50 avg: 11.2
seed=242 ...
last-50 avg: 24.2
fig3 = plot_comparison(
res_with_target, res_no_target,
label_a='With Target Network',
label_b='Without Target Network',
title='Effect of Target Network\nCartPole-v1 (shading = $\\pm 1\\sigma$)',
filename='fig3_target_comparison.pdf',
color_a=C_BLUE,
color_b=C_PURPLE
)
Saved: fig3_target_comparison.pdf
print('All figures saved:')
print(' fig1_qtable_motivation.pdf')
print(' fig2_replay_comparison.pdf')
print(' fig3_target_comparison.pdf')
All figures saved:
fig1_qtable_motivation.pdf
fig2_replay_comparison.pdf
fig3_target_comparison.pdf
Summary#
Function approximation solves representation: Q-tables explode with dimension; network parameters decouple from it;
Experience replay solves data: decorrelation + reuse of old experience;
The target network solves optimization: a fixed regression target prevents bootstrap oscillation;
Together they stabilize neural Q-learning on CartPole. The same pieces are what later scaled DQN to high-dimensional inputs.