ezppo 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ezppo/__init__.py +29 -0
- ezppo/agents.py +475 -0
- ezppo/networks.py +145 -0
- ezppo/normalizer.py +105 -0
- ezppo-0.0.0.dist-info/METADATA +149 -0
- ezppo-0.0.0.dist-info/RECORD +8 -0
- ezppo-0.0.0.dist-info/WHEEL +5 -0
- ezppo-0.0.0.dist-info/top_level.txt +1 -0
ezppo/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EzPPO - A simple, unified PPO library for Discrete and Continuous environments.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from ezppo import DiscretePPOAgent, ContinuousPPOAgent
|
|
6
|
+
|
|
7
|
+
# Discrete example (CartPole):
|
|
8
|
+
agent = DiscretePPOAgent(state_dim=4, action_dim=2)
|
|
9
|
+
action, log_prob = agent.act(state)
|
|
10
|
+
|
|
11
|
+
# Continuous example (Pendulum):
|
|
12
|
+
agent = ContinuousPPOAgent(state_dim=3, action_dim=1)
|
|
13
|
+
action, log_prob, raw_action = agent.act(state)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .networks import DiscretePPO, ContinuousPPO, BasePPO
|
|
17
|
+
from .normalizer import RunningNormalizer
|
|
18
|
+
from .agents import BasePPOAgent, DiscretePPOAgent, ContinuousPPOAgent
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DiscretePPO",
|
|
23
|
+
"ContinuousPPO",
|
|
24
|
+
"BasePPO",
|
|
25
|
+
"RunningNormalizer",
|
|
26
|
+
"BasePPOAgent",
|
|
27
|
+
"DiscretePPOAgent",
|
|
28
|
+
"ContinuousPPOAgent",
|
|
29
|
+
]
|
ezppo/agents.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent classes for Discrete and Continuous PPO.
|
|
3
|
+
|
|
4
|
+
Both agents share a common base class with:
|
|
5
|
+
- Trajectory collection (store/clear)
|
|
6
|
+
- GAE computation
|
|
7
|
+
- Observation normalization
|
|
8
|
+
- Mini-batch training loop
|
|
9
|
+
- Device management
|
|
10
|
+
- Save/load checkpointing
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import torch
|
|
14
|
+
import numpy as np
|
|
15
|
+
import torch.optim as optim
|
|
16
|
+
from torch.distributions import Categorical, Normal
|
|
17
|
+
from torch.nn import MSELoss
|
|
18
|
+
|
|
19
|
+
from .networks import DiscretePPO, ContinuousPPO
|
|
20
|
+
from .normalizer import RunningNormalizer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BasePPOAgent:
|
|
24
|
+
"""Shared base for both discrete and continuous agents."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, network, lr=3e-4, gamma=0.99, gae_lambda=0.95,
|
|
27
|
+
epsilon=0.2, critic_coef=0.5, entropy_coef=0.01,
|
|
28
|
+
num_envs=1, trajectory_size=2048, device="cpu",
|
|
29
|
+
normalize_observations=True, default_num_epochs=4,
|
|
30
|
+
default_batch_size=64):
|
|
31
|
+
self.network = network
|
|
32
|
+
self.optimizer = optim.Adam(network.parameters(), lr=lr, eps=1e-5)
|
|
33
|
+
|
|
34
|
+
self.trajectory = []
|
|
35
|
+
self._num_envs = num_envs
|
|
36
|
+
self._trajectory_size = trajectory_size
|
|
37
|
+
|
|
38
|
+
# PPO hyperparameters
|
|
39
|
+
self.gamma = gamma
|
|
40
|
+
self.gae_lambda = gae_lambda
|
|
41
|
+
self.epsilon = epsilon
|
|
42
|
+
self.critic_coef = critic_coef
|
|
43
|
+
self.entropy_coef = entropy_coef
|
|
44
|
+
self.lr = lr
|
|
45
|
+
self.device = torch.device(device)
|
|
46
|
+
|
|
47
|
+
self.normalize_observations = normalize_observations
|
|
48
|
+
self.default_num_epochs = default_num_epochs
|
|
49
|
+
self.default_batch_size = default_batch_size
|
|
50
|
+
|
|
51
|
+
# Set up observation normalizer (optional)
|
|
52
|
+
if self.normalize_observations:
|
|
53
|
+
self.obs_normalizer = RunningNormalizer(network.input_dim)
|
|
54
|
+
self.obs_normalizer.to(self.device)
|
|
55
|
+
else:
|
|
56
|
+
self.obs_normalizer = None
|
|
57
|
+
|
|
58
|
+
self.network.to(self.device)
|
|
59
|
+
|
|
60
|
+
def store(self, packet: tuple) -> None:
|
|
61
|
+
"""Store a transition in the trajectory."""
|
|
62
|
+
self.trajectory.append(packet)
|
|
63
|
+
|
|
64
|
+
def clear(self) -> None:
|
|
65
|
+
"""Clear the trajectory buffer."""
|
|
66
|
+
self.trajectory = []
|
|
67
|
+
|
|
68
|
+
def _compute_gae(self, states, rewards, values, next_values, terminal_mask):
|
|
69
|
+
"""
|
|
70
|
+
Compute Generalized Advantage Estimation (GAE).
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
states: [T, B, obs_dim] or [T, obs_dim]
|
|
74
|
+
rewards: [T, B] or [T]
|
|
75
|
+
values: [T, B] or [T] - value of current states
|
|
76
|
+
next_values: [T, B] or [T] - value of next states
|
|
77
|
+
terminal_mask: [T, B] or [T] - 0 if terminal, 1 if not
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
advantages: [T, B] or [T]
|
|
81
|
+
critic_targets: [T, B] or [T] - advantages + values (for critic loss)
|
|
82
|
+
"""
|
|
83
|
+
# Handle both single-env [T] and multi-env [T, B] cases
|
|
84
|
+
if rewards.ndim == 1:
|
|
85
|
+
T = rewards.shape[0]
|
|
86
|
+
advantages = torch.zeros(T, device=self.device)
|
|
87
|
+
last_advantage = 0
|
|
88
|
+
for t in reversed(range(T)):
|
|
89
|
+
td_error = rewards[t] + self.gamma * next_values[t] * terminal_mask[t] - values[t]
|
|
90
|
+
last_advantage = td_error + self.gamma * self.gae_lambda * last_advantage * terminal_mask[t]
|
|
91
|
+
advantages[t] = last_advantage
|
|
92
|
+
else:
|
|
93
|
+
T, B = rewards.shape
|
|
94
|
+
advantages = torch.zeros(T, B, device=self.device)
|
|
95
|
+
last_advantage = 0
|
|
96
|
+
for t in reversed(range(T)):
|
|
97
|
+
td_error = rewards[t] + self.gamma * next_values[t] * terminal_mask[t] - values[t]
|
|
98
|
+
last_advantage = td_error + self.gamma * self.gae_lambda * last_advantage * terminal_mask[t]
|
|
99
|
+
advantages[t] = last_advantage
|
|
100
|
+
|
|
101
|
+
critic_targets = advantages + values
|
|
102
|
+
return advantages, critic_targets
|
|
103
|
+
|
|
104
|
+
def _flatten_batch(self, *tensors):
|
|
105
|
+
"""Flatten [T, B, ...] to [T*B, ...] for batched training."""
|
|
106
|
+
return [t.flatten(end_dim=1) for t in tensors]
|
|
107
|
+
|
|
108
|
+
def train(self, num_epochs=None, batch_size=None):
|
|
109
|
+
"""
|
|
110
|
+
Train PPO on the collected trajectory.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
num_epochs: Number of PPO update epochs (uses default if None).
|
|
114
|
+
batch_size: Mini-batch size for training (uses default if None).
|
|
115
|
+
"""
|
|
116
|
+
if not self.trajectory:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
num_epochs = num_epochs if num_epochs is not None else self.default_num_epochs
|
|
120
|
+
batch_size = batch_size if batch_size is not None else self.default_batch_size
|
|
121
|
+
|
|
122
|
+
# Unpack trajectory
|
|
123
|
+
states, actions, rewards, old_log_probs, next_states, terminateds = zip(*self.trajectory)
|
|
124
|
+
|
|
125
|
+
# Convert to tensors
|
|
126
|
+
states = torch.from_numpy(np.stack(states)).float().to(self.device)
|
|
127
|
+
actions = torch.from_numpy(np.stack(actions)).float().to(self.device)
|
|
128
|
+
rewards = torch.from_numpy(np.stack(rewards)).float().to(self.device)
|
|
129
|
+
old_log_probs = torch.from_numpy(np.stack(old_log_probs)).float().to(self.device)
|
|
130
|
+
next_states = torch.from_numpy(np.stack(next_states)).float().to(self.device)
|
|
131
|
+
terminateds = torch.from_numpy(np.stack(terminateds)).float().to(self.device)
|
|
132
|
+
|
|
133
|
+
# Ensure consistent shapes
|
|
134
|
+
# For single env, stacking gives [T], [T, obs_dim] etc.
|
|
135
|
+
# We want [T, B] for scalar tensors and [T, B, obs_dim] for state tensors
|
|
136
|
+
if rewards.ndim == 1:
|
|
137
|
+
# Single environment case
|
|
138
|
+
rewards = rewards.unsqueeze(1) # [T, 1]
|
|
139
|
+
old_log_probs = old_log_probs.unsqueeze(1) # [T, 1]
|
|
140
|
+
terminateds = terminateds.unsqueeze(1) # [T, 1]
|
|
141
|
+
actions = actions.unsqueeze(1) # [T, 1]
|
|
142
|
+
if states.ndim == 2:
|
|
143
|
+
states = states.unsqueeze(1) # [T, 1, obs_dim]
|
|
144
|
+
if next_states.ndim == 2:
|
|
145
|
+
next_states = next_states.unsqueeze(1) # [T, 1, obs_dim]
|
|
146
|
+
|
|
147
|
+
terminal_mask = (1 - terminateds).float()
|
|
148
|
+
|
|
149
|
+
# Compute values
|
|
150
|
+
values = self.network.predict_value(states).squeeze(-1)
|
|
151
|
+
next_values = self.network.predict_value(next_states).squeeze(-1)
|
|
152
|
+
|
|
153
|
+
# Compute GAE
|
|
154
|
+
advantages, critic_targets = self._compute_gae(
|
|
155
|
+
states, rewards, values, next_values, terminal_mask
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Normalize advantages
|
|
159
|
+
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
|
160
|
+
|
|
161
|
+
# Detach targets (they shouldn't be used for backprop)
|
|
162
|
+
old_log_probs = old_log_probs.detach()
|
|
163
|
+
advantages = advantages.detach()
|
|
164
|
+
critic_targets = critic_targets.detach()
|
|
165
|
+
|
|
166
|
+
# Flatten for batched training
|
|
167
|
+
states, actions, old_log_probs, critic_targets, advantages = \
|
|
168
|
+
self._flatten_batch(states, actions, old_log_probs, critic_targets, advantages)
|
|
169
|
+
|
|
170
|
+
total_samples = states.shape[0]
|
|
171
|
+
avg_policy_loss = 0
|
|
172
|
+
avg_critic_loss = 0
|
|
173
|
+
avg_entropy = 0
|
|
174
|
+
num_updates = 0
|
|
175
|
+
|
|
176
|
+
# Training loop
|
|
177
|
+
for _ in range(num_epochs):
|
|
178
|
+
indices = np.arange(total_samples)
|
|
179
|
+
np.random.shuffle(indices)
|
|
180
|
+
|
|
181
|
+
for start in range(0, total_samples, batch_size):
|
|
182
|
+
end = min(start + batch_size, total_samples)
|
|
183
|
+
batch_idx = indices[start:end]
|
|
184
|
+
|
|
185
|
+
batch_states = states[batch_idx]
|
|
186
|
+
batch_actions = actions[batch_idx]
|
|
187
|
+
batch_old_log_probs = old_log_probs[batch_idx]
|
|
188
|
+
batch_critic_targets = critic_targets[batch_idx]
|
|
189
|
+
batch_advantages = advantages[batch_idx]
|
|
190
|
+
|
|
191
|
+
# Get new log probs and entropy (implemented by subclass)
|
|
192
|
+
new_log_probs, entropy = self._get_log_prob_and_entropy(batch_states, batch_actions)
|
|
193
|
+
|
|
194
|
+
# Compute ratios
|
|
195
|
+
ratio = torch.exp(new_log_probs - batch_old_log_probs)
|
|
196
|
+
clipped_ratio = torch.clamp(ratio, 1 - self.epsilon, 1 + self.epsilon)
|
|
197
|
+
|
|
198
|
+
# Policy loss (clipped surrogate)
|
|
199
|
+
policy_loss = -torch.min(
|
|
200
|
+
ratio * batch_advantages,
|
|
201
|
+
clipped_ratio * batch_advantages
|
|
202
|
+
).mean()
|
|
203
|
+
|
|
204
|
+
# Critic loss (MSE)
|
|
205
|
+
new_values = self.network.predict_value(batch_states).squeeze(-1)
|
|
206
|
+
critic_loss = MSELoss()(new_values, batch_critic_targets)
|
|
207
|
+
|
|
208
|
+
# Entropy bonus
|
|
209
|
+
entropy_loss = -entropy.mean()
|
|
210
|
+
|
|
211
|
+
# Total loss
|
|
212
|
+
total_loss = policy_loss + self.critic_coef * critic_loss + self.entropy_coef * entropy_loss
|
|
213
|
+
|
|
214
|
+
# Optimizer step
|
|
215
|
+
self.optimizer.zero_grad()
|
|
216
|
+
total_loss.backward()
|
|
217
|
+
torch.nn.utils.clip_grad_norm_(self.network.parameters(), max_norm=0.5)
|
|
218
|
+
self.optimizer.step()
|
|
219
|
+
|
|
220
|
+
avg_policy_loss += policy_loss.item()
|
|
221
|
+
avg_critic_loss += critic_loss.item()
|
|
222
|
+
avg_entropy += entropy_loss.item()
|
|
223
|
+
num_updates += 1
|
|
224
|
+
|
|
225
|
+
# Update observation normalizer (if enabled)
|
|
226
|
+
if self.obs_normalizer is not None:
|
|
227
|
+
self.obs_normalizer.update(states.cpu().detach())
|
|
228
|
+
|
|
229
|
+
# Move back to CPU
|
|
230
|
+
self.network.to("cpu")
|
|
231
|
+
if self.obs_normalizer is not None:
|
|
232
|
+
self.obs_normalizer.to("cpu")
|
|
233
|
+
|
|
234
|
+
# Log
|
|
235
|
+
if num_updates > 0:
|
|
236
|
+
print(f"[{type(self).__name__}] Policy: {avg_policy_loss/num_updates:.4f} | "
|
|
237
|
+
f"Critic: {avg_critic_loss/num_updates:.4f} | "
|
|
238
|
+
f"Entropy: {avg_entropy/num_updates:.4f}")
|
|
239
|
+
|
|
240
|
+
def save(self, model_path: str, normalizer_path: str = None) -> None:
|
|
241
|
+
"""Save checkpoint."""
|
|
242
|
+
torch.save(self.network.state_dict(), model_path)
|
|
243
|
+
if self.obs_normalizer is not None:
|
|
244
|
+
self.obs_normalizer.save(normalizer_path)
|
|
245
|
+
|
|
246
|
+
def load(self, model_path: str, normalizer_path: str | None = None) -> None:
|
|
247
|
+
"""Load checkpoint.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
model_path: Path to the network state dict file.
|
|
251
|
+
normalizer_path: Path to the normalizer file (only used if
|
|
252
|
+
normalize_observations=True; defaults to None).
|
|
253
|
+
"""
|
|
254
|
+
self.network.load_state_dict(torch.load(model_path, weights_only=True))
|
|
255
|
+
if self.obs_normalizer is not None:
|
|
256
|
+
self.obs_normalizer.load(normalizer_path)
|
|
257
|
+
self.network.to(self.device)
|
|
258
|
+
if self.obs_normalizer is not None:
|
|
259
|
+
self.obs_normalizer.to(self.device)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class DiscretePPOAgent(BasePPOAgent):
|
|
263
|
+
"""
|
|
264
|
+
PPO agent for discrete action spaces.
|
|
265
|
+
|
|
266
|
+
Usage:
|
|
267
|
+
agent = DiscretePPOAgent(state_dim=4, action_dim=2)
|
|
268
|
+
action, log_prob = agent.act(state)
|
|
269
|
+
agent.store((state, action, reward, log_prob, next_state, terminated))
|
|
270
|
+
agent.train()
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self, state_dim: int, action_dim: int,
|
|
274
|
+
architecture: dict = None,
|
|
275
|
+
lr: float = 3e-4, gamma: float = 0.99,
|
|
276
|
+
gae_lambda: float = 0.95, epsilon: float = 0.2,
|
|
277
|
+
critic_coef: float = 0.5, entropy_coef: float = 0.001,
|
|
278
|
+
num_envs: int = 1, trajectory_size: int = 2048,
|
|
279
|
+
device: str = "cpu", checkpoint: str = None,
|
|
280
|
+
normalize_observations: bool = True,
|
|
281
|
+
default_num_epochs: int = 4,
|
|
282
|
+
default_batch_size: int = 64):
|
|
283
|
+
|
|
284
|
+
# Default architecture
|
|
285
|
+
if architecture is None:
|
|
286
|
+
architecture = {
|
|
287
|
+
"actor": [state_dim, 64, 64, action_dim],
|
|
288
|
+
"critic": [state_dim, 64, 64, 1]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
network = DiscretePPO(architecture)
|
|
292
|
+
super().__init__(network, lr, gamma, gae_lambda, epsilon,
|
|
293
|
+
critic_coef, entropy_coef, num_envs,
|
|
294
|
+
trajectory_size, device,
|
|
295
|
+
normalize_observations, default_num_epochs,
|
|
296
|
+
default_batch_size)
|
|
297
|
+
|
|
298
|
+
if checkpoint:
|
|
299
|
+
if self.obs_normalizer is not None:
|
|
300
|
+
self.load(checkpoint + ".pth", checkpoint + "_norm.npy")
|
|
301
|
+
else:
|
|
302
|
+
self.load(checkpoint + ".pth", None)
|
|
303
|
+
|
|
304
|
+
def act(self, state) -> tuple:
|
|
305
|
+
"""
|
|
306
|
+
Select an action (stochastic).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
state: Observation (numpy array or tensor).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
(action, log_prob)
|
|
313
|
+
"""
|
|
314
|
+
state = torch.as_tensor(state, dtype=torch.float32)
|
|
315
|
+
if state.dim() == 1:
|
|
316
|
+
state = state.unsqueeze(0)
|
|
317
|
+
if self.normalize_observations:
|
|
318
|
+
state = self.obs_normalizer.normalize(state).to(self.device)
|
|
319
|
+
else:
|
|
320
|
+
state = state.to(self.device)
|
|
321
|
+
|
|
322
|
+
logits = self.network(state)
|
|
323
|
+
dist = Categorical(logits=logits)
|
|
324
|
+
action = dist.sample()
|
|
325
|
+
log_prob = dist.log_prob(action.squeeze(0))
|
|
326
|
+
|
|
327
|
+
return action.squeeze(0).cpu().detach(), log_prob.cpu().detach()
|
|
328
|
+
|
|
329
|
+
def act_deterministic(self, state) -> int:
|
|
330
|
+
"""Select action greedily (no sampling)."""
|
|
331
|
+
state = torch.as_tensor(state, dtype=torch.float32)
|
|
332
|
+
if state.dim() == 1:
|
|
333
|
+
state = state.unsqueeze(0)
|
|
334
|
+
if self.normalize_observations:
|
|
335
|
+
state = self.obs_normalizer.normalize(state).to(self.device)
|
|
336
|
+
else:
|
|
337
|
+
state = state.to(self.device)
|
|
338
|
+
|
|
339
|
+
logits = self.network(state)
|
|
340
|
+
action = logits.argmax(dim=-1).squeeze(0)
|
|
341
|
+
return action.cpu().detach().item()
|
|
342
|
+
|
|
343
|
+
def _get_log_prob_and_entropy(self, states, actions):
|
|
344
|
+
"""Compute log probability and entropy for a batch."""
|
|
345
|
+
logits = self.network(states)
|
|
346
|
+
dist = Categorical(logits=logits)
|
|
347
|
+
log_probs = dist.log_prob(actions)
|
|
348
|
+
entropies = dist.entropy().sum(-1)
|
|
349
|
+
return log_probs, entropies
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class ContinuousPPOAgent(BasePPOAgent):
|
|
353
|
+
"""
|
|
354
|
+
PPO agent for continuous action spaces with tanh squashing.
|
|
355
|
+
|
|
356
|
+
Uses reparameterization trick with tanh:
|
|
357
|
+
- Sample raw action from Gaussian
|
|
358
|
+
- Squash with tanh: action = tanh(raw)
|
|
359
|
+
- Correct log_prob with Jacobian: log_prob = raw_log_prob - log(1 - tanh^2 + eps)
|
|
360
|
+
|
|
361
|
+
Usage:
|
|
362
|
+
agent = ContinuousPPOAgent(state_dim=3, action_dim=1)
|
|
363
|
+
action, log_prob, raw_action = agent.act(state)
|
|
364
|
+
agent.store((state, raw_action, reward, log_prob, next_state, terminated))
|
|
365
|
+
agent.train()
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(self, state_dim: int, action_dim: int,
|
|
369
|
+
architecture: dict = None,
|
|
370
|
+
lr: float = 3e-4, gamma: float = 0.99,
|
|
371
|
+
gae_lambda: float = 0.95, epsilon: float = 0.2,
|
|
372
|
+
critic_coef: float = 0.5, entropy_coef: float = 0.01,
|
|
373
|
+
num_envs: int = 1, trajectory_size: int = 2048,
|
|
374
|
+
device: str = "cpu", checkpoint: str = None,
|
|
375
|
+
normalize_observations: bool = True,
|
|
376
|
+
default_num_epochs: int = 4,
|
|
377
|
+
default_batch_size: int = 64):
|
|
378
|
+
|
|
379
|
+
# Default architecture
|
|
380
|
+
if architecture is None:
|
|
381
|
+
architecture = {
|
|
382
|
+
"actor": [state_dim, 64, 64, action_dim],
|
|
383
|
+
"critic": [state_dim, 64, 64, 1]
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
network = ContinuousPPO(architecture)
|
|
387
|
+
super().__init__(network, lr, gamma, gae_lambda, epsilon,
|
|
388
|
+
critic_coef, entropy_coef, num_envs,
|
|
389
|
+
trajectory_size, device,
|
|
390
|
+
normalize_observations, default_num_epochs,
|
|
391
|
+
default_batch_size)
|
|
392
|
+
|
|
393
|
+
if checkpoint:
|
|
394
|
+
if self.obs_normalizer is not None:
|
|
395
|
+
self.load(checkpoint + ".pth", checkpoint + "_norm.npy")
|
|
396
|
+
else:
|
|
397
|
+
self.load(checkpoint + ".pth", None)
|
|
398
|
+
|
|
399
|
+
def act(self, state) -> tuple:
|
|
400
|
+
"""
|
|
401
|
+
Select an action (stochastic, with tanh squashing).
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
state: Observation (numpy array or tensor).
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
(action, log_prob, raw_action)
|
|
408
|
+
- action: Clamped to [-1, 1]
|
|
409
|
+
- log_prob: Corrected for tanh squashing
|
|
410
|
+
- raw_action: Unsquashed Gaussian sample
|
|
411
|
+
"""
|
|
412
|
+
state = torch.as_tensor(state, dtype=torch.float32)
|
|
413
|
+
if state.dim() == 1:
|
|
414
|
+
state = state.unsqueeze(0)
|
|
415
|
+
if self.normalize_observations:
|
|
416
|
+
state = self.obs_normalizer.normalize(state).to(self.device)
|
|
417
|
+
else:
|
|
418
|
+
state = state.to(self.device)
|
|
419
|
+
|
|
420
|
+
mean, log_std = self.network(state)
|
|
421
|
+
log_std = torch.clamp(log_std, -5, 2)
|
|
422
|
+
std = log_std.exp()
|
|
423
|
+
|
|
424
|
+
dist = Normal(mean, std)
|
|
425
|
+
raw_action = dist.rsample()
|
|
426
|
+
action = torch.tanh(raw_action)
|
|
427
|
+
|
|
428
|
+
# Jacobian correction for tanh squashing
|
|
429
|
+
log_prob = (dist.log_prob(raw_action) - torch.log(
|
|
430
|
+
1 - action ** 2 + 1e-6
|
|
431
|
+
)).sum(-1)
|
|
432
|
+
|
|
433
|
+
return action.squeeze(0).cpu().detach(), \
|
|
434
|
+
log_prob.cpu().detach(), \
|
|
435
|
+
raw_action.squeeze(0).cpu().detach()
|
|
436
|
+
|
|
437
|
+
def act_deterministic(self, state) -> tuple:
|
|
438
|
+
"""
|
|
439
|
+
Select action deterministically (mean of distribution).
|
|
440
|
+
Useful for evaluation.
|
|
441
|
+
"""
|
|
442
|
+
state = torch.as_tensor(state, dtype=torch.float32)
|
|
443
|
+
if state.dim() == 1:
|
|
444
|
+
state = state.unsqueeze(0)
|
|
445
|
+
if self.normalize_observations:
|
|
446
|
+
state = self.obs_normalizer.normalize(state).to(self.device)
|
|
447
|
+
else:
|
|
448
|
+
state = state.to(self.device)
|
|
449
|
+
|
|
450
|
+
mean, log_std = self.network(state)
|
|
451
|
+
action = torch.tanh(mean)
|
|
452
|
+
|
|
453
|
+
log_prob = torch.zeros(mean.shape[0], device=self.device)
|
|
454
|
+
raw_action = torch.zeros(mean.shape[0], device=self.device)
|
|
455
|
+
|
|
456
|
+
return action.squeeze(0).cpu().detach(), \
|
|
457
|
+
log_prob.cpu().detach(), \
|
|
458
|
+
raw_action.cpu().detach()
|
|
459
|
+
|
|
460
|
+
def _get_log_prob_and_entropy(self, states, raw_actions):
|
|
461
|
+
"""Compute log probability and entropy for a batch of raw actions."""
|
|
462
|
+
mean, log_std = self.network(states)
|
|
463
|
+
log_std = torch.clamp(log_std, -5, 2)
|
|
464
|
+
std = log_std.exp()
|
|
465
|
+
|
|
466
|
+
dist = Normal(mean, std)
|
|
467
|
+
actions = torch.tanh(raw_actions)
|
|
468
|
+
|
|
469
|
+
# Jacobian correction
|
|
470
|
+
log_probs = (dist.log_prob(raw_actions) - torch.log(
|
|
471
|
+
1 - actions ** 2 + 1e-6
|
|
472
|
+
)).sum(-1)
|
|
473
|
+
|
|
474
|
+
entropy = dist.entropy().sum(-1)
|
|
475
|
+
return log_probs, entropy
|
ezppo/networks.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyTorch neural network modules for Discrete and Continuous PPO.
|
|
3
|
+
|
|
4
|
+
Both networks share a common base class (BasePPO) that provides:
|
|
5
|
+
- Configurable architecture via dict (actor, critic, optional shared backbone)
|
|
6
|
+
- Orthogonal weight initialization (standard for RL)
|
|
7
|
+
- Small output layer initialization (std=0.01) for stability
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BasePPO(nn.Module):
|
|
16
|
+
"""
|
|
17
|
+
Base PPO network with configurable architecture.
|
|
18
|
+
|
|
19
|
+
Architecture dict format:
|
|
20
|
+
{
|
|
21
|
+
"actor": [input_dim, hidden1, hidden2, ..., output_dim],
|
|
22
|
+
"critic": [input_dim, hidden1, hidden2, ..., 1],
|
|
23
|
+
"shared": [input_dim, hidden1, ...] # optional: shared backbone
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
If "shared" is provided, both actor and critic share an initial backbone.
|
|
27
|
+
Otherwise, actor and critic are independent networks.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, architecture: dict, hidden_activation=nn.Tanh):
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
self.actor_head = self._init_layer(architecture["actor"], hidden_activation)
|
|
34
|
+
self.critic_head = self._init_layer(architecture["critic"], hidden_activation)
|
|
35
|
+
|
|
36
|
+
self.backbone = self._init_layer(
|
|
37
|
+
architecture["shared"], hidden_activation
|
|
38
|
+
) if "shared" in architecture else None
|
|
39
|
+
|
|
40
|
+
self.input_dim = architecture["shared"][0] if self.backbone else architecture["actor"][0]
|
|
41
|
+
self.output_dim = architecture["actor"][-1]
|
|
42
|
+
|
|
43
|
+
# Choose forward methods based on whether we have a shared backbone
|
|
44
|
+
if self.backbone:
|
|
45
|
+
self._act_forward = self._act_forward_with_backbone
|
|
46
|
+
self._crit_forward = self._crit_forward_with_backbone
|
|
47
|
+
else:
|
|
48
|
+
self._act_forward = self._act_forward_independent
|
|
49
|
+
self._crit_forward = self._crit_forward_independent
|
|
50
|
+
|
|
51
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
52
|
+
"""Actor forward pass (returns logits for discrete, mean for continuous)."""
|
|
53
|
+
return self._act_forward(x)
|
|
54
|
+
|
|
55
|
+
def predict_value(self, x: torch.Tensor) -> torch.Tensor:
|
|
56
|
+
"""Critic forward pass (returns state value estimate)."""
|
|
57
|
+
return self._crit_forward(x)
|
|
58
|
+
|
|
59
|
+
def _init_layer(self, arch: list[int], activation):
|
|
60
|
+
"""Build a sequential network from an architecture list."""
|
|
61
|
+
seq = nn.Sequential()
|
|
62
|
+
for i, v in enumerate(arch):
|
|
63
|
+
if i == len(arch) - 1:
|
|
64
|
+
break # Don't add output layer activation
|
|
65
|
+
input_n = v
|
|
66
|
+
output_n = arch[i + 1]
|
|
67
|
+
|
|
68
|
+
# Initialize weights with orthogonal init
|
|
69
|
+
if i + 1 == len(arch) - 1:
|
|
70
|
+
# Output layer: use small initialization for stability
|
|
71
|
+
linear = self._ortho_init(nn.Linear(input_n, output_n), std=0.01)
|
|
72
|
+
else:
|
|
73
|
+
# Hidden layers: use sqrt(2) orthogonal init (standard for RL)
|
|
74
|
+
linear = self._ortho_init(nn.Linear(input_n, output_n))
|
|
75
|
+
|
|
76
|
+
# Only add activation for hidden layers, not the output layer
|
|
77
|
+
if i + 1 != len(arch) - 1:
|
|
78
|
+
seq.extend([linear, activation()])
|
|
79
|
+
else:
|
|
80
|
+
seq.append(linear)
|
|
81
|
+
return seq
|
|
82
|
+
|
|
83
|
+
def _ortho_init(self, layer, std=np.sqrt(2), bias_const=0.0):
|
|
84
|
+
"""Initialize a linear layer with orthogonal weights."""
|
|
85
|
+
nn.init.orthogonal_(layer.weight, std)
|
|
86
|
+
nn.init.constant_(layer.bias, bias_const)
|
|
87
|
+
return layer
|
|
88
|
+
|
|
89
|
+
# --- Forward method variants ---
|
|
90
|
+
|
|
91
|
+
def _act_forward_independent(self, x):
|
|
92
|
+
"""Actor forward without shared backbone."""
|
|
93
|
+
return self.actor_head(x)
|
|
94
|
+
|
|
95
|
+
def _crit_forward_independent(self, x):
|
|
96
|
+
"""Critic forward without shared backbone."""
|
|
97
|
+
return self.critic_head(x)
|
|
98
|
+
|
|
99
|
+
def _act_forward_with_backbone(self, x):
|
|
100
|
+
"""Actor forward with shared backbone."""
|
|
101
|
+
return self.actor_head(self.backbone(x))
|
|
102
|
+
|
|
103
|
+
def _crit_forward_with_backbone(self, x):
|
|
104
|
+
"""Critic forward with shared backbone."""
|
|
105
|
+
return self.critic_head(self.backbone(x))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DiscretePPO(BasePPO):
|
|
109
|
+
"""
|
|
110
|
+
Discrete action PPO network.
|
|
111
|
+
|
|
112
|
+
Output:
|
|
113
|
+
forward(x) -> action logits [batch, n_actions]
|
|
114
|
+
predict_value(x) -> value estimates [batch, 1]
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, architecture: dict, hidden_activation=nn.Tanh):
|
|
118
|
+
super().__init__(architecture, hidden_activation)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ContinuousPPO(BasePPO):
|
|
122
|
+
"""
|
|
123
|
+
Continuous action PPO network with learnable log_std.
|
|
124
|
+
|
|
125
|
+
Output:
|
|
126
|
+
forward(x) -> (mean [batch, n_actions], log_std [n_actions])
|
|
127
|
+
predict_value(x) -> value estimates [batch, 1]
|
|
128
|
+
|
|
129
|
+
The log_std parameter is shared across all batches and is clamped
|
|
130
|
+
during action sampling to prevent explosion.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, architecture: dict, hidden_activation=nn.Tanh, default_log=-1.0):
|
|
134
|
+
super().__init__(architecture, hidden_activation)
|
|
135
|
+
|
|
136
|
+
# Learnable log_std parameter (shared across all layers/batches)
|
|
137
|
+
self.log_std = nn.Parameter(
|
|
138
|
+
torch.ones(self.output_dim) * default_log,
|
|
139
|
+
requires_grad=True
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def forward(self, x: torch.Tensor):
|
|
143
|
+
"""Returns (mean, log_std). Mean has shape [batch, n_actions]."""
|
|
144
|
+
mean = super().forward(x)
|
|
145
|
+
return mean, self.log_std
|
ezppo/normalizer.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Running normalizer for observation preprocessing.
|
|
3
|
+
Uses online algorithm to track running mean and variance.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunningNormalizer:
|
|
11
|
+
"""
|
|
12
|
+
Online running normalizer for observations.
|
|
13
|
+
|
|
14
|
+
Uses Welford's algorithm for numerically stable running statistics.
|
|
15
|
+
Clips normalized values to prevent extreme outliers.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, shape: int | list[int], epsilon: float = 1e-8, clip: float = 10.0):
|
|
19
|
+
"""
|
|
20
|
+
Args:
|
|
21
|
+
shape: Dimension of observations (int or list of ints).
|
|
22
|
+
Will be flattened to a 1D shape.
|
|
23
|
+
epsilon: Small constant for numerical stability.
|
|
24
|
+
clip: Maximum absolute value for normalized observations.
|
|
25
|
+
"""
|
|
26
|
+
# Flatten shape if list or tuple
|
|
27
|
+
if isinstance(shape, (list, tuple)):
|
|
28
|
+
flat_shape = [int(np.prod(shape))]
|
|
29
|
+
else:
|
|
30
|
+
flat_shape = [int(shape)]
|
|
31
|
+
|
|
32
|
+
self.mean = torch.zeros(flat_shape)
|
|
33
|
+
self.var = torch.ones(flat_shape)
|
|
34
|
+
self.count = epsilon
|
|
35
|
+
self.epsilon = epsilon
|
|
36
|
+
self.clip = clip
|
|
37
|
+
|
|
38
|
+
def update(self, x: torch.Tensor) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Update running statistics with a batch of observations.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
x: Tensor of shape [batch, ...] or [batch, obs_dim]
|
|
44
|
+
"""
|
|
45
|
+
batch_mean = torch.mean(x, dim=0)
|
|
46
|
+
batch_var = torch.var(x, dim=0, unbiased=False)
|
|
47
|
+
batch_count = x.shape[0]
|
|
48
|
+
|
|
49
|
+
total_count = self.count + batch_count
|
|
50
|
+
delta = batch_mean - self.mean
|
|
51
|
+
|
|
52
|
+
self.mean += delta * batch_count / total_count
|
|
53
|
+
self.var = (
|
|
54
|
+
(self.var * self.count + batch_var * batch_count +
|
|
55
|
+
delta ** 2 * self.count * batch_count / total_count)
|
|
56
|
+
/ total_count
|
|
57
|
+
)
|
|
58
|
+
self.count = total_count
|
|
59
|
+
|
|
60
|
+
def normalize(self, x: torch.Tensor) -> torch.Tensor:
|
|
61
|
+
"""
|
|
62
|
+
Normalize observations.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
x: Tensor of observations.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Normalized tensor, clipped to [-clip, clip].
|
|
69
|
+
"""
|
|
70
|
+
normed = (x - self.mean) / (torch.sqrt(self.var) + self.epsilon)
|
|
71
|
+
return torch.clip(normed, -self.clip, self.clip)
|
|
72
|
+
|
|
73
|
+
def denormalize(self, x: torch.Tensor) -> torch.Tensor:
|
|
74
|
+
"""
|
|
75
|
+
Denormalize observations (inverse of normalize).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
x: Normalized tensor.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Original-scale tensor.
|
|
82
|
+
"""
|
|
83
|
+
return (x * (torch.sqrt(self.var) + self.epsilon)) + self.mean
|
|
84
|
+
|
|
85
|
+
def save(self, path: str) -> None:
|
|
86
|
+
"""Save statistics to a numpy file."""
|
|
87
|
+
np.save(path, [self.mean.cpu(), self.var.cpu()])
|
|
88
|
+
|
|
89
|
+
def load(self, path: str) -> None:
|
|
90
|
+
"""Load statistics from a numpy file."""
|
|
91
|
+
data = np.load(path, allow_pickle=True)
|
|
92
|
+
self.mean = torch.as_tensor(data[0]).to(self.mean.device)
|
|
93
|
+
self.var = torch.as_tensor(data[1]).to(self.var.device)
|
|
94
|
+
|
|
95
|
+
def to(self, device: torch.device | str) -> "RunningNormalizer":
|
|
96
|
+
"""Move statistics to device."""
|
|
97
|
+
self.mean = self.mean.to(device)
|
|
98
|
+
self.var = self.var.to(device)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def reset(self) -> None:
|
|
102
|
+
"""Reset statistics (useful for new training run)."""
|
|
103
|
+
self.mean = torch.zeros_like(self.mean)
|
|
104
|
+
self.var = torch.ones_like(self.var)
|
|
105
|
+
self.count = self.epsilon
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ezppo
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A simple PPO library for Discrete and Continuous environments
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: torch
|
|
8
|
+
Requires-Dist: gymnasium
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# EzPPO
|
|
14
|
+
|
|
15
|
+
A simple, unified PPO (Proximal Policy Optimization) library for PyTorch. Supports both **discrete** and **continuous** action spaces.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Unified API**: Same training loop for discrete and continuous
|
|
20
|
+
- **Observation normalization**: Online running statistics
|
|
21
|
+
- **GAE (Generalized Advantage Estimation)**: For stable advantage estimation
|
|
22
|
+
- **Clipped surrogate objective**: Standard PPO clipping
|
|
23
|
+
- **Mini-batch training**: With random shuffling
|
|
24
|
+
- **Gradient clipping**: Prevents exploding gradients
|
|
25
|
+
- **Checkpoint save/load**: Resume training easily
|
|
26
|
+
- **Multi-environment support**: Parallel environments built-in
|
|
27
|
+
- **GPU support**: Automatic device management
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Discrete Actions (CartPole)
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from ezppo import DiscretePPOAgent
|
|
35
|
+
import gymnasium as gym
|
|
36
|
+
|
|
37
|
+
agent = DiscretePPOAgent(state_dim=4, action_dim=2)
|
|
38
|
+
env = gym.make("CartPole-v1")
|
|
39
|
+
|
|
40
|
+
state, _ = env.reset()
|
|
41
|
+
action, log_prob = agent.act(state) # Stochastic action
|
|
42
|
+
# action = agent.act_deterministic(state) # Greedy action (for eval)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Continuous Actions (Pendulum)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from ezppo import ContinuousPPOAgent
|
|
49
|
+
import gymnasium as gym
|
|
50
|
+
|
|
51
|
+
agent = ContinuousPPOAgent(state_dim=3, action_dim=1)
|
|
52
|
+
env = gym.make("Pendulum-v1")
|
|
53
|
+
|
|
54
|
+
state, _ = env.reset()
|
|
55
|
+
action, log_prob, raw_action = agent.act(state)
|
|
56
|
+
# action = agent.act_deterministic(state)[0] # Mean action (for eval)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install torch gymnasium numpy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Architecture
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
ezppo/
|
|
69
|
+
├── __init__.py # Public API
|
|
70
|
+
├── networks.py # DiscretePPO, ContinuousPPO, BasePPO
|
|
71
|
+
├── normalizer.py # RunningNormalizer
|
|
72
|
+
└── agents.py # DiscretePPOAgent, ContinuousPPOAgent
|
|
73
|
+
|
|
74
|
+
examples/
|
|
75
|
+
├── discrete_cartpole.py
|
|
76
|
+
└── continuous_pendulum.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Training Loop Template
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
trajectory = []
|
|
83
|
+
max_trajectory = 2048
|
|
84
|
+
|
|
85
|
+
for episode in range(num_episodes):
|
|
86
|
+
state, _ = env.reset()
|
|
87
|
+
done = False
|
|
88
|
+
|
|
89
|
+
while not done:
|
|
90
|
+
action, log_prob = agent.act(state)
|
|
91
|
+
next_state, reward, terminated, truncated, _ = env.step(action.item())
|
|
92
|
+
done = terminated or truncated
|
|
93
|
+
|
|
94
|
+
trajectory.append((state, action.item(), reward, log_prob.item(),
|
|
95
|
+
next_state, terminated))
|
|
96
|
+
state = next_state
|
|
97
|
+
|
|
98
|
+
if len(trajectory) >= max_trajectory:
|
|
99
|
+
agent.train(num_epochs=4, batch_size=64)
|
|
100
|
+
trajectory.clear()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
### Networks
|
|
106
|
+
|
|
107
|
+
| Class | Purpose |
|
|
108
|
+
|-------|---------|
|
|
109
|
+
| `BasePPO` | Base class with configurable architecture |
|
|
110
|
+
| `DiscretePPO` | Discrete actions → logits output |
|
|
111
|
+
| `ContinuousPPO` | Continuous actions → mean + learnable log_std |
|
|
112
|
+
|
|
113
|
+
### Agents
|
|
114
|
+
|
|
115
|
+
| Class | Purpose |
|
|
116
|
+
|-------|---------|
|
|
117
|
+
| `DiscretePPOAgent` | Discrete action space |
|
|
118
|
+
| `ContinuousPPOAgent` | Continuous action space |
|
|
119
|
+
|
|
120
|
+
### Key Methods
|
|
121
|
+
|
|
122
|
+
**Agent.act(state)** → `(action, log_prob)` or `(action, log_prob, raw_action)`
|
|
123
|
+
**Agent.act_deterministic(state)** → Deterministic action (for evaluation)
|
|
124
|
+
**Agent.train(num_epochs=4, batch_size=64)** → Train on trajectory
|
|
125
|
+
**Agent.store(packet)** → Add transition to trajectory
|
|
126
|
+
**Agent.clear()** → Clear trajectory buffer
|
|
127
|
+
**Agent.save(path)** → Save checkpoint
|
|
128
|
+
**Agent.load(path)** → Load checkpoint
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
agent = DiscretePPOAgent(
|
|
134
|
+
state_dim=4, action_dim=2,
|
|
135
|
+
architecture={ # Custom network architecture
|
|
136
|
+
"actor": [state_dim, 64, 64, action_dim],
|
|
137
|
+
"critic": [state_dim, 64, 64, 1]
|
|
138
|
+
},
|
|
139
|
+
lr=3e-4, gamma=0.99,
|
|
140
|
+
gae_lambda=0.95, epsilon=0.2,
|
|
141
|
+
critic_coef=0.5, entropy_coef=0.01,
|
|
142
|
+
num_envs=1, trajectory_size=2048,
|
|
143
|
+
device="cpu"
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ezppo/__init__.py,sha256=axJruk8hVTjLLzFJpSVqbjwfGhzbSASobOirgpKGJwY,786
|
|
2
|
+
ezppo/agents.py,sha256=BnIqvczJpAUA0ySCUAZUUHQ7fuwl7V0FPvoaicc2AD0,18176
|
|
3
|
+
ezppo/networks.py,sha256=HlzZMtFuHnLoctb3BnbVBdJvMPhT8a2Pgxr41ivQOE0,5261
|
|
4
|
+
ezppo/normalizer.py,sha256=83pE-fbp0ts5aFSQFul6JF21tPnGNnuM9jYAqcay_k0,3398
|
|
5
|
+
ezppo-0.0.0.dist-info/METADATA,sha256=5UAUJ5LMVdpT3xy1YyWOhmwaWsNWBxB8BeK4nqaZRPc,3980
|
|
6
|
+
ezppo-0.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
ezppo-0.0.0.dist-info/top_level.txt,sha256=HwH4QV3kbktJE8cepSw2GIriNvXi66KwxfD6ubGeCgg,6
|
|
8
|
+
ezppo-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ezppo
|