tinybaselines 0.1.0a1__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.
- tinybaselines/__init__.py +5 -0
- tinybaselines/evaluation.py +6 -0
- tinybaselines/policies.py +75 -0
- tinybaselines/ppo.py +155 -0
- tinybaselines/rollout.py +80 -0
- tinybaselines/training.py +32 -0
- tinybaselines-0.1.0a1.dist-info/METADATA +94 -0
- tinybaselines-0.1.0a1.dist-info/RECORD +11 -0
- tinybaselines-0.1.0a1.dist-info/WHEEL +5 -0
- tinybaselines-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- tinybaselines-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from tinygrad import Tensor, TinyJit, nn
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MLP:
|
|
8
|
+
def __init__(self, input_dim, output_dim, hidden_dim, layer_count):
|
|
9
|
+
self.layers = [nn.Linear(input_dim, hidden_dim)]
|
|
10
|
+
self.layers.extend(nn.Linear(hidden_dim, hidden_dim) for _ in range(layer_count - 1))
|
|
11
|
+
self.layers.append(nn.Linear(hidden_dim, output_dim))
|
|
12
|
+
|
|
13
|
+
def __call__(self, x):
|
|
14
|
+
for layer in self.layers[:-1]:
|
|
15
|
+
x = layer(x).relu()
|
|
16
|
+
return self.layers[-1](x)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GaussianActorCritic:
|
|
20
|
+
def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers, observation_scale=None):
|
|
21
|
+
self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
|
|
22
|
+
actor_layers=actor_layers, critic_layers=critic_layers)
|
|
23
|
+
self.actor = MLP(state_dim, action_dim, hidden_dim, actor_layers)
|
|
24
|
+
self.critic = MLP(state_dim, 1, hidden_dim, critic_layers)
|
|
25
|
+
self.actor.layers[-1].weight.assign(self.actor.layers[-1].weight * 0.01).realize()
|
|
26
|
+
self.actor.layers[-1].bias.assign(Tensor.zeros_like(self.actor.layers[-1].bias)).realize()
|
|
27
|
+
self.log_std = Tensor.full((action_dim,), -0.7).realize()
|
|
28
|
+
scale = np.ones(state_dim, np.float32) if observation_scale is None else np.asarray(observation_scale, np.float32)
|
|
29
|
+
self.observation_scale = Tensor(scale.copy()).is_param_(False).realize()
|
|
30
|
+
self.jit_shape = None
|
|
31
|
+
self.actor_jit = TinyJit(lambda state: self.actor(state).realize())
|
|
32
|
+
self.refresh_inference()
|
|
33
|
+
|
|
34
|
+
def __call__(self, state):
|
|
35
|
+
action_mean = self.actor(state)
|
|
36
|
+
log_std = self.log_std.clip(-5, 2)
|
|
37
|
+
value = self.critic(state)
|
|
38
|
+
return action_mean, log_std, value
|
|
39
|
+
|
|
40
|
+
def preprocess(self, observations):
|
|
41
|
+
return Tensor(observations.astype(np.float32), device=self.observation_scale.device) * self.observation_scale
|
|
42
|
+
|
|
43
|
+
def act(self, state, deterministic=False):
|
|
44
|
+
state_tensor = self.preprocess(state)
|
|
45
|
+
if self.jit_shape != state_tensor.shape:
|
|
46
|
+
self.actor_jit.reset()
|
|
47
|
+
self.jit_shape = state_tensor.shape
|
|
48
|
+
mean = self.actor_jit(state_tensor).numpy()
|
|
49
|
+
if deterministic:
|
|
50
|
+
action = np.clip(mean, -1, 1)
|
|
51
|
+
return action, mean
|
|
52
|
+
sample = np.random.normal(mean, self.action_std).astype(np.float32)
|
|
53
|
+
action = np.clip(sample, -1, 1)
|
|
54
|
+
return action, sample
|
|
55
|
+
|
|
56
|
+
def state_dict(self):
|
|
57
|
+
return nn.state.get_state_dict(dict(actor=self.actor, critic=self.critic, log_std=self.log_std,
|
|
58
|
+
observation_scale=self.observation_scale))
|
|
59
|
+
|
|
60
|
+
def save(self, path):
|
|
61
|
+
nn.state.safe_save(self.state_dict(), path, metadata={'config': json.dumps(self.config)})
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def load(cls, path):
|
|
65
|
+
_, _, header = nn.state.safe_load_metadata(path)
|
|
66
|
+
model = cls(**json.loads(header['__metadata__']['config']))
|
|
67
|
+
nn.state.load_state_dict(model.state_dict(), nn.state.safe_load(path), verbose=False)
|
|
68
|
+
model.refresh_inference()
|
|
69
|
+
return model
|
|
70
|
+
|
|
71
|
+
def refresh_inference(self):
|
|
72
|
+
self.actor_jit.reset()
|
|
73
|
+
self.action_std = np.exp(self.log_std.clip(-5, 2).numpy())
|
|
74
|
+
if not np.isfinite(self.action_std).all():
|
|
75
|
+
raise FloatingPointError('non-finite policy standard deviation')
|
tinybaselines/ppo.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from tempfile import TemporaryDirectory
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from tinygrad import Context, Tensor, TinyJit, nn
|
|
7
|
+
|
|
8
|
+
from .policies import GaussianActorCritic
|
|
9
|
+
|
|
10
|
+
LOG_2PI = float(np.log(2.0 * np.pi))
|
|
11
|
+
NORMAL_ENTROPY_CONSTANT = float(0.5 * np.log(2.0 * np.pi * np.e))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normal_log_prob(action_mean, log_std, actions):
|
|
15
|
+
normalized = (actions - action_mean) / log_std.exp()
|
|
16
|
+
return (-0.5 * (normalized.square() + 2.0 * log_std + LOG_2PI)).sum(axis=-1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def clip_grad_norm(parameters, max_norm):
|
|
20
|
+
gradients = [parameter.grad for parameter in parameters if parameter.grad is not None]
|
|
21
|
+
if not gradients:
|
|
22
|
+
return
|
|
23
|
+
total_norm = sum((gradient.square().sum() for gradient in gradients)).sqrt()
|
|
24
|
+
scale = (max_norm / (total_norm + 1e-6)).clip(max_=1.0)
|
|
25
|
+
for parameter in parameters:
|
|
26
|
+
if parameter.grad is not None:
|
|
27
|
+
parameter.grad = parameter.grad * scale
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PPO:
|
|
31
|
+
def __init__(self, actor_critic, pi_lr, vf_lr, gamma, lamda, K_epochs, eps_clip, batch_size, vf_coef, entropy_coef):
|
|
32
|
+
self.actor_critic = actor_critic
|
|
33
|
+
self.device = actor_critic.log_std.device
|
|
34
|
+
self.actor_parameters = nn.state.get_parameters(actor_critic.actor) + [actor_critic.log_std]
|
|
35
|
+
self.critic_parameters = nn.state.get_parameters(actor_critic.critic)
|
|
36
|
+
self.pi_optimizer = nn.optim.Adam(self.actor_parameters, lr=pi_lr)
|
|
37
|
+
self.vf_optimizer = nn.optim.Adam(self.critic_parameters, lr=vf_lr)
|
|
38
|
+
self.optimizer = nn.optim.OptimizerGroup(self.pi_optimizer, self.vf_optimizer)
|
|
39
|
+
self.gamma, self.lamda, self.K_epochs = gamma, lamda, K_epochs
|
|
40
|
+
self.eps_clip, self.batch_size = eps_clip, batch_size
|
|
41
|
+
self.vf_coef, self.entropy_coef = vf_coef, entropy_coef
|
|
42
|
+
self._train_steps = {}
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def config(self):
|
|
46
|
+
return dict(pi_lr=self.pi_optimizer.lr.item(), vf_lr=self.vf_optimizer.lr.item(),
|
|
47
|
+
gamma=self.gamma, lamda=self.lamda, K_epochs=self.K_epochs, eps_clip=self.eps_clip,
|
|
48
|
+
batch_size=self.batch_size, vf_coef=self.vf_coef, entropy_coef=self.entropy_coef)
|
|
49
|
+
|
|
50
|
+
def state_dict(self):
|
|
51
|
+
state = dict(policy=self.actor_critic.state_dict())
|
|
52
|
+
for name in ('pi_optimizer', 'vf_optimizer'):
|
|
53
|
+
optimizer = getattr(self, name)
|
|
54
|
+
state[name] = {key: getattr(optimizer, key) for key in ('lr', 'b1_t', 'b2_t', 'm', 'v')}
|
|
55
|
+
return nn.state.get_state_dict(state)
|
|
56
|
+
|
|
57
|
+
def save(self, path):
|
|
58
|
+
path = Path(path)
|
|
59
|
+
metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(self.actor_critic.config),
|
|
60
|
+
ppo_config=json.dumps(self.config))
|
|
61
|
+
with TemporaryDirectory(dir=path.parent) as directory:
|
|
62
|
+
temporary = Path(directory) / path.name
|
|
63
|
+
nn.state.safe_save(self.state_dict(), temporary, metadata=metadata)
|
|
64
|
+
temporary.replace(path)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def load(cls, path):
|
|
68
|
+
_, _, header = nn.state.safe_load_metadata(path)
|
|
69
|
+
metadata = header.get('__metadata__', {})
|
|
70
|
+
if metadata.get('format') != 'tinybaselines.ppo.v1':
|
|
71
|
+
raise ValueError('unsupported PPO checkpoint format')
|
|
72
|
+
policy = GaussianActorCritic(**json.loads(metadata['policy_config']))
|
|
73
|
+
ppo = cls(policy, **json.loads(metadata['ppo_config']))
|
|
74
|
+
nn.state.load_state_dict(ppo.state_dict(), nn.state.safe_load(path), verbose=False)
|
|
75
|
+
policy.refresh_inference()
|
|
76
|
+
return ppo
|
|
77
|
+
|
|
78
|
+
def compute_advantages(self, rewards, state_values, terminated, truncated, next_values):
|
|
79
|
+
T, N = rewards.shape
|
|
80
|
+
advantages = np.zeros_like(rewards, dtype=np.float32)
|
|
81
|
+
gae = np.zeros(N, dtype=np.float32)
|
|
82
|
+
episode_ends = terminated | truncated
|
|
83
|
+
for t in reversed(range(T)):
|
|
84
|
+
delta = rewards[t] + self.gamma * next_values[t] * ~terminated[t] - state_values[t]
|
|
85
|
+
gae = delta + self.gamma * self.lamda * ~episode_ends[t] * gae
|
|
86
|
+
advantages[t] = gae
|
|
87
|
+
returns = advantages + state_values
|
|
88
|
+
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
|
89
|
+
return advantages.reshape(-1), returns.reshape(-1)
|
|
90
|
+
|
|
91
|
+
def compute_loss(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
|
|
92
|
+
action_means, log_std, state_values = self.actor_critic(batch_states)
|
|
93
|
+
action_logprobs = normal_log_prob(action_means, log_std, batch_actions)
|
|
94
|
+
ratios = (action_logprobs - batch_logprobs).exp()
|
|
95
|
+
unclipped = ratios * batch_advantages
|
|
96
|
+
clipped = ratios.clip(1 - self.eps_clip, 1 + self.eps_clip) * batch_advantages
|
|
97
|
+
actor_loss = -unclipped.minimum(clipped).mean()
|
|
98
|
+
critic_loss = (state_values.squeeze(-1) - batch_returns).square().mean()
|
|
99
|
+
entropy = (log_std + NORMAL_ENTROPY_CONSTANT).sum()
|
|
100
|
+
return actor_loss + self.vf_coef * critic_loss - self.entropy_coef * entropy
|
|
101
|
+
|
|
102
|
+
def train_step(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
|
|
103
|
+
size = batch_states.shape[0]
|
|
104
|
+
if size not in self._train_steps:
|
|
105
|
+
self._train_steps[size] = TinyJit(self._train_step)
|
|
106
|
+
loss = self._train_steps[size](batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns)
|
|
107
|
+
self.actor_critic.refresh_inference()
|
|
108
|
+
return loss
|
|
109
|
+
|
|
110
|
+
@Context(TRAINING=1)
|
|
111
|
+
def _train_step(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
|
|
112
|
+
self.optimizer.zero_grad()
|
|
113
|
+
loss = self.compute_loss(
|
|
114
|
+
batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns
|
|
115
|
+
)
|
|
116
|
+
loss.backward()
|
|
117
|
+
clip_grad_norm(self.actor_parameters, 0.5)
|
|
118
|
+
clip_grad_norm(self.critic_parameters, 0.5)
|
|
119
|
+
return loss.realize(*self.optimizer.schedule_step())
|
|
120
|
+
|
|
121
|
+
def prepare(self, batch):
|
|
122
|
+
T, N = batch.rewards.shape
|
|
123
|
+
obs_flat = self.actor_critic.preprocess(batch.observations.reshape(T * N, -1))
|
|
124
|
+
raw_act_np = batch.raw_actions.reshape(T * N, -1).astype(np.float32)
|
|
125
|
+
raw_act_flat = Tensor(raw_act_np, device=self.device)
|
|
126
|
+
old_mean, old_log_std, old_value = self.actor_critic(obs_flat)
|
|
127
|
+
old_logprobs_np = normal_log_prob(old_mean, old_log_std, raw_act_flat).numpy()
|
|
128
|
+
old_values_np = old_value.numpy().reshape(T, N)
|
|
129
|
+
next_obs_t = self.actor_critic.preprocess(batch.next_observations.reshape(T * N, -1))
|
|
130
|
+
next_values_np = self.actor_critic.critic(next_obs_t).numpy().reshape(T, N)
|
|
131
|
+
advantages_np, returns_np = self.compute_advantages(
|
|
132
|
+
batch.rewards, old_values_np, batch.terminated, batch.truncated, next_values_np
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
old_logprobs = Tensor(old_logprobs_np, device=self.device)
|
|
136
|
+
advantages = Tensor(advantages_np, device=self.device)
|
|
137
|
+
returns = Tensor(returns_np, device=self.device)
|
|
138
|
+
return obs_flat, raw_act_flat, old_logprobs, advantages, returns
|
|
139
|
+
|
|
140
|
+
def update(self, batch):
|
|
141
|
+
obs_flat, raw_act_flat, old_logprobs, advantages, returns = self.prepare(batch)
|
|
142
|
+
num_samples = obs_flat.shape[0]
|
|
143
|
+
|
|
144
|
+
for _ in range(self.K_epochs):
|
|
145
|
+
permutation = np.random.permutation(num_samples).astype(np.int32)
|
|
146
|
+
for start in range(0, num_samples, self.batch_size):
|
|
147
|
+
idx = Tensor(permutation[start:start + self.batch_size], device=self.device)
|
|
148
|
+
batch_states = obs_flat[idx].contiguous().realize()
|
|
149
|
+
batch_actions = raw_act_flat[idx].contiguous().realize()
|
|
150
|
+
batch_logprobs = old_logprobs[idx].contiguous().realize()
|
|
151
|
+
batch_advantages = advantages[idx].contiguous().realize()
|
|
152
|
+
batch_returns = returns[idx].contiguous().realize()
|
|
153
|
+
self.train_step(
|
|
154
|
+
batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns
|
|
155
|
+
)
|
tinybaselines/rollout.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RolloutState:
|
|
9
|
+
observations: np.ndarray
|
|
10
|
+
episode_returns: np.ndarray
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RolloutBatch(NamedTuple):
|
|
14
|
+
observations: np.ndarray
|
|
15
|
+
raw_actions: np.ndarray
|
|
16
|
+
rewards: np.ndarray
|
|
17
|
+
terminated: np.ndarray
|
|
18
|
+
truncated: np.ndarray
|
|
19
|
+
next_observations: np.ndarray
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def track_episode_returns(done_mask, ep_returns, ep_rets, remaining=None):
|
|
23
|
+
for idx in np.where(done_mask)[0]:
|
|
24
|
+
if remaining is None or remaining[idx] > 0:
|
|
25
|
+
ep_returns.append(ep_rets[idx])
|
|
26
|
+
if remaining is not None:
|
|
27
|
+
remaining[idx] -= 1
|
|
28
|
+
ep_rets[idx] = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=False, state=None):
|
|
32
|
+
assert (num_steps is None) != (num_episodes is None), 'Specify exactly one: num_steps or num_episodes'
|
|
33
|
+
if num_episodes is not None and num_episodes <= 0:
|
|
34
|
+
raise ValueError('num_episodes must be positive')
|
|
35
|
+
|
|
36
|
+
N = env.num_envs
|
|
37
|
+
if state is None:
|
|
38
|
+
state = RolloutState(env.reset()[0], np.zeros(N))
|
|
39
|
+
states = state.observations
|
|
40
|
+
ep_returns, ep_rets = [], state.episode_returns
|
|
41
|
+
|
|
42
|
+
collect = num_steps is not None
|
|
43
|
+
remaining = None if collect else (num_episodes + np.arange(N)) // N
|
|
44
|
+
if collect:
|
|
45
|
+
T = num_steps // N
|
|
46
|
+
obs = np.empty((T, *states.shape), dtype=np.float32)
|
|
47
|
+
next_obs = np.empty_like(obs)
|
|
48
|
+
rew = np.empty((T, N), dtype=np.float32)
|
|
49
|
+
terminations = np.empty((T, N), dtype=bool)
|
|
50
|
+
truncations = np.empty((T, N), dtype=bool)
|
|
51
|
+
|
|
52
|
+
t = 0
|
|
53
|
+
while True:
|
|
54
|
+
actions, raw_actions = actor_critic.act(states, deterministic=deterministic)
|
|
55
|
+
if collect:
|
|
56
|
+
if t == 0:
|
|
57
|
+
raw_act = np.empty((T, *raw_actions.shape), dtype=np.float32)
|
|
58
|
+
obs[t], raw_act[t] = states, raw_actions
|
|
59
|
+
|
|
60
|
+
states, rewards, terminated, truncated, infos = env.step(actions)
|
|
61
|
+
d = np.logical_or(terminated, truncated)
|
|
62
|
+
if collect:
|
|
63
|
+
rew[t], terminations[t], truncations[t] = rewards, terminated, truncated
|
|
64
|
+
next_obs[t] = states
|
|
65
|
+
final_obs = infos.get('final_obs')
|
|
66
|
+
for idx in np.flatnonzero(d):
|
|
67
|
+
if final_obs is not None and final_obs[idx] is not None:
|
|
68
|
+
next_obs[t, idx] = final_obs[idx]
|
|
69
|
+
elif truncated[idx] and not terminated[idx]:
|
|
70
|
+
raise ValueError('truncated transitions require final_obs')
|
|
71
|
+
|
|
72
|
+
ep_rets += rewards
|
|
73
|
+
track_episode_returns(d, ep_returns, ep_rets, remaining)
|
|
74
|
+
t += 1
|
|
75
|
+
|
|
76
|
+
if (collect and t >= T) or (not collect and not remaining.any()):
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
state.observations = states
|
|
80
|
+
return (RolloutBatch(obs, raw_act, rew, terminations, truncations, next_obs), ep_returns) if collect else ep_returns
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from time import perf_counter
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from .rollout import RolloutState, rollout
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TrainingStep(NamedTuple):
|
|
10
|
+
timesteps: int
|
|
11
|
+
episode_returns: list[float]
|
|
12
|
+
rollout_seconds: float
|
|
13
|
+
update_seconds: float
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def learn(env, ppo, total_timesteps, rollout_steps):
|
|
17
|
+
"""Train in full rollouts, yielding progress after each update. The timestep budget is a minimum."""
|
|
18
|
+
if total_timesteps < 0:
|
|
19
|
+
raise ValueError('total_timesteps must be nonnegative')
|
|
20
|
+
if rollout_steps <= 0 or rollout_steps % env.num_envs:
|
|
21
|
+
raise ValueError('rollout_steps must be a positive multiple of env.num_envs')
|
|
22
|
+
|
|
23
|
+
state = None
|
|
24
|
+
for timesteps in range(0, total_timesteps, rollout_steps):
|
|
25
|
+
if state is None:
|
|
26
|
+
state = RolloutState(env.reset()[0], np.zeros(env.num_envs))
|
|
27
|
+
started = perf_counter()
|
|
28
|
+
batch, returns = rollout(env, ppo.actor_critic, num_steps=rollout_steps, state=state)
|
|
29
|
+
collected = perf_counter()
|
|
30
|
+
ppo.update(batch)
|
|
31
|
+
updated = perf_counter()
|
|
32
|
+
yield TrainingStep(timesteps + batch.rewards.size, returns, collected - started, updated - collected)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinybaselines
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: PPO in tinygrad.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: tinygrad==0.14.0
|
|
10
|
+
Requires-Dist: numpy<3,>=1.26
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# tinybaselines
|
|
14
|
+
|
|
15
|
+
PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
|
|
16
|
+
|
|
17
|
+
Python 3.11 and clang.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python3.11 -m venv .venv
|
|
21
|
+
source .venv/bin/activate
|
|
22
|
+
pip install -e . -r examples/lunar_lander/requirements.txt
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Train, save, and load.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from statistics import mean
|
|
29
|
+
from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
|
|
30
|
+
from examples.lunar_lander.config import OBS_SCALE
|
|
31
|
+
from examples.lunar_lander.train import make_env
|
|
32
|
+
|
|
33
|
+
env = make_env(4, asynchronous=False)
|
|
34
|
+
policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
|
|
35
|
+
actor_layers=4, critic_layers=4, observation_scale=OBS_SCALE)
|
|
36
|
+
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
|
|
37
|
+
K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
|
|
38
|
+
|
|
39
|
+
for step in learn(env, ppo, total_timesteps=2_000_000, rollout_steps=20_000):
|
|
40
|
+
print(step.timesteps, step.rollout_seconds, step.update_seconds)
|
|
41
|
+
|
|
42
|
+
ppo.save('ppo.safetensors')
|
|
43
|
+
ppo = PPO.load('ppo.safetensors')
|
|
44
|
+
policy = ppo.actor_critic
|
|
45
|
+
observations, _ = env.reset()
|
|
46
|
+
actions, raw_actions = policy.act(observations, deterministic=True)
|
|
47
|
+
env.close()
|
|
48
|
+
|
|
49
|
+
eval_env = make_env(4, asynchronous=False)
|
|
50
|
+
returns = evaluate(eval_env, policy, num_episodes=100)
|
|
51
|
+
eval_env.close()
|
|
52
|
+
print(mean(returns))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`total_timesteps` and `rollout_steps` count transitions across all environments.
|
|
56
|
+
Use a positive multiple of `env.num_envs` for `rollout_steps`. Training stops
|
|
57
|
+
after the first full rollout that reaches the budget. Each step contains completed
|
|
58
|
+
`episode_returns` and collection/update times. The caller owns the environment.
|
|
59
|
+
Use a separate environment for evaluation during training.
|
|
60
|
+
`evaluate` resets its environment and returns complete episode returns. It samples
|
|
61
|
+
actions by default; `deterministic=True` uses clipped policy means.
|
|
62
|
+
`actions` go to the environment; `raw_actions` go into PPO.
|
|
63
|
+
The collector uses Gymnasium vector environments with same-step autoreset.
|
|
64
|
+
For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
|
|
65
|
+
|
|
66
|
+
PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
|
|
67
|
+
Continue training by passing the loaded PPO to `learn` with a new transition budget.
|
|
68
|
+
Each `learn` call starts fresh episodes and a new transition counter.
|
|
69
|
+
`policy.save(path)` exports a policy for inference.
|
|
70
|
+
|
|
71
|
+
The full LunarLander trainer, evaluation, and PyTorch reference live in
|
|
72
|
+
`examples/lunar_lander`.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
python -m examples.lunar_lander.train
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pendulum uses three observations and one action. Its example rescales policy actions
|
|
79
|
+
from `[-1, 1]` to torque in `[-2, 2]`.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
pip install -e . -r examples/pendulum/requirements.txt
|
|
83
|
+
DEV=CPU BEAM=0 python -m examples.pendulum.train --output runs/pendulum
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The output directory contains the configuration, episode returns, update times,
|
|
87
|
+
and a PPO checkpoint. Initial and final evaluations each use 100 fresh stochastic episodes.
|
|
88
|
+
|
|
89
|
+
Three independent four-environment CPU runs reached mean returns of -141.4, -162.2,
|
|
90
|
+
and -150.8 after 200,704 transitions each. Each final evaluation used the reloaded checkpoint.
|
|
91
|
+
Our acceptance cutoff is -200 over 100 fresh stochastic episodes per run; all three passed.
|
|
92
|
+
Gymnasium defines no solved threshold for Pendulum. We chose this cutoff after the first run.
|
|
93
|
+
Run records: [1](examples/pendulum/results/cpu.jsonl),
|
|
94
|
+
[2](examples/pendulum/results/cpu-2.jsonl), [3](examples/pendulum/results/cpu-3.jsonl).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
tinybaselines/__init__.py,sha256=YF5iiW_X6vwTmNRdiaQHMNgE7VMa2idpulh6IfqRzds,195
|
|
2
|
+
tinybaselines/evaluation.py,sha256=3ti_T2gtJgpulgX1C0HEVxXITXfxPGkACsJnKfUOE_o,246
|
|
3
|
+
tinybaselines/policies.py,sha256=u1fOphgpKufGYBquVdpE3cWhotBUUVcq79-XfQcL3DM,3358
|
|
4
|
+
tinybaselines/ppo.py,sha256=PmChTu-iHEHIhCr1NxL60qK3AXs2jItQOPMj0zhcGY8,7877
|
|
5
|
+
tinybaselines/rollout.py,sha256=9niFh9epX-W4P5rlzPc3b-UYrmHHqM7MOK-CnE4L9O8,2864
|
|
6
|
+
tinybaselines/training.py,sha256=5VfZq4S7xlBs6PtdIgDP51Ei1uNGdBqi6f-SNESLYls,1171
|
|
7
|
+
tinybaselines-0.1.0a1.dist-info/licenses/LICENSE,sha256=voIoq73Yv9HUCAMenLnIuZ-t4TJa_CfIO7TKUlwhWlc,1069
|
|
8
|
+
tinybaselines-0.1.0a1.dist-info/METADATA,sha256=O_qFoN-6Jeja_wmcxoslMIdglQAK3bq1ZqLwQZeq9q0,3728
|
|
9
|
+
tinybaselines-0.1.0a1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
tinybaselines-0.1.0a1.dist-info/top_level.txt,sha256=P-g_X6Qa5L4-vsMCPSgUkolHGQMiDfxhra0_KSeEhpg,14
|
|
11
|
+
tinybaselines-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Utkarsh Gill
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tinybaselines
|