lookarooka commited on
Commit
7cd7106
·
verified ·
1 Parent(s): 57f1bd5

Upload dqn_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. dqn_agent.py +197 -0
dqn_agent.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from collections import deque
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.optim as optim
7
+
8
+ class DuelingQNetwork(nn.Module):
9
+ """
10
+ Dueling DQN Network Architecture:
11
+ Splits into Value stream V(s) and Advantage stream A(s, a)
12
+ Q(s, a) = V(s) + (A(s, a) - mean(A(s, a)))
13
+ """
14
+ def __init__(self, state_dim=8, action_dim=4, hidden_dim=128):
15
+ super(DuelingQNetwork, self).__init__()
16
+
17
+ # Shared feature extractor
18
+ self.feature_layer = nn.Sequential(
19
+ nn.Linear(state_dim, hidden_dim),
20
+ nn.ReLU(),
21
+ nn.Linear(hidden_dim, hidden_dim),
22
+ nn.ReLU()
23
+ )
24
+
25
+ # State-Value stream V(s)
26
+ self.value_stream = nn.Sequential(
27
+ nn.Linear(hidden_dim, 64),
28
+ nn.ReLU(),
29
+ nn.Linear(64, 1)
30
+ )
31
+
32
+ # Advantage stream A(s, a)
33
+ self.advantage_stream = nn.Sequential(
34
+ nn.Linear(hidden_dim, 64),
35
+ nn.ReLU(),
36
+ nn.Linear(64, action_dim)
37
+ )
38
+
39
+ def forward(self, state):
40
+ features = self.feature_layer(state)
41
+ value = self.value_stream(features)
42
+ advantages = self.advantage_stream(features)
43
+ # Combine streams with mean subtraction for stability
44
+ q_values = value + (advantages - advantages.mean(dim=-1, keepdim=True))
45
+ return q_values
46
+
47
+
48
+ class ReplayBuffer:
49
+ def __init__(self, capacity=100000):
50
+ self.buffer = deque(maxlen=capacity)
51
+
52
+ def push(self, state, action, reward, next_state, done):
53
+ self.buffer.append((state, action, reward, next_state, done))
54
+
55
+ def sample(self, batch_size):
56
+ states, actions, rewards, next_states, dones = zip(*random.sample(self.buffer, batch_size))
57
+ return (
58
+ np.array(states, dtype=np.float32),
59
+ np.array(actions, dtype=np.int64),
60
+ np.array(rewards, dtype=np.float32),
61
+ np.array(next_states, dtype=np.float32),
62
+ np.array(dones, dtype=np.float32)
63
+ )
64
+
65
+ def __len__(self):
66
+ return len(self.buffer)
67
+
68
+
69
+ class DQNAgent:
70
+ def __init__(
71
+ self,
72
+ state_dim=8,
73
+ action_dim=4,
74
+ lr=5e-4,
75
+ gamma=0.99,
76
+ tau=0.005,
77
+ buffer_size=100000,
78
+ batch_size=64,
79
+ eps_start=1.0,
80
+ eps_end=0.05,
81
+ eps_decay_episodes=800,
82
+ device=None
83
+ ):
84
+ self.state_dim = state_dim
85
+ self.action_dim = action_dim
86
+ self.gamma = gamma
87
+ self.tau = tau
88
+ self.batch_size = batch_size
89
+ self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
90
+
91
+ # Epsilon schedule parameters (1.0 -> 0.05 over 1000 episodes)
92
+ self.eps_start = eps_start
93
+ self.eps_end = eps_end
94
+ self.eps_decay_episodes = eps_decay_episodes
95
+ self.epsilon = eps_start
96
+
97
+ # Networks
98
+ self.q_network = DuelingQNetwork(state_dim, action_dim).to(self.device)
99
+ self.target_network = DuelingQNetwork(state_dim, action_dim).to(self.device)
100
+ self.target_network.load_state_dict(self.q_network.state_dict())
101
+ self.target_network.eval()
102
+
103
+ self.optimizer = optim.Adam(self.q_network.parameters(), lr=lr)
104
+ self.criterion = nn.SmoothL1Loss() # Huber loss for gradient robustness
105
+ self.memory = ReplayBuffer(capacity=buffer_size)
106
+
107
+ self.total_steps = 0
108
+ self.current_episode = 0
109
+
110
+ def update_epsilon(self, episode):
111
+ """Linearly decay epsilon from 1.0 (100%) to 0.05 (5%) over decay episodes"""
112
+ self.current_episode = episode
113
+ progress = min(1.0, episode / max(1, self.eps_decay_episodes))
114
+ self.epsilon = self.eps_start - progress * (self.eps_start - self.eps_end)
115
+ return self.epsilon
116
+
117
+ def act(self, state, evaluate=False):
118
+ """
119
+ Choose action using epsilon-greedy policy.
120
+ Returns: (action, q_values_list)
121
+ """
122
+ state_t = torch.FloatTensor(state).unsqueeze(0).to(self.device)
123
+ self.q_network.eval()
124
+ with torch.no_grad():
125
+ q_values = self.q_network(state_t).cpu().numpy()[0]
126
+ self.q_network.train()
127
+
128
+ # Epsilon-greedy
129
+ if not evaluate and random.random() < self.epsilon:
130
+ action = random.randrange(self.action_dim)
131
+ else:
132
+ action = int(np.argmax(q_values))
133
+
134
+ return action, q_values.tolist()
135
+
136
+ def step(self, state, action, reward, next_state, done):
137
+ self.memory.push(state, action, reward, next_state, done)
138
+ self.total_steps += 1
139
+
140
+ loss = None
141
+ if len(self.memory) >= self.batch_size:
142
+ loss = self.learn()
143
+ return loss
144
+
145
+ def learn(self):
146
+ states, actions, rewards, next_states, dones = self.memory.sample(self.batch_size)
147
+
148
+ states_t = torch.FloatTensor(states).to(self.device)
149
+ actions_t = torch.LongTensor(actions).unsqueeze(1).to(self.device)
150
+ rewards_t = torch.FloatTensor(rewards).unsqueeze(1).to(self.device)
151
+ next_states_t = torch.FloatTensor(next_states).to(self.device)
152
+ dones_t = torch.FloatTensor(dones).unsqueeze(1).to(self.device)
153
+
154
+ # Current Q-values: Q(s, a)
155
+ curr_q = self.q_network(states_t).gather(1, actions_t)
156
+
157
+ # Double DQN: select best action using online network, evaluate Q using target network
158
+ with torch.no_grad():
159
+ best_actions = self.q_network(next_states_t).argmax(1, keepdim=True)
160
+ next_q = self.target_network(next_states_t).gather(1, best_actions)
161
+ target_q = rewards_t + (1.0 - dones_t) * self.gamma * next_q
162
+
163
+ loss = self.criterion(curr_q, target_q)
164
+
165
+ self.optimizer.zero_grad()
166
+ loss.backward()
167
+ nn.utils.clip_grad_norm_(self.q_network.parameters(), max_norm=10.0)
168
+ self.optimizer.step()
169
+
170
+ # Soft target network update
171
+ self.soft_update()
172
+
173
+ return float(loss.item())
174
+
175
+ def soft_update(self):
176
+ """Soft update target network: θ_target = τ*θ_local + (1 - τ)*θ_target"""
177
+ for target_param, local_param in zip(self.target_network.parameters(), self.q_network.parameters()):
178
+ target_param.data.copy_(self.tau * local_param.data + (1.0 - self.tau) * target_param.data)
179
+
180
+ def save(self, filepath="dqn_lunar_lander.pt"):
181
+ torch.save({
182
+ 'q_network_state_dict': self.q_network.state_dict(),
183
+ 'target_network_state_dict': self.target_network.state_dict(),
184
+ 'optimizer_state_dict': self.optimizer.state_dict(),
185
+ 'epsilon': self.epsilon,
186
+ 'current_episode': self.current_episode,
187
+ 'total_steps': self.total_steps
188
+ }, filepath)
189
+
190
+ def load(self, filepath="dqn_lunar_lander.pt"):
191
+ checkpoint = torch.load(filepath, map_location=self.device)
192
+ self.q_network.load_state_dict(checkpoint['q_network_state_dict'])
193
+ self.target_network.load_state_dict(checkpoint['target_network_state_dict'])
194
+ self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
195
+ self.epsilon = checkpoint.get('epsilon', self.eps_end)
196
+ self.current_episode = checkpoint.get('current_episode', 0)
197
+ self.total_steps = checkpoint.get('total_steps', 0)