강의자료 · 전기공학을 위한 강화학습

5주차 Dueling DQN

[핵심 목표]

[Dueling DQN 아이디어]

[Dueling DQN 장점]

[Dueling DQN 네트워크]

[Dueling DQN 학습 과정]

1. Q값 계산

2. Loss Function 계산

3. 네트워크 업데이트

4. 타겟 네트워크 업데이트

[실습: Dueling DQN을 이용한 CartPole 실습]

1. 라이브러리 설치 및 가져오기

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. GPU 설정 및 하이퍼파라미터 정의

# GPU 사용 가능 여부 확인 및 장치 설정
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 하이퍼파라미터
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. Dueling DQN 네트워크 정의

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. 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. 학습 함수 정의

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

            # Replay Buffer에 경험 저장
            replay_buffer.push(state, action, reward, next_state, done)
            state = next_state

            # Replay Buffer에서 샘플링 및 학습
            if len(replay_buffer) >= BATCH_SIZE:
                state_batch, action_batch, reward_batch, next_state_batch, done_batch = replay_buffer.sample(BATCH_SIZE)

                # Double DQN Target Q-value 계산
                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

                # 현재 Q-value 계산
                q_values = policy_net(state_batch)
                q_values_for_actions = q_values.gather(1, action_batch.unsqueeze(1)).squeeze(1)

                # Loss 계산 및 최적화
                loss = nn.functional.mse_loss(q_values_for_actions, target_q_values)
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

        all_rewards.append(total_reward)

        # Target Network 업데이트
        if (episode+1) % TARGET_UPDATE == 0:
            target_net.load_state_dict(policy_net.state_dict())

        # 평균 리워드 출력
        if (episode+1) % PROGRESS_INTERVAL == 0:
            print(f"Episode {episode+1}: Average Reward over last {PROGRESS_INTERVAL} episodes = {np.mean(all_rewards[-PROGRESS_INTERVAL:])}")

        # 오랜 기간 리워드가 충분히 크다면 학습 종료
        if np.min(all_rewards[-TARGET_UPDATE*2:]) >= 450:
            print(f"Solved in {episode+1} episodes with reward {total_reward}!")
            break

6. 학습 실행 및 결과 확인

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!
# 렌더링 및 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