lunar-lander-dueling-dqn / dqn_agent.py
jongchullee's picture
Upload dqn_agent.py with huggingface_hub
b0759ac verified
Raw History Blame Contribute Delete
7.28 kB
import random
from collections import deque
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
class DuelingDQN(nn.Module):
"""
Dueling DQN ꡬ쑰:
κ°€μΉ˜(Value) ν•¨μˆ˜μ™€ 이점(Advantage) ν•¨μˆ˜λ₯Ό λΆ„λ¦¬ν•˜μ—¬ 더 λΉ λ₯΄κ³  μ•ˆμ •μ μΈ Q-ν•™μŠ΅ 제곡
Q(s, a) = V(s) + (A(s, a) - mean(A(s)))
"""
def __init__(self, state_dim=8, action_dim=4):
super(DuelingDQN, self).__init__()
# 곡톡 νŠΉμ§• μΆ”μΆœμΈ΅
self.feature_network = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU()
)
# μƒνƒœ κ°€μΉ˜(State Value) 슀트림 V(s)
self.value_stream = nn.Sequential(
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
# 행동 이점(Action Advantage) 슀트림 A(s, a)
self.advantage_stream = nn.Sequential(
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, action_dim)
)
def forward(self, state):
features = self.feature_network(state)
values = self.value_stream(features)
advantages = self.advantage_stream(features)
# Dueling 곡식: Q = V + (A - mean(A))
q_values = values + (advantages - advantages.mean(dim=-1, keepdim=True))
return q_values
class ReplayBuffer:
"""κ²½ν—˜ λ¦¬ν”Œλ ˆμ΄ 버퍼"""
def __init__(self, capacity=100000):
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)
states, actions, rewards, next_states, dones = zip(*batch)
return (
np.array(states, dtype=np.float32),
np.array(actions, dtype=np.int64),
np.array(rewards, dtype=np.float32),
np.array(next_states, dtype=np.float32),
np.array(dones, dtype=np.float32),
)
def __len__(self):
return len(self.buffer)
class DQNAgent:
"""
LunarLander μ°©λ₯™μš© Dueling Double-DQN Agent
- Double DQN: μ˜€λ²„μ—μŠ€ν‹°λ©”μ΄μ…˜ λ°©μ§€
- Epsilon Decay: 100% -> 5% 점진적 감쇄
- Soft Target Update (Polyak averaging)
"""
def __init__(
self,
state_dim=8,
action_dim=4,
lr=5e-4,
gamma=0.99,
tau=0.005,
buffer_size=100000,
batch_size=64,
epsilon_start=1.0,
epsilon_end=0.05,
total_episodes=1000
):
self.state_dim = state_dim
self.action_dim = action_dim
self.gamma = gamma
self.tau = tau
self.batch_size = batch_size
# 탐색λ₯ (Epsilon) νŒŒλΌλ―Έν„° (1.0 -> 0.05)
self.epsilon_start = epsilon_start
self.epsilon_end = epsilon_end
self.total_episodes = total_episodes
self.epsilon = epsilon_start
# 1000 μ—ν”Όμ†Œλ“œ 쀑 μ•½ 75% 지점(750 ep)μ—μ„œ epsilon_end(0.05)에 λ„λ‹¬ν•˜λ„λ‘ κ°μ‡„μœ¨ μ„€μ •
self.epsilon_decay = (epsilon_end / epsilon_start) ** (1.0 / (total_episodes * 0.75))
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 정책망 & 타깃망 생성
self.policy_net = DuelingDQN(state_dim, action_dim).to(self.device)
self.target_net = DuelingDQN(state_dim, action_dim).to(self.device)
self.target_net.load_state_dict(self.policy_net.state_dict())
self.target_net.eval()
self.optimizer = optim.AdamW(self.policy_net.parameters(), lr=lr, weight_decay=1e-4)
self.criterion = nn.SmoothL1Loss() # Huber Loss (λ…Έμ΄μ¦ˆμ— 강건)
self.memory = ReplayBuffer(buffer_size)
def select_action(self, state, evaluate=False):
"""
행동 선택 및 각 행동별 Q-Value λ°˜ν™˜ (μ›Ή λŒ€μ‹œλ³΄λ“œ μ‹œκ°ν™”μš©)
evaluate=True 일 경우 순수 Greedy 행동 (κ°„μ§€ μ°©λ₯™ μ‹œμ—°)
"""
state_t = torch.FloatTensor(state).unsqueeze(0).to(self.device)
with torch.no_grad():
q_values = self.policy_net(state_t).cpu().numpy()[0]
if not evaluate and random.random() < self.epsilon:
action = random.randrange(self.action_dim)
else:
action = int(np.argmax(q_values))
return action, q_values.tolist()
def update(self):
"""Double DQN 기반 신경망 κ°€μ€‘μΉ˜ 1μŠ€ν… μ—…λ°μ΄νŠΈ"""
if len(self.memory) < self.batch_size:
return None
states, actions, rewards, next_states, dones = self.memory.sample(self.batch_size)
states_t = torch.FloatTensor(states).to(self.device)
actions_t = torch.LongTensor(actions).unsqueeze(1).to(self.device)
rewards_t = torch.FloatTensor(rewards).unsqueeze(1).to(self.device)
next_states_t = torch.FloatTensor(next_states).to(self.device)
dones_t = torch.FloatTensor(dones).unsqueeze(1).to(self.device)
# ν˜„μž¬ μƒνƒœμ˜ Q-κ°’ 계산: Q(s, a)
curr_q = self.policy_net(states_t).gather(1, actions_t)
# Double DQN: Policy Net으둜 졜적 행동 선택 -> Target Net으둜 ν•΄λ‹Ή ν–‰λ™μ˜ Q-κ°’ 평가
with torch.no_grad():
best_actions = self.policy_net(next_states_t).argmax(1, keepdim=True)
next_q = self.target_net(next_states_t).gather(1, best_actions)
target_q = rewards_t + (1.0 - dones_t) * self.gamma * next_q
# Loss 계산 및 μ—­μ „νŒŒ
loss = self.criterion(curr_q, target_q)
self.optimizer.zero_grad()
loss.backward()
# μ•ˆμ •μ μΈ ν•™μŠ΅μ„ μœ„ν•œ Gradient Clipping
nn.utils.clip_grad_norm_(self.policy_net.parameters(), max_norm=10.0)
self.optimizer.step()
# 타깃망 Soft Update
self.soft_update()
return loss.item()
def soft_update(self):
"""Polyak Averaging 타깃망 μ†Œν”„νŠΈ μ—…λ°μ΄νŠΈ: ΞΈ_target = Ο„*ΞΈ_local + (1 - Ο„)*ΞΈ_target"""
for target_param, policy_param in zip(self.target_net.parameters(), self.policy_net.parameters()):
target_param.data.copy_(self.tau * policy_param.data + (1.0 - self.tau) * target_param.data)
def decay_epsilon(self):
"""μ—ν”Όμ†Œλ“œ μ’…λ£Œ μ‹œ Epsilon 감쇄"""
self.epsilon = max(self.epsilon_end, self.epsilon * self.epsilon_decay)
def save(self, filepath="best_lunar_lander_dqn.pth"):
torch.save({
'policy_net': self.policy_net.state_dict(),
'target_net': self.target_net.state_dict(),
'optimizer': self.optimizer.state_dict(),
'epsilon': self.epsilon
}, filepath)
def load(self, filepath="best_lunar_lander_dqn.pth"):
checkpoint = torch.load(filepath, map_location=self.device)
self.policy_net.load_state_dict(checkpoint['policy_net'])
self.target_net.load_state_dict(checkpoint['target_net'])
if 'optimizer' in checkpoint:
self.optimizer.load_state_dict(checkpoint['optimizer'])
if 'epsilon' in checkpoint:
self.epsilon = checkpoint['epsilon']