Download code/minijev/training/relabel.py from markuz89/minijev-snake: direct link, hf CLI and curl.
- Browser
- Download file 3.43 kB
-
https://huggingface.co/markuz89/minijev-snake/resolve/main/code/minijev/training/relabel.py
- Command line
-
hf download hf://markuz89/minijev-snake/code/minijev/training/relabel.py
-
curl -L -o relabel.py https://huggingface.co/markuz89/minijev-snake/resolve/main/code/minijev/training/relabel.py
3.43 kB
| """Rename a trained model's choice labels without retraining the policy. | |
| The core only sees choice *embeddings*. To move a model from "UP" to "move up" | |
| we fit the ChoiceEncoder (not the shared embeddings, not the core) so that the | |
| new label produces the vector the old label produced. The decision function is | |
| then unchanged up to the fit error, which is verified on real states. | |
| minijev relabel --checkpoint runs/x/final.pt --out runs/x/final-semantic.pt \\ | |
| --map UP="move up" DOWN="move down" LEFT="move left" RIGHT="move right" \\ | |
| --set task.choice_style=semantic | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import numpy as np | |
| import torch | |
| def relabel_checkpoint(checkpoint: str, out: str, mapping: dict[str, str], cfg_updates: dict | None = None, | |
| steps: int = 3000, lr: float = 3e-3, anchors: list[str] | None = None) -> dict: | |
| from minijev.inference.runtime import TorchDecider | |
| from minijev.training.supervised import make_model | |
| ck = torch.load(checkpoint, map_location="cpu", weights_only=False) | |
| cfg = copy.deepcopy(ck["config"]) | |
| model = make_model(cfg) | |
| model.load_state_dict(ck["model"]) | |
| model.eval() | |
| old_dec = TorchDecider(model=copy.deepcopy(model), cfg=cfg) | |
| tok, max_len = old_dec.tokenizer, old_dec.max_choice_len | |
| old, new = list(mapping), list(mapping.values()) | |
| anchors = list(anchors or []) | |
| def emb(labels): | |
| if model.backbone is None: | |
| ids = torch.as_tensor(tok.encode_batch(labels, max_len)) | |
| return model.choice_encoder(ids) | |
| ids = torch.as_tensor(tok.encode_batch(labels, max_len, dynamic=True)) | |
| return model.choice_encoder(pooled=model.backbone(ids)[2]) | |
| with torch.no_grad(): | |
| target = emb(old + anchors).detach() | |
| shared = {id(p) for n, p in model.named_parameters() if not n.startswith("choice_encoder.")} | |
| params = [p for p in model.choice_encoder.parameters() if id(p) not in shared] | |
| for p in model.parameters(): | |
| p.requires_grad_(False) | |
| for p in params: | |
| p.requires_grad_(True) | |
| opt = torch.optim.Adam(params, lr=lr) | |
| sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, steps) | |
| for _ in range(steps): | |
| loss = (emb(new + anchors) - target).pow(2).mean() / target.pow(2).mean() | |
| opt.zero_grad() | |
| loss.backward() | |
| opt.step() | |
| sched.step() | |
| for p in model.parameters(): | |
| p.requires_grad_(True) | |
| for k, v in (cfg_updates or {}).items(): | |
| node = cfg | |
| parts = k.split(".") | |
| for part in parts[:-1]: | |
| node = node.setdefault(part, {}) | |
| node[parts[-1]] = v | |
| new_dec = TorchDecider(model=model, cfg=cfg) | |
| new_dec.temperature = old_dec.temperature | |
| states = new_dec.task.example_states(256, seed=123) | |
| p_old = old_dec.probs(states, new_dec.task.default_question or "", old) | |
| p_new = new_dec.probs(states, new_dec.task.default_question or "", new) | |
| report = {"rel_mse": float(loss.detach()), "max_abs_prob_diff": float(np.abs(p_old - p_new).max()), | |
| "argmax_agreement": float((p_old.argmax(1) == p_new.argmax(1)).mean()), "states": len(states)} | |
| torch.save({**{k: v for k, v in ck.items() if k not in ("model", "config", "optimizer")}, | |
| "model": model.state_dict(), "config": cfg, "relabel": {"from": checkpoint, "map": mapping, **report}}, | |
| out) | |
| return report | |