Download dqn_agent.py from jongchullee/lunar-lander-dueling-dqn: direct link, hf CLI and curl.
- Browser
- Download file 7.28 kB
-
https://huggingface.co/jongchullee/lunar-lander-dueling-dqn/resolve/main/dqn_agent.py
- Command line
-
hf download hf://jongchullee/lunar-lander-dueling-dqn/dqn_agent.py
-
curl -L -o dqn_agent.py https://huggingface.co/jongchullee/lunar-lander-dueling-dqn/resolve/main/dqn_agent.py
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'] | |