AI

DQN으로 CartPole 정복하기: 딥러닝 기반 강화학습 입문

Royzero 2025. 7. 11. 22:00
반응형

딥러닝으로 강화학습을? DQN의 시작

강화학습을 공부하다 보면, 누구나 한 번쯤 마주치는 개념이 있습니다. 바로 **DQN(Deep Q-Network)**입니다.
Q-Learning의 한계를 극복하기 위해 등장한 이 알고리즘은 딥러닝과 강화학습의 만남이라는 점에서 매우 중요한 의미를 가지고 있습니다.

이번 글에서는 DQN을 활용해 CartPole 환경을 해결하는 방법을 자세히 소개합니다. 단순히 코드를 따라 하기보다는,
이해를 바탕으로 직접 개선하고 실험해볼 수 있도록 이론과 실습을 함께 다룹니다.


1. DQN이란 무엇인가요?

기존의 Q-Learning은 Q-Table이라는 표 형태로 모든 상태와 행동의 조합에 대한 가치를 저장합니다.
하지만 현실 세계는 매우 복잡하고 상태 공간이 방대하기 때문에 Q-Table만으로는 감당이 어렵습니다.

DQN의 핵심 아이디어는 다음과 같습니다:

“Q-Table 대신 딥러닝 모델(신경망)을 사용해 Q값을 예측하자!”

즉, 에이전트가 주어진 상태를 입력으로 받아, 가능한 모든 행동에 대한 Q값을 출력하도록 딥러닝 모델을 학습하는 방식입니다.


2. CartPole 문제 다시 보기

CartPole은 OpenAI Gym에서 제공하는 대표적인 입문 환경입니다.

목표:

수레(cart) 위에 막대기(pole)가 세워져 있을 때,
막대가 넘어지지 않도록 수레를 좌우로 움직여 균형을 유지해야 합니다.

환경 정보:

  • 상태(state): 4개의 값 (막대 각도, 각속도, 수레 위치, 속도)
  • 행동(action): 왼쪽(0), 오른쪽(1)
  • 보상(reward): 매 timestep마다 +1 (막대가 쓰러지지 않는 동안)
  • 종료 조건: 막대가 일정 각도 이상으로 넘어가거나, 수레가 화면 밖으로 나가면 종료

3. Q-Learning의 한계

이전 포스트에서 무작위(Random) 행동을 하는 에이전트를 구현해봤습니다.
또 Q-Learning을 적용하면 성능이 올라가긴 하지만 다음과 같은 한계가 있습니다:

  • 상태 공간이 연속적(continuous)이라서 Q-Table로 표현이 불가능
  • 학습 속도 느림
  • 일반화 어려움 (새로운 상태에 대한 추론 불가)

이 문제를 해결하기 위해 등장한 것이 바로 DQN입니다.


4. DQN 알고리즘 구성요소

DQN은 다음과 같은 구성으로 작동합니다:

1. 신경망 모델

입력: 현재 상태
출력: 각 행동에 대한 Q값

2. 경험 리플레이 (Replay Buffer)

  • 과거의 경험 (상태, 행동, 보상, 다음 상태)을 저장하고,
  • 랜덤 샘플링을 통해 학습 데이터로 사용 (데이터 상관성 제거)

3. 타깃 네트워크 (Target Network)

  • 학습 안정성을 높이기 위해, Q값을 예측하는 네트워크를 복사해 일정 주기로 갱신
  • 학습 대상과 업데이트 모델을 분리함으로써 발산 방지

4. ε-탐욕 정책 (Epsilon-Greedy)

  • 행동 선택 시, 확률적으로 랜덤 선택 (탐험)과 최선의 선택 (활용)을 조절
  • 초기에는 탐험을 많이, 학습이 진행되며 점점 활용으로 전환

5. DQN 실습: CartPole 정복하기

이제 본격적으로 CartPole을 DQN으로 해결해보겠습니다.

필요한 패키지 설치

pip install gym torch numpy

1. 신경망 모델 정의 (PyTorch 기준)

import torch
import torch.nn as nn
import torch.nn.functional as F

class DQN(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(state_dim, 128)
        self.fc2 = nn.Linear(128, 128)
        self.out = nn.Linear(128, action_dim)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        return self.out(x)

2. 경험 리플레이 버퍼

from collections import deque
import random

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

    def push(self, transition):
        self.buffer.append(transition)

    def sample(self, batch_size):
        return random.sample(self.buffer, batch_size)

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

3. 학습 루프 (요약 버전)

import gym
import numpy as np
import torch.optim as optim

env = gym.make("CartPole-v1")
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n

policy_net = DQN(state_dim, action_dim)
target_net = DQN(state_dim, action_dim)
target_net.load_state_dict(policy_net.state_dict())

optimizer = optim.Adam(policy_net.parameters(), lr=1e-3)
replay_buffer = ReplayBuffer(10000)
batch_size = 64
gamma = 0.99
epsilon = 1.0

for episode in range(300):
    state = env.reset()
    state = torch.FloatTensor(state)
    total_reward = 0

    for t in range(500):
        if np.random.rand() < epsilon:
            action = np.random.randint(action_dim)
        else:
            with torch.no_grad():
                action = policy_net(state).argmax().item()

        next_state, reward, done, _ = env.step(action)
        next_state = torch.FloatTensor(next_state)
        replay_buffer.push((state, action, reward, next_state, done))
        state = next_state
        total_reward += reward

        # 학습 시작
        if len(replay_buffer) >= batch_size:
            transitions = replay_buffer.sample(batch_size)
            b_state, b_action, b_reward, b_next_state, b_done = zip(*transitions)

            b_state = torch.stack(b_state)
            b_action = torch.LongTensor(b_action).unsqueeze(1)
            b_reward = torch.FloatTensor(b_reward).unsqueeze(1)
            b_next_state = torch.stack(b_next_state)
            b_done = torch.FloatTensor(b_done).unsqueeze(1)

            q_values = policy_net(b_state).gather(1, b_action)
            next_q_values = target_net(b_next_state).max(1)[0].detach().unsqueeze(1)
            target = b_reward + gamma * next_q_values * (1 - b_done)

            loss = F.mse_loss(q_values, target)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        if done:
            break

    epsilon = max(0.01, epsilon * 0.995)  # 점점 탐험 줄이기

    # 타깃 네트워크 업데이트
    if episode % 10 == 0:
        target_net.load_state_dict(policy_net.state_dict())

    print(f"Episode {episode}, Total Reward: {total_reward}")

6. 결과 분석

위 코드를 실행하면 에이전트가 점점 막대를 오래 세우는 능력이 향상됩니다.
처음에는 몇 초도 버티지 못하던 에이전트가, 100번째 에피소드 이후부터는 200 이상 점수를 기록하며 거의 완벽하게 문제를 해결합니다.


7. DQN의 한계와 발전 방향

DQN은 놀라운 성능을 보이지만 다음과 같은 한계도 있습니다:

  • 환경이 복잡해질수록 학습 불안정
  • 이산 행동 공간만 지원 (연속값 불가)
  • 경험을 모두 동일한 중요도로 학습

이러한 문제를 해결하기 위해 등장한 알고리즘이 있습니다:

  • Double DQN
  • Dueling DQN
  • Prioritized Replay
  • Rainbow DQN

앞으로의 글에서는 이들 알고리즘도 단계별로 소개해드릴 예정입니다.


반응형