Download code/environments/snake/dagger.py from markuz89/minijev-snake: direct link, hf CLI and curl.
- Browser
- Download file 4.68 kB
-
https://huggingface.co/markuz89/minijev-snake/resolve/main/code/environments/snake/dagger.py
- Command line
-
hf download hf://markuz89/minijev-snake/code/environments/snake/dagger.py
-
curl -L -o dagger.py https://huggingface.co/markuz89/minijev-snake/resolve/main/code/environments/snake/dagger.py
4.68 kB
| """DAgger for Snake (Ross et al., 2011). | |
| Behaviour cloning only sees states the oracle visits; once the student makes a | |
| mistake it lands in states it never trained on and errors compound. DAgger lets | |
| the student play, labels *its* states with the oracle, aggregates them with the | |
| original data and fine-tunes. Cheap (no reward engineering, the oracle is | |
| free) and directly targets the student's own failure modes. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import json | |
| import shutil | |
| from pathlib import Path | |
| from minijev.datasets.decision_data import DecisionData | |
| from minijev.inference.runtime import TorchDecider | |
| from minijev.training.supervised import train | |
| from .dataset import QUESTION, collect_with_policy | |
| def run_dagger(cfg: dict, checkpoint: str, iterations: int = 2, samples: int = 100_000, | |
| run_dir: str | None = None) -> list[dict]: | |
| import torch | |
| # model/training settings come from the checkpoint (DAgger fine-tunes *that* model); | |
| # the dagger config only contributes its `dagger`, `env` and `eval` sections | |
| ck_cfg = torch.load(checkpoint, map_location="cpu", weights_only=False)["config"] | |
| cfg = {**copy.deepcopy(ck_cfg), **{k: cfg[k] for k in ("dagger", "env", "eval") if k in cfg}} | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| root = Path(run_dir or f"runs/{ck_cfg.get('name', 'snake')}-dagger") | |
| root.mkdir(parents=True, exist_ok=True) | |
| base_dir = Path(cfg["data"]["dir"]) | |
| agg = DecisionData.load(base_dir / "train.npz") | |
| dcfg = cfg.get("dagger", {}) | |
| board_sizes = cfg["data"].get("generate", {}).get("board_sizes", [cfg.get("env", {}).get("board_size", 10)]) | |
| history = json.loads((root / "dagger.json").read_text()) if (root / "dagger.json").exists() else [] | |
| for rec in history: # resume: finished iterations are kept, their data is rebuilt below | |
| checkpoint = rec["checkpoint"] | |
| if history: | |
| agg = DecisionData.load(root / f"data-it{history[-1]['iteration']}" / "train.npz") | |
| print(f"[dagger] resuming after iteration {history[-1]['iteration']}") | |
| for it in range(len(history) + 1, iterations + 1): | |
| dec = TorchDecider(checkpoint, threads=4, device=device) | |
| # probs are aligned with ACTIONS order whatever the labels ("UP" or "move up") | |
| policy = lambda states: dec.probs(states, QUESTION, list(dec.task.default_choices)) # noqa: E731 | |
| print(f"[dagger] iteration {it}: collecting {samples:,} student states ...") | |
| new = collect_with_policy(policy, samples, seed=1000 + it, board_sizes=board_sizes, | |
| keep_prob=dcfg.get("keep_prob", 0.2), explore_eps=dcfg.get("explore_eps", 0.02), | |
| env_cfg={k: v for k, v in cfg.get("env", {}).items() if k != "board_size"}, | |
| game_id_base=10**9 * it, parallel=256) | |
| agg = DecisionData.concat([agg, new]) | |
| data_dir = root / f"data-it{it}" | |
| data_dir.mkdir(exist_ok=True) | |
| agg.save(data_dir / "train.npz") | |
| for split in ("val", "test"): # evaluation splits stay the original, untouched ones | |
| shutil.copy(base_dir / f"{split}.npz", data_dir / f"{split}.npz") | |
| c = copy.deepcopy(cfg) | |
| c["name"] = f"{cfg.get('name', 'snake')}-dagger{it}" | |
| c["data"] = {"dir": str(data_dir)} | |
| c["training"]["init_checkpoint"] = checkpoint | |
| c["training"]["epochs"] = dcfg.get("epochs", 1) | |
| c["training"]["lr"] = dcfg.get("lr", cfg["training"].get("lr", 1e-3) * 0.5) | |
| c["training"]["warmup_steps"] = 100 | |
| it_dir = root / f"it{it}" | |
| summary = train(c, run_dir=it_dir, resume=(it_dir / "checkpoints" / "last.pt").exists()) | |
| checkpoint = summary["final_checkpoint"] | |
| from .evaluate import model_policy, play_games | |
| games = play_games(model_policy(TorchDecider(checkpoint, threads=4, device=device)), | |
| cfg.get("eval", {}).get("games", 200), cfg.get("env", {}), parallel=200) | |
| (root / f"it{it}" / "games.json").write_text(json.dumps(games, indent=2)) | |
| rec = {"iteration": it, "rows": len(agg), "new_rows": len(new), "checkpoint": checkpoint, | |
| "test_acc": summary["test_calibrated"]["accuracy"], "test_ece": summary["test_calibrated"]["ece"], | |
| "avg_score": games["avg_score"], "fatal_move_rate": games["fatal_move_rate"]} | |
| print(f"[dagger] it {it}: avg score {games['avg_score']:.1f} | test acc {rec['test_acc']:.4f} " | |
| f"| fatal-move {games['fatal_move_rate']:.4%}") | |
| history.append(rec) | |
| (root / "dagger.json").write_text(json.dumps(history, indent=2)) | |
| return history | |