Download scripts/plot_diag_L15.py from Avra98/latent_backtrack: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/Avra98/latent_backtrack/resolve/main/scripts/plot_diag_L15.py
- Command line
-
hf download hf://Avra98/latent_backtrack/scripts/plot_diag_L15.py
-
curl -L -o plot_diag_L15.py https://huggingface.co/Avra98/latent_backtrack/resolve/main/scripts/plot_diag_L15.py
12.7 kB
| #!/usr/bin/env python3 | |
| """Live L15 diagnostic plots from training logs. | |
| Parses `logs/diag_L15_*.log` and writes/overwrites figures under `figs/diag_L15/`: | |
| A_train_loss.png train CE loss vs epoch (per arm) | |
| B_stage_timeline.png scheduled stage vs epoch + promote markers | |
| C_frontier_heatmap.png per-hop frontier accuracy over eval epochs | |
| D_ce_score_heatmap.png per-hop ce_score (backtracking metric) over evals | |
| E_time_to_threshold.png epochs-to-first-hit of frontier>=thr per hop | |
| F_latest_bars.png latest per-hop frontier vs ce_score | |
| Also writes `figs/diag_L15/summary.json` with time-to-threshold tables. | |
| Usage: | |
| PYTHONPATH=. python scripts/plot_diag_L15.py | |
| PYTHONPATH=. python scripts/plot_diag_L15.py --watch 120 # replot every 120s | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import ast | |
| import json | |
| import re | |
| import time | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parents[1] | |
| LOG_DIR = ROOT / "logs" | |
| OUT_DIR = ROOT / "figs" / "diag_L15" | |
| # Labels encode BOTH gates: promX = stage advance, btY = backtrack check. | |
| ARMS = [ | |
| ("promF.95 / btCE.50", "diag_L15_promF095_btCE050", "#1b7837"), | |
| ("promF.95 / btCE.90", "diag_L15_promF095_btCE090", "#00441b"), | |
| ("promF.95 / btCE.95", "diag_L15_promF095_btCE095", "#006d2c"), | |
| ("promF.99 / btCE.90", "diag_L15_promF099_btCE090", "#762a83"), | |
| ("promF.99 / btCE.50", "diag_L15_promF099_btCE050", "#c51b7d"), | |
| ("promF.95 / btF.95", "diag_L15_promF095_btF095", "#1f78b4"), | |
| ("promCE.90 / btCE.90", "diag_L15_promCE090_btCE090", "#d95f02"), | |
| ("promF.95 / btNONE", "diag_L15_promF095_btNONE", "#a6611a"), | |
| ] | |
| def parse_log(path: Path) -> dict: | |
| if not path.exists(): | |
| return {} | |
| # stream large logs; keep last 12MB for speed (full history still usually fits) | |
| with open(path, "rb") as f: | |
| f.seek(0, 2) | |
| size = f.tell() | |
| f.seek(0 if size < 12_000_000 else size - 12_000_000) | |
| text = f.read().decode("utf-8", errors="ignore") | |
| # If we truncated mid-file, also pull promote history from a head+grep via | |
| # a second full regex on a larger window when small enough. | |
| if size < 40_000_000: | |
| text = path.read_text(errors="ignore") | |
| train = [ | |
| (int(e), int(s), float(l)) | |
| for e, s, l in re.findall( | |
| r"train epoch (\d+)/\d+ stage=(\d+) loss=([0-9.]+)", text | |
| ) | |
| ] | |
| promotes = [ | |
| (int(a), int(b), int(ep)) | |
| for a, b, ep in re.findall( | |
| r"PROMOTE stage (\d+) -> (\d+) \|.*? in (\d+) epochs", text | |
| ) | |
| ] | |
| backtracks = [ | |
| (m, None if t == "None" else int(t), float(acc)) | |
| for m, t, acc in re.findall( | |
| r"backtrack\[([^\]>=]+)[^]]*\]: target_stage=(\S+) min_mastered[_a-z]*=([0-9.]+)", | |
| text, | |
| ) | |
| ] | |
| evals = [] | |
| for ep_m, metric, raw in re.findall( | |
| r"train epoch (\d+)/\d+.*?\n.*?eval per-hop \(staging_metric=(\w+)\): (\{.*?\})", | |
| text, | |
| flags=re.S, | |
| ): | |
| # The above may be too greedy; fall back below. | |
| pass | |
| # Robust: pair nearest preceding train epoch with each eval line | |
| lines = text.splitlines() | |
| cur_ep, cur_stage = None, None | |
| for line in lines: | |
| m = re.match(r"train epoch (\d+)/\d+ stage=(\d+) loss=([0-9.]+)", line) | |
| if m: | |
| cur_ep, cur_stage = int(m.group(1)), int(m.group(2)) | |
| continue | |
| # Compact format: | |
| # eval (prom=frontier@0.95 bt=ce_score@0.5) frontier=[1:1.00 2:0.98 ...] ce_score=[1:0.95 ...] | |
| m = re.match( | |
| r"eval \(prom=(\w+)@([0-9.]+) bt=(\w+)@([0-9.]+)\) " | |
| r"frontier=\[([^\]]*)\]\s+ce_score=\[([^\]]*)\]", | |
| line, | |
| ) | |
| if m and cur_ep is not None: | |
| def _parse_pairs(s): | |
| out = {} | |
| for tok in s.split(): | |
| if ":" not in tok: | |
| continue | |
| h, v = tok.split(":", 1) | |
| try: | |
| out[int(h)] = float(v) | |
| except ValueError: | |
| pass | |
| return out | |
| fr = _parse_pairs(m.group(5)) | |
| ce = _parse_pairs(m.group(6)) | |
| hops = { | |
| h: { | |
| "frontier": fr.get(h, float("nan")), | |
| "ce_score": ce.get(h, float("nan")), | |
| "reachable": float("nan"), | |
| "optimal": float("nan"), | |
| "superposition": float("nan"), | |
| } | |
| for h in sorted(set(fr) | set(ce)) | |
| } | |
| evals.append( | |
| { | |
| "epoch": cur_ep, | |
| "stage": cur_stage, | |
| "metric": m.group(1), | |
| "hops": hops, | |
| } | |
| ) | |
| continue | |
| # Legacy full-dict format | |
| m = re.match( | |
| r"eval per-hop \((?:staging_metric=(\w+)|promote=(\w+)@([0-9.]+), " | |
| r"backtrack=(\w+)@([0-9.]+))\): (\{.*\})", | |
| line, | |
| ) | |
| if m and cur_ep is not None: | |
| try: | |
| d = ast.literal_eval(m.group(6)) | |
| except Exception: | |
| continue | |
| metric = m.group(1) or m.group(2) or "?" | |
| evals.append( | |
| { | |
| "epoch": cur_ep, | |
| "stage": cur_stage, | |
| "metric": metric, | |
| "hops": d, | |
| } | |
| ) | |
| return { | |
| "train": train, | |
| "promotes": promotes, | |
| "backtracks": backtracks, | |
| "evals": evals, | |
| } | |
| def time_to_threshold(evals, key: str, thr: float, max_hop: int = 15): | |
| """First eval epoch where hop h's `key` >= thr, for each hop.""" | |
| first = {} | |
| for ev in evals: | |
| for h, c in ev["hops"].items(): | |
| if h in first: | |
| continue | |
| if c.get(key, 0) >= thr: | |
| first[h] = ev["epoch"] | |
| return {h: first.get(h) for h in range(1, max_hop + 1)} | |
| def plot_all(): | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| parsed = {} | |
| for label, name, color in ARMS: | |
| p = parse_log(LOG_DIR / f"{name}.log") | |
| if p.get("train") or p.get("evals"): | |
| parsed[name] = {**p, "label": label, "color": color} | |
| if not parsed: | |
| print("no diag logs yet") | |
| return | |
| # A: train loss | |
| fig, ax = plt.subplots(figsize=(9, 4)) | |
| for name, p in parsed.items(): | |
| if not p["train"]: | |
| continue | |
| xs = [t[0] for t in p["train"]] | |
| ys = [t[2] for t in p["train"]] | |
| ax.plot(xs, ys, color=p["color"], label=p["label"], lw=1.2, alpha=0.9) | |
| ax.set_xlabel("epoch") | |
| ax.set_ylabel("train CE loss") | |
| ax.set_title("L15 diagnostic — training loss") | |
| ax.legend(fontsize=8) | |
| ax.grid(True, alpha=0.3) | |
| fig.tight_layout() | |
| fig.savefig(OUT_DIR / "A_train_loss.png", dpi=140) | |
| plt.close(fig) | |
| # B: stage timeline | |
| fig, ax = plt.subplots(figsize=(9, 4)) | |
| for name, p in parsed.items(): | |
| if not p["train"]: | |
| continue | |
| xs = [t[0] for t in p["train"]] | |
| ys = [t[1] for t in p["train"]] | |
| ax.step(xs, ys, where="post", color=p["color"], label=p["label"], lw=1.5) | |
| for a, b, ep in p["promotes"]: | |
| ax.axvline(ep, color=p["color"], alpha=0.15, lw=0.8) | |
| ax.set_xlabel("epoch") | |
| ax.set_ylabel("scheduled stage") | |
| ax.set_title("L15 diagnostic — stage timeline (promote markers faint)") | |
| ax.legend(fontsize=8) | |
| ax.grid(True, alpha=0.3) | |
| fig.tight_layout() | |
| fig.savefig(OUT_DIR / "B_stage_timeline.png", dpi=140) | |
| plt.close(fig) | |
| # C/D heatmaps per arm | |
| for key, fname, title, vmin, vmax, cmap in [ | |
| ("frontier", "C_frontier_heatmap", "frontier accuracy", 0, 1, "viridis"), | |
| ("ce_score", "D_ce_score_heatmap", "ce_score (BT metric)", 0, 1, "magma"), | |
| ]: | |
| n_arms = len(parsed) | |
| fig, axes = plt.subplots(1, n_arms, figsize=(4 * n_arms, 4), squeeze=False) | |
| for ax, (name, p) in zip(axes[0], parsed.items()): | |
| evs = p["evals"] | |
| if not evs: | |
| ax.set_title(p["label"] + " (no eval)") | |
| continue | |
| hops = sorted({h for e in evs for h in e["hops"]}) | |
| mat = np.full((len(hops), len(evs)), np.nan) | |
| for j, e in enumerate(evs): | |
| for i, h in enumerate(hops): | |
| if h in e["hops"]: | |
| mat[i, j] = e["hops"][h].get(key, np.nan) | |
| im = ax.imshow( | |
| mat, | |
| aspect="auto", | |
| origin="lower", | |
| vmin=vmin, | |
| vmax=vmax, | |
| cmap=cmap, | |
| interpolation="nearest", | |
| ) | |
| ax.set_yticks(range(len(hops))) | |
| ax.set_yticklabels([str(h) for h in hops], fontsize=7) | |
| # x ticks: a few epochs | |
| xt = np.linspace(0, max(len(evs) - 1, 0), num=min(6, len(evs)), dtype=int) | |
| ax.set_xticks(xt) | |
| ax.set_xticklabels([str(evs[j]["epoch"]) for j in xt], fontsize=7, rotation=45) | |
| ax.set_xlabel("epoch") | |
| ax.set_ylabel("hop") | |
| ax.set_title(p["label"], fontsize=9) | |
| fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | |
| fig.suptitle(f"L15 diagnostic — {title} over training") | |
| fig.tight_layout() | |
| fig.savefig(OUT_DIR / f"{fname}.png", dpi=140) | |
| plt.close(fig) | |
| # E: time to frontier thresholds | |
| fig, ax = plt.subplots(figsize=(9, 4)) | |
| summary = {} | |
| for name, p in parsed.items(): | |
| summary[name] = {} | |
| for thr, ls in [(0.85, "--"), (0.95, "-")]: | |
| tt = time_to_threshold(p["evals"], "frontier", thr) | |
| summary[name][f"frontier>={thr}"] = tt | |
| xs = [h for h, e in tt.items() if e is not None] | |
| ys = [tt[h] for h in xs] | |
| if xs: | |
| ax.plot( | |
| xs, | |
| ys, | |
| ls, | |
| color=p["color"], | |
| marker="o", | |
| ms=3, | |
| label=f"{p['label']} thr={thr}", | |
| ) | |
| tt_ce = time_to_threshold(p["evals"], "ce_score", 0.5) | |
| summary[name]["ce_score>=0.5"] = tt_ce | |
| ax.set_xlabel("hop") | |
| ax.set_ylabel("first epoch reaching threshold") | |
| ax.set_title("L15 diagnostic — time-to-threshold (frontier)") | |
| ax.legend(fontsize=7, ncol=2) | |
| ax.grid(True, alpha=0.3) | |
| fig.tight_layout() | |
| fig.savefig(OUT_DIR / "E_time_to_threshold.png", dpi=140) | |
| plt.close(fig) | |
| # F: latest bars frontier vs ce | |
| n_arms = len(parsed) | |
| fig, axes = plt.subplots(1, n_arms, figsize=(4 * n_arms, 3.5), squeeze=False) | |
| for ax, (name, p) in zip(axes[0], parsed.items()): | |
| if not p["evals"]: | |
| continue | |
| last = p["evals"][-1]["hops"] | |
| hops = sorted(last) | |
| fr = [last[h]["frontier"] for h in hops] | |
| ce = [last[h]["ce_score"] for h in hops] | |
| x = np.arange(len(hops)) | |
| ax.bar(x - 0.2, fr, 0.4, label="frontier", color="#1b7837") | |
| ax.bar(x + 0.2, ce, 0.4, label="ce_score", color="#d95f02") | |
| ax.axhline(0.95, color="gray", ls="--", lw=0.8) | |
| ax.axhline(0.5, color="#d95f02", ls=":", lw=0.8) | |
| ax.set_xticks(x) | |
| ax.set_xticklabels([str(h) for h in hops], fontsize=7) | |
| ax.set_ylim(0, 1.05) | |
| ax.set_title( | |
| f"{p['label']}\nep{p['evals'][-1]['epoch']} stage={p['evals'][-1]['stage']}", | |
| fontsize=8, | |
| ) | |
| ax.legend(fontsize=7) | |
| ax.grid(True, axis="y", alpha=0.3) | |
| fig.suptitle("L15 diagnostic — latest per-hop frontier vs ce_score") | |
| fig.tight_layout() | |
| fig.savefig(OUT_DIR / "F_latest_bars.png", dpi=140) | |
| plt.close(fig) | |
| (OUT_DIR / "summary.json").write_text(json.dumps(summary, indent=2, default=str)) | |
| print(f"wrote plots -> {OUT_DIR}") | |
| for name, p in parsed.items(): | |
| last_ep = p["train"][-1][0] if p["train"] else "?" | |
| last_st = p["train"][-1][1] if p["train"] else "?" | |
| n_prom = len(p["promotes"]) | |
| print(f" {p['label']}: epoch={last_ep} stage={last_st} promotes={n_prom} evals={len(p['evals'])}") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--watch", type=int, default=0, help="replot every N seconds") | |
| args = ap.parse_args() | |
| if args.watch <= 0: | |
| plot_all() | |
| return | |
| while True: | |
| try: | |
| plot_all() | |
| except Exception as e: | |
| print("plot error:", e) | |
| time.sleep(args.watch) | |
| if __name__ == "__main__": | |
| main() | |