Week 5 Dueling DQN

English translation

[Key objectives]

[Dueling DQN idea]

[Dueling DQN advantages]

[Dueling DQN network]

[Dueling DQN training process]

1. Compute Q-values

2. Calculate the loss function

3. Update the networks

4. Update the target network

[Exercise: CartPole with Dueling DQN]

1. Install and import libraries

import gymnasium as gym
import torch
import torch.nn as nn
import torch.optim as optim
import random
from collections import deque
import numpy as np
import imageio

2. Configure the GPU and define hyperparameters

# Check GPU availability and set the device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Hyperparameters
BATCH_SIZE = 128
GAMMA = 0.99
EPS_START = 1.0
EPS_END = 0.01
EPS_DECAY = 0.995
TARGET_UPDATE = 5
MEMORY_SIZE = 30000
LR = 0.0003
NUM_EPISODES = 2000
PROGRESS_INTERVAL = 50

3. Define the Dueling DQN network

class DuelingDQN(nn.Module):
    def __init__(self, state_size, action_size, hidden_size):
        super(DuelingDQN, self).__init__()
        self.feature_layer = nn.Sequential(
            nn.Linear(state_size, hidden_size),
            nn.ReLU()
        )
        self.value_stream = nn.Sequential(
            nn.Linear(hidden_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, 1)
        )
        self.advantage_stream = nn.Sequential(
            nn.Linear(hidden_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, action_size)
        )

    def forward(self, state):
        x = self.feature_layer(state)
        value = self.value_stream(x)
        advantage = self.advantage_stream(x)
        q_values = value + (advantage - advantage.mean(dim=1, keepdim=True))
        return q_values

4. Define the replay buffer

class ReplayBuffer:
    def __init__(self, capacity):
        self.buffer = deque(maxlen=capacity)

    def push(self, state, action, reward, next_state, done):
        state = np.array(state)
        next_state = np.array(next_state)
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):
        state, action, reward, next_state, done = zip(*random.sample(self.buffer, batch_size))
        return torch.tensor(np.array(state), dtype=torch.float32, device=device), \
           torch.tensor(np.array(action), dtype=torch.int64, device=device), \
           torch.tensor(np.array(reward), dtype=torch.float32, device=device), \
           torch.tensor(np.array(next_state), dtype=torch.float32, device=device), \
           torch.tensor(np.array(done), dtype=torch.float32, device=device)

    def __len__(self):
        return len(self.buffer)

5. Define the training function

def train(env, policy_net, target_net, optimizer, replay_buffer):
    all_rewards = []

    for episode in range(NUM_EPISODES):
        state, _ = env.reset()
        total_reward = 0
        done = False

        while not done:
            # Epsilon-Greedy Exploration
            epsilon = max(EPS_END, EPS_START * (EPS_DECAY ** episode))
            if random.random() > epsilon:
                with torch.no_grad():
                    state_tensor = torch.tensor(state, dtype=torch.float32, device=device).unsqueeze(0)
                    q_values = policy_net(state_tensor)
                    action = q_values.argmax(dim=1).item()
            else:
                action = env.action_space.sample()

            next_state, reward, terminated, truncated, _ = env.step(action)
            total_reward += reward
            done = terminated or truncated

            # Store experiences in the Replay Buffer
            replay_buffer.push(state, action, reward, next_state, done)
            state = next_state

            # Sample from the Replay Buffer and train
            if len(replay_buffer) >= BATCH_SIZE:
                state_batch, action_batch, reward_batch, next_state_batch, done_batch = replay_buffer.sample(BATCH_SIZE)

                # Compute Double DQN target Q-values
                with torch.no_grad():
                    next_actions = policy_net(next_state_batch).argmax(dim=1, keepdim=True)
                    next_q_values = target_net(next_state_batch).gather(1, next_actions).squeeze(1)
                    target_q_values = reward_batch + (1 - done_batch) * GAMMA * next_q_values

                # Compute current Q-values
                q_values = policy_net(state_batch)
                q_values_for_actions = q_values.gather(1, action_batch.unsqueeze(1)).squeeze(1)

                # Compute the loss and optimize
                loss = nn.functional.mse_loss(q_values_for_actions, target_q_values)
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

        all_rewards.append(total_reward)

        # Update the Target Network
        if (episode+1) % TARGET_UPDATE == 0:
            target_net.load_state_dict(policy_net.state_dict())

        # Print the mean reward
        if (episode+1) % PROGRESS_INTERVAL == 0:
            print(f"Episode {episode+1}: Average Reward over last {PROGRESS_INTERVAL} episodes = {np.mean(all_rewards[-PROGRESS_INTERVAL:])}")

        # Stop training if rewards remain sufficiently high for a sustained period
        if np.min(all_rewards[-TARGET_UPDATE*2:]) >= 450:
            print(f"Solved in {episode+1} episodes with reward {total_reward}!")
            break

6. Run training and inspect results

env = gym.make("CartPole-v1", render_mode = 'rgb_array')
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
hidden_size = 128

policy_net = DuelingDQN(state_size, action_size, hidden_size).to(device)
target_net = DuelingDQN(state_size, action_size, hidden_size).to(device)
target_net.load_state_dict(policy_net.state_dict())
target_net.eval()

optimizer = optim.Adam(policy_net.parameters(), lr=LR)
replay_buffer = ReplayBuffer(MEMORY_SIZE)

train(env, policy_net, target_net, optimizer, replay_buffer)
Episode 50: Average Reward over last 50 episodes = 27.6
Episode 100: Average Reward over last 50 episodes = 42.24
Episode 150: Average Reward over last 50 episodes = 93.7
Episode 200: Average Reward over last 50 episodes = 79.7
Episode 250: Average Reward over last 50 episodes = 116.52
Episode 300: Average Reward over last 50 episodes = 107.16
Episode 350: Average Reward over last 50 episodes = 128.58
Episode 400: Average Reward over last 50 episodes = 122.18
Episode 450: Average Reward over last 50 episodes = 185.16
Episode 500: Average Reward over last 50 episodes = 168.18
Episode 550: Average Reward over last 50 episodes = 291.82
Solved in 575 episodes with reward 500.0!
# Render and save a GIF
frames = []
state, _ = env.reset()
done = False
total_reward = 0
while not done:
    with torch.no_grad():
        state_tensor = torch.tensor(state, dtype=torch.float32, device=device).unsqueeze(0)
        q_values = policy_net(state_tensor)
        action = q_values.argmax(dim=1).item()
    next_state, reward, terminated, truncated, _ = env.step(action)
    total_reward += reward
    state = next_state
    done = terminated or truncated
    frames.append(env.render())

imageio.mimsave('cartpole_simulation_dueling_double_DQN.gif', frames, duration=33)
print(f"Final episode reward: {total_reward}")
env.close()
Final episode reward: 399.0