plot + summary generator
Browse files- make_report.py +173 -190
make_report.py
CHANGED
|
@@ -1,15 +1,10 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
Reads results/*_eval.json
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
results/summary.csv one row per run with budget + headline accuracies
|
| 9 |
-
results/REPORT_DRAFT.md tables filled from the numbers (prose written by
|
| 10 |
-
the experimenter afterwards)
|
| 11 |
-
|
| 12 |
-
Usage: python make_report.py --results-dir results [--hist-dir out]
|
| 13 |
"""
|
| 14 |
|
| 15 |
import argparse
|
|
@@ -18,218 +13,206 @@ import json
|
|
| 18 |
import os
|
| 19 |
from collections import defaultdict
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
import numpy as np
|
| 22 |
|
| 23 |
-
|
| 24 |
-
"fixed4x1": "fixed-4 (
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
-
"rec1xdynamic": "rec
|
| 28 |
-
"rec4x4": "rec
|
| 29 |
-
"fixed4-long": "fixed-4
|
| 30 |
}
|
| 31 |
|
| 32 |
|
| 33 |
-
def
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
return None
|
| 38 |
|
| 39 |
|
| 40 |
-
def
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
| 47 |
|
| 48 |
-
def acc_vs_k_plot(evals, out):
|
| 49 |
-
import matplotlib
|
| 50 |
-
matplotlib.use("Agg")
|
| 51 |
-
import matplotlib.pyplot as plt
|
| 52 |
|
|
|
|
| 53 |
fig, ax = plt.subplots(figsize=(7, 4.5))
|
| 54 |
-
|
| 55 |
-
for
|
| 56 |
-
|
| 57 |
-
if
|
| 58 |
continue
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
ax.
|
| 68 |
-
ax.
|
| 69 |
-
ax.set_xlabel("hop count k (
|
| 70 |
ax.set_ylabel("exact-match accuracy")
|
| 71 |
-
ax.set_title("OOD chain length (trained on k=1..8, fresh random f)")
|
| 72 |
ax.set_ylim(0, 1.02)
|
| 73 |
-
ax.
|
|
|
|
|
|
|
| 74 |
fig.tight_layout()
|
| 75 |
-
fig.savefig(
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
-
def acc_vs_iters_plot(evals, out):
|
| 79 |
-
import matplotlib
|
| 80 |
-
matplotlib.use("Agg")
|
| 81 |
-
import matplotlib.pyplot as plt
|
| 82 |
|
|
|
|
| 83 |
fig, ax = plt.subplots(figsize=(7, 4.5))
|
| 84 |
-
|
| 85 |
-
for
|
| 86 |
-
|
| 87 |
-
if
|
| 88 |
continue
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
ax.set_xlabel("inference-time unrolls R (trained R: fixed-4=1, rec models=4 or dyn 2..8)")
|
| 102 |
ax.set_ylabel("exact-match accuracy")
|
| 103 |
-
ax.
|
| 104 |
-
ax.
|
|
|
|
|
|
|
| 105 |
fig.tight_layout()
|
| 106 |
-
fig.savefig(
|
| 107 |
-
|
| 108 |
|
| 109 |
-
def acc_vs_state_plot(evals, out):
|
| 110 |
-
import matplotlib
|
| 111 |
-
matplotlib.use("Agg")
|
| 112 |
-
import matplotlib.pyplot as plt
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
if fam is None:
|
| 120 |
continue
|
| 121 |
-
|
| 122 |
-
for
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
ax.set_ylabel("exact-match accuracy")
|
| 138 |
-
ax.set_title("OOD state-space size")
|
| 139 |
-
ax.legend(fontsize=6)
|
| 140 |
fig.tight_layout()
|
| 141 |
-
fig.savefig(
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
-
def
|
| 145 |
-
import matplotlib
|
| 146 |
-
matplotlib.use("Agg")
|
| 147 |
-
import matplotlib.pyplot as plt
|
| 148 |
-
|
| 149 |
fig, ax = plt.subplots(figsize=(7, 4.5))
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
| 155 |
continue
|
| 156 |
-
|
| 157 |
-
for
|
| 158 |
-
|
| 159 |
-
for fam, per_step in by_fam.items():
|
| 160 |
-
steps = sorted(per_step)
|
| 161 |
-
mean = np.array([np.mean(per_step[s]) for s in steps])
|
| 162 |
-
ax.plot(steps, mean, label=FAMILIES[fam], lw=1.8)
|
| 163 |
ax.set_xlabel("training step")
|
| 164 |
-
ax.set_ylabel("
|
| 165 |
-
ax.set_title("
|
| 166 |
-
ax.legend(fontsize=
|
|
|
|
| 167 |
fig.tight_layout()
|
| 168 |
-
fig.savefig(
|
|
|
|
| 169 |
|
| 170 |
|
| 171 |
-
def
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
| 177 |
continue
|
| 178 |
-
|
| 179 |
-
idm =
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
"
|
| 193 |
-
"
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
"", "```csv", buf.getvalue().strip(), "```", ""]
|
| 217 |
-
with open(out, "w") as fh:
|
| 218 |
-
fh.write("\n".join(md))
|
| 219 |
|
| 220 |
|
| 221 |
if __name__ == "__main__":
|
| 222 |
-
|
| 223 |
-
ap.add_argument("--results-dir", default="results")
|
| 224 |
-
ap.add_argument("--hist-dir", default="out")
|
| 225 |
-
args = ap.parse_args()
|
| 226 |
-
os.makedirs("plots", exist_ok=True)
|
| 227 |
-
evals = load_evals(args.results_dir)
|
| 228 |
-
print(f"loaded {len(evals)} eval files")
|
| 229 |
-
acc_vs_k_plot(evals, "plots/acc_vs_k.png")
|
| 230 |
-
acc_vs_iters_plot(evals, "plots/acc_vs_iters.png")
|
| 231 |
-
acc_vs_state_plot(evals, "plots/acc_vs_state.png")
|
| 232 |
-
training_plot(evals, args.hist_dir, "plots/training.png")
|
| 233 |
-
rows = summary_csv(evals, "results/summary.csv")
|
| 234 |
-
draft_report(evals, rows, "results/REPORT_DRAFT.md")
|
| 235 |
-
print("wrote plots/ and results/summary.csv, results/REPORT_DRAFT.md")
|
|
|
|
| 1 |
+
"""Generate figures and the results tables from the evaluation JSONs.
|
| 2 |
+
|
| 3 |
+
Reads results/*_eval.json and results/*_history.json (downloaded from the Hub
|
| 4 |
+
repo), writes plots to results/plots/ and results/summary_table.md.
|
| 5 |
+
All numbers reported are empirical; no claims beyond the data.
|
| 6 |
+
|
| 7 |
+
Usage: python make_report.py [--results-dir results] [--out-dir results/plots]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
import argparse
|
|
|
|
| 13 |
import os
|
| 14 |
from collections import defaultdict
|
| 15 |
|
| 16 |
+
import matplotlib
|
| 17 |
+
matplotlib.use("Agg")
|
| 18 |
+
import matplotlib.pyplot as plt
|
| 19 |
import numpy as np
|
| 20 |
|
| 21 |
+
FAMILY = {
|
| 22 |
+
"fixed4x1": ("fixed-4 (matched FLOPs)", "#d62728"),
|
| 23 |
+
"rec1x4": ("rec 1x4 (matched FLOPs)", "#1f77b4"),
|
| 24 |
+
"rec2x2": ("rec 2x2 (matched FLOPs)", "#2ca02c"),
|
| 25 |
+
"rec1xdynamic": ("rec 1xdynamic R~U{2..8}", "#17becf"),
|
| 26 |
+
"rec4x4": ("rec 4x4 (iso-param with fixed-4)", "#9467bd"),
|
| 27 |
+
"fixed4-long": ("fixed-4 long (iso-FLOP with rec4x4)", "#ff7f0e"),
|
| 28 |
}
|
| 29 |
|
| 30 |
|
| 31 |
+
def load(results_dir):
|
| 32 |
+
runs = {}
|
| 33 |
+
for p in sorted(glob.glob(os.path.join(results_dir, "*_eval.json"))):
|
| 34 |
+
with open(p) as fh:
|
| 35 |
+
ev = json.load(fh)
|
| 36 |
+
name = ev["run_name"]
|
| 37 |
+
base = name.rsplit("-seed", 1)[0]
|
| 38 |
+
seed = int(name.rsplit("-seed", 1)[1]) if "-seed" in name else -1
|
| 39 |
+
runs.setdefault(base, {})[seed] = ev
|
| 40 |
+
return runs
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def mean_over_seeds(runs, base, get):
|
| 44 |
+
seeds = runs.get(base, {})
|
| 45 |
+
if not seeds:
|
| 46 |
+
return None, None, 0
|
| 47 |
+
vals = np.array([[get(ev) for ev in seeds[s].values()] for s in sorted(seeds)])
|
| 48 |
+
# get applied per eval dict -> we actually flatten manually below
|
| 49 |
return None
|
| 50 |
|
| 51 |
|
| 52 |
+
def stack(runs, base, key):
|
| 53 |
+
"""values across seeds for a dict-valued metric, e.g. acc_vs_k."""
|
| 54 |
+
seeds = sorted(runs.get(base, {}))
|
| 55 |
+
if not seeds:
|
| 56 |
+
return None, None
|
| 57 |
+
ks = list(runs[base][seeds[0]][key].keys())
|
| 58 |
+
arr = np.array([[runs[base][s][key][k] for k in ks] for s in seeds])
|
| 59 |
+
return ks, arr # arr shape [n_seeds, n_ks]
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
def plot_acc_vs_k(runs, out_dir):
|
| 63 |
fig, ax = plt.subplots(figsize=(7, 4.5))
|
| 64 |
+
rows = []
|
| 65 |
+
for base, (label, color) in FAMILY.items():
|
| 66 |
+
ks, arr = stack(runs, base, "acc_vs_k")
|
| 67 |
+
if arr is None:
|
| 68 |
continue
|
| 69 |
+
m, sd = arr.mean(0), arr.std(0)
|
| 70 |
+
x = [int(k) for k in ks]
|
| 71 |
+
ax.plot(x, m, label=label, color=color)
|
| 72 |
+
ax.fill_between(x, m - sd, m + sd, color=color, alpha=0.18)
|
| 73 |
+
rows.append((label, m))
|
| 74 |
+
ax.axhline(1 / 64, ls=":", c="gray", lw=1)
|
| 75 |
+
ax.text(45, 1 / 64 + 0.008, "chance (1/64)", fontsize=8, color="gray")
|
| 76 |
+
ax.set_xscale("log")
|
| 77 |
+
ax.set_xticks([1, 2, 4, 8, 16, 32, 64])
|
| 78 |
+
ax.set_xticklabels([1, 2, 4, 8, 16, 32, 64])
|
| 79 |
+
ax.set_xlabel("hop count k (chain length)")
|
| 80 |
ax.set_ylabel("exact-match accuracy")
|
|
|
|
| 81 |
ax.set_ylim(0, 1.02)
|
| 82 |
+
ax.set_title("Accuracy vs chain length (k), trained on k<=8, S=64")
|
| 83 |
+
ax.legend(fontsize=7, loc="upper right")
|
| 84 |
+
ax.grid(alpha=0.3)
|
| 85 |
fig.tight_layout()
|
| 86 |
+
fig.savefig(os.path.join(out_dir, "acc_vs_k.png"), dpi=150)
|
| 87 |
+
plt.close(fig)
|
| 88 |
+
return rows
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
def plot_acc_vs_iters(runs, out_dir):
|
| 92 |
fig, ax = plt.subplots(figsize=(7, 4.5))
|
| 93 |
+
colors = {8: "#1f77b4", 16: "#ff7f0e", 32: "#2ca02c", 64: "#d62728"}
|
| 94 |
+
for base in ("rec1x4", "rec1xdynamic", "rec4x4", "fixed4x1", "fixed4-long"):
|
| 95 |
+
ks, arr = stack(runs, base, "acc_vs_iters")
|
| 96 |
+
if arr is None:
|
| 97 |
continue
|
| 98 |
+
# arr[seed, R, k] flattened dict: acc_vs_iters is {R: {k: acc}}
|
| 99 |
+
Rs = sorted(int(r) for r in runs[base][0]["acc_vs_iters"])
|
| 100 |
+
for ki, k in enumerate(("8", "16", "32", "64")):
|
| 101 |
+
if k not in runs[base][0]["acc_vs_iters"]["1"]:
|
| 102 |
+
continue
|
| 103 |
+
m = np.array([[runs[base][s]["acc_vs_iters"][str(r)][k]
|
| 104 |
+
for r in Rs] for s in sorted(runs[base])]).mean(0)
|
| 105 |
+
label = f"{FAMILY.get(base, (base,))[0]} k={k}"
|
| 106 |
+
ax.plot(Rs, m, label=label,
|
| 107 |
+
color=colors[k], ls="--" if base.startswith("fixed") else "-",
|
| 108 |
+
alpha=0.9 if base in ("rec1x4", "rec4x4") else 0.5)
|
| 109 |
+
ax.set_xlabel("inference-time unroll count R")
|
|
|
|
| 110 |
ax.set_ylabel("exact-match accuracy")
|
| 111 |
+
ax.set_xticks(range(1, 17))
|
| 112 |
+
ax.set_title("Accuracy vs inference-time recurrence count (OOD iterations)")
|
| 113 |
+
ax.legend(fontsize=6, ncol=2)
|
| 114 |
+
ax.grid(alpha=0.3)
|
| 115 |
fig.tight_layout()
|
| 116 |
+
fig.savefig(os.path.join(out_dir, "acc_vs_iters.png"), dpi=150)
|
| 117 |
+
plt.close(fig)
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
def plot_state_size(runs, out_dir):
|
| 121 |
+
fig, axes = plt.subplots(1, 3, figsize=(12, 3.8), sharey=True)
|
| 122 |
+
for ax, base in zip(axes, ("fixed4x1", "rec1x4", "rec1xdynamic")):
|
| 123 |
+
found = runs.get(base)
|
| 124 |
+
if not found:
|
|
|
|
| 125 |
continue
|
| 126 |
+
ev = found[sorted(found)[0]]
|
| 127 |
+
for Sp, style in (("16", "--"), ("32", "-."), ("64", "-")):
|
| 128 |
+
if Sp not in ev["acc_vs_state_size"]:
|
| 129 |
+
continue
|
| 130 |
+
d = ev["acc_vs_state_size"][Sp]
|
| 131 |
+
ks = [int(k) for k in d]
|
| 132 |
+
ax.plot(ks, [d[k] for k in d], style, label=f"S'={Sp}")
|
| 133 |
+
ax.set_xscale("log")
|
| 134 |
+
ax.set_xticks([1, 2, 4, 8, 16, 32, 64])
|
| 135 |
+
ax.set_xticklabels([1, 2, 4, 8, 16, 32, 64])
|
| 136 |
+
ax.set_xlabel("k")
|
| 137 |
+
ax.set_title(FAMILY.get(base, (base,))[0], fontsize=9)
|
| 138 |
+
ax.grid(alpha=0.3)
|
| 139 |
+
axes[0].set_ylabel("exact-match accuracy")
|
| 140 |
+
axes[0].legend(fontsize=8)
|
| 141 |
+
fig.suptitle("Accuracy vs state-space size (seed 0 shown)", fontsize=10)
|
|
|
|
|
|
|
|
|
|
| 142 |
fig.tight_layout()
|
| 143 |
+
fig.savefig(os.path.join(out_dir, "acc_vs_state_size.png"), dpi=150)
|
| 144 |
+
plt.close(fig)
|
| 145 |
|
| 146 |
|
| 147 |
+
def plot_training(runs, hist_dir, out_dir):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
fig, ax = plt.subplots(figsize=(7, 4.5))
|
| 149 |
+
for p in sorted(glob.glob(os.path.join(hist_dir, "*_history.json"))):
|
| 150 |
+
with open(p) as fh:
|
| 151 |
+
h = json.load(fh)
|
| 152 |
+
name = os.path.basename(p).replace("_history.json", "")
|
| 153 |
+
base = name.rsplit("-seed", 1)[0]
|
| 154 |
+
if base not in FAMILY:
|
| 155 |
continue
|
| 156 |
+
steps = [r["step"] for r in h["id_acc"]]
|
| 157 |
+
ax.plot(steps, [r["mean"] for r in h["id_acc"]],
|
| 158 |
+
label=name, color=FAMILY[base][1], alpha=0.7)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
ax.set_xlabel("training step")
|
| 160 |
+
ax.set_ylabel("in-distribution acc (k=1..8 mean)")
|
| 161 |
+
ax.set_title("In-distribution accuracy during training")
|
| 162 |
+
ax.legend(fontsize=6)
|
| 163 |
+
ax.grid(alpha=0.3)
|
| 164 |
fig.tight_layout()
|
| 165 |
+
fig.savefig(os.path.join(out_dir, "training_id_acc.png"), dpi=150)
|
| 166 |
+
plt.close(fig)
|
| 167 |
|
| 168 |
|
| 169 |
+
def summary_table(runs, out_dir):
|
| 170 |
+
lines = ["| model | params | train FLOPs (est.) | seeds | in-dist acc "
|
| 171 |
+
"k=1..8 | acc k=16 | acc k=32 | acc k=64 | best R@k=64 |",
|
| 172 |
+
"|---|---|---|---|---|---|---|---|---|"]
|
| 173 |
+
for base, (label, _) in FAMILY.items():
|
| 174 |
+
seeds = sorted(runs.get(base, {}))
|
| 175 |
+
if not seeds:
|
| 176 |
continue
|
| 177 |
+
ev0 = runs[base][seeds[0]]
|
| 178 |
+
idm = np.mean([[ev["acc_vs_k"][str(k)] for k in range(1, 9)]
|
| 179 |
+
for s in seeds for ev in [runs[base][s]]])
|
| 180 |
+
ood = {k: np.mean([runs[base][s]["acc_vs_k"][str(k)] for s in seeds])
|
| 181 |
+
for k in (16, 32, 64)}
|
| 182 |
+
best_r = None
|
| 183 |
+
try:
|
| 184 |
+
per_r = {int(r): np.mean([runs[base][s]["acc_vs_iters"][r]["64"]
|
| 185 |
+
for s in seeds]) for r in
|
| 186 |
+
ev0["acc_vs_iters"]}
|
| 187 |
+
best_r = max(per_r, key=per_r.get)
|
| 188 |
+
except Exception:
|
| 189 |
+
pass
|
| 190 |
+
lines.append(
|
| 191 |
+
f"| {label} | {ev0['budget']['params_total']:,} | "
|
| 192 |
+
f"{ev0['budget']['train_flops_total']:.2e} | {len(seeds)} | "
|
| 193 |
+
f"{idm:.3f} | {ood[16]:.3f} | {ood[32]:.3f} | {ood[64]:.3f} | "
|
| 194 |
+
f"R={best_r} |")
|
| 195 |
+
txt = "\n".join(lines)
|
| 196 |
+
with open(os.path.join(out_dir, "..", "summary_table.md"), "w") as fh:
|
| 197 |
+
fh.write(txt + "\n")
|
| 198 |
+
print(txt)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def main():
|
| 202 |
+
ap = argparse.ArgumentParser()
|
| 203 |
+
ap.add_argument("--results-dir", default="results")
|
| 204 |
+
ap.add_argument("--out-dir", default="results/plots")
|
| 205 |
+
a = ap.parse_args()
|
| 206 |
+
os.makedirs(a.out_dir, exist_ok=True)
|
| 207 |
+
runs = load(a.results_dir)
|
| 208 |
+
print("found families:", {b: list(s) for b, s in runs.items()})
|
| 209 |
+
plot_acc_vs_k(runs, a.out_dir)
|
| 210 |
+
plot_acc_vs_iters(runs, a.out_dir)
|
| 211 |
+
plot_state_size(runs, a.out_dir)
|
| 212 |
+
plot_training(runs, a.results_dir, a.out_dir)
|
| 213 |
+
summary_table(runs, a.out_dir)
|
| 214 |
+
print("plots written to", a.out_dir)
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
|
| 217 |
if __name__ == "__main__":
|
| 218 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|