alextoti1's picture
Publish full project code and checkpoint
9ede8c0 verified
Raw
History Blame Contribute Delete
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)