Understanding Deep Q-Networks: combining Q-Learning with neural networks for high-dimensional state spaces.
DQN combines Q-Learning with deep neural networks to handle high-dimensional state spaces like images.
Problem with Tabular Q-Learning
Curse of Dimensionality:
- Image state: 84x84 RGB = 253,952 dimensions
- Q-table size: exponential in dimensions
- Infeasible for large state spaces
Solution: Use neural network to approximate Q-values
DQN Architecture
| Input | State (84x84x4 image) |
| Conv Layer 1 | 32 filters, 8x8, stride 4 |
| Conv Layer 2 | 64 filters, 4x4, stride 2 |
| Conv Layer 3 | 64 filters, 3x3, stride 1 |
| Dense | 512 units, ReLU |
| Output | Q-values for each action (18 outputs for Atari) |
Key Innovations
1. Experience Replay
Store transitions and train on mini-batches:
replay_buffer = [] # (s, a, r, s', done)
# Store transition
replay_buffer.append((state, action, reward, next_state, done))
# Sample mini-batch for training
mini_batch = random.sample(replay_buffer, batch_size=32)
Benefits:
- Breaks correlation between samples
- More stable training
- Better sample efficiency
2. Target Network
Use separate network for stability:
Q-network: updated every step (fast)
Target network: updated every N steps (stable)
Loss = (r + γ*max Q_target(s', a') - Q(s,a))²
# Every N steps
target_network.set_weights(main_network.get_weights())
3. Reward Clipping
Normalize rewards to [-1, +1]:
clipped_reward = max(-1, min(1, reward))
DQN Algorithm
def dqn(env, episodes=10000):
# Create networks
Q = build_network() # Main network
Q_target = build_network() # Target network
Q_target.set_weights(Q.get_weights())
replay_buffer = []
epsilon = 1.0
for episode in range(episodes):
state = env.reset()
done = False
while not done:
# ε-greedy action selection
if random() < epsilon:
action = env.action_space.sample()
else:
Q_values = Q.predict(state[np.newaxis])[0]
action = np.argmax(Q_values)
# Take action
next_state, reward, done, _ = env.step(action)
# Store in replay buffer
replay_buffer.append((state, action, reward, next_state, done))
if len(replay_buffer) > 100000:
replay_buffer.pop(0)
# Train on mini-batch
if len(replay_buffer) > 32:
mini_batch = random.sample(replay_buffer, 32)
train_step(Q, Q_target, mini_batch)
state = next_state
# Update target network
if episode % 10000 == 0:
Q_target.set_weights(Q.get_weights())
# Decay epsilon
epsilon = max(0.01, epsilon * 0.995)
return Q
def train_step(Q, Q_target, mini_batch, learning_rate=0.00025):
states, actions, rewards, next_states, dones = zip(*mini_batch)
# Current Q-values
Q_values = Q.predict_on_batch(np.array(states))
# Next Q-values from target network
next_Q_values = Q_target.predict_on_batch(np.array(next_states))
# Bellman update
for i in range(len(mini_batch)):
if dones[i]:
Q_values[i, actions[i]] = rewards[i]
else:
Q_values[i, actions[i]] = rewards[i] + 0.99 * np.max(next_Q_values[i])
# Train
Q.fit(np.array(states), Q_values, verbose=0)
Implementation with TensorFlow
import tensorflow as tf
import numpy as np
class DQN:
def __init__(self, state_size, action_size):
self.state_size = state_size
self.action_size = action_size
self.memory = []
self.epsilon = 1.0
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
self.learning_rate = 0.00025
self.gamma = 0.99
self.model = self.build_model()
self.target_model = self.build_model()
self.update_target_model()
def build_model(self):
"""Build neural network"""
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 8, strides=4, activation='relu',
input_shape=self.state_size),
tf.keras.layers.Conv2D(64, 4, strides=2, activation='relu'),
tf.keras.layers.Conv2D(64, 3, strides=1, activation='relu'),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(512, activation='relu'),
tf.keras.layers.Dense(self.action_size, activation='linear')
])
model.compile(optimizer=tf.keras.optimizers.Adam(self.learning_rate),
loss='mse')
return model
def update_target_model(self):
"""Copy weights from model to target_model"""
self.target_model.set_weights(self.model.get_weights())
def remember(self, state, action, reward, next_state, done):
"""Store experience in memory"""
self.memory.append((state, action, reward, next_state, done))
def act(self, state):
"""Choose action (ε-greedy)"""
if np.random.random() <= self.epsilon:
return np.random.randint(0, self.action_size)
Q_values = self.model.predict(state, verbose=0)
return np.argmax(Q_values[0])
def replay(self, batch_size):
"""Train on mini-batch"""
if len(self.memory) < batch_size:
return
mini_batch = np.random.choice(len(self.memory), batch_size, replace=False)
states = np.array([self.memory[i][0] for i in mini_batch])
actions = np.array([self.memory[i][1] for i in mini_batch])
rewards = np.array([self.memory[i][2] for i in mini_batch])
next_states = np.array([self.memory[i][3] for i in mini_batch])
dones = np.array([self.memory[i][4] for i in mini_batch])
# Current Q-values
target_Q_values = self.model.predict(states, verbose=0)
# Next Q-values
next_Q_values = self.target_model.predict(next_states, verbose=0)
# Bellman equation
for i in range(batch_size):
if dones[i]:
target_Q_values[i, actions[i]] = rewards[i]
else:
target_Q_values[i, actions[i]] = (
rewards[i] + self.gamma * np.max(next_Q_values[i])
)
# Train
self.model.fit(states, target_Q_values, epochs=1, verbose=0)
# Decay epsilon
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
DQN achieves superhuman performance on Atari games:
- Breakout: 30x human
- Pong: Perfect play
- Space Invaders: 6x human
- Many games: within human range
Summary
DQN:
- Approximates Q-values with neural network
- Handles high-dimensional state spaces
- Uses experience replay for stability
- Uses target network for stability
- Achieves superhuman performance