Stoicheia-code / analysis /paper_tables.py
Ericu950's picture
Stoicheia: training and evaluation code
5952424 verified
Raw
History Blame Contribute Delete
10.2 kB
"""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))
# joint_tune_* are single-fold LR-sweep diagnostics -- reported by lr_control(),
# not systems for the main table (an n=1 row next to n=10 rows invites misreading)
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")
# Headline margin must be against the STRONGEST baseline at ITS OWN tuned LR, not
# against a baseline handicapped by our shared recipe (see lr_control below).
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]
# joint_pilot is an older, different config -- not part of the seed-matched ablation
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()