eac123 commited on
Commit
9783cf6
·
verified ·
1 Parent(s): 78479a6

plot + summary generator

Browse files
Files changed (1) hide show
  1. make_report.py +173 -190
make_report.py CHANGED
@@ -1,15 +1,10 @@
1
- """Aggregate eval JSONs into plots, a summary CSV, and a drafted report.
2
-
3
- Reads results/*_eval.json (and out/*_history.json if present), writes:
4
- plots/acc_vs_k.png accuracy vs hop count k (1..64) per family
5
- plots/acc_vs_iters.png accuracy vs inference-time unroll count
6
- plots/acc_vs_state.png accuracy vs state-space size
7
- plots/training.png in-distribution accuracy vs training step
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
- FAMILIES = {
24
- "fixed4x1": "fixed-4 (4P)",
25
- "rec2x2": "rec-2x2 (2P)",
26
- "rec1x4": "rec-1x4 (1P)",
27
- "rec1xdynamic": "rec-1xdyn (1P)",
28
- "rec4x4": "rec-4x4 (iso-param, 4x flops/step)",
29
- "fixed4-long": "fixed-4-long (iso-flop, 4x steps)",
30
  }
31
 
32
 
33
- def family_of(name):
34
- for fam in FAMILIES:
35
- if name.startswith(fam):
36
- return fam
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  return None
38
 
39
 
40
- def load_evals(results_dir):
41
- evals = []
42
- for path in sorted(glob.glob(os.path.join(results_dir, "*_eval.json"))):
43
- with open(path) as fh:
44
- evals.append(json.load(fh))
45
- return evals
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
- by_fam = defaultdict(lambda: defaultdict(list))
55
- for ev in evals:
56
- fam = family_of(ev["run_name"])
57
- if fam is None:
58
  continue
59
- for k, v in ev["acc_vs_k"].items():
60
- by_fam[fam][int(k)].append(v)
61
- for fam, per_k in by_fam.items():
62
- ks = sorted(per_k)
63
- mean = np.array([np.mean(per_k[k]) for k in ks])
64
- std = np.array([np.std(per_k[k]) for k in ks])
65
- ax.plot(ks, mean, label=FAMILIES[fam], lw=1.8)
66
- ax.fill_between(ks, mean - std, mean + std, alpha=0.15)
67
- ax.axhline(1 / 64, color="gray", ls=":", lw=1, label="chance (1/64)")
68
- ax.axvline(8, color="gray", ls="--", lw=1, label="max train k")
69
- ax.set_xlabel("hop count k (f applied k times)")
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.legend(fontsize=7)
 
 
74
  fig.tight_layout()
75
- fig.savefig(out, dpi=150)
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
- by_fam = defaultdict(lambda: defaultdict(list))
85
- for ev in evals:
86
- fam = family_of(ev["run_name"])
87
- if fam is None:
88
  continue
89
- for r, per_k in ev["acc_vs_iters"].items():
90
- for k, v in per_k.items():
91
- if int(k) in (8, 16, 32, 64):
92
- by_fam[(fam, int(k))][int(r)].append(v)
93
- palette = {}
94
- for (fam, k), per_r in sorted(by_fam.items()):
95
- rs = sorted(per_r)
96
- mean = np.array([np.mean(per_r[r]) for r in rs])
97
- lbl = f"{FAMILIES[fam].split(' (')[0]}, k={k}"
98
- color = palette.setdefault(fam, None)
99
- line, = ax.plot(rs, mean, lw=1.4, label=lbl)
100
- palette[fam] = line.get_color()
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.set_title("OOD inference-time recurrence (more/fewer unrolls than trained)")
104
- ax.legend(fontsize=6)
 
 
105
  fig.tight_layout()
106
- fig.savefig(out, dpi=150)
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
- fig, ax = plt.subplots(figsize=(6, 4.5))
115
- markers = {8: "o", 32: "s"}
116
- by_fam = defaultdict(list)
117
- for ev in evals:
118
- fam = family_of(ev["run_name"])
119
- if fam is None:
120
  continue
121
- row = {}
122
- for Sstr, per_k in ev["acc_vs_state_size"].items():
123
- row[int(Sstr)] = {int(k): v for k, v in per_k.items()}
124
- by_fam[fam].append(row)
125
- for fam, rows in by_fam.items():
126
- for k, mk in markers.items():
127
- xs, ys = [], []
128
- for row in rows:
129
- for S, per_k in sorted(row.items()):
130
- if k in per_k:
131
- xs.append(S)
132
- ys.append(per_k[k])
133
- if xs:
134
- ax.plot(xs, ys, mk + "-", label=f"{FAMILIES[fam].split(' (')[0]} k={k}",
135
- alpha=0.8, lw=1.3, markersize=4)
136
- ax.set_xlabel("state-space size S' (trained S=64)")
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(out, dpi=150)
 
142
 
143
 
144
- def training_plot(evals, hist_dir, out):
145
- import matplotlib
146
- matplotlib.use("Agg")
147
- import matplotlib.pyplot as plt
148
-
149
  fig, ax = plt.subplots(figsize=(7, 4.5))
150
- by_fam = defaultdict(lambda: defaultdict(list))
151
- for path in sorted(glob.glob(os.path.join(hist_dir, "*_history.json"))):
152
- name = os.path.basename(path).replace("_history.json", "")
153
- fam = family_of(name)
154
- if fam is None:
 
155
  continue
156
- h = json.load(open(path))
157
- for rec in h["id_acc"]:
158
- by_fam[fam][rec["step"]].append(rec["mean"])
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("mean id accuracy (k=1..8)")
165
- ax.set_title("Training progress")
166
- ax.legend(fontsize=7)
 
167
  fig.tight_layout()
168
- fig.savefig(out, dpi=150)
 
169
 
170
 
171
- def summary_csv(evals, out):
172
- import csv
173
- rows = []
174
- for ev in evals:
175
- fam = family_of(ev["run_name"])
176
- if fam is None:
 
177
  continue
178
- ak = {int(k): v for k, v in ev["acc_vs_k"].items()}
179
- idm = float(np.mean([ak[k] for k in range(1, 9)]))
180
- best_r, best64 = max(
181
- ((int(r), per_k["64"]) for r, per_k in ev["acc_vs_iters"].items()),
182
- key=lambda t: t[1])
183
- rows.append({
184
- "run_name": ev["run_name"], "family": fam,
185
- "params_total": ev["budget"]["params_total"],
186
- "train_flops_total": ev["budget"]["train_flops_total"],
187
- "id_acc_k1_8": round(idm, 4),
188
- "acc_k16": round(ak.get(16, float("nan")), 4),
189
- "acc_k32": round(ak.get(32, float("nan")), 4),
190
- "acc_k64": round(ak.get(64, float("nan")), 4),
191
- "best_acc_k64_over_R": round(best64, 4),
192
- "best_R_for_k64": best_r,
193
- "state16_acc_k32": round(
194
- ev["acc_vs_state_size"].get("16", {}).get("32", float("nan")), 4),
195
- })
196
- with open(out, "w", newline="") as fh:
197
- w = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
198
- w.writeheader()
199
- w.writerows(rows)
200
- return rows
201
-
202
-
203
- def draft_report(evals, rows, out):
204
- import csv as _csv
205
- from io import StringIO
206
- buf = StringIO()
207
- w = _csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
208
- w.writeheader()
209
- w.writerows(rows)
210
- md = ["# Results tables (auto-generated draft)",
211
- "",
212
- "Numbers come from `results/*_eval.json`; interpretation and the",
213
- "empirical-vs-theoretical distinction are written up in README.md.",
214
- "",
215
- "## Summary table",
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
- ap = argparse.ArgumentParser()
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()