| """Regenerate every paper table from results already on disk. Reads only; safe to run |
| at any time and shows partial state (n=) while jobs are still finishing. |
| |
| python3 analysis/paper_tables.py # all three experiments |
| python3 analysis/paper_tables.py parsing # documentary | parsing | meter |
| """ |
| from __future__ import annotations |
| import collections, glob, json, os, re, sys |
| import numpy as np |
|
|
| GCB=os.path.expandvars("$STOICHEIA_DATA") |
| INSC=os.path.expandvars("$STOICHEIA_DATA") |
| REPO=os.path.expandvars("$STOICHEIA_ROOT") |
|
|
| def _agg(v): return (np.mean(v), np.std(v), len(v)) if v else (float("nan"), 0.0, 0) |
|
|
| def parsing(): |
| print("\n"+"="*78); print("EXPERIMENT 2 -- PARSING (joint tagger+parser, OGA 5 folds x 2 seeds)"); print("="*78) |
| g=collections.defaultdict(list) |
| for d in sorted(glob.glob(f"{GCB}/parser_data/runs/joint_*_f*_s*")): |
| p=os.path.join(d,"test_scores_greedy.json") |
| if not os.path.exists(p): continue |
| j=json.load(open(p)) |
| if j.get("las") is None: continue |
| tag=re.sub(r"_f\d+_s\d+$","",os.path.basename(d)) |
| |
| |
| if tag.startswith("joint_tune_"): continue |
| g[tag].append((j["las"], j["uas"], j["xpos_exact"], j["upos"])) |
| if not g: print(" (no results yet)"); return |
| print(f"{'system':<24}{'n':>3} {'LAS':>15} {'UAS':>8} {'XPOS':>8} {'UPOS':>8}") |
| for tag,v in sorted(g.items(), key=lambda kv:-np.mean([x[0] for x in kv[1]])): |
| m,s,n=_agg([x[0] for x in v]) |
| star=" <- ours" if tag=="joint_docclean" else (" <- pretraining ablation" if tag=="joint_randinit" else "") |
| print(f"{tag.replace('joint_',''):<24}{n:>3} {m:.4f}+-{s:.4f} " |
| f"{np.mean([x[1] for x in v]):.4f} {np.mean([x[2] for x in v]):.4f} " |
| f"{np.mean([x[3] for x in v]):.4f}{star}") |
| if g.get("joint_docclean") and g.get("joint_randinit"): |
| a=np.mean([x[0] for x in g["joint_docclean"]]); b=np.mean([x[0] for x in g["joint_randinit"]]) |
| print(f"\n PRETRAINING EFFECT: {100*(a-b):+.1f} LAS points") |
| |
| |
| if g.get("joint_docclean"): |
| a=np.mean([x[0] for x in g["joint_docclean"]]) |
| cands={k:np.mean([x[0] for x in v]) for k,v in g.items() |
| if k not in ("joint_docclean","joint_docclean_long","joint_randinit", |
| "joint_randinit_long") and len(v)>=8} |
| if cands: |
| bk=max(cands,key=cands.get) |
| print(f" vs strongest baseline ({bk.replace('joint_','')}, n>=8): " |
| f"{100*(a-cands[bk]):+.1f} LAS points") |
| if g.get("joint_docclean") and g.get("joint_randinit_long"): |
| a=np.mean([x[0] for x in g["joint_docclean"]]) |
| b=np.mean([x[0] for x in g["joint_randinit_long"]]) |
| print(f" PRETRAINING EFFECT (converged random-init): {100*(a-b):+.1f} LAS points") |
|
|
| def lr_control(): |
| print("\n" + "-"*78) |
| print("BASELINE-TUNING CONTROL (fold 0 / seed 0): is the shared recipe unfair to the") |
| print("subword baselines? lr_enc swept for them AND for us.") |
| print("-"*78) |
| S=f"{GCB}/parser_data/runs" |
| for enc,label in (("docclean","ours"),("philberta","PhilBerta"),("greberta","GreBerta")): |
| row=[] |
| b=os.path.join(S,f"joint_{enc}_f0_s0","test_scores_greedy.json") |
| if os.path.exists(b): row.append(("1e-04*", json.load(open(b))["las"])) |
| for d in sorted(glob.glob(f"{S}/joint_tune_{enc}_lr*_f0_s0")): |
| q=os.path.join(d,"test_scores_greedy.json") |
| if not os.path.exists(q): continue |
| v=re.search(r"_lr([0-9.e-]+)_f0",os.path.basename(d)).group(1) |
| row.append((f"{float(v):.0e}", json.load(open(q))["las"])) |
| if not row: continue |
| row.sort(key=lambda r: float(r[0].rstrip("*"))) |
| best=max(row,key=lambda r:r[1]) |
| print(f" {label:<10} " + " ".join(f"{k}:{v:.4f}" for k,v in row) |
| + f" -> best {best[0]} ({best[1]:.4f})") |
| print(" (* = shared recipe used for the main table)") |
|
|
|
|
| def meter(): |
| print("\n"+"="*78); print("EXPERIMENT 3 -- METER (macronization, Norma test, git source, n=2660)"); print("="*78) |
| rows=collections.defaultdict(dict) |
| for f in sorted(glob.glob(f"{REPO}/logs/eval-*.out")): |
| txt=open(f, errors="ignore").read() |
| if "--norma" not in txt: continue |
| mm=re.search(r"model=(\S+)", txt); mt=re.search(r"macron test: (\{.*?\})", txt) |
| if not (mm and mt): continue |
| run=mm.group(1).split("/runs/")[-1].split("/")[0] |
| if not run.startswith("meter_mac_v2"): continue |
| d=json.loads(mt.group(1)) |
| rows["random-init" if "randinit" in run else "pretrained"][run]=( |
| d["bal_acc"], d["acc"], d["macro_bal_acc"]) |
| if not rows: print(" (no results yet)"); return |
| print(f"{'arm':<14}{'n':>3} {'balanced acc':>16} {'acc':>8} {'macro-bal':>10}") |
| means={} |
| for arm in ("pretrained","random-init"): |
| v=list(rows.get(arm,{}).values()) |
| if not v: continue |
| m,s,n=_agg([x[0] for x in v]); means[arm]=m |
| print(f"{arm:<14}{n:>3} {m:.4f}+-{s:.4f} {np.mean([x[1] for x in v]):.4f} " |
| f"{np.mean([x[2] for x in v]):.4f}") |
| if len(means)==2: |
| print(f"\n PRETRAINING EFFECT: {100*(means['pretrained']-means['random-init']):+.1f} points balanced accuracy") |
|
|
| def documentary(): |
| print("\n"+"="*78); print("EXPERIMENT 1 -- DOCUMENTARY RECONSTRUCTION (inscriptions + papyri)"); print("="*78) |
| prot={"iphi_whole_unk":"inscriptions, whole-doc (no meta)", |
| "iphi_clean_unk":"inscriptions, clean ctx (no meta)", |
| "iphi_whole_meta":"inscriptions, whole-doc (+meta)", |
| "pap_whole_unk":"papyri, whole-doc (no meta)"} |
| for ver,label in (("v4","v4 (6-tier mix, pilot)"), |
| ("v3","v3 (pretrained)"), |
| ("v3_randinit","v3 RANDOM-INIT (pretraining ablation)"), |
| ("v2","v2 (undertrained, superseded)")): |
| agg=collections.defaultdict(list) |
| for f in glob.glob(f"{REPO}/.scratch/evals/{ver}_t*.json"): |
| p=os.path.basename(f)[len(ver)+1:-5][5:] |
| if p not in prot: continue |
| d=json.load(open(f)); agg[p].append((d["avg"]["CER"], d["avg"]["top1"], d["avg"]["top20"])) |
| if not agg: continue |
| print(f"\n -- {label} --") |
| for k,lab in prot.items(): |
| if k not in agg: continue |
| m,s,n=_agg([x[0] for x in agg[k]]) |
| print(f" {lab:<36} n={n:>2} CER={m:.4f}+-{s:.4f} " |
| f"top1={np.mean([x[1] for x in agg[k]]):.4f} top20={np.mean([x[2] for x in agg[k]]):.4f}") |
| print("\n -- STRICT Ithaca-comparable (PHI digit-3 test, whole inscriptions,") |
| print(" real lacunae in context, SPACES COUNTED, beam 20, no metadata) --") |
| def merge(files): |
| a=collections.defaultdict(lambda: dict(n=0,cer=0.0,cl=0.0,t1=0.0,t20=0.0)) |
| for f in files: |
| try: d=json.load(open(f)) |
| except Exception: continue |
| for r in d.get("per_L",[]): |
| x=a[r["L"]]; n=r["n"]; x["n"]+=n; x["cer"]+=r["CER"]*n |
| x["cl"]+=(r.get("CER_letters") or 0)*n; x["t1"]+=r["top1"]*n; x["t20"]+=r["top20"]*n |
| return a |
| for tag,pat in (("Ithaca (DeepMind release)", f"{INSC}/strict_f3/ithaca_shard*.json"), |
| ("ours old (both_ft_docclean)", f"{INSC}/strict_f3/ours_old.json"), |
| ("ours v2 (undertrained)", f"{INSC}/strict_f3/ours_v2_shard*.json"), |
| ("ours v3", f"{INSC}/strict_f3/ours_v3_shard*.json"), |
| ("ours v4 (pilot)", f"{INSC}/strict_f3/ours_v4_shard*.json")): |
| fs=sorted(glob.glob(pat)) |
| if not fs: continue |
| a=merge(fs) |
| if not a: continue |
| Ls=sorted(a) |
| cer=np.mean([a[L]["cer"]/max(a[L]["n"],1) for L in Ls]) |
| cl=np.mean([a[L]["cl"]/max(a[L]["n"],1) for L in Ls]) |
| t1=np.mean([a[L]["t1"]/max(a[L]["n"],1) for L in Ls]) |
| t20=np.mean([a[L]["t20"]/max(a[L]["n"],1) for L in Ls]) |
| ntot=sum(a[L]["n"] for L in Ls) |
| extra=f" CER_let={cl:.4f}" if cl>0 else "" |
| print(f" {tag:<30} CER={cer:.4f}{extra} top1={t1:.4f} top20={t20:.4f} (n={ntot})") |
|
|
| def scanner(): |
| print("\n" + "="*78) |
| print("EXPERIMENT 3b -- SCANNER (Norma syllabify test, joint model, git source)") |
| print("="*78) |
| rows=collections.defaultdict(dict) |
| for f in sorted(glob.glob(f"{REPO}/logs/eval-*.out")): |
| t=open(f,errors="ignore").read() |
| mm=re.search(r"model=(\S+)",t); ms=re.search(r"norma syllabify \(test\) ===\n (\{.*?\})",t) |
| if not(mm and ms): continue |
| run=mm.group(1).split("/runs/")[-1].split("/")[0] |
| |
| if not run.startswith("meter_joint") or run=="meter_joint_pilot": continue |
| d=json.loads(ms.group(1)) |
| arm="random-init" if "randinit" in run else "pretrained" |
| rows[arm][run]=(d["bal_acc"], d["acc"], d["boundary_f1"]) |
| if not rows: print(" (no results yet)"); return |
| print(f"{'arm':<14}{'n':>3} {'balanced acc':>16} {'acc':>8} {'boundary F1':>12}") |
| means={} |
| for arm in ("pretrained","random-init"): |
| v=list(rows.get(arm,{}).values()) |
| if not v: continue |
| m,sd,n=_agg([x[0] for x in v]); means[arm]=m |
| print(f"{arm:<14}{n:>3} {m:.4f}+-{sd:.4f} {np.mean([x[1] for x in v]):.4f} " |
| f"{np.mean([x[2] for x in v]):.4f}") |
| if len(means)==2: |
| print(f"\n PRETRAINING EFFECT: {100*(means['pretrained']-means['random-init']):+.1f} pts balanced accuracy") |
|
|
|
|
| if __name__=="__main__": |
| w=sys.argv[1] if len(sys.argv)>1 else "all" |
| if w in ("all","documentary"): documentary() |
| if w in ("all","parsing"): parsing(); lr_control() |
| if w in ("all","meter"): meter(); scanner() |
| print() |
|
|