Replace with max-safety weights (start 1 of the 6-run frontier): mean 231.2, landings 99.67%, crashes 0.25% - 23x fewer crashes than the teacher
bb8222a verified Download model.py from Dimitrius174/lunarlander-lin32: direct link, hf CLI and curl.
- Browser
- Download file 3.6 kB
-
https://huggingface.co/Dimitrius174/lunarlander-lin32/resolve/main/model.py
- Command line
-
hf download hf://Dimitrius174/lunarlander-lin32/model.py
-
curl -L -o model.py https://huggingface.co/Dimitrius174/lunarlander-lin32/resolve/main/model.py
3.6 kB
| #!/usr/bin/env python3 | |
| """TinyLunarPolicy β a 32-parameter single-layer neural network that lands LunarLander-v3. | |
| logits = Linear(8 -> 4, bias=False)(obs) # 32 parameters | |
| action = argmax(logits) | |
| This is the *minimal* policy that solves Gymnasium `LunarLander-v3`: | |
| mean return ~231.2, 99.7% landings (0.25% crashes), 60/60 evaluation seeds solved (see README). | |
| It is exactly a single linear layer + argmax, and is mathematically identical to a | |
| single-centroid nearest-neighbour policy in the C2 space (s (x) onehot(a)). | |
| Dependencies: torch, safetensors (numpy optional, only for `.act`). | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| OBS_DIM = 8 | |
| N_ACTIONS = 4 | |
| # Gymnasium LunarLander-v3 observation layout (for interpretability only). | |
| OBS_NAMES = [ | |
| "x", "y", "vx", "vy", "angle", "angular_velocity", | |
| "leg_left_contact", "leg_right_contact", | |
| ] | |
| class TinyLunarPolicy(nn.Module): | |
| """Linear 8->4 policy head; the action is the argmax of the four logits.""" | |
| def __init__(self, obs_dim: int = OBS_DIM, n_actions: int = N_ACTIONS, | |
| bias: bool = False) -> None: | |
| super().__init__() | |
| self.obs_dim = int(obs_dim) | |
| self.n_actions = int(n_actions) | |
| self.linear = nn.Linear(self.obs_dim, self.n_actions, bias=bias) | |
| # ββ introspection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def n_params(self) -> int: | |
| return sum(p.numel() for p in self.parameters()) | |
| def weights(self) -> np.ndarray: | |
| """Return W of shape (obs_dim, n_actions) such that logits = obs @ W.""" | |
| return self.linear.weight.detach().cpu().numpy().T # (8, 4) | |
| # ββ policy βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def forward(self, obs: torch.Tensor) -> torch.Tensor: | |
| """obs: (..., 8) -> logits (..., 4).""" | |
| return self.linear(obs) | |
| def logits(self, obs) -> torch.Tensor: | |
| o = torch.as_tensor(np.asarray(obs, dtype=np.float32)) | |
| return self.forward(o) | |
| def act(self, obs) -> int: | |
| """Greedy action for a single observation (array-like of length 8).""" | |
| return int(torch.argmax(self.logits(obs)).item()) | |
| # ββ loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def from_safetensors(cls, path: str, **kwargs) -> "TinyLunarPolicy": | |
| """Load from a local `.safetensors` file.""" | |
| from safetensors.torch import load_file | |
| model = cls(**kwargs) | |
| model.load_state_dict(load_file(path)) | |
| model.eval() | |
| return model | |
| def from_pretrained(cls, repo_id_or_path: str = "Dimitrius174/lunarlander-lin32", | |
| filename: str = "model.safetensors", **kwargs) -> "TinyLunarPolicy": | |
| """Load from the Hugging Face Hub (or a local directory).""" | |
| import os | |
| if os.path.isdir(repo_id_or_path): | |
| return cls.from_safetensors(os.path.join(repo_id_or_path, filename), **kwargs) | |
| from huggingface_hub import hf_hub_download | |
| return cls.from_safetensors(hf_hub_download(repo_id_or_path, filename), **kwargs) | |