Skip to content
AILinkDeepTech
Go back
Reinforcement Learning Medium

Deep Q-Network (DQN) Implementation in PyTorch

Abstract

A PyTorch implementation of Deep Q-Network (DQN): a Q-value MLP, a replay buffer, an ε-greedy agent with target network, Bellman loss update, and a CartPole-v1 training/test loop using Gymnasium.

Deep Q-Network (DQN) Implementation in PyTorch

This implementation builds Deep Q-Network (DQN) from scratch. It includes an MLP Q-network, a replay buffer for off-policy storage, an ε-greedy agent with a periodically-updated target network and Bellman loss training step, plus CartPole-v1 test cases using Gymnasium.

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

class DQN(nn.Module):
    def __init__(self, input_size, output_size, hidden_size=64):
        super(DQN, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, output_size)
        )

    def forward(self, x):
        return self.network(x)
    
class ReplayBuffer:
    def __init__(self, capacity):
        self.buffer = deque(maxlen=capacity)

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

    def sample(self, batch_size):
        transitions = random.sample(self.buffer, batch_size)
        state, action, reward, next_state, done = zip(*transitions)
        return state, action, reward, next_state, done
    
    def __len__(self):
        return len(self.buffer)
    
class DQNAgent:
    def __init__(self, state_size, action_size, hidden_size=64, buffer_size=10000, 
                 batch_size=64, gamma=0.99, lr=1e-3, epsilon_start=1.0,
                 epsilon_end=0.01, epsilon_decay=0.995):
        self.state_size = state_size
        self.action_size = action_size
        self.batch_size = batch_size
        self.gamma = gamma
        self.epsilon = epsilon_start
        self.epsilon_end = epsilon_end
        self.epsilon_decay = epsilon_decay

        # Initialize networks
        self.policy_net = DQN(state_size, action_size, hidden_size)
        self.target_net = DQN(state_size, action_size, hidden_size)
        self.target_net.load_state_dict(self.policy_net.state_dict())

        self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
        self.memory = ReplayBuffer(buffer_size)
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

        self.policy_net.to(self.device)
        self.target_net.to(self.device)

    def select_action(self, state):
        #Epsilon-greedy action selection
        if random.random() > self.epsilon:
            with torch.no_grad():
                state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
                q_values = self.policy_net(state)
                return q_values.max(1)[1].item()
        else:
            return random.randrange(self.action_size)
        
    def update_epsilon(self):
        #Decay epsilon value
        self.epsilon = max(self.epsilon_end, self.epsilon * self.epsilon_decay)

    def train_step(self):
        if len(self.memory) < self.batch_size:
            return
        
        states, actions, rewards, next_states, dones = self.memory.sample(self.batch_size)
        
        states = torch.FloatTensor(states).to(self.device)
        actions = torch.LongTensor(actions).to(self.device)
        rewards = torch.FloatTensor(rewards).to(self.device)
        next_states = torch.FloatTensor(next_states).to(self.device)
        dones = torch.FloatTensor(dones).to(self.device)

        current_q_values = self.policy_net(states).gather(1, actions.unsqueeze(1))

        # Compute next Q values
        with torch.no_grad():
            next_q_values = self.target_net(next_states).max(1)[0]
            target_q_values = rewards + (1 - dones) * self.gamma * next_q_values

        # Compute loss and update
        loss = nn.MSELoss()(current_q_values.squeeze(), target_q_values)
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

        return loss.item()
    
    def update_target_network(self):
        self.target_net.load_state_dict(self.policy_net.state_dict())

def test_dqn():
    # Initialize environment and agent
    env = gym.make('CartPole-v1')
    state_size = env.observation_space.shape[0]
    action_size = env.action_space.n
    agent = DQNAgent(state_size, action_size)
    assert agent.state_size == 4, "State size should be 4 for CartPole"
    assert agent.action_size == 2, "Action size should be 2 for CartPole"

    # Test replay buffer
    state = env.reset()[0]
    action = agent.select_action(state)
    next_state, reward, done, _, _ = env.step(action)
    agent.memory.push(state, action, reward, next_state, done)
    assert len(agent.memory) == 1, "Memory should contain one transition"

    # Test epsilon decay
    initial_epsilon = agent.epsilon
    agent.update_epsilon()
    assert agent.epsilon < initial_epsilon, "Epsilon should decrease after update"

    # Training loop for one episode
    episode_reward = 0
    state = env.reset()[0]
    for t in range(100):
        action = agent.select_action(state)
        next_state, reward, done, _, _ = env.step(action)
        agent.memory.push(state, action, reward, next_state, done)
        state = next_state
        episode_reward += reward
        
        if len(agent.memory) >= agent.batch_size:
            loss = agent.train_step()
            assert isinstance(loss, float), "Training loss should be a float"
        
        if done:
            break

    print("All test cases passed!")
    return agent, env

if __name__ == "__main__":
    agent, env = test_dqn()

    # Train for multiple episodes
    num_episodes = 100
    target_update_frequency = 10

    for episode in range(num_episodes):
        state = env.reset()[0]
        episode_reward = 0
        done = False
        
        while not done:
            action = agent.select_action(state)
            next_state, reward, done, _, _ = env.step(action)
            agent.memory.push(state, action, reward, next_state, done)
            state = next_state
            episode_reward += reward
            
            if len(agent.memory) >= agent.batch_size:
                loss = agent.train_step()
        
        # Update target network periodically
        if episode % target_update_frequency == 0:
            agent.update_target_network()
        
        agent.update_epsilon()
        print(f"Episode {episode + 1}, Total Reward: {episode_reward}, Epsilon: {agent.epsilon:.3f}")


Cite this Explanation

@article{ailinkdeeptech2025dqnalgo,
  title={Deep Q-Network (DQN) Implementation in PyTorch},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/dqn_algo}
}

Related Explanations