DQN의 등장 배경
지난 편에서 OpenAI Gym 환경 설정과 기본적인 Q-Learning 에이전트를 구현해봤습니다. 하지만 테이블 기반 Q-Learning은 상태 공간이 큰 환경에서는 사용할 수 없다는 치명적인 한계가 있었죠. Atari 게임처럼 화면 픽셀을 입력으로 받는 경우, 가능한 상태의 수가 천문학적으로 많아지기 때문입니다.
DeepMind는 2013년 이 문제를 딥러닝으로 해결한 DQN(Deep Q-Network)을 발표하며 강화학습 역사에 한 획을 그었습니다. 사람 수준 이상의 성능으로 49개 Atari 게임을 플레이한 이 알고리즘은 강화학습과 딥러닝의 완벽한 결합을 보여줬습니다.
DQN의 핵심 아이디어
1. Q-Table을 신경망으로 대체
DQN의 첫 번째 혁신은 Q-Table을 신경망으로 근사(approximation)한 것입니다:
import torch
import torch.nn as nn
class DQN(nn.Module):
def __init__(self, input_shape, n_actions):
super(DQN, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1),
nn.ReLU()
)
conv_out_size = self._get_conv_out(input_shape)
self.fc = nn.Sequential(
nn.Linear(conv_out_size, 512),
nn.ReLU(),
nn.Linear(512, n_actions)
)
def _get_conv_out(self, shape):
o = self.conv(torch.zeros(1, *shape))
return int(np.prod(o.size()))
def forward(self, x):
conv_out = self.conv(x).view(x.size()[0], -1)
return self.fc(conv_out)
이 네트워크는 84×84 크기의 4개 연속 프레임을 입력받아 각 행동의 Q-value를 출력합니다.
2. Experience Replay
신경망 학습의 안정성을 위해 리플레이 버퍼를 도입했습니다:
from collections import deque
import random
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):
batch = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = zip(*batch)
return np.array(state), action, reward, np.array(next_state), done
def __len__(self):
return len(self.buffer)
왜 필요할까요?
| 문제 | 해결 방법 |
|---|---|
| 연속된 경험의 상관관계 | 랜덤 샘플링으로 독립성 확보 |
| 데이터 효율성 낮음 | 과거 경험 재사용 |
| 희귀한 경험 손실 | 버퍼에 저장해 반복 학습 |
3. Target Network
Q-value 업데이트의 불안정성을 해결하기 위해 별도의 타겟 네트워크를 유지합니다:
class DQNAgent:
def __init__(self, state_shape, n_actions):
self.policy_net = DQN(state_shape, n_actions).to(device)
self.target_net = DQN(state_shape, n_actions).to(device)
self.target_net.load_state_dict(self.policy_net.state_dict())
self.target_net.eval()
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=1e-4)
self.memory = ReplayBuffer(100000)
self.steps_done = 0
def update_target_network(self):
self.target_net.load_state_dict(self.policy_net.state_dict())
Target network는 일정 주기(보통 10,000 step)마다만 업데이트되어, 학습 목표가 안정적으로 유지됩니다.
전체 학습 루프 구현
import gymnasium as gym
import ale_py
def train_dqn():
env = gym.make('ALE/Breakout-v5', render_mode='rgb_array')
env = gym.wrappers.AtariPreprocessing(env, frame_skip=4)
env = gym.wrappers.FrameStack(env, 4)
agent = DQNAgent(
state_shape=(4, 84, 84),
n_actions=env.action_space.n
)
epsilon = 1.0
epsilon_decay = 0.999995
epsilon_min = 0.1
for episode in range(10000):
state, _ = env.reset()
episode_reward = 0
while True:
# Epsilon-greedy 행동 선택
if random.random() < epsilon:
action = env.action_space.sample()
else:
with torch.no_grad():
state_t = torch.FloatTensor(state).unsqueeze(0).to(device)
q_values = agent.policy_net(state_t)
action = q_values.max(1)[1].item()
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
agent.memory.push(state, action, reward, next_state, done)
state = next_state
episode_reward += reward
# 학습
if len(agent.memory) > 10000:
loss = agent.train_step(batch_size=32)
# Target network 업데이트
if agent.steps_done % 10000 == 0:
agent.update_target_network()
agent.steps_done += 1
epsilon = max(epsilon_min, epsilon * epsilon_decay)
if done:
break
if episode % 10 == 0:
print(f"Episode {episode}, Reward: {episode_reward}, Epsilon: {epsilon:.3f}")
학습 단계 상세 구현
def train_step(self, batch_size):
states, actions, rewards, next_states, dones = self.memory.sample(batch_size)
states = torch.FloatTensor(states).to(device)
actions = torch.LongTensor(actions).to(device)
rewards = torch.FloatTensor(rewards).to(device)
next_states = torch.FloatTensor(next_states).to(device)
dones = torch.FloatTensor(dones).to(device)
# 현재 Q-value
current_q = self.policy_net(states).gather(1, actions.unsqueeze(1))
# 타겟 Q-value (Double DQN)
with torch.no_grad():
next_actions = self.policy_net(next_states).max(1)[1]
next_q = self.target_net(next_states).gather(1, next_actions.unsqueeze(1))
target_q = rewards.unsqueeze(1) + (1 - dones.unsqueeze(1)) * 0.99 * next_q
# Huber Loss
loss = nn.SmoothL1Loss()(current_q, target_q)
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.policy_net.parameters(), 10)
self.optimizer.step()
return loss.item()
DQN의 주요 개선 기법
Double DQN
기존 DQN은 Q-value를 과대평가하는 경향이 있습니다. Double DQN은 행동 선택과 평가를 분리합니다:
핵심: Policy network로 행동을 선택하고, Target network로 그 행동의 가치를 평가
Dueling DQN
Q-value를 State Value와 Advantage로 분해합니다:
class DuelingDQN(nn.Module):
def __init__(self, input_shape, n_actions):
super(DuelingDQN, self).__init__()
self.conv = nn.Sequential(...)
conv_out_size = self._get_conv_out(input_shape)
# Value stream
self.value_stream = nn.Sequential(
nn.Linear(conv_out_size, 512),
nn.ReLU(),
nn.Linear(512, 1)
)
# Advantage stream
self.advantage_stream = nn.Sequential(
nn.Linear(conv_out_size, 512),
nn.ReLU(),
nn.Linear(512, n_actions)
)
def forward(self, x):
conv_out = self.conv(x).view(x.size()[0], -1)
value = self.value_stream(conv_out)
advantage = self.advantage_stream(conv_out)
# Q(s,a) = V(s) + (A(s,a) - mean(A(s,a')))
q_values = value + (advantage - advantage.mean(dim=1, keepdim=True))
return q_values
실전 팁
하이퍼파라미터 설정:
| 파라미터 | 추천값 | 설명 |
|---|---|---|
| Learning Rate | 1e-4 | Adam optimizer 기준 |
| Batch Size | 32 | 메모리 효율과 성능 균형 |
| Replay Buffer | 100K~1M | 게임 복잡도에 따라 조정 |
| Target Update | 10K steps | 너무 자주 업데이트하면 불안정 |
| Gamma | 0.99 | 장기 보상 중시 |
| Epsilon Decay | 0.999995 | 100만 step 기준 |
주의사항:
– 초반 10만 step은 학습이 거의 안 됩니다 (탐험 단계)
– GPU 필수 (CPU로는 학습에 수 주 소요)
– 프레임 스킵(4)과 스택(4) 필수
– Reward clipping (-1, 0, 1)으로 안정성 확보
마무리
DQN은 강화학습을 실용적인 수준으로 끌어올린 혁신적인 알고리즘입니다. Experience Replay와 Target Network라는 간단하지만 강력한 아이디어로 딥러닝의 불안정성을 극복했죠.
다음 편에서는 DQN의 한계인 연속 행동 공간을 다루는 PPO 알고리즘을 살펴보겠습니다. 로봇 제어나 레이싱 게임처럼 섬세한 조작이 필요한 환경에서 어떻게 강화학습을 적용하는지 알아봅시다!
이 글이 도움이 되셨나요?
Buy me a coffee
PPO 알고리즘으로 복잡한 게임 마스터하기 – 연속 행동 공간 다루기 – DevTips에 답글 남기기 응답 취소