Download lam_model.py from erensckin/hand2throw-lam: direct link, hf CLI and curl.
- Browser
- Download file 5.99 kB
-
https://huggingface.co/erensckin/hand2throw-lam/resolve/main/lam_model.py
- Command line
-
hf download hf://erensckin/hand2throw-lam/lam_model.py
-
curl -L -o lam_model.py https://huggingface.co/erensckin/hand2throw-lam/resolve/main/lam_model.py
5.99 kB
| """Small latent action model that doubles as a world model (LAPO-style). | |
| encoder: (frame t, frame t+k) -> 4 tokens of 3 numbers -> FSQ, 5 levels each | |
| decoder: frame t + quantised latent -> predicted frame t+k | |
| The latent is tiny (125 codes per token, about 28 bits per frame pair), so it cannot carry | |
| the image. The decoder gets the scene from frame t through skip connections, and the latent | |
| only has room for what changed. Quantisation is FSQ (Mentzer et al. 2023), which has no | |
| codebook that could collapse. An earlier version with an EMA VQ codebook and zero-initialised | |
| FiLM layers did collapse: the decoder ignored the latent and the encoder became constant. | |
| References: LAPO, Learning to Act without Actions (Schmidt & Jiang, ICLR 2024); LAPA, Latent | |
| Action Pretraining from Videos (Ye et al., ICLR 2025), which scales the idea to VLA | |
| pretraining. | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| def conv_block(cin: int, cout: int, stride: int) -> nn.Sequential: | |
| return nn.Sequential( | |
| nn.Conv2d(cin, cout, 3, stride, 1), nn.GroupNorm(8, cout), nn.SiLU(), | |
| nn.Conv2d(cout, cout, 3, 1, 1), nn.GroupNorm(8, cout), nn.SiLU(), | |
| ) | |
| class Encoder(nn.Module): | |
| """(B, cin, S, S) -> features at S/2, S/4, S/8, S/16.""" | |
| def __init__(self, cin: int, widths=(32, 64, 128, 256)): | |
| super().__init__() | |
| chans = [cin, *widths] | |
| self.levels = nn.ModuleList(conv_block(chans[i], chans[i + 1], 2) for i in range(len(widths))) | |
| def forward(self, x): | |
| feats = [] | |
| for level in self.levels: | |
| x = level(x) | |
| feats.append(x) | |
| return feats | |
| class FiLM(nn.Module): | |
| """Condition a feature map on the latent action: h * (1 + gamma) + beta. Default (non-zero) | |
| initialisation, so the latent shapes the prediction from the first step.""" | |
| def __init__(self, zdim: int, ch: int): | |
| super().__init__() | |
| self.lin = nn.Linear(zdim, 2 * ch) | |
| def forward(self, h, z): | |
| gamma, beta = self.lin(z).chunk(2, dim=-1) | |
| return h * (1 + gamma[..., None, None]) + beta[..., None, None] | |
| class Decoder(nn.Module): | |
| """U-Net decoder over the context frame's features, FiLM-conditioned on the latent at | |
| every scale; outputs the change to add to the context frame (tanh, in [-1, 1]).""" | |
| def __init__(self, zdim: int, widths=(32, 64, 128, 256)): | |
| super().__init__() | |
| w0, w1, w2, w3 = widths | |
| self.mid, self.film3 = conv_block(w3, w3, 1), FiLM(zdim, w3) | |
| self.up2, self.film2 = conv_block(w3 + w2, w2, 1), FiLM(zdim, w2) | |
| self.up1, self.film1 = conv_block(w2 + w1, w1, 1), FiLM(zdim, w1) | |
| self.up0, self.film0 = conv_block(w1 + w0, w0, 1), FiLM(zdim, w0) | |
| self.out = nn.Conv2d(w0, 3, 3, 1, 1) | |
| def forward(self, feats, z): | |
| f0, f1, f2, f3 = feats | |
| h = self.film3(self.mid(f3), z) | |
| for skip, block, film in ((f2, self.up2, self.film2), (f1, self.up1, self.film1), (f0, self.up0, self.film0)): | |
| h = F.interpolate(h, scale_factor=2, mode="nearest") | |
| h = film(block(torch.cat([h, skip], dim=1)), z) | |
| h = F.interpolate(h, scale_factor=2, mode="nearest") | |
| return torch.tanh(self.out(h)) | |
| class FSQ(nn.Module): | |
| """Finite scalar quantisation with odd level counts: z -> tanh -> round to `levels` values, | |
| straight-through gradient. Returns values in [-1, 1] and an integer code per token.""" | |
| def __init__(self, levels=(5, 5, 5)): | |
| super().__init__() | |
| assert all(n % 2 == 1 for n in levels), "use odd level counts" | |
| self.n_codes = math.prod(levels) | |
| # (not named "half": nn.Module.half() exists) | |
| self.register_buffer("half_range", (torch.tensor(levels, dtype=torch.float32) - 1) / 2) | |
| basis = [1] | |
| for n in levels[:-1]: | |
| basis.append(basis[-1] * n) | |
| self.register_buffer("basis", torch.tensor(basis, dtype=torch.long)) | |
| self.register_buffer("levels_t", torch.tensor(levels, dtype=torch.long)) | |
| def forward(self, z): | |
| bounded = torch.tanh(z) * self.half_range | |
| rounded = torch.round(bounded) | |
| q = bounded + (rounded - bounded).detach() | |
| idx = ((rounded + self.half_range).long() * self.basis).sum(-1) | |
| return q / self.half_range, idx | |
| def codes_to_values(self, idx): | |
| digits = (idx[..., None] // self.basis) % self.levels_t | |
| return (digits.float() - self.half_range) / self.half_range | |
| class LatentActionModel(nn.Module): | |
| def __init__(self, img: int = 96, n_tokens: int = 4, levels=(5, 5, 5)): | |
| super().__init__() | |
| assert img % 16 == 0 | |
| levels = tuple(levels) | |
| self.cfg = {"img": img, "n_tokens": n_tokens, "levels": list(levels)} | |
| self.n_tokens, self.token_dim = n_tokens, len(levels) | |
| self.action_enc = Encoder(6) | |
| self.to_z = nn.Sequential( | |
| nn.Flatten(), nn.Linear(256 * (img // 16) ** 2, 512), nn.SiLU(), nn.Linear(512, n_tokens * len(levels)) | |
| ) | |
| self.fsq = FSQ(levels) | |
| self.ctx_enc = Encoder(3) | |
| self.dec = Decoder(n_tokens * len(levels)) | |
| def n_codes(self) -> int: | |
| return self.fsq.n_codes | |
| def encode(self, x0, x1): | |
| """Continuous latent (B, T, d), quantised latent (B, T, d), code per token (B, T).""" | |
| z = self.to_z(self.action_enc(torch.cat([x0, x1], dim=1))[-1]).view(-1, self.n_tokens, self.token_dim) | |
| q, idx = self.fsq(z) | |
| return z, q, idx | |
| def decode(self, x0, q): | |
| """World-model step: predicted next frame from the current frame and a latent action.""" | |
| return x0 + self.dec(self.ctx_enc(x0), q.flatten(1)) | |
| def codes_to_latent(self, idx): | |
| """Codes (B, T) -> quantised latent (B, T, d), e.g. to roll out chosen latent actions.""" | |
| return self.fsq.codes_to_values(idx) | |
| def forward(self, x0, x1): | |
| z, q, idx = self.encode(x0, x1) | |
| return self.decode(x0, q), idx | |