File size: 7,364 Bytes
5952424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""Paired significance tests for ALL three experiments.

Everything is paired on the unit that was actually replicated -- (fold, seed) for parsing,
fold for documentary, seed for meter -- so each comparison controls for split difficulty
and only the system varies. The headline p-value is an EXACT sign-flip permutation test
(no distributional assumption); note its floor is 2/2^n, so p=0.002 at n=10 is the
smallest attainable value, not a coincidence. d_z is paired Cohen's d.
"""
from __future__ import annotations
import collections, glob, json, math, os, re, sys
import numpy as np

GCB=os.path.expandvars("$STOICHEIA_DATA")
REPO=os.path.expandvars("$STOICHEIA_ROOT")

def _ttest_rel_p(d):
    """Two-sided paired t-test p-value without scipy (survival fn of |t| via a
    continued-fraction incomplete beta -- keeps this script dependency-free so the
    released code needs only numpy)."""
    n=len(d); sd=d.std(ddof=1)
    if sd==0 or n<2: return float("nan")
    t=abs(d.mean())/(sd/np.sqrt(n)); v=n-1
    x=v/(v+t*t)
    # regularised incomplete beta I_x(v/2, 1/2) via Lentz continued fraction
    a,b=v/2.0,0.5
    lbeta=(math.lgamma(a)+math.lgamma(b)-math.lgamma(a+b))
    def betacf(a,b,x,itmax=300,eps=3e-16):
        qab,qap,qam=a+b,a+1.0,a-1.0
        c=1.0; d_=1.0-qab*x/qap
        if abs(d_)<1e-30: d_=1e-30
        d_=1.0/d_; h=d_
        for m in range(1,itmax+1):
            m2=2*m
            aa=m*(b-m)*x/((qam+m2)*(a+m2))
            d_=1.0+aa*d_;  c=1.0+aa/c
            if abs(d_)<1e-30: d_=1e-30
            if abs(c)<1e-30: c=1e-30
            d_=1.0/d_; h*=d_*c
            aa=-(a+m)*(qab+m)*x/((a+m2)*(qap+m2))
            d_=1.0+aa*d_;  c=1.0+aa/c
            if abs(d_)<1e-30: d_=1e-30
            if abs(c)<1e-30: c=1e-30
            d_=1.0/d_; delta=d_*c; h*=delta
            if abs(delta-1.0)<eps: break
        return h
    if x < (a+1.0)/(a+b+2.0):
        ib=np.exp(a*np.log(x)+b*np.log(1-x)-lbeta)*betacf(a,b,x)/a
    else:
        ib=1.0-np.exp(b*np.log(1-x)+a*np.log(x)-lbeta)*betacf(b,a,1-x)/b
    return float(min(1.0, max(0.0, ib)))


def perm_p(diff):
    n=len(diff); obs=abs(diff.mean())
    if n<=20:
        c=t=0
        for m in range(1<<n):
            s=np.array([1 if m>>i&1 else -1 for i in range(n)])
            c+= abs((diff*s).mean())>=obs-1e-12; t+=1
        return c/t
    rng=np.random.default_rng(0)
    s=rng.choice([-1,1],size=(100000,n))
    return float((np.abs((diff*s).mean(1))>=obs-1e-12).mean())

def report(title, unit, a_name, a, b_name, b, higher_better=True):
    keys=sorted(set(a)&set(b))
    if len(keys)<3:
        print(f"  {a_name} vs {b_name}: only {len(keys)} paired {unit}s -- not testable yet"); return
    x=np.array([a[k] for k in keys]); y=np.array([b[k] for k in keys])
    d=(x-y) if higher_better else (y-x)
    p=perm_p(d); dz=d.mean()/(d.std(ddof=1)+1e-12)
    t=_ttest_rel_p(d)
    print(f"  {a_name} vs {b_name}: n={len(keys)} {unit}s  mean diff={d.mean():+.4f}  "
          f"perm p={p:.2e}  t p={t:.1e}  d_z={dz:.1f}")

def parsing():
    print("\nPARSING (LAS, paired by fold+seed; higher better)")
    S=f"{GCB}/parser_data/runs"; g=collections.defaultdict(dict)
    for d in sorted(glob.glob(f"{S}/joint_*_f*_s*")):
        f=os.path.join(d,"test_scores_greedy.json")
        if not os.path.exists(f): continue
        j=json.load(open(f))
        if j.get("las") is None: continue
        b=os.path.basename(d); m=re.search(r"_(f\d+_s\d+)$",b)
        g[re.sub(r"_f\d+_s\d+$","",b)][m.group(1)]=j["las"]
    ref=g.get("joint_docclean",{})
    for k in sorted(g):
        if k=="joint_docclean": continue
        # joint_tune_* are single-fold LR-sweep diagnostics (reported separately as the
        # baseline-tuning control), not systems to significance-test.
        if k.startswith("joint_tune_"): continue
        report("", "run", "ours", ref, k.replace("joint_",""), g[k])

def documentary():
    print("\nDOCUMENTARY (CER, paired by fold; LOWER better)")
    def load(ver):
        out={}
        for f in glob.glob(f"{REPO}/.scratch/evals/{ver}_t*_iphi_whole_unk.json"):
            tv=re.search(r"_(t\dv\d)_",os.path.basename(f)).group(1)
            out[tv]=json.load(open(f))["avg"]["CER"]
        return out
    v3=load("v3"); ri=load("v3_randinit"); v2=load("v2")
    if v3 and ri: report("", "fold", "v3", v3, "random-init", ri, higher_better=False)
    if v3 and v2: report("", "fold", "v3", v3, "v2 (superseded)", v2, higher_better=False)
    if not v3: print("  (v3 evals not written yet)")

def strict_ithaca():
    print("\nSTRICT vs ITHACA (top-1/top-20 exact-match, two-proportion z-test, n=3000 each side)")
    print("  (samples are IDENTICAL on both sides -- same frozen file -- so this unpaired")
    print("   test is conservative; a paired McNemar test would only be stronger)")
    INSC=os.path.expandvars("$STOICHEIA_DATA")
    import collections
    def merge(pat):
        a=collections.defaultdict(lambda: dict(n=0,t1=0.0,t20=0.0))
        for f in glob.glob(pat):
            try: d=json.load(open(f))
            except Exception: continue
            for r in d.get("per_L",[]):
                x=a[r["L"]]; x["n"]+=r["n"]; x["t1"]+=r["top1"]*r["n"]; x["t20"]+=r["top20"]*r["n"]
        n=sum(v["n"] for v in a.values())
        return (n, sum(v["t1"] for v in a.values()), sum(v["t20"] for v in a.values())) if n else None
    def ztest(k1,n1,k2,n2):
        p1,p2=k1/n1,k2/n2; p=(k1+k2)/(n1+n2)
        se=math.sqrt(p*(1-p)*(1/n1+1/n2))
        z=(p1-p2)/se
        # two-sided normal sf via erfc
        pv=math.erfc(abs(z)/math.sqrt(2))
        return z,pv
    ith=merge(f"{INSC}/strict_f3/ithaca_shard*.json")
    if not ith: print("  (ithaca results missing)"); return
    for tag,pat in (("ours old",f"{INSC}/strict_f3/ours_old.json"),
                    ("ours v3",f"{INSC}/strict_f3/ours_v3_shard*.json"),
                    ("ours v4",f"{INSC}/strict_f3/ours_v4_shard*.json")):
        m=merge(pat)
        if not m: continue
        for name,ki,ko in (("top-1",1,1),("top-20",2,2)):
            z,pv=ztest(m[ki],m[0],ith[ko],ith[0])
            print(f"  {tag:<9} vs Ithaca {name:<7}: {m[ki]/m[0]:.4f} vs {ith[ko]/ith[0]:.4f}"
                  f"   z={z:+.2f}  p={pv:.2e}")


def meter():
    print("\nMETER (macron balanced acc, paired by seed; higher better)")
    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
        seed=re.search(r"_s(\d)$",run); seed=seed.group(1) if seed else "0"
        arm="randinit" if "randinit" in run else "pretrained"
        rows[arm][seed]=json.loads(mt.group(1))["bal_acc"]
    if rows.get("pretrained") and rows.get("randinit"):
        report("", "seed", "pretrained", rows["pretrained"], "random-init", rows["randinit"])
    else: print("  (not enough scored runs yet)")

if __name__=="__main__":
    w=sys.argv[1] if len(sys.argv)>1 else "all"
    if w in ("all","parsing"): parsing()
    if w in ("all","documentary"): documentary(); strict_ithaca()
    if w in ("all","meter"): meter()
    print()