Skip to content

Commit dfc8e3d

Browse files
mstrathmanclaude
andcommitted
bench: adopt out-of-fold teacher labeling; report that it does not lift here
Adopts the stratified OOF soft-labeling fix from Tanna et al., 'Pocket Foundation Models' (arXiv:2605.18654): in-context teachers leak labels when scoring rows in their own context, collapsing soft targets toward one-hot. New shared oof_proba() helper; the permissive campaign gains a soft-oof column for TabICL with a head-to-head win count. The honest result: on this suite the fix does not lift accuracy (17/38 wins, median delta 0.000). The results doc states why the two findings coexist: 1500-row datasets make each fold-fit teacher weaker, their gains are AUC where soft structure matters while this table is accuracy, and their pipeline adds temperature scaling ours does not. The distillation guide teaches the trap with the same measured framing: it matters more with larger data and probability metrics, less for small-data argmax. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent bcf5503 commit dfc8e3d

5 files changed

Lines changed: 209 additions & 105 deletions

File tree

‎benchmarks/permissive_arena.py‎

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,29 @@ def run_mitra(Xtr, ytr, Xte, task):
103103
RUNNERS = {"tabicl": run_tabicl, "mitra": run_mitra}
104104

105105

106+
OOF_TEACHERS = {"tabicl"} # mitra via autogluon is too slow for 5 fold fits
107+
108+
106109
def run_teacher(name, Xtr, ytr, Xte, task, teacher):
107110
os.makedirs(os.path.join(CACHE, teacher), exist_ok=True)
108111
pkl = os.path.join(CACHE, teacher, f"{name}.pkl")
112+
out = None
109113
if os.path.exists(pkl):
110114
with open(pkl, "rb") as f:
111-
return pickle.load(f)
112-
out = RUNNERS[teacher](Xtr, ytr, Xte, task)
115+
out = pickle.load(f)
116+
if out is None:
117+
out = RUNNERS[teacher](Xtr, ytr, Xte, task)
118+
# out-of-fold soft labels (arXiv:2605.18654): in-context teachers
119+
# leak labels on their own context rows, collapsing in-context soft
120+
# targets toward one-hot. Computed lazily so cached runs upgrade.
121+
if (task == "cls" and teacher in OOF_TEACHERS and out is not None
122+
and "proba_tr_oof" not in out):
123+
if teacher == "tabicl":
124+
from tabicl import TabICLClassifier
125+
make = lambda: TabICLClassifier(device=DEVICE) # noqa: E731
126+
proba, classes = TA.oof_proba(make, Xtr, ytr)
127+
out["proba_tr_oof"] = proba
128+
out["classes_oof"] = classes
113129
with open(pkl, "wb") as f:
114130
pickle.dump(out, f)
115131
return out
@@ -139,6 +155,11 @@ def one_dataset(name, X, y, task):
139155
Xtr, ytr, np.asarray(t["proba_tr"]),
140156
[str(c) for c in t["classes"]], Xte, kind="gbt")
141157
row[f"gbt<-{teacher} soft"] = TA.score(yte, preds, task)
158+
if task == "cls" and t.get("proba_tr_oof") is not None:
159+
preds, blob, hold = TA.run_ours_distill_soft(
160+
Xtr, ytr, np.asarray(t["proba_tr_oof"]),
161+
t["classes_oof"], Xte, kind="gbt")
162+
row[f"gbt<-{teacher} soft-oof"] = TA.score(yte, preds, task)
142163
return row
143164

144165

