tinybaselines 0.1.0a1__tar.gz → 0.1.0a3__tar.gz
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-0.1.0a1/tinybaselines.egg-info → tinybaselines-0.1.0a3}/PKG-INFO +49 -26
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/README.md +48 -25
- tinybaselines-0.1.0a3/examples/cartpole/results/cpu.jsonl +4 -0
- tinybaselines-0.1.0a3/examples/cartpole/train.py +33 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/train.py +1 -1
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/pyproject.toml +1 -1
- tinybaselines-0.1.0a3/tests/test_cartpole.py +39 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_checkpoint.py +12 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_core.py +41 -0
- tinybaselines-0.1.0a3/tests/test_learning.py +89 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_parity.py +2 -1
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_pendulum.py +9 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/__init__.py +1 -1
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/policies.py +72 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/ppo.py +18 -17
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/rollout.py +5 -1
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3/tinybaselines.egg-info}/PKG-INFO +49 -26
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/SOURCES.txt +3 -0
- tinybaselines-0.1.0a1/tests/test_learning.py +0 -47
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/LICENSE +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/MANIFEST.in +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/__init__.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/__init__.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/config.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/reference.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements-reference.txt +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements.txt +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-initial-tinygrad-thread4.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-initial-torch.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-protocol.json +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-tinygrad-thread10.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-tinygrad-thread4-error.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-torch-thread4.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/protocol.json +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/tinygrad-cpu.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/torch-cpu.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/run_learning.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/__init__.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/env.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/requirements.txt +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu-2.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu-3.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu.jsonl +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/train.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/requirements.txt +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/setup.cfg +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_evaluation.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_learning_run.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_rollout.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_training.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/evaluation.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/training.py +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/dependency_links.txt +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/requires.txt +0 -0
- {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinybaselines
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a3
|
|
4
4
|
Summary: PPO in tinygrad.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -12,44 +12,53 @@ Dynamic: license-file
|
|
|
12
12
|
|
|
13
13
|
# tinybaselines
|
|
14
14
|
|
|
15
|
-
PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
|
|
15
|
+
PPO in tinygrad for discrete and continuous actions. Continuous actions are clipped to `[-1, 1]`.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## LunarLander from PyPI
|
|
18
|
+
|
|
19
|
+
Use Python 3.11 and clang. The library install does not include Gymnasium.
|
|
18
20
|
|
|
19
21
|
```sh
|
|
20
22
|
python3.11 -m venv .venv
|
|
21
23
|
source .venv/bin/activate
|
|
22
|
-
|
|
24
|
+
python -m pip install tinybaselines==0.1.0a3
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Install Gymnasium and Box2D to run this example:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
python -m pip install 'gymnasium[box2d]==1.4.0'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Save this as `train_lander.py`. It runs one PPO update and evaluates the reloaded checkpoint.
|
|
26
34
|
|
|
27
35
|
```python
|
|
36
|
+
from contextlib import closing
|
|
28
37
|
from statistics import mean
|
|
38
|
+
|
|
39
|
+
import gymnasium as gym
|
|
40
|
+
|
|
29
41
|
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
42
|
|
|
33
|
-
env = make_env(4, asynchronous=False)
|
|
34
43
|
policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
|
|
35
|
-
|
|
44
|
+
actor_layers=4, critic_layers=4,
|
|
45
|
+
observation_scale=[10, 6.666, 5, 7.5, 1, 2.5, 1, 1])
|
|
36
46
|
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
|
|
37
47
|
K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ppo =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
env
|
|
49
|
+
with closing(gym.vector.SyncVectorEnv(
|
|
50
|
+
[lambda: gym.make('LunarLander-v3', continuous=True)] * 4,
|
|
51
|
+
autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
|
|
52
|
+
)) as env:
|
|
53
|
+
for step in learn(env, ppo, total_timesteps=20_000, rollout_steps=20_000):
|
|
54
|
+
print(step.timesteps, step.rollout_seconds, step.update_seconds)
|
|
55
|
+
ppo.save('ppo.safetensors')
|
|
56
|
+
policy = PPO.load('ppo.safetensors').actor_critic
|
|
57
|
+
print(mean(evaluate(env, policy, num_episodes=10)))
|
|
58
|
+
```
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
eval_env.close()
|
|
52
|
-
print(mean(returns))
|
|
60
|
+
```sh
|
|
61
|
+
DEV=CPU BEAM=0 python train_lander.py
|
|
53
62
|
```
|
|
54
63
|
|
|
55
64
|
`total_timesteps` and `rollout_steps` count transitions across all environments.
|
|
@@ -63,18 +72,32 @@ actions by default; `deterministic=True` uses clipped policy means.
|
|
|
63
72
|
The collector uses Gymnasium vector environments with same-step autoreset.
|
|
64
73
|
For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
|
|
65
74
|
|
|
66
|
-
PPO checkpoints contain the policy,
|
|
75
|
+
PPO checkpoints contain the policy, Adam state, and PPO settings.
|
|
67
76
|
Continue training by passing the loaded PPO to `learn` with a new transition budget.
|
|
68
77
|
Each `learn` call starts fresh episodes and a new transition counter.
|
|
69
|
-
`
|
|
78
|
+
`GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
|
|
79
|
+
|
|
80
|
+
## Repository examples
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
`examples/lunar_lander`.
|
|
82
|
+
From a checkout, run the full LunarLander trainer:
|
|
73
83
|
|
|
74
84
|
```sh
|
|
85
|
+
pip install -e . -r examples/lunar_lander/requirements.txt
|
|
75
86
|
python -m examples.lunar_lander.train
|
|
76
87
|
```
|
|
77
88
|
|
|
89
|
+
CartPole uses `CategoricalActorCritic`. The example trains, saves a checkpoint,
|
|
90
|
+
and evaluates the reloaded policy over 10 episodes.
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pip install -e . 'gymnasium==1.4.0'
|
|
94
|
+
DEV=CPU BEAM=0 python -m examples.cartpole.train --checkpoint cartpole.safetensors
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Three independent four-environment CPU runs averaged 500.00, 496.67, and 494.37
|
|
98
|
+
after 102,400 transitions each. All cleared 475 over 100 fresh stochastic episodes
|
|
99
|
+
per reloaded checkpoint. [Run records](examples/cartpole/results/cpu.jsonl).
|
|
100
|
+
|
|
78
101
|
Pendulum uses three observations and one action. Its example rescales policy actions
|
|
79
102
|
from `[-1, 1]` to torque in `[-2, 2]`.
|
|
80
103
|
|
|
@@ -1,43 +1,52 @@
|
|
|
1
1
|
# tinybaselines
|
|
2
2
|
|
|
3
|
-
PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
|
|
3
|
+
PPO in tinygrad for discrete and continuous actions. Continuous actions are clipped to `[-1, 1]`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## LunarLander from PyPI
|
|
6
|
+
|
|
7
|
+
Use Python 3.11 and clang. The library install does not include Gymnasium.
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
10
|
python3.11 -m venv .venv
|
|
9
11
|
source .venv/bin/activate
|
|
10
|
-
|
|
12
|
+
python -m pip install tinybaselines==0.1.0a3
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
Install Gymnasium and Box2D to run this example:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
python -m pip install 'gymnasium[box2d]==1.4.0'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Save this as `train_lander.py`. It runs one PPO update and evaluates the reloaded checkpoint.
|
|
14
22
|
|
|
15
23
|
```python
|
|
24
|
+
from contextlib import closing
|
|
16
25
|
from statistics import mean
|
|
26
|
+
|
|
27
|
+
import gymnasium as gym
|
|
28
|
+
|
|
17
29
|
from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
|
|
18
|
-
from examples.lunar_lander.config import OBS_SCALE
|
|
19
|
-
from examples.lunar_lander.train import make_env
|
|
20
30
|
|
|
21
|
-
env = make_env(4, asynchronous=False)
|
|
22
31
|
policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
|
|
23
|
-
|
|
32
|
+
actor_layers=4, critic_layers=4,
|
|
33
|
+
observation_scale=[10, 6.666, 5, 7.5, 1, 2.5, 1, 1])
|
|
24
34
|
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
|
|
25
35
|
K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
ppo =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
env
|
|
37
|
+
with closing(gym.vector.SyncVectorEnv(
|
|
38
|
+
[lambda: gym.make('LunarLander-v3', continuous=True)] * 4,
|
|
39
|
+
autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
|
|
40
|
+
)) as env:
|
|
41
|
+
for step in learn(env, ppo, total_timesteps=20_000, rollout_steps=20_000):
|
|
42
|
+
print(step.timesteps, step.rollout_seconds, step.update_seconds)
|
|
43
|
+
ppo.save('ppo.safetensors')
|
|
44
|
+
policy = PPO.load('ppo.safetensors').actor_critic
|
|
45
|
+
print(mean(evaluate(env, policy, num_episodes=10)))
|
|
46
|
+
```
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
eval_env.close()
|
|
40
|
-
print(mean(returns))
|
|
48
|
+
```sh
|
|
49
|
+
DEV=CPU BEAM=0 python train_lander.py
|
|
41
50
|
```
|
|
42
51
|
|
|
43
52
|
`total_timesteps` and `rollout_steps` count transitions across all environments.
|
|
@@ -51,18 +60,32 @@ actions by default; `deterministic=True` uses clipped policy means.
|
|
|
51
60
|
The collector uses Gymnasium vector environments with same-step autoreset.
|
|
52
61
|
For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
|
|
53
62
|
|
|
54
|
-
PPO checkpoints contain the policy,
|
|
63
|
+
PPO checkpoints contain the policy, Adam state, and PPO settings.
|
|
55
64
|
Continue training by passing the loaded PPO to `learn` with a new transition budget.
|
|
56
65
|
Each `learn` call starts fresh episodes and a new transition counter.
|
|
57
|
-
`
|
|
66
|
+
`GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
|
|
67
|
+
|
|
68
|
+
## Repository examples
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
`examples/lunar_lander`.
|
|
70
|
+
From a checkout, run the full LunarLander trainer:
|
|
61
71
|
|
|
62
72
|
```sh
|
|
73
|
+
pip install -e . -r examples/lunar_lander/requirements.txt
|
|
63
74
|
python -m examples.lunar_lander.train
|
|
64
75
|
```
|
|
65
76
|
|
|
77
|
+
CartPole uses `CategoricalActorCritic`. The example trains, saves a checkpoint,
|
|
78
|
+
and evaluates the reloaded policy over 10 episodes.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
pip install -e . 'gymnasium==1.4.0'
|
|
82
|
+
DEV=CPU BEAM=0 python -m examples.cartpole.train --checkpoint cartpole.safetensors
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Three independent four-environment CPU runs averaged 500.00, 496.67, and 494.37
|
|
86
|
+
after 102,400 transitions each. All cleared 475 over 100 fresh stochastic episodes
|
|
87
|
+
per reloaded checkpoint. [Run records](examples/cartpole/results/cpu.jsonl).
|
|
88
|
+
|
|
66
89
|
Pendulum uses three observations and one action. Its example rescales policy actions
|
|
67
90
|
from `[-1, 1]` to torque in `[-2, 2]`.
|
|
68
91
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"event": "config", "source_commit": "536bfa324af5ce2793a2332844af36b4033a6cb2", "environment": "CartPole-v1", "num_envs": 4, "seed": null, "requested_timesteps": 102400, "rollout_steps": 1024, "evaluation_episodes": 100, "deterministic": false, "acceptance_mean": 475, "policy": {"state_dim": 4, "action_dim": 2, "hidden_dim": 64, "actor_layers": 2, "critic_layers": 2}, "ppo": {"pi_lr": 0.0010000000474974513, "vf_lr": 0.0010000000474974513, "gamma": 0.99, "lamda": 0.95, "K_epochs": 4, "eps_clip": 0.2, "batch_size": 256, "vf_coef": 0.5, "entropy_coef": 0.01}, "device": "CPU", "cpu_threads": 10, "beam": "0", "jit": "1", "python": "3.11.14", "platform": "macOS-27.0.1-arm64-arm-64bit", "versions": {"tinygrad": "0.14.0", "numpy": "2.4.6", "gymnasium": "1.1.1"}}
|
|
2
|
+
{"event": "final", "run": 1, "timesteps": 102400, "mean_return": 500.0, "episode_returns": [500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0], "passed": true, "training_seconds": null, "evaluation_seconds": 14.94911529199453, "checkpoint": "run-1.safetensors", "note": "Repeated evaluation from the same checkpoint after a logging error."}
|
|
3
|
+
{"event": "final", "run": 2, "timesteps": 102400, "mean_return": 496.67, "episode_returns": [500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 390.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 445.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 332.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0], "passed": true, "training_seconds": 56.7557016669889, "evaluation_seconds": 16.15585825004382, "checkpoint": "run-2.safetensors"}
|
|
4
|
+
{"event": "final", "run": 3, "timesteps": 102400, "mean_return": 494.37, "episode_returns": [500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 96.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 341.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0], "passed": true, "training_seconds": 55.03045945899794, "evaluation_seconds": 16.498802624992095, "checkpoint": "run-3.safetensors"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from contextlib import closing
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from statistics import mean
|
|
5
|
+
|
|
6
|
+
import gymnasium as gym
|
|
7
|
+
|
|
8
|
+
from tinybaselines import CategoricalActorCritic, PPO, evaluate, learn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description='Train PPO on CartPole-v1.')
|
|
13
|
+
parser.add_argument('--timesteps', type=int, default=40_960)
|
|
14
|
+
parser.add_argument('--checkpoint', type=Path, required=True)
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
|
|
17
|
+
policy = CategoricalActorCritic(4, 2, 64, 2, 2)
|
|
18
|
+
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=4,
|
|
19
|
+
eps_clip=0.2, batch_size=256, vf_coef=0.5, entropy_coef=0.01)
|
|
20
|
+
with closing(gym.vector.SyncVectorEnv(
|
|
21
|
+
[lambda: gym.make('CartPole-v1')] * 4,
|
|
22
|
+
autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
|
|
23
|
+
)) as env:
|
|
24
|
+
for step in learn(env, ppo, total_timesteps=args.timesteps, rollout_steps=1024):
|
|
25
|
+
print(f'{step.timesteps} transitions')
|
|
26
|
+
ppo.save(args.checkpoint)
|
|
27
|
+
policy = PPO.load(args.checkpoint).actor_critic
|
|
28
|
+
returns = evaluate(env, policy, num_episodes=10)
|
|
29
|
+
print(f'Mean return over {len(returns)} episodes: {mean(returns):.1f}')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
main()
|
|
@@ -12,7 +12,7 @@ from tinygrad import Tensor, TinyJit, nn, Context
|
|
|
12
12
|
from tqdm import trange
|
|
13
13
|
from .config import ENV_NAME, OBS_SCALE
|
|
14
14
|
from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
|
|
15
|
-
from tinybaselines.ppo import clip_grad_norm
|
|
15
|
+
from tinybaselines.ppo import clip_grad_norm
|
|
16
16
|
from tinybaselines.rollout import RolloutBatch, RolloutState, rollout, track_episode_returns
|
|
17
17
|
|
|
18
18
|
warnings.filterwarnings('ignore', message='pkg_resources is deprecated')
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from contextlib import closing
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from tempfile import TemporaryDirectory
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
import gymnasium as gym
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from tinybaselines import CategoricalActorCritic, PPO, evaluate, learn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CartPoleTest(unittest.TestCase):
|
|
13
|
+
def test_categorical_policy_trains_with_ppo(self):
|
|
14
|
+
policy = CategoricalActorCritic(4, 2, 16, 1, 1)
|
|
15
|
+
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=2,
|
|
16
|
+
eps_clip=0.2, batch_size=64, vf_coef=0.5, entropy_coef=0.01)
|
|
17
|
+
weight = policy.actor.layers[-1].weight
|
|
18
|
+
before = weight.numpy().copy()
|
|
19
|
+
with closing(gym.vector.SyncVectorEnv([lambda: gym.make('CartPole-v1')] * 4,
|
|
20
|
+
autoreset_mode=gym.vector.AutoresetMode.SAME_STEP)) as env:
|
|
21
|
+
actions, _ = policy.act(env.reset()[0])
|
|
22
|
+
self.assertTrue(env.action_space.contains(actions))
|
|
23
|
+
steps = list(learn(env, ppo, total_timesteps=384, rollout_steps=128))
|
|
24
|
+
self.assertEqual(steps[-1].timesteps, 384)
|
|
25
|
+
self.assertFalse(np.array_equal(weight.numpy(), before))
|
|
26
|
+
observations, _ = env.reset()
|
|
27
|
+
with TemporaryDirectory() as directory:
|
|
28
|
+
path = Path(directory) / 'ppo.safetensors'
|
|
29
|
+
ppo.save(path)
|
|
30
|
+
restored = PPO.load(path)
|
|
31
|
+
self.assertIsInstance(restored.actor_critic, CategoricalActorCritic)
|
|
32
|
+
np.testing.assert_array_equal(restored.actor_critic.act(observations, deterministic=True)[0],
|
|
33
|
+
policy.act(observations, deterministic=True)[0])
|
|
34
|
+
state = restored.state_dict()
|
|
35
|
+
self.assertEqual(state.keys(), ppo.state_dict().keys())
|
|
36
|
+
for name, tensor in ppo.state_dict().items():
|
|
37
|
+
np.testing.assert_array_equal(state[name].numpy(), tensor.numpy())
|
|
38
|
+
self.assertEqual(len(evaluate(env, restored.actor_critic, num_episodes=8)), 8)
|
|
39
|
+
self.assertEqual(next(learn(env, restored, total_timesteps=128, rollout_steps=128)).timesteps, 128)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import tempfile
|
|
2
3
|
import unittest
|
|
3
4
|
from pathlib import Path
|
|
@@ -83,6 +84,17 @@ class CheckpointTest(unittest.TestCase):
|
|
|
83
84
|
np.testing.assert_array_equal(first.actor_critic.log_std.numpy(), before)
|
|
84
85
|
np.testing.assert_array_equal(second.actor_critic.log_std.numpy(), [0.25])
|
|
85
86
|
|
|
87
|
+
def test_loads_gaussian_checkpoint_without_policy_type(self):
|
|
88
|
+
ppo = self.make_ppo()
|
|
89
|
+
metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(ppo.actor_critic.config),
|
|
90
|
+
ppo_config=json.dumps(ppo.config))
|
|
91
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
92
|
+
path = Path(directory) / 'legacy.safetensors'
|
|
93
|
+
lander.nn.state.safe_save(ppo.state_dict(), path, metadata=metadata)
|
|
94
|
+
restored = PPO.load(path)
|
|
95
|
+
self.assertIsInstance(restored.actor_critic, GaussianActorCritic)
|
|
96
|
+
np.testing.assert_array_equal(restored.actor_critic.observation_scale.numpy(), [2, 3, 4])
|
|
97
|
+
|
|
86
98
|
def test_failed_save_preserves_previous_checkpoint(self):
|
|
87
99
|
ppo = self.make_ppo()
|
|
88
100
|
|
|
@@ -6,12 +6,53 @@ import unittest
|
|
|
6
6
|
from unittest.mock import patch
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
|
+
from tinygrad import Tensor
|
|
9
10
|
|
|
10
11
|
from tinybaselines import GaussianActorCritic, PPO, rollout
|
|
11
12
|
from tinybaselines.rollout import RolloutBatch
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class CoreTest(unittest.TestCase):
|
|
16
|
+
def test_action_dtype_and_shape_survive_minibatching(self):
|
|
17
|
+
observations = np.zeros((4, 3), np.float32)
|
|
18
|
+
done = np.zeros(4, bool)
|
|
19
|
+
env = SimpleNamespace(num_envs=4, reset=lambda: (observations, {}),
|
|
20
|
+
step=lambda actions: (observations, np.ones(4), done, done, {}))
|
|
21
|
+
policy = GaussianActorCritic(3, 2, 4, 1, 1)
|
|
22
|
+
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=1,
|
|
23
|
+
eps_clip=0.2, batch_size=4, vf_coef=1.0, entropy_coef=0.0)
|
|
24
|
+
values = Tensor.zeros(8, 1, device=policy.device)
|
|
25
|
+
for actions in (np.arange(4, dtype=np.int32), np.arange(8, dtype=np.float32).reshape(4, 2) / 8):
|
|
26
|
+
with self.subTest(dtype=actions.dtype, shape=actions.shape):
|
|
27
|
+
with patch.object(policy, 'act', return_value=(actions, actions)):
|
|
28
|
+
batch, _ = rollout(env, policy, num_steps=8)
|
|
29
|
+
self.assertEqual(batch.raw_actions.dtype, actions.dtype)
|
|
30
|
+
np.testing.assert_array_equal(batch.raw_actions, np.stack([actions, actions]))
|
|
31
|
+
with patch.object(policy, 'evaluate_actions', return_value=(values, values.squeeze(-1), None)), \
|
|
32
|
+
patch.object(ppo, 'train_step') as step, patch('numpy.random.permutation', return_value=np.arange(8)):
|
|
33
|
+
ppo.update(batch)
|
|
34
|
+
minibatches = np.concatenate([call.args[1].numpy() for call in step.call_args_list])
|
|
35
|
+
self.assertEqual(minibatches.dtype, actions.dtype)
|
|
36
|
+
np.testing.assert_array_equal(minibatches, np.concatenate([actions, actions]))
|
|
37
|
+
|
|
38
|
+
def test_ppo_gets_actor_parameters_and_device_from_policy(self):
|
|
39
|
+
model = GaussianActorCritic(3, 1, 4, 1, 1)
|
|
40
|
+
weight = model.actor.layers[0].weight
|
|
41
|
+
policy = SimpleNamespace(actor_parameters=[weight], device=weight.device, critic=model.critic)
|
|
42
|
+
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=1,
|
|
43
|
+
eps_clip=0.2, batch_size=4, vf_coef=1.0, entropy_coef=0.0)
|
|
44
|
+
self.assertEqual(ppo.device, policy.device)
|
|
45
|
+
self.assertIs(ppo.pi_optimizer.params[0], weight)
|
|
46
|
+
|
|
47
|
+
def test_rejects_nonpositive_update_settings(self):
|
|
48
|
+
policy = GaussianActorCritic(3, 1, 4, 1, 1)
|
|
49
|
+
settings = dict(pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=1,
|
|
50
|
+
eps_clip=0.2, batch_size=4, vf_coef=1.0, entropy_coef=0.0)
|
|
51
|
+
for name in ('K_epochs', 'batch_size'):
|
|
52
|
+
for value in (0, -1):
|
|
53
|
+
with self.subTest(name=name, value=value), self.assertRaisesRegex(ValueError, f'{name} must be positive'):
|
|
54
|
+
PPO(policy, **(settings | {name: value}))
|
|
55
|
+
|
|
15
56
|
def test_terminals_do_not_require_final_observations(self):
|
|
16
57
|
observations = np.zeros((2, 3), np.float32)
|
|
17
58
|
reset_observations = np.full((2, 3), 100, np.float32)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from tinygrad import Tensor
|
|
5
|
+
|
|
6
|
+
from tinybaselines import CategoricalActorCritic, GaussianActorCritic, PPO, learn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IdentityEnv:
|
|
10
|
+
num_envs = 4
|
|
11
|
+
|
|
12
|
+
def __init__(self, seed):
|
|
13
|
+
self.rng = np.random.default_rng(seed)
|
|
14
|
+
|
|
15
|
+
def reset(self):
|
|
16
|
+
self.observations = self.rng.uniform(-0.75, 0.75, (self.num_envs, 1)).astype(np.float32)
|
|
17
|
+
return self.observations, {}
|
|
18
|
+
|
|
19
|
+
def step(self, actions):
|
|
20
|
+
rewards = -np.square(actions[:, 0] - self.observations[:, 0])
|
|
21
|
+
observations, _ = self.reset()
|
|
22
|
+
return observations, rewards, np.ones(self.num_envs, bool), np.zeros(self.num_envs, bool), {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def identity_errors(seed):
|
|
26
|
+
Tensor.manual_seed(seed)
|
|
27
|
+
np.random.seed(seed)
|
|
28
|
+
policy = GaussianActorCritic(1, 1, 16, 1, 1)
|
|
29
|
+
ppo = PPO(policy, pi_lr=0.01, vf_lr=0.01, gamma=0.99, lamda=0.95, K_epochs=4,
|
|
30
|
+
eps_clip=0.2, batch_size=256, vf_coef=0.5, entropy_coef=0.0)
|
|
31
|
+
observations = np.linspace(-0.75, 0.75, 65, dtype=np.float32)[:, None]
|
|
32
|
+
|
|
33
|
+
def error():
|
|
34
|
+
actions, _ = policy.act(observations, deterministic=True)
|
|
35
|
+
return np.square(actions - observations).mean()
|
|
36
|
+
|
|
37
|
+
before = error()
|
|
38
|
+
for _ in learn(IdentityEnv(seed), ppo, total_timesteps=2048, rollout_steps=256):
|
|
39
|
+
pass
|
|
40
|
+
return before, error(), np.square(observations).mean()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DiscreteIdentityEnv:
|
|
44
|
+
num_envs = 4
|
|
45
|
+
observations = np.eye(4, dtype=np.float32)
|
|
46
|
+
|
|
47
|
+
def __init__(self, seed):
|
|
48
|
+
self.rng = np.random.default_rng(seed)
|
|
49
|
+
|
|
50
|
+
def reset(self):
|
|
51
|
+
self.targets = self.rng.integers(4, size=self.num_envs)
|
|
52
|
+
return self.observations[self.targets], {}
|
|
53
|
+
|
|
54
|
+
def step(self, actions):
|
|
55
|
+
rewards = (actions == self.targets).astype(np.float32)
|
|
56
|
+
observations, _ = self.reset()
|
|
57
|
+
return observations, rewards, np.ones(self.num_envs, bool), np.zeros(self.num_envs, bool), {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def discrete_identity_scores(seed):
|
|
61
|
+
Tensor.manual_seed(seed)
|
|
62
|
+
np.random.seed(seed)
|
|
63
|
+
env = DiscreteIdentityEnv(seed)
|
|
64
|
+
policy = CategoricalActorCritic(4, 4, 16, 1, 1)
|
|
65
|
+
ppo = PPO(policy, pi_lr=0.02, vf_lr=0.01, gamma=0.99, lamda=0.95, K_epochs=4,
|
|
66
|
+
eps_clip=0.2, batch_size=64, vf_coef=0.5, entropy_coef=0.0)
|
|
67
|
+
states = policy.preprocess(env.observations)
|
|
68
|
+
targets = Tensor(np.arange(4, dtype=np.int32), device=policy.device)
|
|
69
|
+
|
|
70
|
+
def score():
|
|
71
|
+
_, log_probs, _ = policy.evaluate_actions(states, targets)
|
|
72
|
+
return log_probs.exp().mean().item()
|
|
73
|
+
|
|
74
|
+
before = score()
|
|
75
|
+
for _ in learn(env, ppo, total_timesteps=1024, rollout_steps=128):
|
|
76
|
+
pass
|
|
77
|
+
return before, score()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LearningTest(unittest.TestCase):
|
|
81
|
+
def test_learns_identity(self):
|
|
82
|
+
self.addCleanup(np.random.set_state, np.random.get_state())
|
|
83
|
+
before, after, zero = identity_errors(seed=0)
|
|
84
|
+
self.assertLess(after, 0.25 * min(before, zero), (before, after, zero))
|
|
85
|
+
|
|
86
|
+
def test_learns_discrete_identity(self):
|
|
87
|
+
self.addCleanup(np.random.set_state, np.random.get_state())
|
|
88
|
+
before, after = discrete_identity_scores(seed=0)
|
|
89
|
+
self.assertGreater(after, max(before, 0.9), (before, after))
|
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
|
5
5
|
import torch
|
|
6
6
|
|
|
7
7
|
from examples.lunar_lander import train as lander, reference as reference_lander
|
|
8
|
+
from tinybaselines.policies import normal_log_prob
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ParityTest(unittest.TestCase):
|
|
@@ -205,7 +206,7 @@ class ParityTest(unittest.TestCase):
|
|
|
205
206
|
torch_mean = torch.tensor(means, requires_grad=True)
|
|
206
207
|
torch_log_std = torch.tensor(log_std, requires_grad=True)
|
|
207
208
|
with lander.Context(TRAINING=1):
|
|
208
|
-
actual =
|
|
209
|
+
actual = normal_log_prob(tiny_mean, tiny_log_std, lander.Tensor(actions))
|
|
209
210
|
actual.sum().backward()
|
|
210
211
|
expected = torch.distributions.Normal(torch_mean, torch_log_std.exp()).log_prob(torch.tensor(actions)).sum(-1)
|
|
211
212
|
expected.sum().backward()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from types import SimpleNamespace
|
|
2
2
|
import unittest
|
|
3
3
|
|
|
4
|
+
import gymnasium as gym
|
|
4
5
|
import numpy as np
|
|
5
6
|
|
|
6
7
|
from examples.pendulum.env import make_env
|
|
@@ -8,6 +9,14 @@ from tinybaselines import GaussianActorCritic, PPO, learn, rollout
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class PendulumTest(unittest.TestCase):
|
|
12
|
+
def test_rejects_default_vector_autoreset(self):
|
|
13
|
+
env = gym.vector.SyncVectorEnv([lambda: gym.make('Pendulum-v1')] * 4)
|
|
14
|
+
self.addCleanup(env.close)
|
|
15
|
+
actions = np.zeros((4, 1), np.float32)
|
|
16
|
+
policy = SimpleNamespace(act=lambda obs, deterministic: (actions, actions))
|
|
17
|
+
with self.assertRaisesRegex(ValueError, 'autoreset_mode=SAME_STEP'):
|
|
18
|
+
rollout(env, policy, num_steps=8)
|
|
19
|
+
|
|
11
20
|
def test_normalized_actions_cover_torque_range(self):
|
|
12
21
|
env = make_env(4)
|
|
13
22
|
try:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .ppo import PPO
|
|
2
|
-
from .policies import GaussianActorCritic
|
|
2
|
+
from .policies import CategoricalActorCritic, GaussianActorCritic
|
|
3
3
|
from .rollout import RolloutBatch, RolloutState, rollout
|
|
4
4
|
from .training import TrainingStep, learn
|
|
5
5
|
from .evaluation import evaluate
|
|
@@ -3,6 +3,14 @@ import json
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from tinygrad import Tensor, TinyJit, nn
|
|
5
5
|
|
|
6
|
+
LOG_2PI = float(np.log(2.0 * np.pi))
|
|
7
|
+
NORMAL_ENTROPY_CONSTANT = float(0.5 * np.log(2.0 * np.pi * np.e))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normal_log_prob(action_mean, log_std, actions):
|
|
11
|
+
normalized = (actions - action_mean) / log_std.exp()
|
|
12
|
+
return (-0.5 * (normalized.square() + 2.0 * log_std + LOG_2PI)).sum(axis=-1)
|
|
13
|
+
|
|
6
14
|
|
|
7
15
|
class MLP:
|
|
8
16
|
def __init__(self, input_dim, output_dim, hidden_dim, layer_count):
|
|
@@ -17,6 +25,8 @@ class MLP:
|
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
class GaussianActorCritic:
|
|
28
|
+
_checkpoint_type = 'gaussian'
|
|
29
|
+
|
|
20
30
|
def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers, observation_scale=None):
|
|
21
31
|
self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
|
|
22
32
|
actor_layers=actor_layers, critic_layers=critic_layers)
|
|
@@ -31,12 +41,27 @@ class GaussianActorCritic:
|
|
|
31
41
|
self.actor_jit = TinyJit(lambda state: self.actor(state).realize())
|
|
32
42
|
self.refresh_inference()
|
|
33
43
|
|
|
44
|
+
@property
|
|
45
|
+
def actor_parameters(self):
|
|
46
|
+
return nn.state.get_parameters(self.actor) + [self.log_std]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def device(self):
|
|
50
|
+
return self.log_std.device
|
|
51
|
+
|
|
34
52
|
def __call__(self, state):
|
|
35
53
|
action_mean = self.actor(state)
|
|
36
54
|
log_std = self.log_std.clip(-5, 2)
|
|
37
55
|
value = self.critic(state)
|
|
38
56
|
return action_mean, log_std, value
|
|
39
57
|
|
|
58
|
+
def evaluate_actions(self, states, actions):
|
|
59
|
+
"""Return values, action log probabilities, and mean entropy for preprocessed states."""
|
|
60
|
+
means, log_std, values = self(states)
|
|
61
|
+
log_probs = normal_log_prob(means, log_std, actions)
|
|
62
|
+
entropy = (log_std + NORMAL_ENTROPY_CONSTANT).sum()
|
|
63
|
+
return values, log_probs, entropy
|
|
64
|
+
|
|
40
65
|
def preprocess(self, observations):
|
|
41
66
|
return Tensor(observations.astype(np.float32), device=self.observation_scale.device) * self.observation_scale
|
|
42
67
|
|
|
@@ -73,3 +98,50 @@ class GaussianActorCritic:
|
|
|
73
98
|
self.action_std = np.exp(self.log_std.clip(-5, 2).numpy())
|
|
74
99
|
if not np.isfinite(self.action_std).all():
|
|
75
100
|
raise FloatingPointError('non-finite policy standard deviation')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class CategoricalActorCritic:
|
|
104
|
+
"""Actor-critic for a single discrete action per observation."""
|
|
105
|
+
|
|
106
|
+
_checkpoint_type = 'categorical'
|
|
107
|
+
|
|
108
|
+
def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers):
|
|
109
|
+
self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
|
|
110
|
+
actor_layers=actor_layers, critic_layers=critic_layers)
|
|
111
|
+
self.actor = MLP(state_dim, action_dim, hidden_dim, actor_layers)
|
|
112
|
+
self.critic = MLP(state_dim, 1, hidden_dim, critic_layers)
|
|
113
|
+
self.actor_jit = TinyJit(lambda states: self.actor(states).realize())
|
|
114
|
+
self.jit_shape = None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def actor_parameters(self):
|
|
118
|
+
return nn.state.get_parameters(self.actor)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def device(self):
|
|
122
|
+
return self.actor.layers[0].weight.device
|
|
123
|
+
|
|
124
|
+
def preprocess(self, observations):
|
|
125
|
+
return Tensor(observations.astype(np.float32), device=self.device)
|
|
126
|
+
|
|
127
|
+
def evaluate_actions(self, states, actions):
|
|
128
|
+
log_probs = self.actor(states).log_softmax(axis=-1)
|
|
129
|
+
selected = log_probs.gather(-1, actions.unsqueeze(-1)).squeeze(-1)
|
|
130
|
+
entropy = -(log_probs.exp() * log_probs).sum(axis=-1).mean()
|
|
131
|
+
return self.critic(states), selected, entropy
|
|
132
|
+
|
|
133
|
+
def act(self, state, deterministic=False):
|
|
134
|
+
states = self.preprocess(state)
|
|
135
|
+
if self.jit_shape != states.shape:
|
|
136
|
+
self.refresh_inference()
|
|
137
|
+
self.jit_shape = states.shape
|
|
138
|
+
logits = self.actor_jit(states).numpy()
|
|
139
|
+
scores = logits if deterministic else logits + np.random.gumbel(size=logits.shape)
|
|
140
|
+
actions = scores.argmax(axis=-1).astype(np.int32)
|
|
141
|
+
return actions, actions
|
|
142
|
+
|
|
143
|
+
def refresh_inference(self):
|
|
144
|
+
self.actor_jit.reset()
|
|
145
|
+
|
|
146
|
+
def state_dict(self):
|
|
147
|
+
return nn.state.get_state_dict(dict(actor=self.actor, critic=self.critic))
|
|
@@ -5,15 +5,9 @@ from tempfile import TemporaryDirectory
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from tinygrad import Context, Tensor, TinyJit, nn
|
|
7
7
|
|
|
8
|
-
from .policies import GaussianActorCritic
|
|
8
|
+
from .policies import CategoricalActorCritic, GaussianActorCritic
|
|
9
9
|
|
|
10
|
-
|
|
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)
|
|
10
|
+
_CHECKPOINT_POLICIES = {policy._checkpoint_type: policy for policy in (GaussianActorCritic, CategoricalActorCritic)}
|
|
17
11
|
|
|
18
12
|
|
|
19
13
|
def clip_grad_norm(parameters, max_norm):
|
|
@@ -29,9 +23,13 @@ def clip_grad_norm(parameters, max_norm):
|
|
|
29
23
|
|
|
30
24
|
class PPO:
|
|
31
25
|
def __init__(self, actor_critic, pi_lr, vf_lr, gamma, lamda, K_epochs, eps_clip, batch_size, vf_coef, entropy_coef):
|
|
26
|
+
if K_epochs <= 0:
|
|
27
|
+
raise ValueError('K_epochs must be positive')
|
|
28
|
+
if batch_size <= 0:
|
|
29
|
+
raise ValueError('batch_size must be positive')
|
|
32
30
|
self.actor_critic = actor_critic
|
|
33
|
-
self.device = actor_critic.
|
|
34
|
-
self.actor_parameters =
|
|
31
|
+
self.device = actor_critic.device
|
|
32
|
+
self.actor_parameters = actor_critic.actor_parameters
|
|
35
33
|
self.critic_parameters = nn.state.get_parameters(actor_critic.critic)
|
|
36
34
|
self.pi_optimizer = nn.optim.Adam(self.actor_parameters, lr=pi_lr)
|
|
37
35
|
self.vf_optimizer = nn.optim.Adam(self.critic_parameters, lr=vf_lr)
|
|
@@ -57,6 +55,7 @@ class PPO:
|
|
|
57
55
|
def save(self, path):
|
|
58
56
|
path = Path(path)
|
|
59
57
|
metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(self.actor_critic.config),
|
|
58
|
+
policy_type=self.actor_critic._checkpoint_type,
|
|
60
59
|
ppo_config=json.dumps(self.config))
|
|
61
60
|
with TemporaryDirectory(dir=path.parent) as directory:
|
|
62
61
|
temporary = Path(directory) / path.name
|
|
@@ -69,7 +68,11 @@ class PPO:
|
|
|
69
68
|
metadata = header.get('__metadata__', {})
|
|
70
69
|
if metadata.get('format') != 'tinybaselines.ppo.v1':
|
|
71
70
|
raise ValueError('unsupported PPO checkpoint format')
|
|
72
|
-
|
|
71
|
+
policy_type = metadata.get('policy_type', 'gaussian')
|
|
72
|
+
policy_class = _CHECKPOINT_POLICIES.get(policy_type)
|
|
73
|
+
if policy_class is None:
|
|
74
|
+
raise ValueError(f'unsupported PPO checkpoint policy type: {policy_type!r}')
|
|
75
|
+
policy = policy_class(**json.loads(metadata['policy_config']))
|
|
73
76
|
ppo = cls(policy, **json.loads(metadata['ppo_config']))
|
|
74
77
|
nn.state.load_state_dict(ppo.state_dict(), nn.state.safe_load(path), verbose=False)
|
|
75
78
|
policy.refresh_inference()
|
|
@@ -89,14 +92,12 @@ class PPO:
|
|
|
89
92
|
return advantages.reshape(-1), returns.reshape(-1)
|
|
90
93
|
|
|
91
94
|
def compute_loss(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
|
|
92
|
-
|
|
93
|
-
action_logprobs = normal_log_prob(action_means, log_std, batch_actions)
|
|
95
|
+
state_values, action_logprobs, entropy = self.actor_critic.evaluate_actions(batch_states, batch_actions)
|
|
94
96
|
ratios = (action_logprobs - batch_logprobs).exp()
|
|
95
97
|
unclipped = ratios * batch_advantages
|
|
96
98
|
clipped = ratios.clip(1 - self.eps_clip, 1 + self.eps_clip) * batch_advantages
|
|
97
99
|
actor_loss = -unclipped.minimum(clipped).mean()
|
|
98
100
|
critic_loss = (state_values.squeeze(-1) - batch_returns).square().mean()
|
|
99
|
-
entropy = (log_std + NORMAL_ENTROPY_CONSTANT).sum()
|
|
100
101
|
return actor_loss + self.vf_coef * critic_loss - self.entropy_coef * entropy
|
|
101
102
|
|
|
102
103
|
def train_step(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
|
|
@@ -121,10 +122,10 @@ class PPO:
|
|
|
121
122
|
def prepare(self, batch):
|
|
122
123
|
T, N = batch.rewards.shape
|
|
123
124
|
obs_flat = self.actor_critic.preprocess(batch.observations.reshape(T * N, -1))
|
|
124
|
-
raw_act_np = batch.raw_actions.reshape(T * N,
|
|
125
|
+
raw_act_np = batch.raw_actions.reshape(T * N, *batch.raw_actions.shape[2:])
|
|
125
126
|
raw_act_flat = Tensor(raw_act_np, device=self.device)
|
|
126
|
-
|
|
127
|
-
old_logprobs_np =
|
|
127
|
+
old_value, old_logprobs, _ = self.actor_critic.evaluate_actions(obs_flat, raw_act_flat)
|
|
128
|
+
old_logprobs_np = old_logprobs.numpy()
|
|
128
129
|
old_values_np = old_value.numpy().reshape(T, N)
|
|
129
130
|
next_obs_t = self.actor_critic.preprocess(batch.next_observations.reshape(T * N, -1))
|
|
130
131
|
next_values_np = self.actor_critic.critic(next_obs_t).numpy().reshape(T, N)
|
|
@@ -33,6 +33,10 @@ def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=
|
|
|
33
33
|
if num_episodes is not None and num_episodes <= 0:
|
|
34
34
|
raise ValueError('num_episodes must be positive')
|
|
35
35
|
|
|
36
|
+
mode = (getattr(env, 'metadata', None) or {}).get('autoreset_mode')
|
|
37
|
+
if getattr(mode, 'value', mode) not in (None, 'SameStep'):
|
|
38
|
+
raise ValueError(f'Expected autoreset_mode=SAME_STEP, got {mode!r}')
|
|
39
|
+
|
|
36
40
|
N = env.num_envs
|
|
37
41
|
if state is None:
|
|
38
42
|
state = RolloutState(env.reset()[0], np.zeros(N))
|
|
@@ -54,7 +58,7 @@ def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=
|
|
|
54
58
|
actions, raw_actions = actor_critic.act(states, deterministic=deterministic)
|
|
55
59
|
if collect:
|
|
56
60
|
if t == 0:
|
|
57
|
-
raw_act = np.empty((T, *raw_actions.shape), dtype=
|
|
61
|
+
raw_act = np.empty((T, *raw_actions.shape), dtype=raw_actions.dtype)
|
|
58
62
|
obs[t], raw_act[t] = states, raw_actions
|
|
59
63
|
|
|
60
64
|
states, rewards, terminated, truncated, infos = env.step(actions)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinybaselines
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a3
|
|
4
4
|
Summary: PPO in tinygrad.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -12,44 +12,53 @@ Dynamic: license-file
|
|
|
12
12
|
|
|
13
13
|
# tinybaselines
|
|
14
14
|
|
|
15
|
-
PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
|
|
15
|
+
PPO in tinygrad for discrete and continuous actions. Continuous actions are clipped to `[-1, 1]`.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## LunarLander from PyPI
|
|
18
|
+
|
|
19
|
+
Use Python 3.11 and clang. The library install does not include Gymnasium.
|
|
18
20
|
|
|
19
21
|
```sh
|
|
20
22
|
python3.11 -m venv .venv
|
|
21
23
|
source .venv/bin/activate
|
|
22
|
-
|
|
24
|
+
python -m pip install tinybaselines==0.1.0a3
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Install Gymnasium and Box2D to run this example:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
python -m pip install 'gymnasium[box2d]==1.4.0'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Save this as `train_lander.py`. It runs one PPO update and evaluates the reloaded checkpoint.
|
|
26
34
|
|
|
27
35
|
```python
|
|
36
|
+
from contextlib import closing
|
|
28
37
|
from statistics import mean
|
|
38
|
+
|
|
39
|
+
import gymnasium as gym
|
|
40
|
+
|
|
29
41
|
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
42
|
|
|
33
|
-
env = make_env(4, asynchronous=False)
|
|
34
43
|
policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
|
|
35
|
-
|
|
44
|
+
actor_layers=4, critic_layers=4,
|
|
45
|
+
observation_scale=[10, 6.666, 5, 7.5, 1, 2.5, 1, 1])
|
|
36
46
|
ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
|
|
37
47
|
K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ppo =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
env
|
|
49
|
+
with closing(gym.vector.SyncVectorEnv(
|
|
50
|
+
[lambda: gym.make('LunarLander-v3', continuous=True)] * 4,
|
|
51
|
+
autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
|
|
52
|
+
)) as env:
|
|
53
|
+
for step in learn(env, ppo, total_timesteps=20_000, rollout_steps=20_000):
|
|
54
|
+
print(step.timesteps, step.rollout_seconds, step.update_seconds)
|
|
55
|
+
ppo.save('ppo.safetensors')
|
|
56
|
+
policy = PPO.load('ppo.safetensors').actor_critic
|
|
57
|
+
print(mean(evaluate(env, policy, num_episodes=10)))
|
|
58
|
+
```
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
eval_env.close()
|
|
52
|
-
print(mean(returns))
|
|
60
|
+
```sh
|
|
61
|
+
DEV=CPU BEAM=0 python train_lander.py
|
|
53
62
|
```
|
|
54
63
|
|
|
55
64
|
`total_timesteps` and `rollout_steps` count transitions across all environments.
|
|
@@ -63,18 +72,32 @@ actions by default; `deterministic=True` uses clipped policy means.
|
|
|
63
72
|
The collector uses Gymnasium vector environments with same-step autoreset.
|
|
64
73
|
For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
|
|
65
74
|
|
|
66
|
-
PPO checkpoints contain the policy,
|
|
75
|
+
PPO checkpoints contain the policy, Adam state, and PPO settings.
|
|
67
76
|
Continue training by passing the loaded PPO to `learn` with a new transition budget.
|
|
68
77
|
Each `learn` call starts fresh episodes and a new transition counter.
|
|
69
|
-
`
|
|
78
|
+
`GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
|
|
79
|
+
|
|
80
|
+
## Repository examples
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
`examples/lunar_lander`.
|
|
82
|
+
From a checkout, run the full LunarLander trainer:
|
|
73
83
|
|
|
74
84
|
```sh
|
|
85
|
+
pip install -e . -r examples/lunar_lander/requirements.txt
|
|
75
86
|
python -m examples.lunar_lander.train
|
|
76
87
|
```
|
|
77
88
|
|
|
89
|
+
CartPole uses `CategoricalActorCritic`. The example trains, saves a checkpoint,
|
|
90
|
+
and evaluates the reloaded policy over 10 episodes.
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pip install -e . 'gymnasium==1.4.0'
|
|
94
|
+
DEV=CPU BEAM=0 python -m examples.cartpole.train --checkpoint cartpole.safetensors
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Three independent four-environment CPU runs averaged 500.00, 496.67, and 494.37
|
|
98
|
+
after 102,400 transitions each. All cleared 475 over 100 fresh stochastic episodes
|
|
99
|
+
per reloaded checkpoint. [Run records](examples/cartpole/results/cpu.jsonl).
|
|
100
|
+
|
|
78
101
|
Pendulum uses three observations and one action. Its example rescales policy actions
|
|
79
102
|
from `[-1, 1]` to torque in `[-2, 2]`.
|
|
80
103
|
|
|
@@ -4,6 +4,8 @@ README.md
|
|
|
4
4
|
pyproject.toml
|
|
5
5
|
requirements.txt
|
|
6
6
|
examples/__init__.py
|
|
7
|
+
examples/cartpole/train.py
|
|
8
|
+
examples/cartpole/results/cpu.jsonl
|
|
7
9
|
examples/lunar_lander/__init__.py
|
|
8
10
|
examples/lunar_lander/config.py
|
|
9
11
|
examples/lunar_lander/reference.py
|
|
@@ -27,6 +29,7 @@ examples/pendulum/train.py
|
|
|
27
29
|
examples/pendulum/results/cpu-2.jsonl
|
|
28
30
|
examples/pendulum/results/cpu-3.jsonl
|
|
29
31
|
examples/pendulum/results/cpu.jsonl
|
|
32
|
+
tests/test_cartpole.py
|
|
30
33
|
tests/test_checkpoint.py
|
|
31
34
|
tests/test_core.py
|
|
32
35
|
tests/test_evaluation.py
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from tinygrad import Tensor
|
|
5
|
-
|
|
6
|
-
from tinybaselines import GaussianActorCritic, PPO, learn
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class IdentityEnv:
|
|
10
|
-
num_envs = 4
|
|
11
|
-
|
|
12
|
-
def __init__(self, seed):
|
|
13
|
-
self.rng = np.random.default_rng(seed)
|
|
14
|
-
|
|
15
|
-
def reset(self):
|
|
16
|
-
self.observations = self.rng.uniform(-0.75, 0.75, (self.num_envs, 1)).astype(np.float32)
|
|
17
|
-
return self.observations, {}
|
|
18
|
-
|
|
19
|
-
def step(self, actions):
|
|
20
|
-
rewards = -np.square(actions[:, 0] - self.observations[:, 0])
|
|
21
|
-
observations, _ = self.reset()
|
|
22
|
-
return observations, rewards, np.ones(self.num_envs, bool), np.zeros(self.num_envs, bool), {}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def identity_errors(seed):
|
|
26
|
-
Tensor.manual_seed(seed)
|
|
27
|
-
np.random.seed(seed)
|
|
28
|
-
policy = GaussianActorCritic(1, 1, 16, 1, 1)
|
|
29
|
-
ppo = PPO(policy, pi_lr=0.01, vf_lr=0.01, gamma=0.99, lamda=0.95, K_epochs=4,
|
|
30
|
-
eps_clip=0.2, batch_size=256, vf_coef=0.5, entropy_coef=0.0)
|
|
31
|
-
observations = np.linspace(-0.75, 0.75, 65, dtype=np.float32)[:, None]
|
|
32
|
-
|
|
33
|
-
def error():
|
|
34
|
-
actions, _ = policy.act(observations, deterministic=True)
|
|
35
|
-
return np.square(actions - observations).mean()
|
|
36
|
-
|
|
37
|
-
before = error()
|
|
38
|
-
for _ in learn(IdentityEnv(seed), ppo, total_timesteps=2048, rollout_steps=256):
|
|
39
|
-
pass
|
|
40
|
-
return before, error(), np.square(observations).mean()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class LearningTest(unittest.TestCase):
|
|
44
|
-
def test_learns_identity(self):
|
|
45
|
-
self.addCleanup(np.random.set_state, np.random.get_state())
|
|
46
|
-
before, after, zero = identity_errors(seed=0)
|
|
47
|
-
self.assertLess(after, 0.25 * min(before, zero), (before, after, zero))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements-reference.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|