itstheraj commited on
Commit
1438ac4
·
verified ·
1 Parent(s): b26be75

Add TabFM/GBM head-to-head benchmark, efficiency tables, and TabArena entry

Browse files
README.md CHANGED
@@ -119,6 +119,52 @@ trees and ridge. The numbers below are measured on the shipped weights.
119
  support set) did not improve the big tier on this battery (it measured 0.811, below the single
120
  pass), so the single pass is the number to use.
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  ## Regression (5 datasets)
123
 
124
  On regression accuracy FelaTab alone beats neither untuned scikit-learn nor a tuned LightGBM on any
 
119
  support set) did not improve the big tier on this battery (it measured 0.811, below the single
120
  pass), so the single pass is the number to use.
121
 
122
+ ## Head to head vs Google TabFM (zero shot) and gradient boosted trees
123
+
124
+ Independent battery: 8 OpenML classification datasets (adult, credit-g, blood-transfusion, churn,
125
+ electricity, vehicle, segment, jungle_chess), stratified 80/20 split, seed 42. FelaTab and
126
+ `google/tabfm-1.0.0-pytorch` run zero shot (train split as in context support, capped at 3000
127
+ rows); XGBoost / LightGBM are trained on the full train split. Deep learning models ran on a
128
+ local GPU (ROCm), the GBMs on CPU. Reproduce with `benchmark/benchmark.py`.
129
+
130
+ ### Summary (mean across 8 datasets)
131
+
132
+ | Model | Mean Acc | Mean ROC-AUC | Mean LogLoss | Mean F1 | Avg Rank |
133
+ |---|---|---|---|---|---|
134
+ | TabFM (zero shot) | 0.8899 | 0.9263 | 0.2329 | 0.8484 | 1.02 |
135
+ | XGBoost (trained) | 0.8534 | 0.9052 | 0.3350 | 0.8126 | 2.88 |
136
+ | LightGBM (trained) | 0.8526 | 0.9011 | 0.4538 | 0.8113 | 3.14 |
137
+ | **FelaTab big (zero shot)** | 0.8043 | 0.8674 | 0.4301 | 0.7181 | 3.91 |
138
+ | **FelaTab small (zero shot)** | 0.7929 | 0.8539 | 0.4425 | 0.7057 | 4.06 |
139
+
140
+ ### Efficiency (this is where FelaTab lives)
141
+
142
+ | Model | Params | Device | Fit time | Latency (ms/sample) | Peak VRAM (MB) |
143
+ |---|---|---|---|---|---|
144
+ | **FelaTab small** | 51.6M | gpu (ROCm) / CPU-native | 0 s | **0.79** | 5390 |
145
+ | **FelaTab big** | 411.9M | gpu (ROCm) / CPU-native | 0 s | **1.76** | 5531 |
146
+ | TabFM | 1.64B | gpu (ROCm) | 0.24 s | 52.43 | 7142 |
147
+ | XGBoost | - | cpu | 6.35 s | 0.025 | N/A |
148
+ | LightGBM | - | cpu | 6.66 s | 0.008 | N/A |
149
+
150
+ - FelaTab-small is **~66x lower latency per sample than TabFM** (0.79 vs 52.4 ms) at roughly
151
+ 1/32nd the parameter count (51.6M vs 1.64B), and it does that while also running comfortably
152
+ on a plain CPU.
153
+ - TabFM is more accurate on this battery (it won 7 of 8 datasets on accuracy; FelaTab-big tied
154
+ it on credit-g). FelaTab's trade: give up some accuracy, get an order of magnitude faster,
155
+ smaller, CPU-native, zero-setup predictor.
156
+ - Full per-dataset tables (accuracy / ROC-AUC / log loss / F1) are in
157
+ `benchmark/model_card_snippet.md`; raw numbers in `benchmark/results.csv`.
158
+
159
+ ## TabArena
160
+
161
+ FelaTab is wrapped as an AutoGluon model (`benchmark/tabarena/fela_ag_model.py`) and runnable
162
+ against the official [TabArena](https://huggingface.co/spaces/TabArena/leaderboard) leaderboard
163
+ with `benchmark/tabarena/run_tabarena.py`. On the 3-dataset quickstart subset FelaTab-big scored
164
+ ELO ~938 (small ~897), mid-pack alongside default XGBoost, with 33% of tasks imputed (regression
165
+ tasks are out of scope for the classification wrapper). Full TabArena-Lite / full-benchmark runs
166
+ are in progress; see `benchmark/RUNNING.md`.
167
+
168
  ## Regression (5 datasets)
169
 
170
  On regression accuracy FelaTab alone beats neither untuned scikit-learn nor a tuned LightGBM on any
benchmark/RUNNING.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Running the FelaTab benchmark
2
+
3
+ Compares `lowdown-labs/fela-tab` (zero-shot, in-context) against Google TabFM
4
+ (`google/tabfm-1.0.0-pytorch`, zero-shot) and trained baselines XGBoost / LightGBM on
5
+ 8 OpenML classification datasets, and optionally enters FelaTab into the TabArena
6
+ leaderboard.
7
+
8
+ ## 1. Environment (CPU/GPU)
9
+
10
+ ```bash
11
+ # from the repo root (directory containing fela-tab/ and benchmark/)
12
+ python -m venv --system-site-packages .venv # inherits the system ROCm torch — do NOT pip install stock torch
13
+ .venv/bin/pip install -r benchmark/requirements.txt
14
+ .venv/bin/pip install "tabfm[pytorch]" # Google TabFM client
15
+ ```
16
+
17
+ GPU: the system torch is a ROCm build (HIP). `torch.cuda.is_available()` maps to the
18
+ AMD GPU; no extra setup needed. XGBoost/LightGBM pip builds are CPU-only (normal for
19
+ GBMs) — the output tables record the device per model.
20
+
21
+ TabFM is a **gated, non-commercial** HF repo. Export a token with accepted terms:
22
+
23
+ ```bash
24
+ export HF_TOKEN=hf_...
25
+ ```
26
+
27
+ ## 2. Run the benchmark
28
+
29
+ ```bash
30
+ # smoke test: 2 tiny datasets, FelaTab-small + XGB + LGBM (minutes)
31
+ .venv/bin/python benchmark/benchmark.py --smoke
32
+
33
+ # full battery: 8 datasets, FelaTab big+small, TabFM, XGBoost, LightGBM (GPU)
34
+ .venv/bin/python benchmark/benchmark.py --device gpu
35
+
36
+ # useful variants
37
+ .venv/bin/python benchmark/benchmark.py --device cpu # all-CPU shootout
38
+ .venv/bin/python benchmark/benchmark.py --skip tabfm # no TabFM
39
+ .venv/bin/python benchmark/benchmark.py --tiers big # big tier only
40
+ .venv/bin/python benchmark/benchmark.py --datasets adult,churn # subset
41
+ ```
42
+
43
+ Outputs (in `benchmark/`):
44
+ - `results.csv` — per (dataset, model): ROC-AUC, log loss, accuracy, F1-macro,
45
+ fit time, latency (ms/sample), peak RAM, peak VRAM
46
+ - `model_card_snippet.md` — Markdown tables ready to paste into the HF model card
47
+ (per-dataset tables with bolded winners, mean + average-rank summary, efficiency)
48
+
49
+ Protocol: stratified 80/20 split, seed 42. FelaTab/TabFM get the train split as
50
+ in-context support rows (capped at 3000); XGBoost/LightGBM train on the full split.
51
+ Datasets with >10 classes are skipped (FelaTab/TabFM hard limit).
52
+
53
+ ## 3. TabArena entry
54
+
55
+ TabArena (https://huggingface.co/spaces/TabArena/leaderboard) is an AutoGluon-based
56
+ living benchmark. FelaTab is wrapped as an AutoGluon model in
57
+ `benchmark/tabarena/fela_ag_model.py`.
58
+
59
+ ```bash
60
+ # one-time setup (needs Python 3.11-3.13 + uv; already done in benchmark/tabarena/tabarena-repo)
61
+ git clone https://github.com/autogluon/tabarena.git benchmark/tabarena/tabarena-repo
62
+ cd benchmark/tabarena/tabarena-repo
63
+ uv venv --seed --python 3.12 .venv
64
+ uv pip install --python .venv/bin/python --prerelease=allow -e "./packages/tabarena[benchmark]"
65
+
66
+ # run (from benchmark/tabarena/)
67
+ TMPDIR=<dir-on-disk> ../tabarena/tabarena-repo/.venv/bin/python run_tabarena.py --quickstart # 3 lite datasets
68
+ TMPDIR=<dir-on-disk> ../tabarena/tabarena-repo/.venv/bin/python run_tabarena.py --subset lite # full TabArena-Lite
69
+ TMPDIR=<dir-on-disk> ../tabarena/tabarena-repo/.venv/bin/python run_tabarena.py --full # all 51 datasets (hours)
70
+ ```
71
+
72
+ Notes:
73
+ - **Set TMPDIR to a real disk directory.** AutoGluon pickles one model per CV fold;
74
+ the FelaTab big tier is ~1.6 GB fp32, which overflows a tmpfs `/tmp` (Errno 28).
75
+ - The leaderboard comparison + figures land in `benchmark/tabarena/eval/<run-name>/`,
76
+ including `leaderboard_website.md`.
77
+ - `fela_ag_model.py` runs tiers small+big as two configs; tasks with >10 classes are
78
+ skipped gracefully (`raise_on_failure=False` in the runner).
benchmark/__pycache__/benchmark.cpython-314.pyc ADDED
Binary file (27.9 kB). View file
 
benchmark/benchmark.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark FelaTab vs TabFM vs XGBoost/LightGBM on OpenML classification datasets.
2
+
3
+ FelaTab + TabFM run zero-shot / in-context (train split = support rows, capped).
4
+ XGBoost / LightGBM are trained on the full train split (CPU; pip builds can't use ROCm).
5
+
6
+ Outputs:
7
+ benchmark/results.csv - per (dataset, model) metrics + efficiency
8
+ benchmark/model_card_snippet.md - Hugging Face model-card-ready Markdown tables
9
+
10
+ Usage:
11
+ python benchmark.py --smoke # 2 tiny datasets, 3 models, minutes
12
+ python benchmark.py # full battery
13
+ python benchmark.py --device cpu --skip tabfm
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import os
20
+ import sys
21
+ import time
22
+ import tracemalloc
23
+ import warnings
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+ import psutil
30
+
31
+ warnings.filterwarnings("ignore")
32
+
33
+ SEED = 42
34
+ HERE = Path(__file__).resolve().parent
35
+ # works both as <workspace>/benchmark/benchmark.py and inside the fela-tab repo as
36
+ # fela-tab/benchmark/benchmark.py
37
+ for _cand in (HERE.parent / "fela-tab", HERE.parent, HERE / ".." / "fela-tab"):
38
+ if (_cand / "modeling.py").is_file():
39
+ FELA_DIR = _cand.resolve()
40
+ break
41
+ else:
42
+ FELA_DIR = HERE.parent / "fela-tab"
43
+
44
+ # --------------------------------------------------------------------------
45
+ # Datasets
46
+ # --------------------------------------------------------------------------
47
+ # (display name, openml name, openml version)
48
+ FULL_DATASETS = [
49
+ ("adult", "adult", 2),
50
+ ("credit-g", "credit-g", 1),
51
+ ("blood-transfusion", "blood-transfusion-service-center", 1),
52
+ ("churn", "churn", 1),
53
+ ("electricity", "electricity", 1),
54
+ ("vehicle", "vehicle", 1),
55
+ ("segment", "segment", 1),
56
+ ("jungle_chess", "jungle_chess_2pcs_raw_endgame_complete", 1),
57
+ ]
58
+ SMOKE_DATASETS = [
59
+ ("breast-cancer", "wdbc", 1),
60
+ ("vehicle", "vehicle", 1),
61
+ ]
62
+ MAX_CLASSES = 10 # FelaTab / TabFM constraint
63
+
64
+
65
+ def load_openml(name: str, version: int):
66
+ """Return (X DataFrame, y int array, n_classes). Encodes categoricals, imputes."""
67
+ from sklearn.datasets import fetch_openml
68
+
69
+ ds = fetch_openml(name, version=version, as_frame=True, parser="auto")
70
+ X, y = ds.data, ds.target
71
+ # encode target
72
+ y = pd.Categorical(y)
73
+ y_int = y.codes.astype(np.int64)
74
+ n_classes = len(y.categories)
75
+ # encode features
76
+ X = X.copy()
77
+ for c in X.columns:
78
+ if not pd.api.types.is_numeric_dtype(X[c]):
79
+ X[c] = pd.Categorical(X[c]).codes.astype(np.float64)
80
+ X = X.astype(np.float64)
81
+ # impute with column median
82
+ X = X.fillna(X.median(numeric_only=True)).fillna(0.0)
83
+ return X, y_int, n_classes
84
+
85
+
86
+ # --------------------------------------------------------------------------
87
+ # Efficiency measurement helpers
88
+ # --------------------------------------------------------------------------
89
+ class Meter:
90
+ """Tracks wall time, peak process RAM (tracemalloc + psutil RSS), peak VRAM."""
91
+
92
+ def __init__(self, device: str):
93
+ self.device = device
94
+ self.proc = psutil.Process(os.getpid())
95
+ self._torch = None
96
+ if device.startswith("cuda"):
97
+ try:
98
+ import torch
99
+
100
+ self._torch = torch
101
+ torch.cuda.reset_peak_memory_stats()
102
+ except Exception:
103
+ pass
104
+
105
+ def __enter__(self):
106
+ tracemalloc.start()
107
+ self._rss0 = self.proc.memory_info().rss
108
+ self._peak_rss = self._rss0
109
+ self.t0 = time.perf_counter()
110
+ return self
111
+
112
+ def sample(self):
113
+ self._peak_rss = max(self._peak_rss, self.proc.memory_info().rss)
114
+
115
+ def __exit__(self, *exc):
116
+ self.elapsed = time.perf_counter() - self.t0
117
+ _, py_peak = tracemalloc.get_traced_memory()
118
+ tracemalloc.stop()
119
+ self.sample()
120
+ self.peak_ram_mb = max(py_peak, self._peak_rss - self._rss0) / 1e6
121
+ self.peak_vram_mb = (
122
+ self._torch.cuda.max_memory_allocated() / 1e6 if self._torch else None
123
+ )
124
+ return False
125
+
126
+
127
+ @dataclass
128
+ class Result:
129
+ dataset: str
130
+ model: str
131
+ device: str
132
+ roc_auc: float = np.nan
133
+ log_loss: float = np.nan
134
+ accuracy: float = np.nan
135
+ f1_macro: float = np.nan
136
+ fit_s: float = np.nan
137
+ latency_ms: float = np.nan # per sample
138
+ peak_ram_mb: float = np.nan
139
+ peak_vram_mb: float = np.nan
140
+ status: str = "ok"
141
+ notes: str = ""
142
+
143
+
144
+ # --------------------------------------------------------------------------
145
+ # Model adapters: uniform fit_predict_proba(Xtr, ytr, Xte) -> (proba, fit_s, infer_s)
146
+ # proba shape [n_test, n_classes], columns aligned with class index 0..K-1
147
+ # --------------------------------------------------------------------------
148
+ class Adapter:
149
+ name = "base"
150
+ device = "cpu"
151
+
152
+ def fit_predict_proba(self, Xtr, ytr, Xte, n_classes):
153
+ raise NotImplementedError
154
+
155
+
156
+ class FelaTabAdapter(Adapter):
157
+ def __init__(self, tier: str, device: str):
158
+ import torch
159
+
160
+ sys.path.insert(0, str(FELA_DIR))
161
+ from modeling import load_model # fela-tab/modeling.py
162
+
163
+ self.tier = tier
164
+ self.name = f"FelaTab-{tier}"
165
+ self.device = "gpu" if device.startswith("cuda") else "cpu"
166
+ dev = torch.device("cuda" if device.startswith("cuda") else "cpu")
167
+ self._predict_mod = __import__("modeling")
168
+ self.model = load_model(str(FELA_DIR), tier=tier).to(dev)
169
+
170
+ def fit_predict_proba(self, Xtr, ytr, Xte, n_classes):
171
+ from modeling import predict
172
+
173
+ t0 = time.perf_counter()
174
+ proba = predict(
175
+ self.model, Xtr, ytr, Xte,
176
+ task="classification", n_classes=n_classes, support_cap=3000,
177
+ )
178
+ total = time.perf_counter() - t0
179
+ # zero-shot: no fit; count full in-context forward as inference,
180
+ # report support ingestion separately via fit_s=0
181
+ return proba, 0.0, total
182
+
183
+
184
+ class TabFMAdapter(Adapter):
185
+ """google/tabfm-1.0.0-pytorch zero-shot via the `tabfm` package.
186
+
187
+ Requires HF_TOKEN with accepted license (non-commercial, tabfm-non-commercial-v1.0).
188
+ Sklearn-style: TabFMClassifier.fit(X_train, y_train) -> predict_proba(X_test).
189
+ """
190
+
191
+ name = "TabFM"
192
+
193
+ def __init__(self, device: str):
194
+ from huggingface_hub import model_info
195
+
196
+ self.device = "gpu" if device.startswith("cuda") else "cpu"
197
+ token = os.environ.get("HF_TOKEN")
198
+ if not token:
199
+ raise RuntimeError(
200
+ "HF_TOKEN not set; google/tabfm-1.0.0-pytorch is a gated repo")
201
+ model_info("google/tabfm-1.0.0-pytorch", token=token) # raises if unauthorized
202
+ import torch
203
+ from tabfm import TabFMClassifier, tabfm_v1_0_0_pytorch as tabfm_ckpt
204
+
205
+ self._torch = torch
206
+ self._dev = torch.device("cuda" if device.startswith("cuda") else "cpu")
207
+ model = tabfm_ckpt.load(model_type="classification")
208
+ try:
209
+ model = model.to(self._dev)
210
+ except Exception:
211
+ pass
212
+ self.clf = TabFMClassifier(model=model)
213
+
214
+ def fit_predict_proba(self, Xtr, ytr, Xte, n_classes):
215
+ if n_classes > MAX_CLASSES:
216
+ raise RuntimeError(f"TabFM supports <= {MAX_CLASSES} classes")
217
+ t0 = time.perf_counter()
218
+ self.clf.fit(Xtr, ytr) # in-context: stores support rows, no training
219
+ fit_s = time.perf_counter() - t0
220
+ t1 = time.perf_counter()
221
+ proba = np.asarray(self.clf.predict_proba(Xte), dtype=np.float64)
222
+ infer_s = time.perf_counter() - t1
223
+ return proba[:, :n_classes], fit_s, infer_s
224
+
225
+
226
+ class SklearnAdapter(Adapter):
227
+ def __init__(self, model, name):
228
+ self.model = model
229
+ self.name = name
230
+ self.device = "cpu"
231
+
232
+ def fit_predict_proba(self, Xtr, ytr, Xte, n_classes):
233
+ t0 = time.perf_counter()
234
+ self.model.fit(Xtr, ytr)
235
+ fit_s = time.perf_counter() - t0
236
+ t1 = time.perf_counter()
237
+ proba = self.model.predict_proba(Xte)
238
+ infer_s = time.perf_counter() - t1
239
+ return proba, fit_s, infer_s
240
+
241
+
242
+ def build_models(device: str, tiers, skip: set) -> list[Adapter]:
243
+ models: list[Adapter] = []
244
+ for tier in tiers:
245
+ try:
246
+ models.append(FelaTabAdapter(tier, device))
247
+ print(f"[load] FelaTab-{tier} on {device}")
248
+ except Exception as e:
249
+ print(f"[skip] FelaTab-{tier}: {e}")
250
+ if "tabfm" not in skip:
251
+ try:
252
+ models.append(TabFMAdapter(device))
253
+ print(f"[load] TabFM on {device}")
254
+ except Exception as e:
255
+ print(f"[skip] TabFM: {e}")
256
+ if "xgb" not in skip:
257
+ from xgboost import XGBClassifier
258
+
259
+ models.append(SklearnAdapter(
260
+ XGBClassifier(n_estimators=300, max_depth=6, learning_rate=0.1,
261
+ tree_method="hist", n_jobs=-1, random_state=SEED),
262
+ "XGBoost"))
263
+ if "lgbm" not in skip:
264
+ from lightgbm import LGBMClassifier
265
+
266
+ models.append(SklearnAdapter(
267
+ LGBMClassifier(n_estimators=300, learning_rate=0.1, n_jobs=-1,
268
+ random_state=SEED, verbose=-1),
269
+ "LightGBM"))
270
+ return models
271
+
272
+
273
+ # --------------------------------------------------------------------------
274
+ # Evaluation
275
+ # --------------------------------------------------------------------------
276
+ def evaluate(y_true, proba, n_classes):
277
+ from sklearn.metrics import accuracy_score, f1_score, log_loss, roc_auc_score
278
+
279
+ proba = np.asarray(proba, dtype=np.float64)
280
+ proba = np.clip(proba, 1e-12, 1.0)
281
+ proba = proba / proba.sum(1, keepdims=True)
282
+ pred = proba.argmax(1)
283
+ acc = accuracy_score(y_true, pred)
284
+ f1 = f1_score(y_true, pred, average="macro")
285
+ labels = list(range(n_classes))
286
+ ll = log_loss(y_true, proba, labels=labels)
287
+ if n_classes == 2:
288
+ auc = roc_auc_score(y_true, proba[:, 1])
289
+ else:
290
+ auc = roc_auc_score(y_true, proba, multi_class="ovr", average="weighted",
291
+ labels=labels)
292
+ return auc, ll, acc, f1
293
+
294
+
295
+ def run(args) -> list[Result]:
296
+ from sklearn.model_selection import train_test_split
297
+
298
+ device = "cuda" if args.device == "gpu" else "cpu"
299
+ if device == "cuda":
300
+ import torch
301
+
302
+ if not torch.cuda.is_available():
303
+ print("[warn] GPU requested but unavailable; falling back to CPU")
304
+ device = "cpu"
305
+
306
+ dsets = SMOKE_DATASETS if args.smoke else FULL_DATASETS
307
+ if args.datasets:
308
+ keep = set(args.datasets.split(","))
309
+ dsets = [d for d in dsets if d[0] in keep]
310
+ tiers = ["small"] if args.smoke else args.tiers.split(",")
311
+ skip = set(args.skip.split(",")) if args.skip else set()
312
+ if args.smoke:
313
+ skip.add("tabfm")
314
+ models = build_models(device, tiers, skip)
315
+ if not models:
316
+ sys.exit("no models available")
317
+
318
+ results: list[Result] = []
319
+ for dname, oml_name, ver in dsets:
320
+ print(f"\n=== {dname} (openml:{oml_name} v{ver}) ===")
321
+ try:
322
+ X, y, n_classes = load_openml(oml_name, ver)
323
+ except Exception as e:
324
+ print(f" [skip dataset] {e}")
325
+ continue
326
+ if n_classes > MAX_CLASSES:
327
+ print(f" [skip dataset] {n_classes} classes > {MAX_CLASSES}")
328
+ continue
329
+ Xtr, Xte, ytr, yte = train_test_split(
330
+ X, y, test_size=0.2, random_state=SEED, stratify=y)
331
+ print(f" train={len(Xtr)} test={len(Xte)} feats={Xtr.shape[1]} classes={n_classes}")
332
+ for m in models:
333
+ r = Result(dataset=dname, model=m.name, device=m.device)
334
+ try:
335
+ with Meter(device if m.device == "gpu" else "cpu") as meter:
336
+ proba, fit_s, infer_s = m.fit_predict_proba(
337
+ Xtr.to_numpy(), ytr, Xte.to_numpy(), n_classes)
338
+ r.fit_s = fit_s
339
+ r.latency_ms = 1e3 * infer_s / len(Xte)
340
+ r.peak_ram_mb = meter.peak_ram_mb
341
+ r.peak_vram_mb = meter.peak_vram_mb or np.nan
342
+ r.roc_auc, r.log_loss, r.accuracy, r.f1_macro = evaluate(
343
+ yte, proba, n_classes)
344
+ print(f" {m.name:<14} acc={r.accuracy:.4f} auc={r.roc_auc:.4f} "
345
+ f"ll={r.log_loss:.4f} f1={r.f1_macro:.4f} "
346
+ f"fit={r.fit_s:.2f}s infer={r.latency_ms:.3f}ms/s "
347
+ f"ram={r.peak_ram_mb:.0f}MB vram={r.peak_vram_mb or 0:.0f}MB")
348
+ except Exception as e:
349
+ r.status = f"FAILED"
350
+ r.notes = str(e).split("\n")[0][:120]
351
+ print(f" {m.name:<14} FAILED: {r.notes}")
352
+ results.append(r)
353
+ return results
354
+
355
+
356
+ # --------------------------------------------------------------------------
357
+ # Markdown model-card snippet
358
+ # --------------------------------------------------------------------------
359
+ def _bold_best(df: pd.DataFrame, col: str, higher=True) -> pd.Series:
360
+ best = df[col].max() if higher else df[col].min()
361
+ return df[col].map(lambda v: f"**{v:.4f}**" if v == best else f"{v:.4f}")
362
+
363
+
364
+ def to_markdown(results: list[Result]) -> str:
365
+ df = pd.DataFrame([vars(r) for r in results])
366
+ ok = df[df.status == "ok"]
367
+ models = [m for m in df.model.unique()]
368
+ lines = ["## Benchmark results",
369
+ "",
370
+ "Zero-shot in-context models (FelaTab, TabFM) vs trained baselines "
371
+ "(XGBoost, LightGBM). OpenML datasets, stratified 80/20 split, seed 42.",
372
+ ""]
373
+
374
+ # 1) per-dataset performance tables
375
+ for metric, higher, title in [
376
+ ("accuracy", True, "Accuracy"), ("roc_auc", True, "ROC-AUC"),
377
+ ("log_loss", False, "Log Loss"), ("f1_macro", True, "F1 (macro)")]:
378
+ piv = ok.pivot_table(index="dataset", columns="model", values=metric)
379
+ lines.append(f"### {title} per dataset")
380
+ lines.append("")
381
+ lines.append("| Dataset | " + " | ".join(piv.columns) + " |")
382
+ lines.append("|" + "---|" * (len(piv.columns) + 1))
383
+ for d, row in piv.iterrows():
384
+ best = row.max() if higher else row.min()
385
+ cells = [f"**{v:.4f}**" if v == best else (f"{v:.4f}" if pd.notna(v) else "—")
386
+ for v in row]
387
+ lines.append(f"| {d} | " + " | ".join(cells) + " |")
388
+ lines.append("")
389
+
390
+ # 2) summary with average rank
391
+ lines += ["### Summary (mean across datasets)", ""]
392
+ summ = ok.groupby("model").agg(
393
+ mean_acc=("accuracy", "mean"), mean_auc=("roc_auc", "mean"),
394
+ mean_logloss=("log_loss", "mean"), mean_f1=("f1_macro", "mean"))
395
+ ranks = []
396
+ for metric, higher in [("accuracy", True), ("roc_auc", True),
397
+ ("log_loss", False), ("f1_macro", True)]:
398
+ piv = ok.pivot_table(index="dataset", columns="model", values=metric)
399
+ r = piv.rank(axis=1, ascending=not higher).mean()
400
+ ranks.append(r)
401
+ avg_rank = pd.concat(ranks, axis=1).mean(axis=1)
402
+ summ["avg_rank"] = avg_rank
403
+ lines.append("| Model | Mean Acc | Mean ROC-AUC | Mean LogLoss | Mean F1 | Avg Rank |")
404
+ lines.append("|---|---|---|---|---|---|")
405
+ for m in summ.index:
406
+ s = summ.loc[m]
407
+ lines.append(f"| {m} | {s.mean_acc:.4f} | {s.mean_auc:.4f} | "
408
+ f"{s.mean_logloss:.4f} | {s.mean_f1:.4f} | **{s.avg_rank:.2f}** |")
409
+ lines.append("")
410
+
411
+ # 3) efficiency
412
+ lines += ["### Efficiency", "",
413
+ "| Model | Device | Fit time (s) | Latency (ms/sample) | Peak RAM (MB) | Peak VRAM (MB) |",
414
+ "|---|---|---|---|---|---|"]
415
+ for m in models:
416
+ sub = ok[ok.model == m]
417
+ if sub.empty:
418
+ continue
419
+ dev = sub.device.iloc[0]
420
+ vram = sub.peak_vram_mb.mean()
421
+ lines.append(f"| {m} | {dev} | {sub.fit_s.mean():.2f} | "
422
+ f"{sub.latency_ms.mean():.3f} | {sub.peak_ram_mb.mean():.0f} | "
423
+ f"{'N/A' if pd.isna(vram) or vram == 0 else f'{vram:.0f}'} |")
424
+ lines.append("")
425
+
426
+ failed = df[df.status != "ok"]
427
+ if not failed.empty:
428
+ lines += ["<details><summary>Failed runs</summary>", "",
429
+ "| Dataset | Model | Reason |", "|---|---|---|"]
430
+ for _, f in failed.iterrows():
431
+ lines.append(f"| {f.dataset} | {f.model} | {f.notes} |")
432
+ lines += ["", "</details>", ""]
433
+ return "\n".join(lines)
434
+
435
+
436
+ def main():
437
+ ap = argparse.ArgumentParser()
438
+ ap.add_argument("--smoke", action="store_true")
439
+ ap.add_argument("--device", choices=["gpu", "cpu"], default="gpu")
440
+ ap.add_argument("--tiers", default="big,small")
441
+ ap.add_argument("--skip", default="")
442
+ ap.add_argument("--datasets", default="")
443
+ args = ap.parse_args()
444
+
445
+ results = run(args)
446
+ df = pd.DataFrame([vars(r) for r in results])
447
+ out_csv = HERE / "results.csv"
448
+ df.to_csv(out_csv, index=False)
449
+ md = to_markdown(results)
450
+ (HERE / "model_card_snippet.md").write_text(md)
451
+ print(f"\nwrote {out_csv} and {HERE/'model_card_snippet.md'}")
452
+ print("\n" + md)
453
+
454
+
455
+ if __name__ == "__main__":
456
+ main()
benchmark/model_card_snippet.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Benchmark results
2
+
3
+ Zero-shot in-context models (FelaTab, TabFM) vs trained baselines (XGBoost, LightGBM). OpenML datasets, stratified 80/20 split, seed 42.
4
+
5
+ ### Accuracy per dataset
6
+
7
+ | Dataset | FelaTab-big | FelaTab-small | LightGBM | TabFM | XGBoost |
8
+ |---|---|---|---|---|---|
9
+ | adult | 0.8303 | 0.8172 | 0.8746 | **0.8781** | 0.8772 |
10
+ | blood-transfusion | 0.7867 | 0.7067 | 0.7533 | **0.7933** | 0.7400 |
11
+ | churn | 0.8960 | 0.8970 | 0.9590 | **0.9750** | 0.9580 |
12
+ | credit-g | **0.7350** | 0.7150 | 0.7000 | **0.7350** | 0.7250 |
13
+ | electricity | 0.7087 | 0.7130 | 0.9104 | **0.9629** | 0.9040 |
14
+ | jungle_chess | 0.7773 | 0.7685 | 0.8666 | **0.8881** | 0.8618 |
15
+ | segment | 0.9242 | 0.9372 | 0.9805 | **0.9870** | 0.9784 |
16
+ | vehicle | 0.7765 | 0.7882 | 0.7765 | **0.9000** | 0.7824 |
17
+
18
+ ### ROC-AUC per dataset
19
+
20
+ | Dataset | FelaTab-big | FelaTab-small | LightGBM | TabFM | XGBoost |
21
+ |---|---|---|---|---|---|
22
+ | adult | 0.8795 | 0.8757 | 0.9293 | **0.9317** | 0.9305 |
23
+ | blood-transfusion | 0.7773 | 0.6701 | 0.7393 | **0.7945** | 0.7617 |
24
+ | churn | 0.8606 | 0.8637 | 0.9283 | **0.9408** | 0.9285 |
25
+ | credit-g | 0.7635 | 0.7685 | 0.7268 | **0.7830** | 0.7431 |
26
+ | electricity | 0.8102 | 0.8099 | 0.9717 | **0.9950** | 0.9692 |
27
+ | jungle_chess | 0.9106 | 0.9050 | 0.9772 | **0.9838** | 0.9754 |
28
+ | segment | 0.9947 | 0.9964 | 0.9995 | **0.9998** | 0.9996 |
29
+ | vehicle | 0.9431 | 0.9422 | 0.9363 | **0.9820** | 0.9340 |
30
+
31
+ ### Log Loss per dataset
32
+
33
+ | Dataset | FelaTab-big | FelaTab-small | LightGBM | TabFM | XGBoost |
34
+ |---|---|---|---|---|---|
35
+ | adult | 0.3861 | 0.3824 | 0.2743 | **0.2712** | 0.2721 |
36
+ | blood-transfusion | 0.4584 | 0.5843 | 0.6274 | **0.4463** | 0.5444 |
37
+ | churn | 0.2831 | 0.2807 | 0.2441 | **0.1019** | 0.1674 |
38
+ | credit-g | 0.5250 | 0.5162 | 1.0375 | **0.4996** | 0.6554 |
39
+ | electricity | 0.6046 | 0.5942 | 0.2243 | **0.0906** | 0.2327 |
40
+ | jungle_chess | 0.5325 | 0.5482 | 0.2483 | **0.1912** | 0.2564 |
41
+ | segment | 0.2035 | 0.1783 | 0.0862 | **0.0365** | 0.0612 |
42
+ | vehicle | 0.4473 | 0.4559 | 0.8884 | **0.2259** | 0.4907 |
43
+
44
+ ### F1 (macro) per dataset
45
+
46
+ | Dataset | FelaTab-big | FelaTab-small | LightGBM | TabFM | XGBoost |
47
+ |---|---|---|---|---|---|
48
+ | adult | 0.7445 | 0.7139 | 0.8171 | **0.8207** | 0.8204 |
49
+ | blood-transfusion | 0.6032 | 0.5298 | 0.6445 | **0.6643** | 0.6330 |
50
+ | churn | 0.7279 | 0.7401 | 0.9079 | **0.9453** | 0.9084 |
51
+ | credit-g | 0.6619 | 0.6273 | 0.6429 | **0.6696** | 0.6644 |
52
+ | electricity | 0.7014 | 0.7007 | 0.9083 | **0.9621** | 0.9017 |
53
+ | jungle_chess | 0.6066 | 0.6096 | 0.8149 | **0.8400** | 0.8117 |
54
+ | segment | 0.9231 | 0.9372 | 0.9805 | **0.9871** | 0.9785 |
55
+ | vehicle | 0.7760 | 0.7872 | 0.7745 | **0.8978** | 0.7826 |
56
+
57
+ ### Summary (mean across datasets)
58
+
59
+ | Model | Mean Acc | Mean ROC-AUC | Mean LogLoss | Mean F1 | Avg Rank |
60
+ |---|---|---|---|---|---|
61
+ | FelaTab-big | 0.8043 | 0.8674 | 0.4301 | 0.7181 | **3.91** |
62
+ | FelaTab-small | 0.7929 | 0.8539 | 0.4425 | 0.7057 | **4.06** |
63
+ | LightGBM | 0.8526 | 0.9011 | 0.4538 | 0.8113 | **3.14** |
64
+ | TabFM | 0.8899 | 0.9263 | 0.2329 | 0.8484 | **1.02** |
65
+ | XGBoost | 0.8534 | 0.9052 | 0.3350 | 0.8126 | **2.88** |
66
+
67
+ ### Efficiency
68
+
69
+ | Model | Device | Fit time (s) | Latency (ms/sample) | Peak RAM (MB) | Peak VRAM (MB) |
70
+ |---|---|---|---|---|---|
71
+ | FelaTab-big | gpu | 0.00 | 1.755 | 65 | 5531 |
72
+ | FelaTab-small | gpu | 0.00 | 0.791 | 10 | 5390 |
73
+ | TabFM | gpu | 0.24 | 52.431 | 130 | 7142 |
74
+ | XGBoost | cpu | 6.35 | 0.025 | 5 | N/A |
75
+ | LightGBM | cpu | 6.66 | 0.008 | 7 | N/A |
benchmark/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ scikit-learn>=1.5
2
+ pandas>=2.0
3
+ xgboost>=2.0
4
+ lightgbm>=4.0
5
+ psutil>=5.9
6
+ transformers>=4.45
7
+ safetensors>=0.5
8
+ huggingface-hub>=0.30
9
+ # NOTE: torch with ROCm is already installed system-wide (do NOT pip install stock torch,
10
+ # it would clobber the ROCm build). The venv is created with --system-site-packages.
benchmark/results.csv ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dataset,model,device,roc_auc,log_loss,accuracy,f1_macro,fit_s,latency_ms,peak_ram_mb,peak_vram_mb,status,notes
2
+ credit-g,FelaTab-big,gpu,0.7635,0.525,0.735,0.6619,0.0,4.551,233.0,5297.0,ok,
3
+ credit-g,FelaTab-small,gpu,0.7685,0.5162,0.715,0.6273,0.0,1.247,18.0,5271.0,ok,
4
+ credit-g,TabFM,gpu,0.783,0.4996,0.735,0.6696,0.27,23.476,126.0,5465.0,ok,
5
+ credit-g,XGBoost,cpu,0.7431,0.6554,0.725,0.6644,5.1,0.065,8.0,,ok,
6
+ credit-g,LightGBM,cpu,0.7268,1.0375,0.7,0.6429,2.38,0.009,4.0,,ok,
7
+ blood-transfusion,FelaTab-big,gpu,0.7773,0.4584,0.7867,0.6032,0.0,2.139,1.0,5287.0,ok,
8
+ blood-transfusion,FelaTab-small,gpu,0.6701,0.5843,0.7067,0.5298,0.0,1.123,1.0,5268.0,ok,
9
+ blood-transfusion,TabFM,gpu,0.7945,0.4463,0.7933,0.6643,0.06,17.228,2.0,5331.0,ok,
10
+ blood-transfusion,XGBoost,cpu,0.7617,0.5444,0.74,0.633,1.23,0.021,1.0,,ok,
11
+ blood-transfusion,LightGBM,cpu,0.7393,0.6274,0.7533,0.6445,6.55,0.017,3.0,,ok,
12
+ churn,FelaTab-big,gpu,0.8606,0.2831,0.896,0.7279,0.0,1.412,5.0,5445.0,ok,
13
+ churn,FelaTab-small,gpu,0.8637,0.2807,0.897,0.7401,0.0,0.84,5.0,5347.0,ok,
14
+ churn,TabFM,gpu,0.9408,0.1019,0.975,0.9453,0.25,29.61,58.0,6145.0,ok,
15
+ churn,XGBoost,cpu,0.9285,0.1674,0.958,0.9084,2.58,0.003,1.0,,ok,
16
+ churn,LightGBM,cpu,0.9283,0.2441,0.959,0.9079,0.94,0.002,4.0,,ok,
17
+ electricity,FelaTab-big,gpu,0.8102,0.6046,0.7087,0.7014,0.0,0.461,17.0,5847.0,ok,
18
+ electricity,FelaTab-small,gpu,0.8099,0.5942,0.713,0.7007,0.0,0.274,17.0,5548.0,ok,
19
+ electricity,TabFM,gpu,0.995,0.0906,0.9629,0.9621,0.23,96.876,231.0,9723.0,ok,
20
+ electricity,XGBoost,cpu,0.9692,0.2327,0.904,0.9017,1.62,0.002,12.0,,ok,
21
+ electricity,LightGBM,cpu,0.9717,0.2243,0.9104,0.9083,2.57,0.001,6.0,,ok,
22
+ vehicle,FelaTab-big,gpu,0.9431,0.4473,0.7765,0.776,0.0,2.63,8.0,5294.0,ok,
23
+ vehicle,FelaTab-small,gpu,0.9422,0.4559,0.7882,0.7872,0.0,1.206,1.0,5270.0,ok,
24
+ vehicle,TabFM,gpu,0.982,0.2259,0.9,0.8978,0.22,22.292,9.0,5416.0,ok,
25
+ vehicle,XGBoost,cpu,0.934,0.4907,0.7824,0.7826,11.57,0.078,1.0,,ok,
26
+ vehicle,LightGBM,cpu,0.9363,0.8884,0.7765,0.7745,8.64,0.024,8.0,,ok,
27
+ segment,FelaTab-big,gpu,0.9947,0.2035,0.9242,0.9231,0.0,1.824,3.0,5362.0,ok,
28
+ segment,FelaTab-small,gpu,0.9964,0.1783,0.9372,0.9372,0.0,1.092,3.0,5305.0,ok,
29
+ segment,TabFM,gpu,0.9998,0.0365,0.987,0.9871,0.24,23.559,25.0,5712.0,ok,
30
+ segment,XGBoost,cpu,0.9996,0.0612,0.9784,0.9785,3.85,0.023,4.0,,ok,
31
+ segment,LightGBM,cpu,0.9995,0.0862,0.9805,0.9805,8.34,0.007,8.0,,ok,
32
+ jungle_chess,FelaTab-big,gpu,0.9106,0.5325,0.7773,0.6066,0.0,0.46,16.0,5839.0,ok,
33
+ jungle_chess,FelaTab-small,gpu,0.905,0.5482,0.7685,0.6096,0.0,0.274,16.0,5545.0,ok,
34
+ jungle_chess,TabFM,gpu,0.9838,0.1912,0.8881,0.84,0.15,94.106,182.0,9121.0,ok,
35
+ jungle_chess,XGBoost,cpu,0.9754,0.2564,0.8618,0.8117,19.06,0.003,2.0,,ok,
36
+ jungle_chess,LightGBM,cpu,0.9772,0.2483,0.8666,0.8149,12.73,0.004,12.0,,ok,
37
+ adult,FelaTab-big,gpu,0.8795,0.3861,0.8303,0.7445,0.0,0.567,234.0,5880.0,ok,
38
+ adult,FelaTab-small,gpu,0.8757,0.3824,0.8172,0.7139,0.0,0.273,21.0,5566.0,ok,
39
+ adult,TabFM,gpu,0.9317,0.2712,0.8781,0.8207,0.52,112.3,407.0,10226.0,ok,
40
+ adult,XGBoost,cpu,0.9305,0.2721,0.8772,0.8204,5.77,0.002,12.0,,ok,
41
+ adult,LightGBM,cpu,0.9293,0.2743,0.8746,0.8171,11.14,0.001,9.0,,ok,
benchmark/tabarena/fela_ag_model.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FelaTab as an AutoGluon AbstractModel for TabArena benchmarking.
2
+
3
+ FelaTab is a zero-shot in-context tabular model: `_fit` stores the train split as
4
+ support rows (capped), `_predict_proba` runs a single in-context forward pass.
5
+ Hard limits: <= 10 classes, <= 100 raw features (PCA-reduced internally beyond that),
6
+ support set capped at 3000 rows.
7
+
8
+ Must live in an importable module (Ray workers cannot unpickle classes from __main__).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ import numpy as np
18
+ from autogluon.core.models import AbstractModel
19
+ from autogluon.features import LabelEncoderFeatureGenerator
20
+
21
+ if TYPE_CHECKING:
22
+ import pandas as pd
23
+
24
+ from tabarena.utils.config_utils import ConfigGenerator
25
+
26
+ _here = Path(__file__).resolve()
27
+ # works both as <workspace>/benchmark/tabarena/fela_ag_model.py and inside the
28
+ # fela-tab repo as fela-tab/benchmark/tabarena/fela_ag_model.py
29
+ FELA_DIR = None
30
+ for _cand in (_here.parents[2] / "fela-tab", _here.parents[2]):
31
+ if (_cand / "modeling.py").is_file():
32
+ FELA_DIR = _cand
33
+ break
34
+ assert FELA_DIR is not None, "could not locate fela-tab repo (modeling.py)"
35
+ if str(FELA_DIR) not in sys.path:
36
+ sys.path.insert(0, str(FELA_DIR))
37
+
38
+ MAX_CLASSES = 10
39
+ SUPPORT_CAP = 3000
40
+
41
+
42
+ class FelaTabAGModel(AbstractModel):
43
+ ag_key = "FELA"
44
+ ag_name = "FelaTab"
45
+
46
+ def __init__(self, **kwargs):
47
+ super().__init__(**kwargs)
48
+ self._feature_generator = None
49
+ self._fela_model = None
50
+ self._X_support = None
51
+ self._y_support = None
52
+
53
+ def _preprocess(self, X: "pd.DataFrame", is_train: bool = False, **kwargs) -> np.ndarray:
54
+ X = super()._preprocess(X, **kwargs)
55
+ if is_train:
56
+ self._feature_generator = LabelEncoderFeatureGenerator(verbosity=0)
57
+ self._feature_generator.fit(X=X)
58
+ if self._feature_generator.features_in:
59
+ X = X.copy()
60
+ X[self._feature_generator.features_in] = self._feature_generator.transform(X=X)
61
+ return X.fillna(0).to_numpy(dtype=np.float32)
62
+
63
+ def _fit(self, X: "pd.DataFrame", y: "pd.Series", num_cpus: int = 1, **kwargs) -> None:
64
+ if self.problem_type == "regression":
65
+ raise ValueError("FelaTabAGModel benchmark wrapper is classification-only")
66
+ n_classes = len(np.unique(y))
67
+ if n_classes > MAX_CLASSES:
68
+ raise ValueError(f"FelaTab supports <= {MAX_CLASSES} classes, got {n_classes}")
69
+
70
+ from modeling import load_model
71
+
72
+ params = self._get_model_params()
73
+ tier = params.get("tier", "small")
74
+ self._fela_model = load_model(str(FELA_DIR), tier=tier)
75
+ if params.get("device", "cpu") == "gpu":
76
+ import torch
77
+
78
+ if torch.cuda.is_available():
79
+ self._fela_model = self._fela_model.to("cuda")
80
+
81
+ X_np = self.preprocess(X, y=y, is_train=True)
82
+ y_np = np.asarray(y)
83
+ if len(X_np) > SUPPORT_CAP:
84
+ idx = np.random.RandomState(0).permutation(len(X_np))[:SUPPORT_CAP]
85
+ X_np, y_np = X_np[idx], y_np[idx]
86
+ self._X_support, self._y_support = X_np, y_np
87
+
88
+ def _predict_proba(self, X: "pd.DataFrame", **kwargs) -> np.ndarray:
89
+ from modeling import predict
90
+
91
+ X_np = self.preprocess(X)
92
+ n_classes = len(self.classes_) if hasattr(self, "classes_") else int(
93
+ self._y_support.max() + 1)
94
+ proba = predict(
95
+ self._fela_model, self._X_support, self._y_support, X_np,
96
+ task="classification", n_classes=n_classes, support_cap=SUPPORT_CAP,
97
+ )
98
+ # guard against NaN/inf or degenerate rows: AutoGluon drops NaN-pred rows when
99
+ # scoring, which can leave a single class in y_true on small folds
100
+ proba = np.asarray(proba, dtype=np.float64)
101
+ proba = np.nan_to_num(proba, nan=1.0 / n_classes, posinf=1.0, neginf=0.0)
102
+ proba = np.clip(proba, 1e-9, None)
103
+ proba /= proba.sum(axis=1, keepdims=True)
104
+ # AutoGluon contract: binary problems must return 1-D positive-class proba
105
+ return self._convert_proba_to_unified_form(proba)
106
+
107
+ def _set_default_params(self) -> None:
108
+ for param, val in {"tier": "small", "device": "cpu"}.items():
109
+ self._set_default_param_value(param, val)
110
+
111
+ def _get_default_auxiliary_params(self) -> dict:
112
+ default_auxiliary_params = super()._get_default_auxiliary_params()
113
+ default_auxiliary_params.update({"valid_raw_types": ["int", "float", "category"]})
114
+ return default_auxiliary_params
115
+
116
+ @classmethod
117
+ def supported_problem_types(cls) -> list[str]:
118
+ return ["binary", "multiclass"]
119
+
120
+ @classmethod
121
+ def config_generator(cls) -> "ConfigGenerator":
122
+ from tabarena.utils.config_utils import ConfigGenerator
123
+
124
+ return ConfigGenerator(
125
+ model_cls=cls,
126
+ manual_configs=[{"tier": "small"}, {"tier": "big"}],
127
+ search_space={},
128
+ )
benchmark/tabarena/run_tabarena.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run FelaTab on TabArena and compare against the official leaderboard.
2
+
3
+ Usage (from this directory, with the tabarena venv python):
4
+ ../tabarena-repo/.venv/bin/python run_tabarena.py --quickstart # 3 small lite datasets
5
+ ../tabarena-repo/.venv/bin/python run_tabarena.py --subset lite # full TabArena-Lite
6
+ ../tabarena-repo/.venv/bin/python run_tabarena.py --full # all 51 datasets (hours)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from pathlib import Path
13
+
14
+ from tabarena.benchmark.experiment import TabArenaV0pt1ExperimentBundle
15
+ from tabarena.contexts import TabArenaContext
16
+
17
+ from fela_ag_model import FelaTabAGModel
18
+
19
+ QUICKSTART_DATASETS = ["blood-transfusion-service-center", "QSAR_fish_toxicity", "anneal"]
20
+
21
+
22
+ def main():
23
+ ap = argparse.ArgumentParser()
24
+ ap.add_argument("--quickstart", action="store_true", default=True)
25
+ ap.add_argument("--subset", default=None, help="tabarena subset, e.g. 'lite'")
26
+ ap.add_argument("--full", action="store_true")
27
+ ap.add_argument("--run-name", default="felatab_tabarena")
28
+ args = ap.parse_args()
29
+
30
+ here = Path(__file__).parent
31
+ results_dir = str(here / "experiments" / args.run_name)
32
+ eval_dir = here / "eval" / args.run_name
33
+
34
+ experiments = TabArenaV0pt1ExperimentBundle(
35
+ models=[
36
+ (FelaTabAGModel.config_generator(), 0), # 0 = default/manual configs only
37
+ ("LightGBM", 0), # cross-check against a known registry model
38
+ ],
39
+ ).build_experiments()
40
+
41
+ build_kwargs = {}
42
+ subset = args.subset
43
+ if args.full:
44
+ subset = None
45
+ elif not subset:
46
+ subset = "lite"
47
+ build_kwargs["dataset_names"] = QUICKSTART_DATASETS
48
+
49
+ context = TabArenaContext()
50
+ context.build_and_run_jobs(
51
+ experiments,
52
+ expname=results_dir,
53
+ subset=subset,
54
+ build_kwargs=build_kwargs,
55
+ new_result_prefix="[New] ",
56
+ debug_mode=True,
57
+ raise_on_failure=False, # log failed tasks (e.g. >10-class guard) and continue
58
+ )
59
+
60
+ leaderboard = context.compare(output_dir=eval_dir)
61
+ leaderboard_website = context.leaderboard_to_website_format(leaderboard=leaderboard)
62
+ print("\n=== TabArena leaderboard (website format) ===")
63
+ print(leaderboard_website.to_markdown(index=False))
64
+ (eval_dir / "leaderboard_website.md").write_text(
65
+ leaderboard_website.to_markdown(index=False))
66
+ print(f"\nSaved to {eval_dir}")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()