Download connectx/model.py from alextoti1/WorldModel-ConnectX: direct link, hf CLI and curl.
- Browser
- Download file 3.92 kB
-
https://huggingface.co/alextoti1/WorldModel-ConnectX/resolve/main/connectx/model.py
- Command line
-
hf download hf://alextoti1/WorldModel-ConnectX/connectx/model.py
-
curl -L -o model.py https://huggingface.co/alextoti1/WorldModel-ConnectX/resolve/main/connectx/model.py
3.92 kB
| """ | |
| The four learned pieces: an encoder (real state -> latent), a dynamics model | |
| (latent + action -> predicted next latent + reward), a value head (latent -> | |
| estimated cost-to-go), and a diagnostic decoder (latent -> reconstructed | |
| state, used only to sanity-check the latent isn't collapsing -- never | |
| consulted for a real decision at inference time). | |
| Config-driven: nothing here is hardcoded to Connect-4 beyond `state_dim`/ | |
| `num_actions`, so this file would work for any domain with a fixed-length | |
| tuple state and a fixed discrete action space. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| def mlp(dims, out_activation=None): | |
| layers = [] | |
| for i in range(len(dims) - 1): | |
| layers.append(nn.Linear(dims[i], dims[i + 1])) | |
| is_last = i == len(dims) - 2 | |
| if not is_last: | |
| layers.append(nn.ReLU()) | |
| elif out_activation is not None: | |
| layers.append(out_activation) | |
| return nn.Sequential(*layers) | |
| class Encoder(nn.Module): | |
| def __init__(self, state_dim, latent_dim, hidden_dim=128): | |
| super().__init__() | |
| self.net = mlp([state_dim, hidden_dim, hidden_dim, latent_dim]) | |
| def forward(self, state): | |
| return self.net(state) | |
| class DynamicsModel(nn.Module): | |
| def __init__(self, latent_dim, num_actions, hidden_dim=128): | |
| super().__init__() | |
| self.num_actions = num_actions | |
| self.trunk = mlp([latent_dim + num_actions, hidden_dim, hidden_dim]) | |
| self.next_latent_head = nn.Linear(hidden_dim, latent_dim) | |
| self.reward_head = nn.Linear(hidden_dim, 1) | |
| def forward(self, z, action_idx): | |
| action_onehot = nn.functional.one_hot(action_idx, self.num_actions).float() | |
| h = self.trunk(torch.cat([z, action_onehot], dim=-1)) | |
| next_z = self.next_latent_head(h) | |
| reward = self.reward_head(h).squeeze(-1) | |
| return next_z, reward | |
| class ValueHead(nn.Module): | |
| def __init__(self, latent_dim, hidden_dim=128): | |
| super().__init__() | |
| self.net = mlp([latent_dim, hidden_dim, hidden_dim, 1]) | |
| def forward(self, z): | |
| return self.net(z).squeeze(-1) | |
| class Decoder(nn.Module): | |
| def __init__(self, latent_dim, state_dim, hidden_dim=128): | |
| super().__init__() | |
| self.net = mlp([latent_dim, hidden_dim, hidden_dim, state_dim]) | |
| def forward(self, z): | |
| return self.net(z) | |
| class WorldModel(nn.Module): | |
| def __init__(self, state_dim, num_actions, latent_dim=64, hidden_dim=128): | |
| super().__init__() | |
| self.state_dim = state_dim | |
| self.num_actions = num_actions | |
| self.latent_dim = latent_dim | |
| self.encoder = Encoder(state_dim, latent_dim, hidden_dim) | |
| self.dynamics = DynamicsModel(latent_dim, num_actions, hidden_dim) | |
| self.value = ValueHead(latent_dim, hidden_dim) | |
| self.decoder = Decoder(latent_dim, state_dim, hidden_dim) | |
| # Value-target normalization stats: NOT learned, set once by | |
| # whichever value-training pass runs (see verifier.py's | |
| # train_mc_value_onpolicy) from the actual label distribution it | |
| # sees. Buffers (not plain attributes) so they save/load with the | |
| # checkpoint automatically. | |
| self.register_buffer("value_target_mean", torch.tensor(0.0)) | |
| self.register_buffer("value_target_std", torch.tensor(1.0)) | |
| def encode(self, state): | |
| return self.encoder(state) | |
| def imagine_step(self, z, action_idx): | |
| return self.dynamics(z, action_idx) | |
| def evaluate(self, z): | |
| """Always returns real-scale value estimates (remaining cost, same | |
| units `imagine_step`'s predicted reward uses) -- the head internally | |
| predicts a normalized target, denormalized here so no caller needs | |
| to know normalization is happening.""" | |
| raw = self.value(z) | |
| return raw * self.value_target_std + self.value_target_mean | |
| def reconstruct(self, z): | |
| return self.decoder(z) | |