Add hdppo-InvertedDoublePendulum-v5 package (weights, code, model card)
Browse files- README.md +117 -0
- config.yml +14 -0
- enjoy.py +84 -0
- env_kwargs.yml +1 -0
- hdppo-InvertedDoublePendulum-v5/weights.npz +3 -0
- hdppo-InvertedDoublePendulum-v5/weights_D512_teacher.npz +3 -0
- record_video.py +44 -0
- requirements.txt +8 -0
- results.json +11 -0
- results_D512_teacher.json +11 -0
- train_hdppo.py +721 -0
README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: hd-ppo
|
| 3 |
+
tags:
|
| 4 |
+
- InvertedDoublePendulum-v5
|
| 5 |
+
- deep-reinforcement-learning
|
| 6 |
+
- reinforcement-learning
|
| 7 |
+
- hyperdimensional-computing
|
| 8 |
+
- fractional-power-encoding
|
| 9 |
+
- LTU-AI
|
| 10 |
+
model-index:
|
| 11 |
+
- name: Hybrid-HD-PPO
|
| 12 |
+
results:
|
| 13 |
+
- task:
|
| 14 |
+
type: reinforcement-learning
|
| 15 |
+
name: reinforcement-learning
|
| 16 |
+
dataset:
|
| 17 |
+
name: InvertedDoublePendulum-v5
|
| 18 |
+
type: InvertedDoublePendulum-v5
|
| 19 |
+
metrics:
|
| 20 |
+
- type: mean_reward
|
| 21 |
+
value: 9359.26
|
| 22 |
+
name: mean_reward
|
| 23 |
+
verified: false
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
# **Hybrid-HD-PPO** Agent playing **InvertedDoublePendulum-v5**
|
| 27 |
+
|
| 28 |
+
This is a trained **Hybrid-HD-PPO** (Hyperdimensional Proximal Policy Optimization) agent
|
| 29 |
+
playing **InvertedDoublePendulum-v5** using **gradient-adaptive Fractional Power Encoding (FPE)**
|
| 30 |
+
with a prune-and-fine-tune pipeline.
|
| 31 |
+
|
| 32 |
+
Published by [LTU-AI](https://huggingface.co/LTU-AI).
|
| 33 |
+
|
| 34 |
+
## Pipeline
|
| 35 |
+
|
| 36 |
+
1. Train a teacher at **D=512** with gradient-adaptive single-beta FPE.
|
| 37 |
+
2. Prune by actor-weight importance through **D=512 → 128 → 64**.
|
| 38 |
+
3. Fine-tune each pruned checkpoint with PPO.
|
| 39 |
+
|
| 40 |
+
Published checkpoint: seed **7**, compact **D=64** model
|
| 41 |
+
(held-out eval mean reward **9359.26**).
|
| 42 |
+
|
| 43 |
+
## Usage
|
| 44 |
+
|
| 45 |
+
Install dependencies:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Evaluate the local checkpoint:
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
python enjoy.py --weights hdppo-InvertedDoublePendulum-v5/weights.npz --episodes 10
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Render episodes:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
python enjoy.py --weights hdppo-InvertedDoublePendulum-v5/weights.npz --render --episodes 3
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Record a replay video:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
python record_video.py --weights hdppo-InvertedDoublePendulum-v5/weights.npz --output replay.mp4
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Load from Hugging Face Hub:
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
python enjoy.py --weights LTU-AI/hdppo-InvertedDoublePendulum-v5 --episodes 10
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Training pipeline
|
| 76 |
+
|
| 77 |
+
Reproduce the teacher → prune → fine-tune workflow:
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
python train_hdppo.py
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## Hyperparameters
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
{
|
| 87 |
+
"env": "InvertedDoublePendulum-v5",
|
| 88 |
+
"algo": "Hybrid-HD-PPO (HD actor + MLP critic, gradient-adaptive FPE)",
|
| 89 |
+
"teacher_D": 512,
|
| 90 |
+
"pruned_D": 64,
|
| 91 |
+
"timesteps_per_stage": 1000000,
|
| 92 |
+
"seed": 7
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## Environment Arguments
|
| 97 |
+
|
| 98 |
+
```python
|
| 99 |
+
{
|
| 100 |
+
"render_mode": "rgb_array"
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Model files
|
| 105 |
+
|
| 106 |
+
| File | Description |
|
| 107 |
+
|------|-------------|
|
| 108 |
+
| `hdppo-InvertedDoublePendulum-v5/weights.npz` | Published actor (+ critic if HD) and FPE encoder (D=64) |
|
| 109 |
+
| `hdppo-InvertedDoublePendulum-v5/weights_D512_teacher.npz` | Teacher checkpoint (D=512) |
|
| 110 |
+
| `results.json` | Evaluation summary for the published checkpoint |
|
| 111 |
+
| `results_D512_teacher.json` | Evaluation summary for the teacher |
|
| 112 |
+
| `config.yml` | Training hyperparameters |
|
| 113 |
+
| `train_hdppo.py` / training modules | Self-contained training code |
|
| 114 |
+
|
| 115 |
+
## Citation
|
| 116 |
+
|
| 117 |
+
If you use this model, please cite the HD-PPO / Hybrid-HD-PPO work.
|
config.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
!!python/object/apply:collections.OrderedDict
|
| 2 |
+
-
|
| 3 |
+
- - env
|
| 4 |
+
- InvertedDoublePendulum-v5
|
| 5 |
+
- - algo
|
| 6 |
+
- Hybrid-HD-PPO (HD actor + MLP critic, gradient-adaptive FPE)
|
| 7 |
+
- - teacher_D
|
| 8 |
+
- 512
|
| 9 |
+
- - pruned_D
|
| 10 |
+
- 64
|
| 11 |
+
- - timesteps_per_stage
|
| 12 |
+
- 1000000
|
| 13 |
+
- - seed
|
| 14 |
+
- 7
|
enjoy.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import gymnasium as gym
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import train_hdppo as m
|
| 7 |
+
|
| 8 |
+
def _warmup_policy(encoder, actor, env_id, env_kwargs, n_steps=200):
|
| 9 |
+
env = gym.make(env_id, **env_kwargs)
|
| 10 |
+
obs, _ = env.reset(seed=0)
|
| 11 |
+
for i in range(n_steps):
|
| 12 |
+
Hr, Hi = encoder.encode(obs)
|
| 13 |
+
action = actor.greedy_action_np(Hr, Hi)
|
| 14 |
+
obs, _, term, trunc, _ = env.step(action)
|
| 15 |
+
if term or trunc:
|
| 16 |
+
obs, _ = env.reset(seed=i + 1)
|
| 17 |
+
env.close()
|
| 18 |
+
|
| 19 |
+
def load_policy_from_checkpoint(path, seed=42, warmup=True):
|
| 20 |
+
data = np.load(path)
|
| 21 |
+
cfg = dict(m.CONFIG)
|
| 22 |
+
cfg['D'] = int(data['D'])
|
| 23 |
+
cfg['beta'] = float(data['beta_base'])
|
| 24 |
+
cfg['fpe_phi_init'] = data['fpe_phi']
|
| 25 |
+
if 'feat_lo' in data:
|
| 26 |
+
cfg['feat_lo'] = data['feat_lo'].tolist()
|
| 27 |
+
cfg['feat_hi'] = data['feat_hi'].tolist()
|
| 28 |
+
torch.manual_seed(seed)
|
| 29 |
+
np.random.seed(seed)
|
| 30 |
+
encoder = m.make_encoder(cfg, seed)
|
| 31 |
+
actor = m.HDLinearActor(cfg['D'], cfg['action_dim'], cfg['log_std_init'], float(data['action_low']) if 'action_low' in data else cfg['action_low'], float(data['action_high']) if 'action_high' in data else cfg['action_high'])
|
| 32 |
+
with torch.no_grad():
|
| 33 |
+
actor.W_re.data.copy_(torch.from_numpy(np.asarray(data['W_actor_re'], dtype=np.float32)))
|
| 34 |
+
actor.W_im.data.copy_(torch.from_numpy(np.asarray(data['W_actor_im'], dtype=np.float32)))
|
| 35 |
+
if 'log_std' in data:
|
| 36 |
+
actor.log_std.data.copy_(torch.from_numpy(np.asarray(data['log_std'], dtype=np.float32)))
|
| 37 |
+
if cfg.get('adaptive_beta', True):
|
| 38 |
+
encoder.link_torch_actor(actor)
|
| 39 |
+
actor.eval()
|
| 40 |
+
if warmup:
|
| 41 |
+
_warmup_policy(encoder, actor, 'InvertedDoublePendulum-v5', dict(cfg.get('env_kwargs', {})))
|
| 42 |
+
return (encoder, actor, cfg)
|
| 43 |
+
|
| 44 |
+
def resolve_weights(weights_arg):
|
| 45 |
+
if os.path.isfile(weights_arg):
|
| 46 |
+
return weights_arg
|
| 47 |
+
try:
|
| 48 |
+
from huggingface_hub import hf_hub_download
|
| 49 |
+
except ImportError as exc:
|
| 50 |
+
raise SystemExit('Install huggingface_hub to load remote checkpoints: pip install huggingface_hub') from exc
|
| 51 |
+
return hf_hub_download(repo_id=weights_arg, filename='hdppo-InvertedDoublePendulum-v5/weights.npz')
|
| 52 |
+
|
| 53 |
+
def main():
|
| 54 |
+
parser = argparse.ArgumentParser(description='Enjoy Hybrid-HD-PPO on InvertedDoublePendulum-v5')
|
| 55 |
+
parser.add_argument('--weights', default='hdppo-InvertedDoublePendulum-v5/weights.npz')
|
| 56 |
+
parser.add_argument('--episodes', type=int, default=5)
|
| 57 |
+
parser.add_argument('--seed', type=int, default=10000)
|
| 58 |
+
parser.add_argument('--render', action='store_true')
|
| 59 |
+
args = parser.parse_args()
|
| 60 |
+
weights = resolve_weights(args.weights)
|
| 61 |
+
encoder, actor, cfg = load_policy_from_checkpoint(weights, seed=args.seed)
|
| 62 |
+
render_mode = 'human' if args.render else None
|
| 63 |
+
env_kwargs = dict(cfg.get('env_kwargs', {}))
|
| 64 |
+
if render_mode is not None:
|
| 65 |
+
env_kwargs['render_mode'] = render_mode
|
| 66 |
+
env = gym.make('InvertedDoublePendulum-v5', **env_kwargs)
|
| 67 |
+
rewards = []
|
| 68 |
+
for ep in range(args.episodes):
|
| 69 |
+
obs, _ = env.reset(seed=args.seed + ep)
|
| 70 |
+
done = False
|
| 71 |
+
ep_r = 0.0
|
| 72 |
+
while not done:
|
| 73 |
+
Hr, Hi = encoder.encode(obs)
|
| 74 |
+
action = actor.greedy_action_np(Hr, Hi)
|
| 75 |
+
obs, reward, term, trunc, _ = env.step(action)
|
| 76 |
+
ep_r += float(reward)
|
| 77 |
+
done = term or trunc
|
| 78 |
+
rewards.append(ep_r)
|
| 79 |
+
print(f'episode {ep + 1}: return={ep_r:.1f}')
|
| 80 |
+
env.close()
|
| 81 |
+
arr = np.asarray(rewards, dtype=np.float64)
|
| 82 |
+
print(f'mean={arr.mean():.2f} std={(arr.std(ddof=1) if len(arr) > 1 else 0.0):.2f}')
|
| 83 |
+
if __name__ == '__main__':
|
| 84 |
+
main()
|
env_kwargs.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
render_mode: rgb_array
|
hdppo-InvertedDoublePendulum-v5/weights.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aeed64d704edab4833d005fcf923491b8646c310f140231a6a0228a8770ba2fe
|
| 3 |
+
size 6442
|
hdppo-InvertedDoublePendulum-v5/weights_D512_teacher.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c29adfac5cded935dc6966504cd701a1c07be71da232c7af63ce3c83d9b6e411
|
| 3 |
+
size 26154
|
record_video.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import argparse
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import gymnasium as gym
|
| 6 |
+
import imageio.v2 as imageio
|
| 7 |
+
|
| 8 |
+
from enjoy import load_policy_from_checkpoint
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def record(weights_path, output_path, seed=42, max_steps=1000):
|
| 12 |
+
encoder, actor, cfg = load_policy_from_checkpoint(weights_path, seed=seed)
|
| 13 |
+
env_kwargs = dict(cfg.get("env_kwargs", {}))
|
| 14 |
+
env_kwargs["render_mode"] = "rgb_array"
|
| 15 |
+
env = gym.make("InvertedDoublePendulum-v5", **env_kwargs)
|
| 16 |
+
frames = []
|
| 17 |
+
obs, _ = env.reset(seed=seed)
|
| 18 |
+
for _ in range(max_steps):
|
| 19 |
+
Hr, Hi = encoder.encode(obs)
|
| 20 |
+
action = actor.greedy_action_np(Hr, Hi)
|
| 21 |
+
obs, reward, term, trunc, _ = env.step(action)
|
| 22 |
+
frames.append(env.render())
|
| 23 |
+
if term or trunc:
|
| 24 |
+
break
|
| 25 |
+
|
| 26 |
+
env.close()
|
| 27 |
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
| 28 |
+
imageio.mimsave(output_path, frames, fps=30)
|
| 29 |
+
return len(frames)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
parser = argparse.ArgumentParser(description="Record InvertedDoublePendulum-v5 replay video")
|
| 34 |
+
parser.add_argument("--weights", default="hdppo-InvertedDoublePendulum-v5/weights.npz")
|
| 35 |
+
parser.add_argument("--output", default="replay.mp4")
|
| 36 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 37 |
+
parser.add_argument("--max-steps", type=int, default=1000)
|
| 38 |
+
args = parser.parse_args()
|
| 39 |
+
n = record(args.weights, args.output, seed=args.seed, max_steps=args.max_steps)
|
| 40 |
+
print(f"saved {n} frames to {args.output}")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gymnasium[mujoco]>=0.29.0
|
| 2 |
+
mujoco>=3.0.0
|
| 3 |
+
numpy>=1.24.0
|
| 4 |
+
torch>=2.0.0
|
| 5 |
+
psutil>=5.9.0
|
| 6 |
+
imageio>=2.31.0
|
| 7 |
+
imageio-ffmpeg>=0.4.9
|
| 8 |
+
huggingface_hub>=0.20.0
|
results.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mean_reward": 9359.2568359375,
|
| 3 |
+
"eval_mean_best": 9359.2412109375,
|
| 4 |
+
"is_deterministic": true,
|
| 5 |
+
"D": 64,
|
| 6 |
+
"seed": 7,
|
| 7 |
+
"algo": "Hybrid-HD-PPO",
|
| 8 |
+
"env": "InvertedDoublePendulum-v5",
|
| 9 |
+
"beta_base": 0.7071067690849304,
|
| 10 |
+
"beta_bands": null
|
| 11 |
+
}
|
results_D512_teacher.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mean_reward": 2338.0703125,
|
| 3 |
+
"eval_mean_best": 2338.070556640625,
|
| 4 |
+
"is_deterministic": true,
|
| 5 |
+
"D": 512,
|
| 6 |
+
"seed": 7,
|
| 7 |
+
"algo": "Hybrid-HD-PPO",
|
| 8 |
+
"env": "InvertedDoublePendulum-v5",
|
| 9 |
+
"beta_base": 0.7071067690849304,
|
| 10 |
+
"beta_bands": null
|
| 11 |
+
}
|
train_hdppo.py
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
if sys.platform.startswith('linux'):
|
| 6 |
+
os.environ.setdefault('MUJOCO_GL', 'egl')
|
| 7 |
+
os.environ.setdefault('PYOPENGL_PLATFORM', 'egl')
|
| 8 |
+
import time
|
| 9 |
+
import multiprocessing as mp
|
| 10 |
+
from contextlib import contextmanager
|
| 11 |
+
from collections import deque
|
| 12 |
+
import numpy as np
|
| 13 |
+
import gymnasium as gym
|
| 14 |
+
import psutil
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
ENV_ID = 'InvertedDoublePendulum-v5'
|
| 19 |
+
N_EVAL_EPISODES = 20
|
| 20 |
+
EVAL_SEED_BASE = 10000
|
| 21 |
+
LOG_2PI_HALF = 0.5 * np.log(2.0 * np.pi)
|
| 22 |
+
DEVICE = torch.device('cpu')
|
| 23 |
+
torch.set_num_threads(max(1, psutil.cpu_count(logical=False) or 2))
|
| 24 |
+
|
| 25 |
+
def state_features(obs):
|
| 26 |
+
return np.asarray(obs, dtype=np.float32)
|
| 27 |
+
N_WORKERS = max(1, min(4, os.cpu_count() or 2))
|
| 28 |
+
ROLLOUT_STEPS = 2048
|
| 29 |
+
OBS_DIM = 9
|
| 30 |
+
ACTION_DIM = 1
|
| 31 |
+
ENV_KWARGS = dict()
|
| 32 |
+
CONFIG = dict(env_id=ENV_ID, env_kwargs=ENV_KWARGS, feat_lo=[-10.0] * OBS_DIM, feat_hi=[10.0] * OBS_DIM, feature_fn=state_features, n_feat=OBS_DIM, action_dim=ACTION_DIM, action_low=-1.0, action_high=1.0, D=512, beta=2.0 ** (-0.5), adaptive_beta=True, rollout_steps=ROLLOUT_STEPS, n_workers=N_WORKERS, clip_eps=0.2, vf_coef=0.5, entropy_coef=0.001, entropy_decay=1.0, entropy_min=0.0, gamma=0.99, lam=0.95, adv_clip=5.0, actor_lr=0.0003, critic_lr=0.001, log_std_lr=0.0003, n_epochs=10, minibatch_size=256, max_grad_norm=0.5, target_kl=None, log_std_init=-0.5, log_std_min=-2.0, log_std_max=2.0, critic_hidden=(64, 64), ema_interval=200, ema_alpha=0.15)
|
| 33 |
+
DEFAULT_SEED = 42
|
| 34 |
+
BETA_EFF_MIN_MULT = 0.2
|
| 35 |
+
BETA_EFF_MAX_MULT = 4.0
|
| 36 |
+
G_EMA_DECAY = 0.99
|
| 37 |
+
|
| 38 |
+
class HDEncoderGradAdaptive:
|
| 39 |
+
|
| 40 |
+
def __init__(self, feat_lo, feat_hi, D, seed, feature_fn, beta_base, phi_init=None):
|
| 41 |
+
self.lo = np.array(feat_lo, np.float32)
|
| 42 |
+
self.hi = np.array(feat_hi, np.float32)
|
| 43 |
+
self.feature_fn = feature_fn
|
| 44 |
+
self.beta_base = float(beta_base)
|
| 45 |
+
self.D = D
|
| 46 |
+
self.n_feat = len(feat_lo)
|
| 47 |
+
self.sqrtD = float(np.sqrt(D))
|
| 48 |
+
if phi_init is not None:
|
| 49 |
+
self.Phi = np.asarray(phi_init, dtype=np.float32)
|
| 50 |
+
else:
|
| 51 |
+
rng = np.random.default_rng(seed)
|
| 52 |
+
self.Phi = rng.uniform(-np.pi, np.pi, (self.n_feat, D)).astype(np.float32)
|
| 53 |
+
scale = 2.0 / (self.hi - self.lo + 1e-08)
|
| 54 |
+
self.dtheta_ds_unit = self.Phi * scale[:, None]
|
| 55 |
+
self.W_re = None
|
| 56 |
+
self.W_im = None
|
| 57 |
+
self._torch_actor = None
|
| 58 |
+
self._log_g_ema = 0.0
|
| 59 |
+
self.beta_eff_history = []
|
| 60 |
+
|
| 61 |
+
def set_weights(self, W_re, W_im):
|
| 62 |
+
self.W_re = W_re
|
| 63 |
+
self.W_im = W_im
|
| 64 |
+
|
| 65 |
+
def link_torch_actor(self, actor):
|
| 66 |
+
self._torch_actor = actor
|
| 67 |
+
|
| 68 |
+
def _current_weights(self):
|
| 69 |
+
if self._torch_actor is not None:
|
| 70 |
+
return (self._torch_actor.W_re.detach().cpu().numpy(), self._torch_actor.W_im.detach().cpu().numpy())
|
| 71 |
+
return (self.W_re, self.W_im)
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def beta_vec(self):
|
| 75 |
+
return np.full(self.D, self.beta_base, dtype=np.float32)
|
| 76 |
+
|
| 77 |
+
def encode(self, state):
|
| 78 |
+
W_re, W_im = self._current_weights()
|
| 79 |
+
s = self.feature_fn(state)
|
| 80 |
+
s = np.clip(s, self.lo, self.hi)
|
| 81 |
+
s_norm = (2.0 * (s - self.lo) / (self.hi - self.lo + 1e-08) - 1.0).astype(np.float32)
|
| 82 |
+
proj = s_norm @ self.Phi
|
| 83 |
+
theta_base = self.beta_base * proj
|
| 84 |
+
H_re_base, H_im_base = (np.cos(theta_base), np.sin(theta_base))
|
| 85 |
+
dtheta_ds = self.beta_base * self.dtheta_ds_unit
|
| 86 |
+
dH_re_ds = -H_im_base[None, :] * dtheta_ds
|
| 87 |
+
dH_im_ds = H_re_base[None, :] * dtheta_ds
|
| 88 |
+
J = (dH_re_ds @ W_re + dH_im_ds @ W_im) / self.sqrtD
|
| 89 |
+
g = float(np.linalg.norm(J))
|
| 90 |
+
log_g = np.log1p(g)
|
| 91 |
+
centered = log_g - self._log_g_ema
|
| 92 |
+
self._log_g_ema = G_EMA_DECAY * self._log_g_ema + (1.0 - G_EMA_DECAY) * log_g
|
| 93 |
+
beta_eff = self.beta_base * (1.0 + centered)
|
| 94 |
+
beta_eff = float(np.clip(beta_eff, self.beta_base * BETA_EFF_MIN_MULT, self.beta_base * BETA_EFF_MAX_MULT))
|
| 95 |
+
self.beta_eff_history.append(beta_eff)
|
| 96 |
+
theta = beta_eff * proj
|
| 97 |
+
return (np.cos(theta).astype(np.float32), np.sin(theta).astype(np.float32))
|
| 98 |
+
|
| 99 |
+
class HDEncoderFixed:
|
| 100 |
+
|
| 101 |
+
def __init__(self, feat_lo, feat_hi, D, beta, seed, feature_fn, phi_init=None):
|
| 102 |
+
self.lo = np.array(feat_lo, np.float32)
|
| 103 |
+
self.hi = np.array(feat_hi, np.float32)
|
| 104 |
+
self.feature_fn = feature_fn
|
| 105 |
+
self.D = D
|
| 106 |
+
self.beta_vec = np.full(D, float(beta), dtype=np.float32)
|
| 107 |
+
self.n_feat = len(feat_lo)
|
| 108 |
+
if phi_init is not None:
|
| 109 |
+
self.Phi = np.asarray(phi_init, dtype=np.float32)
|
| 110 |
+
else:
|
| 111 |
+
rng = np.random.default_rng(seed)
|
| 112 |
+
self.Phi = rng.uniform(-np.pi, np.pi, (self.n_feat, D)).astype(np.float32)
|
| 113 |
+
|
| 114 |
+
def encode(self, state):
|
| 115 |
+
s = self.feature_fn(state)
|
| 116 |
+
s = np.clip(s, self.lo, self.hi)
|
| 117 |
+
s_norm = (2.0 * (s - self.lo) / (self.hi - self.lo + 1e-08) - 1.0).astype(np.float32)
|
| 118 |
+
theta = s_norm @ self.Phi * self.beta_vec
|
| 119 |
+
return (np.cos(theta), np.sin(theta))
|
| 120 |
+
|
| 121 |
+
def make_encoder(cfg, seed):
|
| 122 |
+
if cfg.get('adaptive_beta', True):
|
| 123 |
+
return HDEncoderGradAdaptive(cfg['feat_lo'], cfg['feat_hi'], cfg['D'], seed, cfg['feature_fn'], cfg['beta'], phi_init=cfg.get('fpe_phi_init'))
|
| 124 |
+
return HDEncoderFixed(cfg['feat_lo'], cfg['feat_hi'], cfg['D'], cfg['beta'], seed, cfg['feature_fn'], phi_init=cfg.get('fpe_phi_init'))
|
| 125 |
+
|
| 126 |
+
class HDLinearActor(nn.Module):
|
| 127 |
+
|
| 128 |
+
def __init__(self, D, action_dim, log_std_init, action_low, action_high):
|
| 129 |
+
super().__init__()
|
| 130 |
+
self.D = D
|
| 131 |
+
self.action_dim = action_dim
|
| 132 |
+
self.sqrtD = float(np.sqrt(D))
|
| 133 |
+
self.W_re = nn.Parameter(torch.zeros(D, action_dim, dtype=torch.float32))
|
| 134 |
+
self.W_im = nn.Parameter(torch.zeros(D, action_dim, dtype=torch.float32))
|
| 135 |
+
self.log_std = nn.Parameter(torch.full((action_dim,), float(log_std_init), dtype=torch.float32))
|
| 136 |
+
self.a_lo = float(action_low)
|
| 137 |
+
self.a_hi = float(action_high)
|
| 138 |
+
|
| 139 |
+
def mean_from_hd(self, H_re, H_im):
|
| 140 |
+
return (H_re @ self.W_re + H_im @ self.W_im) / self.sqrtD
|
| 141 |
+
|
| 142 |
+
def dist(self, H_re, H_im):
|
| 143 |
+
mu = self.mean_from_hd(H_re, H_im)
|
| 144 |
+
std = self.log_std.exp().unsqueeze(0).expand_as(mu)
|
| 145 |
+
return (mu, std)
|
| 146 |
+
|
| 147 |
+
@torch.no_grad()
|
| 148 |
+
def greedy_action_np(self, H_re_np, H_im_np):
|
| 149 |
+
Hr = torch.from_numpy(H_re_np).unsqueeze(0)
|
| 150 |
+
Hi = torch.from_numpy(H_im_np).unsqueeze(0)
|
| 151 |
+
mu = self.mean_from_hd(Hr, Hi).squeeze(0).cpu().numpy()
|
| 152 |
+
return np.clip(mu, self.a_lo, self.a_hi).astype(np.float32)
|
| 153 |
+
|
| 154 |
+
class MLPCritic(nn.Module):
|
| 155 |
+
|
| 156 |
+
def __init__(self, n_obs, hidden=(64, 64)):
|
| 157 |
+
super().__init__()
|
| 158 |
+
layers = []
|
| 159 |
+
last = n_obs
|
| 160 |
+
for h in hidden:
|
| 161 |
+
lin = nn.Linear(last, h)
|
| 162 |
+
nn.init.orthogonal_(lin.weight, gain=np.sqrt(2))
|
| 163 |
+
nn.init.zeros_(lin.bias)
|
| 164 |
+
layers += [lin, nn.Tanh()]
|
| 165 |
+
last = h
|
| 166 |
+
head = nn.Linear(last, 1)
|
| 167 |
+
nn.init.orthogonal_(head.weight, gain=1.0)
|
| 168 |
+
nn.init.zeros_(head.bias)
|
| 169 |
+
layers.append(head)
|
| 170 |
+
self.net = nn.Sequential(*layers)
|
| 171 |
+
|
| 172 |
+
def forward(self, obs):
|
| 173 |
+
return self.net(obs).squeeze(-1)
|
| 174 |
+
|
| 175 |
+
def compute_gae_with_trunc(rewards, values, next_values, terminated, truncated, last_value, gamma, lam):
|
| 176 |
+
n = rewards.shape[0]
|
| 177 |
+
adv = np.zeros(n, dtype=np.float64)
|
| 178 |
+
gae = 0.0
|
| 179 |
+
for t in range(n - 1, -1, -1):
|
| 180 |
+
if terminated[t]:
|
| 181 |
+
next_v = 0.0
|
| 182 |
+
elif truncated[t]:
|
| 183 |
+
next_v = next_values[t]
|
| 184 |
+
elif t == n - 1:
|
| 185 |
+
next_v = last_value
|
| 186 |
+
else:
|
| 187 |
+
next_v = values[t + 1]
|
| 188 |
+
delta = rewards[t] + gamma * next_v - values[t]
|
| 189 |
+
gae = delta if terminated[t] or truncated[t] else delta + gamma * lam * gae
|
| 190 |
+
adv[t] = gae
|
| 191 |
+
return (adv, adv + values)
|
| 192 |
+
|
| 193 |
+
class SystemMonitor:
|
| 194 |
+
|
| 195 |
+
def __init__(self):
|
| 196 |
+
self.proc = psutil.Process(os.getpid())
|
| 197 |
+
|
| 198 |
+
def _tree_memory_mb(self):
|
| 199 |
+
total = self.proc.memory_info().rss
|
| 200 |
+
for child in self.proc.children(recursive=True):
|
| 201 |
+
try:
|
| 202 |
+
total += child.memory_info().rss
|
| 203 |
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 204 |
+
pass
|
| 205 |
+
return total / 1024 ** 2
|
| 206 |
+
|
| 207 |
+
def snapshot(self):
|
| 208 |
+
return {'ram_process_tree_mb': self._tree_memory_mb()}
|
| 209 |
+
|
| 210 |
+
def rollout_worker(worker_id, cfg, conn, master_seed):
|
| 211 |
+
encoder = make_encoder(cfg, master_seed)
|
| 212 |
+
adaptive = cfg.get('adaptive_beta', True)
|
| 213 |
+
beta_log_writer = None
|
| 214 |
+
if adaptive and cfg.get('adaptive_beta_log_dir'):
|
| 215 |
+
log_dir = cfg['adaptive_beta_log_dir']
|
| 216 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 217 |
+
beta_log_file = open(os.path.join(log_dir, f'worker_{worker_id}_beta_log.csv'), 'w', newline='')
|
| 218 |
+
beta_log_writer = csv.writer(beta_log_file)
|
| 219 |
+
beta_log_writer.writerow(['rollout_idx', 'n', 'mean', 'std', 'min', 'max'])
|
| 220 |
+
rollout_idx = 0
|
| 221 |
+
sqrtD = float(np.sqrt(cfg['D']))
|
| 222 |
+
D = cfg['D']
|
| 223 |
+
n_obs = cfg['n_feat']
|
| 224 |
+
action_dim = cfg['action_dim']
|
| 225 |
+
a_lo = float(cfg['action_low'])
|
| 226 |
+
a_hi = float(cfg['action_high'])
|
| 227 |
+
rng = np.random.default_rng(master_seed + 1 + worker_id * 1000)
|
| 228 |
+
env = gym.make(cfg['env_id'], **cfg.get('env_kwargs', {}))
|
| 229 |
+
state, _ = env.reset(seed=master_seed + 1 + worker_id * 1000)
|
| 230 |
+
while True:
|
| 231 |
+
cmd = conn.recv()
|
| 232 |
+
if cmd[0] == 'exit':
|
| 233 |
+
if beta_log_writer is not None:
|
| 234 |
+
beta_log_file.close()
|
| 235 |
+
env.close()
|
| 236 |
+
return
|
| 237 |
+
_, W_re, W_im, log_std, n_steps = cmd
|
| 238 |
+
if adaptive:
|
| 239 |
+
encoder.set_weights(W_re, W_im)
|
| 240 |
+
std = np.exp(log_std).astype(np.float32)
|
| 241 |
+
log_std_sum = float(log_std.sum())
|
| 242 |
+
H_res = np.empty((n_steps, D), np.float32)
|
| 243 |
+
H_ims = np.empty((n_steps, D), np.float32)
|
| 244 |
+
obs_arr = np.empty((n_steps, n_obs), np.float32)
|
| 245 |
+
nobs_arr = np.empty((n_steps, n_obs), np.float32)
|
| 246 |
+
a_arr = np.empty((n_steps, action_dim), np.float32)
|
| 247 |
+
r_arr = np.empty(n_steps, np.float64)
|
| 248 |
+
lp_arr = np.empty(n_steps, np.float32)
|
| 249 |
+
term_arr = np.empty(n_steps, np.float64)
|
| 250 |
+
trunc_arr = np.empty(n_steps, np.float64)
|
| 251 |
+
ep_rewards = []
|
| 252 |
+
ep_lengths = []
|
| 253 |
+
ep_r = 0.0
|
| 254 |
+
ep_len = 0
|
| 255 |
+
for t in range(n_steps):
|
| 256 |
+
H_re, H_im = encoder.encode(state)
|
| 257 |
+
mu = (H_re @ W_re + H_im @ W_im) / sqrtD
|
| 258 |
+
act = mu + std * rng.standard_normal(action_dim).astype(np.float32)
|
| 259 |
+
act = np.clip(act, a_lo, a_hi)
|
| 260 |
+
diff = act - mu
|
| 261 |
+
lp = float(-0.5 * np.sum((diff / std) ** 2) - log_std_sum - action_dim * LOG_2PI_HALF)
|
| 262 |
+
next_s, reward, term, trunc, _ = env.step(act.astype(np.float32))
|
| 263 |
+
H_res[t] = H_re
|
| 264 |
+
H_ims[t] = H_im
|
| 265 |
+
obs_arr[t] = np.asarray(state, dtype=np.float32)
|
| 266 |
+
nobs_arr[t] = np.asarray(next_s, dtype=np.float32)
|
| 267 |
+
a_arr[t] = act
|
| 268 |
+
r_arr[t] = float(reward)
|
| 269 |
+
lp_arr[t] = np.float32(lp)
|
| 270 |
+
term_arr[t] = float(term)
|
| 271 |
+
trunc_arr[t] = float(trunc)
|
| 272 |
+
ep_r += float(reward)
|
| 273 |
+
ep_len += 1
|
| 274 |
+
if term or trunc:
|
| 275 |
+
ep_rewards.append(ep_r)
|
| 276 |
+
ep_lengths.append(ep_len)
|
| 277 |
+
ep_r = 0.0
|
| 278 |
+
ep_len = 0
|
| 279 |
+
state, _ = env.reset()
|
| 280 |
+
else:
|
| 281 |
+
state = next_s
|
| 282 |
+
if beta_log_writer is not None:
|
| 283 |
+
beta_hist = np.asarray(encoder.beta_eff_history[-n_steps:], dtype=np.float64)
|
| 284 |
+
beta_log_writer.writerow([rollout_idx, len(beta_hist), float(beta_hist.mean()), float(beta_hist.std()), float(beta_hist.min()), float(beta_hist.max())])
|
| 285 |
+
beta_log_file.flush()
|
| 286 |
+
rollout_idx += 1
|
| 287 |
+
conn.send((H_res, H_ims, obs_arr, nobs_arr, a_arr, r_arr, lp_arr, term_arr, trunc_arr, ep_rewards, ep_lengths, state.copy(), bool(term or trunc)))
|
| 288 |
+
|
| 289 |
+
class WorkerPool:
|
| 290 |
+
|
| 291 |
+
def __init__(self, n_workers, cfg, master_seed):
|
| 292 |
+
self.n_workers = n_workers
|
| 293 |
+
self.conns = []
|
| 294 |
+
self.procs = []
|
| 295 |
+
ctx = mp.get_context('fork')
|
| 296 |
+
for i in range(n_workers):
|
| 297 |
+
parent_conn, child_conn = ctx.Pipe()
|
| 298 |
+
p = ctx.Process(target=rollout_worker, args=(i, cfg, child_conn, master_seed), daemon=True)
|
| 299 |
+
p.start()
|
| 300 |
+
child_conn.close()
|
| 301 |
+
self.conns.append(parent_conn)
|
| 302 |
+
self.procs.append(p)
|
| 303 |
+
|
| 304 |
+
def collect(self, W_re, W_im, log_std, total_steps):
|
| 305 |
+
steps_each = total_steps // self.n_workers
|
| 306 |
+
for conn in self.conns:
|
| 307 |
+
conn.send(('rollout', W_re.copy(), W_im.copy(), log_std.copy(), steps_each))
|
| 308 |
+
return [conn.recv() for conn in self.conns]
|
| 309 |
+
|
| 310 |
+
def close(self):
|
| 311 |
+
for conn in self.conns:
|
| 312 |
+
try:
|
| 313 |
+
conn.send(('exit',))
|
| 314 |
+
except Exception:
|
| 315 |
+
pass
|
| 316 |
+
for p in self.procs:
|
| 317 |
+
p.join(timeout=3)
|
| 318 |
+
if p.is_alive():
|
| 319 |
+
p.terminate()
|
| 320 |
+
|
| 321 |
+
def actor_snapshot(actor):
|
| 322 |
+
return {k: v.detach().clone() for k, v in actor.state_dict().items()}
|
| 323 |
+
|
| 324 |
+
def actor_restore(actor, snap, alpha):
|
| 325 |
+
sd = actor.state_dict()
|
| 326 |
+
with torch.no_grad():
|
| 327 |
+
for k in sd:
|
| 328 |
+
sd[k].mul_(1.0 - alpha).add_(snap[k], alpha=alpha)
|
| 329 |
+
|
| 330 |
+
@contextmanager
|
| 331 |
+
def actor_weights_swapped(actor, snap):
|
| 332 |
+
saved = actor_snapshot(actor)
|
| 333 |
+
actor_restore(actor, snap, alpha=1.0)
|
| 334 |
+
try:
|
| 335 |
+
yield
|
| 336 |
+
finally:
|
| 337 |
+
actor_restore(actor, saved, alpha=1.0)
|
| 338 |
+
|
| 339 |
+
def evaluate_agent(encoder, actor, env_id, env_kwargs, n_episodes=N_EVAL_EPISODES, seed_base=EVAL_SEED_BASE):
|
| 340 |
+
env = gym.make(env_id, **env_kwargs)
|
| 341 |
+
rewards = np.empty(n_episodes, dtype=np.float64)
|
| 342 |
+
actor.eval()
|
| 343 |
+
for i in range(n_episodes):
|
| 344 |
+
state, _ = env.reset(seed=seed_base + i)
|
| 345 |
+
ep_r = 0.0
|
| 346 |
+
done = False
|
| 347 |
+
while not done:
|
| 348 |
+
H_re, H_im = encoder.encode(state)
|
| 349 |
+
a = actor.greedy_action_np(H_re, H_im)
|
| 350 |
+
state, reward, term, trunc, _ = env.step(a)
|
| 351 |
+
ep_r += float(reward)
|
| 352 |
+
done = term or trunc
|
| 353 |
+
rewards[i] = ep_r
|
| 354 |
+
actor.train()
|
| 355 |
+
env.close()
|
| 356 |
+
n = len(rewards)
|
| 357 |
+
sem = float(rewards.std(ddof=1) / np.sqrt(n)) if n > 1 else 0.0
|
| 358 |
+
return dict(mean_reward=float(rewards.mean()), ci95_reward=1.96 * sem, rewards=rewards.tolist())
|
| 359 |
+
|
| 360 |
+
class HDPPOHybridAgent:
|
| 361 |
+
|
| 362 |
+
def __init__(self, cfg, seed=DEFAULT_SEED):
|
| 363 |
+
D = cfg['D']
|
| 364 |
+
torch.manual_seed(seed)
|
| 365 |
+
np.random.seed(seed)
|
| 366 |
+
self.actor = HDLinearActor(D, cfg['action_dim'], cfg['log_std_init'], cfg['action_low'], cfg['action_high']).to(DEVICE)
|
| 367 |
+
self.encoder = make_encoder(cfg, seed)
|
| 368 |
+
if cfg.get('adaptive_beta', True):
|
| 369 |
+
self.encoder.link_torch_actor(self.actor)
|
| 370 |
+
self.critic = MLPCritic(cfg['n_feat'], cfg['critic_hidden']).to(DEVICE)
|
| 371 |
+
self.opt_actor = torch.optim.Adam([{'params': [self.actor.W_re, self.actor.W_im], 'lr': cfg['actor_lr']}, {'params': [self.actor.log_std], 'lr': cfg['log_std_lr']}])
|
| 372 |
+
self.opt_critic = torch.optim.Adam(self.critic.parameters(), lr=cfg['critic_lr'])
|
| 373 |
+
self.cfg = cfg
|
| 374 |
+
self.entropy_coef = cfg['entropy_coef']
|
| 375 |
+
self.best_avg100 = -np.inf
|
| 376 |
+
self.best_snap = None
|
| 377 |
+
self.pool = WorkerPool(cfg['n_workers'], cfg, seed)
|
| 378 |
+
|
| 379 |
+
def collect_and_update(self, total_steps):
|
| 380 |
+
cfg = self.cfg
|
| 381 |
+
t_rollout_start = time.time()
|
| 382 |
+
with torch.no_grad():
|
| 383 |
+
W_re_np = self.actor.W_re.detach().cpu().numpy().astype(np.float32)
|
| 384 |
+
W_im_np = self.actor.W_im.detach().cpu().numpy().astype(np.float32)
|
| 385 |
+
log_std_np = self.actor.log_std.detach().cpu().numpy().astype(np.float32)
|
| 386 |
+
results = self.pool.collect(W_re_np, W_im_np, log_std_np, total_steps)
|
| 387 |
+
t_rollout = time.time() - t_rollout_start
|
| 388 |
+
t_update_start = time.time()
|
| 389 |
+
all_H_re, all_H_im, all_obs, all_nobs = ([], [], [], [])
|
| 390 |
+
all_a, all_lp, all_r, all_term, all_trunc = ([], [], [], [], [])
|
| 391 |
+
ep_rews, ep_lens = ([], [])
|
| 392 |
+
for H_re, H_im, obs, nobs, a, r, lp, term, trunc, ep_r, ep_l, ls, ld in results:
|
| 393 |
+
all_H_re.append(H_re)
|
| 394 |
+
all_H_im.append(H_im)
|
| 395 |
+
all_obs.append(obs)
|
| 396 |
+
all_nobs.append(nobs)
|
| 397 |
+
all_a.append(a)
|
| 398 |
+
all_lp.append(lp)
|
| 399 |
+
all_r.append(r)
|
| 400 |
+
all_term.append(term)
|
| 401 |
+
all_trunc.append(trunc)
|
| 402 |
+
ep_rews.extend(ep_r)
|
| 403 |
+
ep_lens.extend(ep_l)
|
| 404 |
+
H_re = np.concatenate(all_H_re)
|
| 405 |
+
H_im = np.concatenate(all_H_im)
|
| 406 |
+
obs_np = np.concatenate(all_obs)
|
| 407 |
+
nobs_np = np.concatenate(all_nobs)
|
| 408 |
+
a_np = np.concatenate(all_a).astype(np.float32)
|
| 409 |
+
old_lps = np.concatenate(all_lp).astype(np.float32)
|
| 410 |
+
r_np = np.concatenate(all_r)
|
| 411 |
+
term_np = np.concatenate(all_term)
|
| 412 |
+
trunc_np = np.concatenate(all_trunc)
|
| 413 |
+
self.critic.eval()
|
| 414 |
+
with torch.no_grad():
|
| 415 |
+
obs_t = torch.from_numpy(obs_np).to(DEVICE)
|
| 416 |
+
nobs_t = torch.from_numpy(nobs_np).to(DEVICE)
|
| 417 |
+
v_pred = self.critic(obs_t).cpu().numpy().astype(np.float64)
|
| 418 |
+
v_next = self.critic(nobs_t).cpu().numpy().astype(np.float64)
|
| 419 |
+
self.critic.train()
|
| 420 |
+
adv_list, ret_list = ([], [])
|
| 421 |
+
cursor = 0
|
| 422 |
+
for (_, _, _, _, _, _, _, _, _, _, _, ls, ld), worker_r in zip(results, all_r):
|
| 423 |
+
n = worker_r.shape[0]
|
| 424 |
+
r_seg = r_np[cursor:cursor + n]
|
| 425 |
+
v_seg = v_pred[cursor:cursor + n]
|
| 426 |
+
nv_seg = v_next[cursor:cursor + n]
|
| 427 |
+
term_seg = term_np[cursor:cursor + n]
|
| 428 |
+
trunc_seg = trunc_np[cursor:cursor + n]
|
| 429 |
+
if ld:
|
| 430 |
+
last_val = 0.0
|
| 431 |
+
else:
|
| 432 |
+
with torch.no_grad():
|
| 433 |
+
ls_t = torch.from_numpy(np.asarray(ls, dtype=np.float32)).unsqueeze(0).to(DEVICE)
|
| 434 |
+
last_val = float(self.critic(ls_t).item())
|
| 435 |
+
adv, ret = compute_gae_with_trunc(r_seg.astype(np.float64), v_seg.astype(np.float64), nv_seg.astype(np.float64), term_seg.astype(np.float64), trunc_seg.astype(np.float64), last_val, cfg['gamma'], cfg['lam'])
|
| 436 |
+
adv_list.append(adv)
|
| 437 |
+
ret_list.append(ret)
|
| 438 |
+
cursor += n
|
| 439 |
+
adv_np = np.concatenate(adv_list)
|
| 440 |
+
ret_np = np.concatenate(ret_list)
|
| 441 |
+
T = len(adv_np)
|
| 442 |
+
adv_std = float(adv_np.std())
|
| 443 |
+
adv_clip = float(cfg['adv_clip'])
|
| 444 |
+
adv_n_np = np.clip((adv_np - adv_np.mean()) / (adv_std + 1e-08), -adv_clip, adv_clip) if adv_std > 0.0001 else np.zeros(T, np.float64)
|
| 445 |
+
var_y = float(np.var(ret_np))
|
| 446 |
+
explained_var = float(1 - np.var(ret_np - v_pred) / var_y) if var_y > 1e-08 else 0.0
|
| 447 |
+
H_re_t = torch.from_numpy(H_re).to(DEVICE)
|
| 448 |
+
H_im_t = torch.from_numpy(H_im).to(DEVICE)
|
| 449 |
+
obs_t = torch.from_numpy(obs_np).to(DEVICE)
|
| 450 |
+
a_t = torch.from_numpy(a_np).to(DEVICE)
|
| 451 |
+
old_lp_t = torch.from_numpy(old_lps).to(DEVICE)
|
| 452 |
+
adv_t = torch.from_numpy(adv_n_np.astype(np.float32)).to(DEVICE)
|
| 453 |
+
ret_t = torch.from_numpy(ret_np.astype(np.float32)).to(DEVICE)
|
| 454 |
+
clip_eps = float(cfg['clip_eps'])
|
| 455 |
+
vf_coef = float(cfg['vf_coef'])
|
| 456 |
+
ent_coef = float(self.entropy_coef)
|
| 457 |
+
mb_size = int(cfg['minibatch_size'])
|
| 458 |
+
max_gn = float(cfg['max_grad_norm'])
|
| 459 |
+
log_std_min = float(cfg['log_std_min'])
|
| 460 |
+
log_std_max = float(cfg['log_std_max'])
|
| 461 |
+
action_dim = cfg['action_dim']
|
| 462 |
+
rng = np.random.default_rng()
|
| 463 |
+
kls, clip_fracs, actor_losses, critic_losses = ([], [], [], [])
|
| 464 |
+
for epoch in range(cfg['n_epochs']):
|
| 465 |
+
perm = rng.permutation(T)
|
| 466 |
+
for start in range(0, T, mb_size):
|
| 467 |
+
idx = perm[start:start + mb_size]
|
| 468 |
+
idx_t = torch.from_numpy(idx).to(DEVICE)
|
| 469 |
+
Hr_b = H_re_t.index_select(0, idx_t)
|
| 470 |
+
Hi_b = H_im_t.index_select(0, idx_t)
|
| 471 |
+
obs_b = obs_t.index_select(0, idx_t)
|
| 472 |
+
a_b = a_t.index_select(0, idx_t)
|
| 473 |
+
oldlp_b = old_lp_t.index_select(0, idx_t)
|
| 474 |
+
adv_b = adv_t.index_select(0, idx_t)
|
| 475 |
+
ret_b = ret_t.index_select(0, idx_t)
|
| 476 |
+
mu, std = self.actor.dist(Hr_b, Hi_b)
|
| 477 |
+
var = std * std
|
| 478 |
+
new_lp = (-0.5 * (a_b - mu).pow(2) / var - self.actor.log_std.unsqueeze(0) - LOG_2PI_HALF).sum(dim=-1)
|
| 479 |
+
ratio = (new_lp - oldlp_b).exp()
|
| 480 |
+
surr1 = ratio * adv_b
|
| 481 |
+
surr2 = torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps) * adv_b
|
| 482 |
+
policy_loss = -torch.min(surr1, surr2).mean()
|
| 483 |
+
entropy = 0.5 * np.log(2 * np.pi * np.e) * action_dim + self.actor.log_std.sum()
|
| 484 |
+
actor_loss = policy_loss - ent_coef * entropy
|
| 485 |
+
self.opt_actor.zero_grad(set_to_none=True)
|
| 486 |
+
actor_loss.backward()
|
| 487 |
+
nn.utils.clip_grad_norm_([self.actor.W_re, self.actor.W_im, self.actor.log_std], max_gn)
|
| 488 |
+
self.opt_actor.step()
|
| 489 |
+
with torch.no_grad():
|
| 490 |
+
self.actor.log_std.clamp_(log_std_min, log_std_max)
|
| 491 |
+
v_b = self.critic(obs_b)
|
| 492 |
+
value_loss = F.mse_loss(v_b, ret_b)
|
| 493 |
+
self.opt_critic.zero_grad(set_to_none=True)
|
| 494 |
+
(vf_coef * value_loss).backward()
|
| 495 |
+
nn.utils.clip_grad_norm_(self.critic.parameters(), max_gn)
|
| 496 |
+
self.opt_critic.step()
|
| 497 |
+
with torch.no_grad():
|
| 498 |
+
log_ratio = new_lp - oldlp_b
|
| 499 |
+
approx_kl = (log_ratio.exp() - 1 - log_ratio).mean().item()
|
| 500 |
+
clip_frac = ((ratio > 1 + clip_eps) | (ratio < 1 - clip_eps)).float().mean().item()
|
| 501 |
+
kls.append(approx_kl)
|
| 502 |
+
clip_fracs.append(clip_frac)
|
| 503 |
+
actor_losses.append(policy_loss.item())
|
| 504 |
+
critic_losses.append(value_loss.item())
|
| 505 |
+
self.entropy_coef = max(self.entropy_coef * cfg['entropy_decay'], cfg['entropy_min'])
|
| 506 |
+
t_update = time.time() - t_update_start
|
| 507 |
+
with torch.no_grad():
|
| 508 |
+
log_std_now = self.actor.log_std.detach().cpu().numpy()
|
| 509 |
+
diag = dict(rollout_s=t_rollout, update_s=t_update, fps=total_steps / (t_rollout + t_update + 1e-08), explained_var=explained_var, policy_loss=float(np.mean(actor_losses)), value_loss=float(np.mean(critic_losses)), approx_kl=float(np.mean(kls)), clip_fraction=float(np.mean(clip_fracs)), entropy_coef=float(self.entropy_coef), log_std_mean=float(log_std_now.mean()), std_mean=float(np.exp(log_std_now).mean()), mean_value=float(v_pred.mean()))
|
| 510 |
+
return (ep_rews, ep_lens, diag)
|
| 511 |
+
|
| 512 |
+
def maybe_snapshot(self, avg100):
|
| 513 |
+
if avg100 > self.best_avg100:
|
| 514 |
+
self.best_avg100 = avg100
|
| 515 |
+
self.best_snap = actor_snapshot(self.actor)
|
| 516 |
+
|
| 517 |
+
def ema_restore(self):
|
| 518 |
+
if self.best_snap is not None:
|
| 519 |
+
actor_restore(self.actor, self.best_snap, self.cfg['ema_alpha'])
|
| 520 |
+
|
| 521 |
+
def evaluate_final(self, n_episodes=N_EVAL_EPISODES, seed_base=EVAL_SEED_BASE):
|
| 522 |
+
return evaluate_agent(self.encoder, self.actor, self.cfg['env_id'], self.cfg.get('env_kwargs', {}), n_episodes, seed_base)
|
| 523 |
+
|
| 524 |
+
def evaluate_best(self, n_episodes=N_EVAL_EPISODES, seed_base=EVAL_SEED_BASE):
|
| 525 |
+
if self.best_snap is None:
|
| 526 |
+
return None
|
| 527 |
+
with actor_weights_swapped(self.actor, self.best_snap):
|
| 528 |
+
return evaluate_agent(self.encoder, self.actor, self.cfg['env_id'], self.cfg.get('env_kwargs', {}), n_episodes, seed_base)
|
| 529 |
+
|
| 530 |
+
def close(self):
|
| 531 |
+
self.pool.close()
|
| 532 |
+
|
| 533 |
+
def train(total_timesteps, seed=DEFAULT_SEED, verbose=True, warm_start=None, save_weights_path=None, log_csv_path=None, eval_csv_path=None, eval_every_n_steps=None):
|
| 534 |
+
csv_file = csv_writer = None
|
| 535 |
+
if log_csv_path is not None:
|
| 536 |
+
csv_file = open(log_csv_path, 'w', newline='')
|
| 537 |
+
csv_writer = csv.writer(csv_file)
|
| 538 |
+
csv_writer.writerow(['global_step', 'wall_time_sec', 'episode', 'episodes_this_update', 'ep_rew_mean', 'ep_rew_max', 'ep_rew_min', 'ep_len_mean', 'best_avg100', 'policy_loss', 'value_loss', 'explained_var', 'approx_kl', 'clip_fraction', 'entropy_coef', 'log_std_mean', 'std_mean', 'mean_value', 'rollout_s', 'update_s', 'fps', 'ram_process_tree_mb'])
|
| 539 |
+
eval_csv_file = eval_csv_writer = None
|
| 540 |
+
if eval_csv_path is not None:
|
| 541 |
+
eval_csv_file = open(eval_csv_path, 'w', newline='')
|
| 542 |
+
eval_csv_writer = csv.writer(eval_csv_file)
|
| 543 |
+
eval_csv_writer.writerow(['global_step', 'eval_mean', 'eval_ci95', 'tag'])
|
| 544 |
+
cfg = dict(CONFIG) if warm_start is not None else CONFIG
|
| 545 |
+
if warm_start is not None:
|
| 546 |
+
cfg['D'] = int(warm_start['D'])
|
| 547 |
+
cfg['beta'] = warm_start.get('beta', cfg['beta'])
|
| 548 |
+
cfg['fpe_phi_init'] = np.asarray(warm_start['fpe_phi'], dtype=np.float32)
|
| 549 |
+
agent = HDPPOHybridAgent(cfg, seed=seed)
|
| 550 |
+
if warm_start is not None:
|
| 551 |
+
with torch.no_grad():
|
| 552 |
+
agent.actor.W_re.copy_(torch.from_numpy(np.asarray(warm_start['W_actor_re'], dtype=np.float32)))
|
| 553 |
+
agent.actor.W_im.copy_(torch.from_numpy(np.asarray(warm_start['W_actor_im'], dtype=np.float32)))
|
| 554 |
+
if 'log_std' in warm_start:
|
| 555 |
+
agent.actor.log_std.copy_(torch.from_numpy(np.asarray(warm_start['log_std'], dtype=np.float32)))
|
| 556 |
+
if verbose:
|
| 557 |
+
print(f' Warm-started from provided checkpoint: D={cfg['D']}')
|
| 558 |
+
if warm_start.get('critic_state_dict') is not None:
|
| 559 |
+
agent.critic.load_state_dict(warm_start['critic_state_dict'])
|
| 560 |
+
if verbose:
|
| 561 |
+
print(' Warm-started critic from provided checkpoint (warm-start, not reset)')
|
| 562 |
+
if eval_csv_writer is not None:
|
| 563 |
+
post_prune_eval = agent.evaluate_final()
|
| 564 |
+
eval_csv_writer.writerow([0, post_prune_eval['mean_reward'], post_prune_eval['ci95_reward'], 'post_prune'])
|
| 565 |
+
eval_csv_file.flush()
|
| 566 |
+
if verbose:
|
| 567 |
+
print(f' Post-prune eval (before fine-tuning): {post_prune_eval['mean_reward']:+.1f} +/- {post_prune_eval['ci95_reward']:.1f}')
|
| 568 |
+
next_eval_at = eval_every_n_steps
|
| 569 |
+
rollout_steps = cfg['rollout_steps']
|
| 570 |
+
ema_interval = cfg['ema_interval']
|
| 571 |
+
sysmon = SystemMonitor()
|
| 572 |
+
recent = deque(maxlen=100)
|
| 573 |
+
len_recent = deque(maxlen=100)
|
| 574 |
+
ep = 0
|
| 575 |
+
total_steps = 0
|
| 576 |
+
update_count = 0
|
| 577 |
+
if verbose:
|
| 578 |
+
print('=' * 80)
|
| 579 |
+
print(f'HD-PPO -> {cfg['env_id']}')
|
| 580 |
+
print(f' D={cfg['D']} beta={cfg['beta']} n_workers={cfg['n_workers']} total_timesteps={total_timesteps:,}')
|
| 581 |
+
print('=' * 80)
|
| 582 |
+
t0 = time.perf_counter()
|
| 583 |
+
while total_steps < total_timesteps:
|
| 584 |
+
ep_batch, ep_len_batch, diag = agent.collect_and_update(rollout_steps)
|
| 585 |
+
total_steps += rollout_steps
|
| 586 |
+
update_count += 1
|
| 587 |
+
wall_sec = int(time.perf_counter() - t0)
|
| 588 |
+
if eval_csv_writer is not None and next_eval_at is not None:
|
| 589 |
+
while total_steps >= next_eval_at:
|
| 590 |
+
periodic_eval = agent.evaluate_final()
|
| 591 |
+
eval_csv_writer.writerow([next_eval_at, periodic_eval['mean_reward'], periodic_eval['ci95_reward'], 'periodic'])
|
| 592 |
+
eval_csv_file.flush()
|
| 593 |
+
if verbose:
|
| 594 |
+
print(f' [eval @ step {next_eval_at:>9,}] {periodic_eval['mean_reward']:+.1f} +/- {periodic_eval['ci95_reward']:.1f}')
|
| 595 |
+
next_eval_at += eval_every_n_steps
|
| 596 |
+
for r, L in zip(ep_batch, ep_len_batch):
|
| 597 |
+
ep += 1
|
| 598 |
+
recent.append(r)
|
| 599 |
+
len_recent.append(L)
|
| 600 |
+
if len(recent) > 0:
|
| 601 |
+
avg100 = float(np.mean(recent))
|
| 602 |
+
agent.maybe_snapshot(avg100)
|
| 603 |
+
if ep % ema_interval == 0 and ep > 0:
|
| 604 |
+
agent.ema_restore()
|
| 605 |
+
if csv_writer is not None:
|
| 606 |
+
sysm = sysmon.snapshot()
|
| 607 |
+
csv_writer.writerow([total_steps, wall_sec, ep, len(ep_batch), avg100, float(np.max(recent)), float(np.min(recent)), float(np.mean(len_recent)) if len_recent else 0.0, agent.best_avg100, diag['policy_loss'], diag['value_loss'], diag['explained_var'], diag['approx_kl'], diag['clip_fraction'], diag['entropy_coef'], diag['log_std_mean'], diag['std_mean'], diag['mean_value'], diag['rollout_s'], diag['update_s'], diag['fps'], sysm['ram_process_tree_mb']])
|
| 608 |
+
csv_file.flush()
|
| 609 |
+
if verbose and update_count % 5 == 0:
|
| 610 |
+
sysm = sysmon.snapshot()
|
| 611 |
+
print(f' [step {total_steps:>9,}] ep {ep:>5} avg100={avg100:>+8.1f} best={agent.best_avg100:>+8.1f} pol_loss={diag['policy_loss']:>+.3f} v_loss={diag['value_loss']:>.2f} fps={diag['fps']:>6.0f} ram={sysm['ram_process_tree_mb']:>5.0f}MB')
|
| 612 |
+
if csv_file is not None:
|
| 613 |
+
csv_file.close()
|
| 614 |
+
total_time = time.perf_counter() - t0
|
| 615 |
+
final_train_avg = float(np.mean(list(recent))) if recent else 0.0
|
| 616 |
+
if verbose:
|
| 617 |
+
print(f'\n Running held-out eval ({N_EVAL_EPISODES} eps)...', flush=True)
|
| 618 |
+
eval_final = agent.evaluate_final()
|
| 619 |
+
eval_best = agent.evaluate_best()
|
| 620 |
+
if eval_csv_file is not None:
|
| 621 |
+
eval_csv_file.close()
|
| 622 |
+
critic_state_dict = {k: v.detach().clone() for k, v in agent.critic.state_dict().items()}
|
| 623 |
+
if save_weights_path is not None:
|
| 624 |
+
if agent.best_snap is not None:
|
| 625 |
+
best_sd = agent.best_snap
|
| 626 |
+
W_re_save = best_sd['W_re'].cpu().numpy()
|
| 627 |
+
W_im_save = best_sd['W_im'].cpu().numpy()
|
| 628 |
+
log_std_save = best_sd['log_std'].cpu().numpy()
|
| 629 |
+
else:
|
| 630 |
+
W_re_save = agent.actor.W_re.detach().cpu().numpy()
|
| 631 |
+
W_im_save = agent.actor.W_im.detach().cpu().numpy()
|
| 632 |
+
log_std_save = agent.actor.log_std.detach().cpu().numpy()
|
| 633 |
+
np.savez(save_weights_path, W_actor_re=W_re_save, W_actor_im=W_im_save, log_std=log_std_save, fpe_phi=agent.encoder.Phi, beta_base=np.float32(cfg['beta']), feat_lo=np.array(cfg['feat_lo'], dtype=np.float32), feat_hi=np.array(cfg['feat_hi'], dtype=np.float32), D=np.int32(cfg['D']), n_feat=np.int32(cfg['n_feat']), action_dim=np.int32(cfg['action_dim']), action_low=np.float32(cfg['action_low']), action_high=np.float32(cfg['action_high']), eval_mean_final=np.float32(eval_final['mean_reward']), eval_mean_best=np.float32(eval_best['mean_reward'] if eval_best is not None else np.nan))
|
| 634 |
+
if verbose:
|
| 635 |
+
print(f' Saved actor -> {save_weights_path} ({os.path.getsize(save_weights_path) / 1024:.1f} KB)')
|
| 636 |
+
if verbose:
|
| 637 |
+
eb_str = f'{eval_best['mean_reward']:.1f}+/-{eval_best['ci95_reward']:.1f}' if eval_best is not None else '-'
|
| 638 |
+
print(f'\n Training time: {total_time:.1f}s')
|
| 639 |
+
print(f' Episodes: {ep:,}')
|
| 640 |
+
print(f' Final train avg100: {final_train_avg:+.1f}')
|
| 641 |
+
print(f' Best train avg100: {agent.best_avg100:+.1f}')
|
| 642 |
+
print(f' Eval (final wts): {eval_final['mean_reward']:+.1f} +/- {eval_final['ci95_reward']:.1f}')
|
| 643 |
+
print(f' Eval (best wts): {eb_str}')
|
| 644 |
+
agent.close()
|
| 645 |
+
return dict(eval_mean_final=eval_final['mean_reward'], eval_ci95_final=eval_final['ci95_reward'], eval_mean_best=eval_best['mean_reward'] if eval_best is not None else None, final_avg100=final_train_avg, best_avg100=float(agent.best_avg100), training_time_sec=total_time, critic_state_dict=critic_state_dict)
|
| 646 |
+
|
| 647 |
+
def prune_actor_global(checkpoint, D_prime):
|
| 648 |
+
W_re, W_im = (np.asarray(checkpoint['W_actor_re']), np.asarray(checkpoint['W_actor_im']))
|
| 649 |
+
Phi = np.asarray(checkpoint['fpe_phi'])
|
| 650 |
+
beta_base = float(checkpoint['beta_base'])
|
| 651 |
+
importance = np.sqrt((W_re ** 2).sum(axis=1) + (W_im ** 2).sum(axis=1))
|
| 652 |
+
keep_idx = np.sort(np.argsort(-importance)[:D_prime])
|
| 653 |
+
return dict(D=D_prime, beta=beta_base, fpe_phi=Phi[:, keep_idx], W_actor_re=W_re[keep_idx], W_actor_im=W_im[keep_idx])
|
| 654 |
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 655 |
+
SEED = DEFAULT_SEED
|
| 656 |
+
STAGES = [(512, 1000000), (128, 1000000), (64, 1000000)]
|
| 657 |
+
OUT_DIR = os.path.join(THIS_DIR, 'prune_finetune')
|
| 658 |
+
|
| 659 |
+
def weights_path(D, stage_label):
|
| 660 |
+
return os.path.join(OUT_DIR, f'hdppo_D{D}_{stage_label}.npz')
|
| 661 |
+
|
| 662 |
+
def curve_csv_path(D, stage_label):
|
| 663 |
+
return os.path.join(OUT_DIR, f'training_curve_D{D}_{stage_label}.csv')
|
| 664 |
+
|
| 665 |
+
def eval_csv_path_for(D, stage_label):
|
| 666 |
+
return os.path.join(OUT_DIR, f'eval_curve_D{D}_{stage_label}.csv')
|
| 667 |
+
|
| 668 |
+
def main():
|
| 669 |
+
os.makedirs(OUT_DIR, exist_ok=True)
|
| 670 |
+
results_json = os.path.join(OUT_DIR, 'results.json')
|
| 671 |
+
table_txt = os.path.join(OUT_DIR, 'results_table.txt')
|
| 672 |
+
stage_records = []
|
| 673 |
+
prev_path = None
|
| 674 |
+
prev_critic_sd = None
|
| 675 |
+
t_chain0 = time.perf_counter()
|
| 676 |
+
for i, (D, timesteps) in enumerate(STAGES):
|
| 677 |
+
if i == 0:
|
| 678 |
+
stage_label = 'teacher_fresh'
|
| 679 |
+
warm_start = None
|
| 680 |
+
print(f'\n{'#' * 90}\nSTAGE {i + 1}/{len(STAGES)}: D={D} FRESH, {timesteps:,} steps\n{'#' * 90}', flush=True)
|
| 681 |
+
else:
|
| 682 |
+
stage_label = 'finetuned'
|
| 683 |
+
prev_D = STAGES[i - 1][0]
|
| 684 |
+
print(f'\n{'#' * 90}\nSTAGE {i + 1}/{len(STAGES)}: prune D={prev_D} -> D={D}, then fine-tune {timesteps:,} steps (critic warm-started)\n{'#' * 90}', flush=True)
|
| 685 |
+
prev_ckpt = np.load(prev_path)
|
| 686 |
+
warm_start = prune_actor_global(prev_ckpt, D_prime=D)
|
| 687 |
+
warm_start['critic_state_dict'] = prev_critic_sd
|
| 688 |
+
print(f' Pruned: kept top-{D}/{prev_D} dimensions by weight importance ({prev_D / D:.1f}x cut)')
|
| 689 |
+
path = weights_path(D, stage_label)
|
| 690 |
+
curve_csv = curve_csv_path(D, stage_label)
|
| 691 |
+
eval_csv = eval_csv_path_for(D, stage_label)
|
| 692 |
+
t0 = time.time()
|
| 693 |
+
result = train(total_timesteps=timesteps, seed=SEED, verbose=True, warm_start=warm_start, save_weights_path=path, log_csv_path=curve_csv, eval_csv_path=eval_csv, eval_every_n_steps=50000)
|
| 694 |
+
wall = time.time() - t0
|
| 695 |
+
print(f' STAGE {i + 1} done: D={D} eval_final={result['eval_mean_final']:+.1f} eval_best={result['eval_mean_best']:+.1f} wall={wall:.0f}s', flush=True)
|
| 696 |
+
stage_records.append(dict(stage=stage_label, D=D, configured_timesteps=timesteps, weights_path=path, curve_csv=curve_csv, eval_csv=eval_csv, eval_mean_final=result['eval_mean_final'], eval_mean_best=result['eval_mean_best'], final_avg100=result['final_avg100'], best_avg100=result['best_avg100'], wall_time_sec=wall))
|
| 697 |
+
with open(results_json, 'w') as f:
|
| 698 |
+
json.dump(dict(seed=SEED, stages=stage_records, complete=False), f, indent=2)
|
| 699 |
+
prev_path = path
|
| 700 |
+
prev_critic_sd = result['critic_state_dict']
|
| 701 |
+
total_time = time.perf_counter() - t_chain0
|
| 702 |
+
print('\n' + '=' * 90)
|
| 703 |
+
header = f'{'stage':<16} {'D':>6} {'steps':>10} {'eval_final':>12} {'eval_best':>12} {'wall (min)':>11}'
|
| 704 |
+
print(header)
|
| 705 |
+
lines_txt = [header]
|
| 706 |
+
for r in stage_records:
|
| 707 |
+
line = f'{r['stage']:<16} {r['D']:>6} {r['configured_timesteps']:>10,} {r['eval_mean_final']:>12.1f} {r['eval_mean_best']:>12.1f} {r['wall_time_sec'] / 60:>11.1f}'
|
| 708 |
+
print(line)
|
| 709 |
+
lines_txt.append(line)
|
| 710 |
+
print(f'\nTotal chain wall time: {total_time / 60:.1f} min')
|
| 711 |
+
print('=' * 90)
|
| 712 |
+
lines_txt.append(f'\nTotal chain wall time: {total_time / 60:.1f} min')
|
| 713 |
+
with open(table_txt, 'w') as f:
|
| 714 |
+
f.write('\n'.join(lines_txt) + '\n')
|
| 715 |
+
print(f'\nWrote {table_txt}')
|
| 716 |
+
with open(results_json, 'w') as f:
|
| 717 |
+
json.dump(dict(seed=SEED, stages=stage_records, complete=True, total_chain_time_sec=total_time), f, indent=2)
|
| 718 |
+
print(f'Wrote {results_json}')
|
| 719 |
+
if __name__ == '__main__':
|
| 720 |
+
mp.set_start_method('fork', force=True)
|
| 721 |
+
main()
|