@@ -150,7 +171,10 @@ def main():
150171
if ln.strip()}
151172
for name, X, y, task in TA.load_datasets():
152173
prev = done.get(name)
153-
if prev is not None and all(t in prev for t in TEACHERS):
174+
want_oof = task == "cls" and any(t in OOF_TEACHERS for t in TEACHERS)
175+
oof_done = not want_oof or any(
176+
k.endswith("soft-oof") for k in (prev or {}))
177+
if prev is not None and all(t in prev for t in TEACHERS) and oof_done:
154178
print(f"cached {name}")
155179
continue
156180
t0 = time.time()
@@ -196,11 +220,12 @@ def better(a, b, task):
196220
f"Device: {DEVICE}. First calls include one-time weight download.",
197221
"",
198222
"| dataset | task | xgboost | TabPFN-2 | TabICL | Mitra |"
199-
" gbt<-TabICL | soft<-TabICL | gbt<-Mitra | s/call (TabICL) |",
200-
"|---|---|---|---|---|---|---|---|---|---|",
223+
" gbt<-TabICL | soft<-TabICL | soft-oof<-TabICL | gbt<-Mitra |"
224+
" s/call (TabICL) |",
225+
"|---|---|---|---|---|---|---|---|---|---|---|",
201226
]
202227
wins = {"ticl_vs_xgb": 0, "ticl_vs_pfn2": 0, "mitra_vs_xgb": 0,
203-
"dticl_vs_xgb": 0}
228+
"dticl_vs_xgb": 0, "oof_vs_soft": 0}
204229
n = {k: 0 for k in wins}
205230
for name, r in rows.items():
206231
fr, pr = full.get(name, {}), pfn.get(name, {})
@@ -213,12 +238,15 @@ def fmt(v):
213238
f"| {name} | {r['task']} | {fmt(xgb)} | {fmt(pfn2)} |"
214239
f" {fmt(r.get('tabicl'))} | {fmt(r.get('mitra'))} |"
215240
f" {fmt(r.get('gbt<-tabicl'))} | {fmt(r.get('gbt<-tabicl soft'))} |"
241+
f" {fmt(r.get('gbt<-tabicl soft-oof'))} |"
216242
f" {fmt(r.get('gbt<-mitra'))} | {r.get('tabicl_s', '-')} |")
217243
t = r["task"]
218244
for key, a, b in (("ticl_vs_xgb", r.get("tabicl"), xgb),
219245
("ticl_vs_pfn2", r.get("tabicl"), pfn2),
220246
("mitra_vs_xgb", r.get("mitra"), xgb),
221-
("dticl_vs_xgb", r.get("gbt<-tabicl"), xgb)):
247+
("dticl_vs_xgb", r.get("gbt<-tabicl"), xgb),
248+
("oof_vs_soft", r.get("gbt<-tabicl soft-oof"),
249+
r.get("gbt<-tabicl soft"))):
222250
if a is not None and b is not None:
223251
n[key] += 1
224252
wins[key] += better(a, b, t)
@@ -227,8 +255,27 @@ def fmt(v):
227255
for k, label in (("ticl_vs_xgb", "TabICL beats xgboost"),
228256
("ticl_vs_pfn2", "TabICL beats TabPFN-2"),
229257
("mitra_vs_xgb", "Mitra beats xgboost"),
230-
("dticl_vs_xgb", "our gbt<-TabICL beats xgboost")):
258+
("dticl_vs_xgb", "our gbt<-TabICL beats xgboost"),
259+
("oof_vs_soft",
260+
"out-of-fold soft labels beat in-context soft labels")):
231261
lines.append(f"- {label}: {wins[k]}/{n[k]}")
262+
lines += [
263+
"",
264+
"The soft-oof column uses stratified out-of-fold teacher labels:",
265+
"an in-context teacher scoring rows already in its own context",
266+
"leaks labels and collapses the soft targets toward one-hot",
267+
"(Tanna et al., \"Pocket Foundation Models\", arXiv:2605.18654;",
268+
"technique adopted from their paper with thanks). Measured",
269+
"honestly: on this suite the fix does not lift accuracy (win",
270+
"count above; median delta 0.000). The likely reasons: these",
271+
"datasets cap at 1500 rows, so each fold-fit teacher loses",
272+
"context it can ill afford; the paper's gains are measured in",
273+
"AUC, where soft-label structure matters, while this table is",
274+
"accuracy, where only the argmax does; and their pipeline adds",
275+
"temperature scaling ours does not. The two results bracket",
276+
"where the technique earns its keep: larger data, probability",
277+
"metrics.",
278+
]
232279
lines.append("")
233280
with open(RESULTS, "w") as f:
234281
f.write("\n".join(lines))

0 commit comments

Comments
 (0)