flydexo commited on
Commit
6b4b3a1
·
verified ·
1 Parent(s): d5cbb43

Add World Models CarRacing-v3 (V+M+C), 915.9 best-agent reward

Browse files
Files changed (8) hide show
  1. .gitattributes +1 -0
  2. README.md +89 -0
  3. config.yaml +12 -0
  4. controller.pt +3 -0
  5. controller_triptych.gif +3 -0
  6. model.py +164 -0
  7. rnn.pt +3 -0
  8. vae.pt +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ controller_triptych.gif filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: pytorch
4
+ tags:
5
+ - reinforcement-learning
6
+ - world-models
7
+ - vae
8
+ - mdn-rnn
9
+ - cma-es
10
+ - car-racing
11
+ - gymnasium
12
+ pipeline_tag: reinforcement-learning
13
+ ---
14
+
15
+ # 🏎️ World Models — CarRacing-v3
16
+
17
+ A faithful reproduction of Ha & Schmidhuber's [*World Models*](https://arxiv.org/abs/1803.10122)
18
+ on `CarRacing-v3`. The agent factorises into three parts — **V**ision, **M**emory,
19
+ **C**ontroller — trained in that order:
20
+
21
+ - **V** — a β-VAE that compresses each `64×64×3` frame into a 32-d latent `z`
22
+ - **M** — an MDN-RNN (LSTM-256, 5-mixture density head) that predicts the next latent `p(z′ | z, a, h)`
23
+ - **C** — a single 867-parameter linear layer mapping `[z; h] → action`, evolved with CMA-ES
24
+
25
+ Only the controller ever touches the reward; V and M are trained once, self-supervised, then frozen.
26
+
27
+ ## 🎯 Result
28
+
29
+ | Metric | This model | Paper (Ha & Schmidhuber) |
30
+ |---|:---:|:---:|
31
+ | **Best-agent reward** (avg / 100 rollouts) | **915.9** | **906 ± 21** |
32
+
33
+ ![Controller-driven episode](controller_triptych.gif)
34
+
35
+ *left: what the car sees · middle: the frame round-tripped through **V** · right: the next frame
36
+ as **M** predicts it, one step ahead.*
37
+
38
+ ## Files
39
+
40
+ | File | Module | Architecture |
41
+ |---|---|---|
42
+ | `vae.pt` | **V** | `AutoEncoder` — 4× stride-2 conv encoder `[32→64→128→256]`, mirror deconv decoder, 32-d latent, β-VAE with free-bits floor (λ = 0.5/dim) |
43
+ | `rnn.pt` | **M** | `RNN` — LSTM (hidden 256) over `[z; a]` (35-d) + `MDN` head, 5 Gaussians × 32 dims |
44
+ | `controller.pt` | **C** | linear `[z(32); h(256)] → a(3)`, 867 params, CMA-ES (popsize 64, avg 16, σ 0.3) |
45
+ | `model.py` | — | the module definitions |
46
+ | `config.yaml` | — | hyperparameters for instantiation |
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import torch
52
+ from omegaconf import OmegaConf
53
+ from huggingface_hub import hf_hub_download
54
+ from model import AutoEncoder, RNN # model.py from this repo
55
+
56
+ repo = "flydexo/world-models-carracing-v3"
57
+ cfg = OmegaConf.load(hf_hub_download(repo, "config.yaml"))
58
+
59
+ vae = AutoEncoder(cfg)
60
+ vae.load_state_dict(torch.load(hf_hub_download(repo, "vae.pt"), map_location="cpu"))
61
+
62
+ rnn = RNN(cfg)
63
+ rnn.load_state_dict(torch.load(hf_hub_download(repo, "rnn.pt"), map_location="cpu"))
64
+
65
+ # Controller: a plain linear [z; h] -> action
66
+ ctrl = torch.nn.Linear(cfg.controller.state_dim + cfg.controller.hidden_dim,
67
+ cfg.controller.action_dim)
68
+ ctrl.load_state_dict(torch.load(hf_hub_download(repo, "controller.pt"), map_location="cpu"))
69
+ ```
70
+
71
+ Rollout loop: encode obs → `z`, concat `[z; h]` → controller → action, step env, feed
72
+ `[z; a]` through the RNN to advance the hidden state `h`.
73
+
74
+ ## Reproduction notes
75
+
76
+ The gap between a naïve implementation (~600) and the paper (~906) came down to a few details:
77
+
78
+ - **VAE** — sum-reduced reconstruction paired with a **free-bits** KL floor (λ = 0.5/dim), KL
79
+ scaled consistently against the recon term. No posterior collapse — all 32 latents stay alive.
80
+ - **MDN-RNN** — trained on `z ~ N(μ, σ)` **sampled** every batch (not the mean μ); softmax
81
+ temperature applied only at sampling, never inside the training loss; correct mixture sampling.
82
+ - **Controller** — input is `[z; h]` (latent **plus** the RNN hidden state).
83
+ - **CMA-ES** — population 64, 16 rollouts averaged per candidate, σ = 0.3.
84
+
85
+ ## Links
86
+
87
+ - 📄 Paper: [World Models](https://arxiv.org/abs/1803.10122) (Ha & Schmidhuber, 2018)
88
+ - 🤗 Collection: [World Models](https://huggingface.co/collections/flydexo/world-models-6a493823e48400161b1cd828)
89
+ - 📊 Live training dashboards (Trackio): [VAE sweep](https://huggingface.co/spaces/flydexo/ha_schmidhuber-vae) · [RNN / controller](https://huggingface.co/spaces/flydexo/ha_schmidhuber)
config.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hyperparameters needed to instantiate model.py (AutoEncoder / RNN / Controller).
2
+ # Load with OmegaConf and pass to the constructors — see the README for a snippet.
3
+ rnn:
4
+ hidden_size: 256
5
+ num_mix: 5
6
+ z_dim: 32
7
+ action_dim: 3
8
+ temp: 0.2 # softmax temperature — applied only at sampling, never in training
9
+ controller:
10
+ state_dim: 32 # z
11
+ hidden_dim: 256 # h (LSTM hidden state)
12
+ action_dim: 3
controller.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e11cd312ef37b54802d2ea5ab20ac5817627eec6ea3ce65760c393d57c963d73
3
+ size 5401
controller_triptych.gif ADDED

Git LFS Details

  • SHA256: b834e07de75502eefa7951f0c501132c2dcd81b79963b7d76b3009fabc8fc693
  • Pointer size: 132 Bytes
  • Size of remote file: 3.32 MB
model.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ from torch.distributions.normal import Normal
5
+ from torch.nn.modules.rnn import LSTM
6
+
7
+
8
+ class PrintShape(nn.Module):
9
+ def __init__(self):
10
+ super().__init__()
11
+
12
+ def forward(self, x):
13
+ print("hey", x.shape)
14
+ return x
15
+
16
+
17
+ class Dense(nn.Module):
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.mu = nn.Linear(1024, 32)
21
+ self.log_sigma = nn.Linear(1024, 32)
22
+
23
+ def forward(self, x):
24
+ x = x.flatten(start_dim=1)
25
+ mu = self.mu(x)
26
+ log_sigma = self.log_sigma(x)
27
+ z = mu + torch.exp(log_sigma) * torch.randn_like(mu)
28
+ return z, mu, log_sigma
29
+
30
+
31
+ class Fit(nn.Module):
32
+ def __init__(self):
33
+ super().__init__()
34
+
35
+ def forward(self, x):
36
+ return x.unsqueeze(-1).unsqueeze(-1)
37
+
38
+
39
+ class AutoEncoder(nn.Module):
40
+ def __init__(self, cfg):
41
+ super().__init__()
42
+ self.cfg = cfg
43
+ self.conv = nn.Sequential(
44
+ nn.Conv2d(3, 32, 4, 2),
45
+ nn.ReLU(),
46
+ nn.Conv2d(32, 64, 4, 2),
47
+ nn.ReLU(),
48
+ nn.Conv2d(64, 128, 4, 2),
49
+ nn.ReLU(),
50
+ nn.Conv2d(128, 256, 4, 2),
51
+ nn.ReLU(),
52
+ )
53
+ self.dense = Dense()
54
+ self.decoder = nn.Sequential(
55
+ nn.Linear(32, 1024),
56
+ Fit(),
57
+ nn.ConvTranspose2d(1024, 128, 5, 2),
58
+ nn.ReLU(),
59
+ nn.ConvTranspose2d(128, 64, 5, 2),
60
+ nn.ReLU(),
61
+ nn.ConvTranspose2d(64, 32, 6, 2),
62
+ nn.ReLU(),
63
+ nn.ConvTranspose2d(32, 3, 6, 2),
64
+ nn.Sigmoid(),
65
+ )
66
+
67
+ def encode(self, x):
68
+ # returns (z, mu, log_sigma)
69
+ return self.dense(self.conv(x))
70
+
71
+ @staticmethod
72
+ @torch.compile
73
+ def kl_divergence(mu, log_sigma):
74
+ # KL(N(mu, sigma^2) || N(0,1)) summed over latent dims, averaged over the batch.
75
+ # log_sigma is log-STD (Dense samples with std = exp(log_sigma)), so variance is
76
+ # exp(2*log_sigma). This is 2x the textbook KL -- the global 0.5 is dropped to match
77
+ # the sum-reduced MSE recon (also 2x a unit-variance Gaussian NLL), keeping the recon:KL
78
+ # scale (and thus beta / the free-bits floor) consistent.
79
+ var = torch.exp(2 * log_sigma)
80
+ return 0.5 * (mu.pow(2) + var - 2 * log_sigma - 1).sum(-1).mean()
81
+
82
+ def forward(self, x):
83
+ # x.shape = B * C * H * W
84
+ z, mu, log_sigma = self.encode(x)
85
+ # (z,mu,log_sigma).shape = B * 32
86
+ x_recon = self.decoder(z)
87
+ kl = self.kl_divergence(mu, log_sigma)
88
+ return x_recon, kl
89
+
90
+
91
+ class MDN(nn.Module):
92
+ def __init__(self, cfg):
93
+ super().__init__()
94
+ h = cfg.rnn.hidden_size
95
+ self.gaussians = cfg.rnn.num_mix
96
+ self.z_dim = cfg.rnn.z_dim
97
+ self.temp = cfg.rnn.temp
98
+ # self.layer = nn.Sequential(nn.Linear(h, h), nn.ReLU())
99
+ self.probs_layer = nn.Linear(h, self.gaussians)
100
+ self.means = nn.Linear(h, self.gaussians * self.z_dim)
101
+ self.stds = nn.Linear(h, self.gaussians * self.z_dim)
102
+
103
+ def forward(self, x):
104
+ # fix #5: return distribution params for NLL loss, not a sampled point
105
+ # x.shape = (B, 256)
106
+ # x = self.layer(x)
107
+ temp = self.temp if not (self.training) else 1
108
+ pi = F.softmax(self.probs_layer(x) / temp, dim=-1) # (B, 5)
109
+ mu = self.means(x).view(-1, self.gaussians, self.z_dim) # (B, 5, 32)
110
+ sigma = torch.exp(self.stds(x)).view(
111
+ -1, self.gaussians, self.z_dim
112
+ ) # (B, 5, 32)
113
+ return pi, mu, sigma
114
+
115
+ def sample(self, pi, mu, sigma):
116
+ # fix #6: correct mixture sampling — pick one component, then sample from it
117
+ # pi: (B, 5), mu/sigma: (B, 5, 32)
118
+ k = torch.multinomial(pi, num_samples=1).squeeze(
119
+ -1
120
+ ) # (B,) — hard component draw
121
+ B = mu.shape[0]
122
+ mu_k = mu[torch.arange(B), k] # (B, 32)
123
+ sigma_k = sigma[torch.arange(B), k] # (B, 32) — temperature scales uncertainty
124
+ return Normal(mu_k, sigma_k).sample() # (B, 32)
125
+
126
+ @staticmethod
127
+ def loss(pi, mu, sigma, target, mask=None):
128
+ # Works for any prefix shape: (B, 32) or (B, T, 32)
129
+ log_pi = torch.log(pi + 1e-8) # (..., K)
130
+ log_prob = Normal(mu, sigma).log_prob(target.unsqueeze(-2)) # (..., K, 32)
131
+ nll = -torch.logsumexp(log_pi + log_prob.sum(-1), dim=-1) # (...)
132
+ return nll[mask].mean() if mask is not None else nll.mean()
133
+
134
+
135
+ class RNN(nn.Module):
136
+ def __init__(self, cfg):
137
+ super().__init__()
138
+ self.lstm = LSTM(
139
+ cfg.rnn.z_dim + cfg.rnn.action_dim, cfg.rnn.hidden_size, batch_first=True
140
+ )
141
+ self.mdn = MDN(cfg)
142
+
143
+ def forward(self, z, a, hidden=None):
144
+ # z: (B, T, z_dim) or (T, z_dim) for single episode
145
+ # a: (B, T, action_dim) or (T, action_dim)
146
+ x = torch.cat([z, a], dim=-1) # (B, T, 35) or (T, 35)
147
+ if x.dim() == 2:
148
+ x, squeeze = x.unsqueeze(0), True
149
+ else:
150
+ squeeze = False
151
+ output, hidden = self.lstm(x, hidden) # (B, T, 256)
152
+ B, T, H = output.shape
153
+ pi, mu, sigma = self.mdn(output.reshape(B * T, H))
154
+ pi = pi.view(B, T, self.mdn.gaussians)
155
+ mu = mu.view(B, T, self.mdn.gaussians, self.mdn.z_dim)
156
+ sigma = sigma.view(B, T, self.mdn.gaussians, self.mdn.z_dim)
157
+ if squeeze:
158
+ pi, mu, sigma, output = (
159
+ pi.squeeze(0),
160
+ mu.squeeze(0),
161
+ sigma.squeeze(0),
162
+ output.squeeze(0),
163
+ )
164
+ return pi, mu, sigma, hidden, output
rnn.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a563113bfc6dc4dafbfb1b8b61dae411e0d232a9829194692bef4e4ebec70674
3
+ size 1538453
vae.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7a76f3e62e6d0ac258ef4a82cd9c62e183964f87fdc129b540b3370934e8038
3
+ size 17404153