multabench 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. multabench-0.0.1/LICENSE +21 -0
  2. multabench-0.0.1/PKG-INFO +277 -0
  3. multabench-0.0.1/README.md +224 -0
  4. multabench-0.0.1/multabench/__init__.py +2 -0
  5. multabench-0.0.1/multabench/baselines/__init__.py +0 -0
  6. multabench-0.0.1/multabench/baselines/abstract_model.py +240 -0
  7. multabench-0.0.1/multabench/baselines/autogluon_mm.py +116 -0
  8. multabench-0.0.1/multabench/baselines/benchmarks/__init__.py +0 -0
  9. multabench-0.0.1/multabench/baselines/benchmarks/evaluate.py +109 -0
  10. multabench-0.0.1/multabench/baselines/catboost.py +40 -0
  11. multabench-0.0.1/multabench/baselines/contexttab.py +30 -0
  12. multabench-0.0.1/multabench/baselines/kumo_tabular.py +103 -0
  13. multabench-0.0.1/multabench/baselines/lgbm.py +28 -0
  14. multabench-0.0.1/multabench/baselines/preprocessing/__init__.py +0 -0
  15. multabench-0.0.1/multabench/baselines/preprocessing/categorical.py +24 -0
  16. multabench-0.0.1/multabench/baselines/preprocessing/dates.py +29 -0
  17. multabench-0.0.1/multabench/baselines/preprocessing/feature_types.py +48 -0
  18. multabench-0.0.1/multabench/baselines/preprocessing/image_embeddings.py +208 -0
  19. multabench-0.0.1/multabench/baselines/preprocessing/nulls.py +10 -0
  20. multabench-0.0.1/multabench/baselines/preprocessing/numerical.py +13 -0
  21. multabench-0.0.1/multabench/baselines/preprocessing/target.py +21 -0
  22. multabench-0.0.1/multabench/baselines/preprocessing/text_embeddings.py +271 -0
  23. multabench-0.0.1/multabench/baselines/preprocessing/validation.py +83 -0
  24. multabench-0.0.1/multabench/baselines/random_forest.py +35 -0
  25. multabench-0.0.1/multabench/baselines/realmlp.py +41 -0
  26. multabench-0.0.1/multabench/baselines/tabdpt.py +37 -0
  27. multabench-0.0.1/multabench/baselines/tabicl_v2.py +22 -0
  28. multabench-0.0.1/multabench/baselines/tabm.py +35 -0
  29. multabench-0.0.1/multabench/baselines/tabpfn.py +70 -0
  30. multabench-0.0.1/multabench/baselines/tabstar_v1.py +86 -0
  31. multabench-0.0.1/multabench/baselines/training/__init__.py +0 -0
  32. multabench-0.0.1/multabench/baselines/training/metrics.py +37 -0
  33. multabench-0.0.1/multabench/baselines/xgboost.py +38 -0
  34. multabench-0.0.1/multabench/benchmark/__init__.py +0 -0
  35. multabench-0.0.1/multabench/benchmark/collect.py +37 -0
  36. multabench-0.0.1/multabench/benchmark/load.py +129 -0
  37. multabench-0.0.1/multabench/benchmark/runs.py +35 -0
  38. multabench-0.0.1/multabench/benchmark/splits.py +53 -0
  39. multabench-0.0.1/multabench/benchmark/sweep.py +122 -0
  40. multabench-0.0.1/multabench/benchmark/utils/__init__.py +0 -0
  41. multabench-0.0.1/multabench/benchmark/utils/constants.py +2 -0
  42. multabench-0.0.1/multabench/benchmark/utils/curation.py +12 -0
  43. multabench-0.0.1/multabench/constants.py +10 -0
  44. multabench-0.0.1/multabench/datasets/__init__.py +0 -0
  45. multabench-0.0.1/multabench/datasets/all_datasets.py +241 -0
  46. multabench-0.0.1/multabench/datasets/hub.py +31 -0
  47. multabench-0.0.1/multabench/datasets/objects.py +37 -0
  48. multabench-0.0.1/multabench/dino/__init__.py +0 -0
  49. multabench-0.0.1/multabench/dino/constants.py +41 -0
  50. multabench-0.0.1/multabench/dino/dino_finetune.py +240 -0
  51. multabench-0.0.1/multabench/dino/image_loading.py +38 -0
  52. multabench-0.0.1/multabench/e5/__init__.py +0 -0
  53. multabench-0.0.1/multabench/e5/constants.py +61 -0
  54. multabench-0.0.1/multabench/e5/e5_finetune.py +317 -0
  55. multabench-0.0.1/multabench/embeddings/__init__.py +0 -0
  56. multabench-0.0.1/multabench/embeddings/compute.py +87 -0
  57. multabench-0.0.1/multabench/embeddings/embed.py +30 -0
  58. multabench-0.0.1/multabench/embeddings/embed_sweep.py +72 -0
  59. multabench-0.0.1/multabench/embeddings/hub.py +150 -0
  60. multabench-0.0.1/multabench/embeddings/jobs.py +32 -0
  61. multabench-0.0.1/multabench/finetune/train_args.py +29 -0
  62. multabench-0.0.1/multabench/finetune/utils.py +54 -0
  63. multabench-0.0.1/multabench/leaderboard/__init__.py +0 -0
  64. multabench-0.0.1/multabench/leaderboard/data/__init__.py +0 -0
  65. multabench-0.0.1/multabench/leaderboard/data/keys.py +45 -0
  66. multabench-0.0.1/multabench/leaderboard/data/loading.py +97 -0
  67. multabench-0.0.1/multabench/leaderboard/elo.py +112 -0
  68. multabench-0.0.1/multabench/leaderboard/large_results.py +193 -0
  69. multabench-0.0.1/multabench/leaderboard/paper_results.py +177 -0
  70. multabench-0.0.1/multabench/leaderboard/plots.py +159 -0
  71. multabench-0.0.1/multabench/leaderboard/utils.py +20 -0
  72. multabench-0.0.1/multabench/preprocessing/__init__.py +0 -0
  73. multabench-0.0.1/multabench/preprocessing/discretize.py +19 -0
  74. multabench-0.0.1/multabench/preprocessing/splits.py +89 -0
  75. multabench-0.0.1/multabench/result_keys.py +52 -0
  76. multabench-0.0.1/multabench/scripts/do_leaderboard.py +17 -0
  77. multabench-0.0.1/multabench/utils/__init__.py +0 -0
  78. multabench-0.0.1/multabench/utils/devices.py +23 -0
  79. multabench-0.0.1/multabench/utils/encoders.py +6 -0
  80. multabench-0.0.1/multabench/utils/hardware.py +143 -0
  81. multabench-0.0.1/multabench/utils/logging.py +23 -0
  82. multabench-0.0.1/multabench/utils/profiling.py +66 -0
  83. multabench-0.0.1/multabench/utils/timing.py +28 -0
  84. multabench-0.0.1/multabench/utils/warnings.py +97 -0
  85. multabench-0.0.1/multabench.egg-info/PKG-INFO +277 -0
  86. multabench-0.0.1/multabench.egg-info/SOURCES.txt +105 -0
  87. multabench-0.0.1/multabench.egg-info/dependency_links.txt +1 -0
  88. multabench-0.0.1/multabench.egg-info/requires.txt +50 -0
  89. multabench-0.0.1/multabench.egg-info/top_level.txt +1 -0
  90. multabench-0.0.1/pyproject.toml +68 -0
  91. multabench-0.0.1/setup.cfg +4 -0
  92. multabench-0.0.1/tests/test_collect.py +36 -0
  93. multabench-0.0.1/tests/test_core_imports.py +12 -0
  94. multabench-0.0.1/tests/test_elo.py +42 -0
  95. multabench-0.0.1/tests/test_embeddings.py +138 -0
  96. multabench-0.0.1/tests/test_feature_types.py +22 -0
  97. multabench-0.0.1/tests/test_hardware.py +74 -0
  98. multabench-0.0.1/tests/test_hub.py +29 -0
  99. multabench-0.0.1/tests/test_leaderboard_results.py +81 -0
  100. multabench-0.0.1/tests/test_metrics.py +41 -0
  101. multabench-0.0.1/tests/test_official_commit.py +37 -0
  102. multabench-0.0.1/tests/test_profiling.py +9 -0
  103. multabench-0.0.1/tests/test_registry.py +22 -0
  104. multabench-0.0.1/tests/test_splits.py +81 -0
  105. multabench-0.0.1/tests/test_sweep.py +89 -0
  106. multabench-0.0.1/tests/test_text_embeddings.py +16 -0
  107. multabench-0.0.1/tests/test_timing.py +21 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alan Arazi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: multabench
3
+ Version: 0.0.1
4
+ Summary: Benchmarking multimodal tabular learning: tables whose columns include text or images
5
+ Author: Alan Arazi, Eilam Shapira, Shoham Grunblat, Mor Ventura, Elad Hoffer, Gioia Blayer, David Holzmüller, Lennart Purucker, Gaël Varoquaux, Frank Hutter, Roi Reichart
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/alanarazi7/MulTaBench
8
+ Project-URL: Leaderboard, https://huggingface.co/spaces/multabench/leaderboard
9
+ Project-URL: Paper, https://arxiv.org/abs/2605.10616
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: huggingface_hub
14
+ Requires-Dist: numpy
15
+ Requires-Dist: pandas>=2.2.2
16
+ Requires-Dist: pyarrow
17
+ Requires-Dist: scikit-learn
18
+ Provides-Extra: bench
19
+ Requires-Dist: autogluon.core; extra == "bench"
20
+ Requires-Dist: nvidia-ml-py; extra == "bench"
21
+ Requires-Dist: peft; extra == "bench"
22
+ Requires-Dist: pillow; extra == "bench"
23
+ Requires-Dist: psutil; extra == "bench"
24
+ Requires-Dist: python-dotenv; extra == "bench"
25
+ Requires-Dist: setuptools<82; extra == "bench"
26
+ Requires-Dist: skrub; extra == "bench"
27
+ Requires-Dist: torch>=2.6.0; extra == "bench"
28
+ Requires-Dist: torchvision; extra == "bench"
29
+ Requires-Dist: transformers>=4.49.0; extra == "bench"
30
+ Provides-Extra: catboost
31
+ Requires-Dist: catboost; extra == "catboost"
32
+ Provides-Extra: lightgbm
33
+ Requires-Dist: lightgbm; extra == "lightgbm"
34
+ Provides-Extra: xgboost
35
+ Requires-Dist: xgboost; extra == "xgboost"
36
+ Provides-Extra: pytabkit
37
+ Requires-Dist: pytabkit; extra == "pytabkit"
38
+ Provides-Extra: tabicl
39
+ Requires-Dist: tabicl>=2.0.3; extra == "tabicl"
40
+ Provides-Extra: tabdpt
41
+ Requires-Dist: tabdpt>=1.1.9; extra == "tabdpt"
42
+ Provides-Extra: tabpfn
43
+ Requires-Dist: tabpfn>=9.2.0; extra == "tabpfn"
44
+ Provides-Extra: tabstar
45
+ Requires-Dist: tabstar>=1.1.15; extra == "tabstar"
46
+ Provides-Extra: autogluon-mm
47
+ Requires-Dist: autogluon.multimodal; extra == "autogluon-mm"
48
+ Provides-Extra: dev
49
+ Requires-Dist: matplotlib; extra == "dev"
50
+ Requires-Dist: pytest; extra == "dev"
51
+ Requires-Dist: streamlit; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ <h1 align="center">
55
+ <picture>
56
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alanarazi7/MulTaBench/master/assets/logo-dark.svg">
57
+ <img src="https://raw.githubusercontent.com/alanarazi7/MulTaBench/master/assets/logo.svg" alt="MulTaBench: Benchmarking Multimodal Tabular Learning" width="640">
58
+ </picture>
59
+ </h1>
60
+
61
+ > **Work in progress:** MulTaBench is moving towards a living leaderboard, and `master` will change
62
+ > gradually on the way there. To reproduce the paper, use the tag
63
+ > [`paper_version`](https://github.com/alanarazi7/MulTaBench/tree/paper_version) or the
64
+ > [`official-paper-version`](https://github.com/alanarazi7/MulTaBench/tree/official-paper-version) branch.
65
+ > The paper's results are kept in `multabench/leaderboard/legacy_results/` while every model is rerun under the
66
+ > leaderboard protocol.
67
+
68
+ A benchmark for multimodal tabular learning: tables whose columns include images or free text, not
69
+ just numbers and categories. It is 20 image-tabular and 20 text-tabular curated datasets, with 40
70
+ further datasets released alongside them, 80 in total. The benchmark evaluates tabular learners
71
+ under a target-aware setting, where the image and text encoders are fine-tuned on the task rather
72
+ than used frozen.
73
+
74
+ **Paper**: [MulTaBench: Benchmarking Multimodal Tabular Learning with Text and Image](https://arxiv.org/abs/2605.10616) (NeurIPS 2026 Spotlight)
75
+ **Datasets**: [huggingface.co/multabench](https://huggingface.co/multabench)
76
+
77
+ ## Leaderboard
78
+
79
+ Default hyperparameters, `--size 10k`, 6 folds, frozen `e5-small` text and `dino-small` image embeddings
80
+ (ConTextTab reads raw text itself). Elo is fit on one battle per dataset and model pair, with RandomForest
81
+ at 1000 and 95% intervals from bootstrapping the datasets; a model with no result on a dataset has no
82
+ battles there. TabPFN-v2 and v2.5 take at most 10 classes, which leaves out 3 of the 80 datasets.
83
+
84
+ | # | Model | Elo, 80 datasets | 95% CI | Text, 40 | Images, 40 | Datasets |
85
+ |---|---|---|---|---|---|---|
86
+ | 1 | TabPFN-3.5 | 2221 | +230 / −176 | 2080 | 2458 | 80 |
87
+ | 2 | Kumo-Tabular-Small | 2139 | +190 / −151 | 2094 | 2268 | 80 |
88
+ | 3 | TabPFN-3.5-Fast | 2010 | +195 / −149 | 1906 | 2199 | 80 |
89
+ | 4 | TabICLv2 | 1745 | +137 / −115 | 1637 | 1941 | 80 |
90
+ | 5 | TabPFN-3 | 1666 | +145 / −117 | 1554 | 1866 | 80 |
91
+ | 6 | TabPFN-v2p5 | 1661 | +136 / −114 | 1647 | 1743 | 77 |
92
+ | 7 | TabPFN-v2 | 1407 | +117 / −97 | 1322 | 1559 | 77 |
93
+ | 8 | TabM | 1366 | +98 / −89 | 1348 | 1432 | 80 |
94
+ | 9 | ConTextTab | 1353 | +113 / −102 | 1279 | 1490 | 80 |
95
+ | 10 | CatBoost | 1298 | +66 / −55 | 1307 | 1321 | 80 |
96
+ | 11 | TabDPT | 1251 | +112 / −93 | 1116 | 1455 | 80 |
97
+ | 12 | RealMLP | 1082 | +81 / −71 | 1103 | 1059 | 80 |
98
+ | 13 | LightGBM | 1058 | +63 / −61 | 1093 | 1013 | 80 |
99
+ | 14 | RandomForest | 1000 | anchor | 1000 | 1000 | 80 |
100
+ | 15 | XGBoost | 988 | +67 / −65 | 1024 | 936 | 80 |
101
+
102
+ Each model's runs are in `multabench/leaderboard/models/<model>.csv`, one row per (dataset, fold), failed
103
+ runs included. `python -m multabench.leaderboard.elo` prints the table, with win rate, average rank and
104
+ normalized error; `--modalities text` or `--modalities image,image-text` restricts it to those datasets.
105
+
106
+ Leaderboard runs pass `--official`, which also requires a clean checkout of a commit on `master`, so the `git`
107
+ column of every row names code anyone can check out. The image runs of the first 11 models are the one exception: they
108
+ ran at `7f28e55`, on the branch of #148, which was squash-merged unchanged.
109
+
110
+ ## Getting started
111
+
112
+ To use the datasets from another project, install the core, which needs neither torch nor any model package:
113
+
114
+ ```bash
115
+ pip install multabench
116
+ ```
117
+
118
+ ```python
119
+ from multabench import Size, load_embeddings, load_split
120
+ split = load_split("MUL_IMAGE_PETFINDER", fold=0, size=Size.TEN_K) # split.x_train, split.y_train, split.x_test, ...
121
+ embeddings = load_embeddings("MUL_TEXT_WINE_REVIEW", fold=0, size=Size.TEN_K, encoder="e5-small")
122
+ ```
123
+
124
+ To run the benchmark:
125
+
126
+ ```bash
127
+ source init.sh && source .venv/bin/activate
128
+ cp .env.example .env # Hugging Face token
129
+
130
+ python benchmark.py --model light --dataset_name MUL_IMAGE_PETFINDER --fold 0 --text_encoder e5-small --image_encoder dino-small
131
+ ```
132
+
133
+ `pyproject.toml` lists the core dependencies, a `bench` extra for `benchmark.py` and one extra per model package, so
134
+ `pip install -e ".[bench,catboost]"` installs what one model needs; each wrapper imports its package only when it runs.
135
+ ConTextTab and Kumo are pinned to a git commit, which PyPI doesn't allow in extras, so `requirements-git.txt` lists them.
136
+ `init.sh` sets up the shared `.venv` from `requirements.txt`, which locks the core, every extra and those two.
137
+
138
+ `benchmark.py` is the single entry point: it evaluates one model on one dataset fold and writes the
139
+ result to `runs/<model>_<dataset>_<text_encoder>_<image_encoder>_<size>_<fold>.json` (`--output_dir` to change), with
140
+ `none` for an encoder the dataset has no column for. A run
141
+ that fails still writes its JSON, with `"status": "error"` and the error, and exits non-zero; a successful one has
142
+ `"status": "ok"`. A run whose JSON already exists is skipped, so a stopped sweep can be relaunched as is; `--overwrite`
143
+ reruns it. `--help` lists the available models. `--text_encoder` picks how text columns are embedded: `tfidf`, frozen
144
+ `e5-small` (the default) or `e5-large`, or `e5-small-tar`, E5 fine-tuned on the task with LoRA (target-aware).
145
+ `--image_encoder` does the same for image columns: frozen `dino-small` (the default) or `dino-large`, or
146
+ `dino-small-tar`. Each applies only to datasets with that kind of column.
147
+
148
+ `sweep.py` runs `benchmark.py` over models × datasets × encoders × folds. It writes the runs that
149
+ don't have a result yet to a jobs file (`sweep_jobs_<timestamp>.txt`, or `--jobs_file`), one command per line,
150
+ which any scheduler can run. `--modalities text`, `image` or `image-text` keeps only those datasets. Text datasets
151
+ run once per text encoder and image datasets once per image encoder; image datasets that also have text columns
152
+ always embed their text with frozen `e5-small`. The default encoders include the fine-tuned `e5-small-tar` and
153
+ `dino-small-tar`, so pass `--text_encoders`/`--image_encoders` to run fewer. Each run is its own process, and a
154
+ failed run doesn't stop the others:
155
+
156
+ ```bash
157
+ python sweep.py --models light,cat --modalities text --text_encoders e5-small # only write the jobs file
158
+ python sweep.py --models light,cat --modalities text --text_encoders e5-small --launcher local # run them here, one after another
159
+ python sweep.py --models light,cat --modalities text --text_encoders e5-small --launcher slurm \
160
+ --sbatch="--partition=cpu --cpus-per-task=8 --mem=32G" --submit
161
+ python sweep.py --models tabm,real --modalities text --text_encoders e5-small --launcher slurm \
162
+ --sbatch="--partition=gpu --gres=gpu:1 --cpus-per-task=8 --mem=32G" --submit
163
+ ```
164
+
165
+ With `--launcher slurm`, each line becomes a task of a Slurm array; `--sbatch="..."` is passed to `sbatch`
166
+ as is (the `=` is needed, since the value starts with `--`), and without `--submit` the `sbatch` commands are only
167
+ printed. With a launcher, the sweep first downloads the runs' datasets and cached embeddings, since runs that
168
+ download the same files at once into a shared Hugging Face cache (e.g. on NFS) can fail. All the runs of a sweep get
169
+ the same `--sbatch`, so sweep the runs that need a GPU apart from those that don't (see below), as in the last two commands. Relaunching a sweep runs only what is still missing.
170
+
171
+ Every leaderboard run must use the same hardware, so their times compare: one NVIDIA H100 (80 GB), an Intel Xeon
172
+ Platinum 8481C with 8 CPU cores and 32 GB of RAM (`GPU_RUN_HARDWARE` in `multabench/utils/hardware.py`). Runs that need
173
+ no GPU, a CPU model (CatBoost, LightGBM, XGBoost, RandomForest) whose text is embedded with TF-IDF or read from the
174
+ cache, use an Intel Xeon Platinum 8581C with 8 CPU cores and 32 GB of RAM instead (`CPU_RUN_HARDWARE`). With
175
+ `--official` (`sweep.py --official` passes it to every run), `benchmark.py` checks the GPU, the processor, the CPUs it
176
+ may use and its memory limit, and raises before running if any differs; leaderboard runs always pass it. On Slurm,
177
+ request `--gres=gpu:1 --cpus-per-task=8 --mem=32G` on an H100 node, or `--cpus-per-task=8 --mem=32G` on an 8-core Xeon
178
+ 8581C node for CPU runs. The sweeps' Slurm jobs run under `srun`, so a run sees only the CPUs its job asked for, even
179
+ when Slurm hands it a whole machine.
180
+ The cached embeddings are computed on one NVIDIA A100, 6 CPU cores and 32 GB of RAM for now
181
+ (`EMBEDDING_HARDWARE`); the hardware they ran on is recorded with them.
182
+
183
+ `collect_results.py` gathers the run JSONs into one CSV per model in `multabench/leaderboard/models/`, one row per
184
+ run, failed runs included: model, dataset, encoders, size and fold, the status, the metric with `test_score` (higher
185
+ is better) and `test_error`, train and inference time per 1K rows apart from embedding, embedding time per 1K rows,
186
+ and the hardware. A run already in a model's file is replaced by its rerun; the other rows are kept.
187
+
188
+ ```bash
189
+ python collect_results.py --output_dir runs
190
+ ```
191
+
192
+ To use a benchmark split in your own code, `load_split` downloads the dataset and returns the same
193
+ train and test rows that `benchmark.py` evaluates on:
194
+
195
+ ```python
196
+ from multabench import Size, load_split
197
+
198
+ split = load_split("MUL_IMAGE_PETFINDER", fold=0, size=Size.TEN_K)
199
+ split.x_train, split.y_train, split.x_test, split.y_test, split.task_type, split.image_folder, split.image_column
200
+ ```
201
+
202
+ ## Cached embeddings
203
+
204
+ The frozen text and image embeddings are cached on Hugging Face, one public dataset repo per
205
+ encoder: `multabench/embeddings-e5-small` and `multabench/embeddings-dino-small`. A frozen encoder
206
+ doesn't depend on the split, so each distinct input of a dataset is embedded once, under
207
+ `<dataset>/`, in `embeddings.npz`, which maps every input, exactly as the encoder got it (e5's
208
+ `"passage: <column>: <value>"` after a run's missing-text fill, or the image file name), to its raw
209
+ encoder output (PCA is fitted per run, on the training rows), and `meta.json`, with the dataset's
210
+ rows and columns, the encoding time per feature, the hardware it ran on, the hardware it should run
211
+ on (`EMBEDDING_HARDWARE` in `multabench/utils/hardware.py`) and how they differ. A run looks its own
212
+ inputs up, so an embedding is never paired with another row's text, and an input missing from the
213
+ cache, e.g. after the dataset changed, raises. The cache is optional: `benchmark.py` reads it when
214
+ the dataset has embeddings for the encoder, and otherwise embeds live, so new encoders and datasets
215
+ need no cache to run. Only the frozen `e5-small` text embeddings are read so far. `load_embeddings`
216
+ returns them aligned with `load_split`'s rows:
217
+
218
+ ```python
219
+ from multabench import Size, load_embeddings
220
+
221
+ embeddings = load_embeddings("MUL_TEXT_WINE_REVIEW", fold=0, size=Size.TEN_K, encoder="e5-small")
222
+ embeddings.train["description"], embeddings.test["description"], embeddings.meta
223
+ ```
224
+
225
+ `python -m multabench.embeddings.embed` embeds one dataset, and `python -m
226
+ multabench.embeddings.embed_sweep` runs it over encoders × datasets, with the same `--launcher` and
227
+ `--sbatch` options as `sweep.py`, and uploads the result with `--upload`:
228
+
229
+ ```bash
230
+ python -m multabench.embeddings.embed_sweep --launcher slurm --sbatch="--partition=a100 --gres=gpu:1 --cpus-per-task=6 --mem=32G" --submit
231
+ python -m multabench.embeddings.embed_sweep --upload
232
+ ```
233
+
234
+ ## Datasets
235
+
236
+ Every dataset is a public dataset repo in the [`multabench`](https://huggingface.co/multabench)
237
+ Hugging Face organization, downloaded on demand. Repos are named
238
+ `<core|extended>-<img|text>-<reg|cls>-<name>`; `core` and `extended` are two slices of the
239
+ benchmark. Each holds `data.parquet` with explicit column types (datetime,
240
+ categorical, numeric, string), `metadata.json` with the target, and, for image datasets, the images
241
+ in `images-*.zip`.
242
+
243
+ The paper used the curated CSV copies on the [`chico89`](https://www.kaggle.com/chico89/datasets)
244
+ Kaggle account, which the Hugging Face datasets were converted from. The two may differ: the
245
+ Hugging Face datasets store dates, categories and numbers as typed columns where the CSVs hold
246
+ strings, and a few non-numeric values in numeric columns were mapped (DVM car's `"1 mile"` to 1,
247
+ Anime Planet's `"Unknown"` to missing). Scores on the two versions are not directly comparable.
248
+
249
+ | What you are looking for | Where it is |
250
+ |--------------------------|-------------|
251
+ | A dataset's Hugging Face repo | the `MulTaBenchDatasetID` values in `multabench/datasets/all_datasets.py` |
252
+ | Where a dataset originally comes from | `MULTABENCH_SOURCES` in `multabench/datasets/all_datasets.py` |
253
+ | How a dataset was curated | the recipes in `multabench/benchmark/datasets/` at the [`paper_version`](https://github.com/alanarazi7/MulTaBench/tree/paper_version) tag |
254
+ | Size, task and feature counts per dataset | `multabench/leaderboard/results/datasets_summary{,_extra}.csv` at the [`paper_version`](https://github.com/alanarazi7/MulTaBench/tree/paper_version) tag |
255
+
256
+ The 40 datasets released alongside the benchmark were admitted on a weaker criterion and carry no
257
+ tier name of their own, so their scores must not be pooled with the 40 MulTaBench datasets.
258
+
259
+ ## Protocol and TabArena
260
+
261
+ MulTaBench follows the [TabArena](https://tabarena.ai) protocol where doing so is cheap or affects
262
+ correctness, and deliberately keeps its own choices where full parity would multiply the compute.
263
+ MulTaBench is not an official TabArena leaderboard.
264
+
265
+ | | TabArena | MulTaBench | Status |
266
+ |---|---|---|---|
267
+ | Metrics | ROC AUC (binary), log loss (multiclass), RMSE (regression), via AutoGluon's scorers | Same | ✅ |
268
+ | Leaderboard | Elo via `bencheval`, RandomForest (default) anchored at 1000, missing results imputed with RandomForest and flagged, bootstrapped confidence intervals | Elo as in the paper, RandomForest anchored at 1000, bootstrapped confidence intervals; missing results are left out, neither imputed nor dropping the model, so a model is ranked on the datasets it ran on, which its Datasets column shows | |
269
+ | Splits | Fixed and part of the task definition | Same: a seeded function of the data | ✅ |
270
+ | Group and time structure | Audited per dataset; group-aware or forward-in-time splits where needed | Not audited: every dataset is split IID. Some datasets may have group or time structure, in which case IID splits can overestimate performance | |
271
+ | Runtime | Train and inference time per 1K rows | Same, with the time spent embedding text and images reported separately | ✅ |
272
+ | Hardware | Recorded per run; 8 CPU cores and 32 GB of RAM, with one NVIDIA L40S (48 GB) for GPU models | Recorded per run; every run uses 8 CPU cores and 32 GB of RAM, GPU models with one NVIDIA H100 (80 GB), checked by `benchmark.py --official` | |
273
+ | Outer splits | 3 folds, repeated 1–10 times depending on dataset size | 3 folds, repeated twice for every size | |
274
+ | Dataset size | Full size | `--size 10k` (the default, and the one the leaderboard reports) caps train and test at 10K and 5K rows; `--size full` keeps every row | |
275
+ | Inner validation | 8-fold bagging for every model | No bagging: one fit per split, and models that need validation hold out 10% of the training rows (at most 1,000). This favors models that ensemble internally (TabPFN, TabICL, TabDPT, TabM, RandomForest) over single GBDTs and RealMLP | |
276
+ | Hyperparameters | Default, tuned, and tuned + ensembled | Default only | |
277
+ | Model implementations | TabArena's model registry | MulTaBench's own wrappers, which add image and text embeddings | |
@@ -0,0 +1,224 @@
1
+ <h1 align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alanarazi7/MulTaBench/master/assets/logo-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/alanarazi7/MulTaBench/master/assets/logo.svg" alt="MulTaBench: Benchmarking Multimodal Tabular Learning" width="640">
5
+ </picture>
6
+ </h1>
7
+
8
+ > **Work in progress:** MulTaBench is moving towards a living leaderboard, and `master` will change
9
+ > gradually on the way there. To reproduce the paper, use the tag
10
+ > [`paper_version`](https://github.com/alanarazi7/MulTaBench/tree/paper_version) or the
11
+ > [`official-paper-version`](https://github.com/alanarazi7/MulTaBench/tree/official-paper-version) branch.
12
+ > The paper's results are kept in `multabench/leaderboard/legacy_results/` while every model is rerun under the
13
+ > leaderboard protocol.
14
+
15
+ A benchmark for multimodal tabular learning: tables whose columns include images or free text, not
16
+ just numbers and categories. It is 20 image-tabular and 20 text-tabular curated datasets, with 40
17
+ further datasets released alongside them, 80 in total. The benchmark evaluates tabular learners
18
+ under a target-aware setting, where the image and text encoders are fine-tuned on the task rather
19
+ than used frozen.
20
+
21
+ **Paper**: [MulTaBench: Benchmarking Multimodal Tabular Learning with Text and Image](https://arxiv.org/abs/2605.10616) (NeurIPS 2026 Spotlight)
22
+ **Datasets**: [huggingface.co/multabench](https://huggingface.co/multabench)
23
+
24
+ ## Leaderboard
25
+
26
+ Default hyperparameters, `--size 10k`, 6 folds, frozen `e5-small` text and `dino-small` image embeddings
27
+ (ConTextTab reads raw text itself). Elo is fit on one battle per dataset and model pair, with RandomForest
28
+ at 1000 and 95% intervals from bootstrapping the datasets; a model with no result on a dataset has no
29
+ battles there. TabPFN-v2 and v2.5 take at most 10 classes, which leaves out 3 of the 80 datasets.
30
+
31
+ | # | Model | Elo, 80 datasets | 95% CI | Text, 40 | Images, 40 | Datasets |
32
+ |---|---|---|---|---|---|---|
33
+ | 1 | TabPFN-3.5 | 2221 | +230 / −176 | 2080 | 2458 | 80 |
34
+ | 2 | Kumo-Tabular-Small | 2139 | +190 / −151 | 2094 | 2268 | 80 |
35
+ | 3 | TabPFN-3.5-Fast | 2010 | +195 / −149 | 1906 | 2199 | 80 |
36
+ | 4 | TabICLv2 | 1745 | +137 / −115 | 1637 | 1941 | 80 |
37
+ | 5 | TabPFN-3 | 1666 | +145 / −117 | 1554 | 1866 | 80 |
38
+ | 6 | TabPFN-v2p5 | 1661 | +136 / −114 | 1647 | 1743 | 77 |
39
+ | 7 | TabPFN-v2 | 1407 | +117 / −97 | 1322 | 1559 | 77 |
40
+ | 8 | TabM | 1366 | +98 / −89 | 1348 | 1432 | 80 |
41
+ | 9 | ConTextTab | 1353 | +113 / −102 | 1279 | 1490 | 80 |
42
+ | 10 | CatBoost | 1298 | +66 / −55 | 1307 | 1321 | 80 |
43
+ | 11 | TabDPT | 1251 | +112 / −93 | 1116 | 1455 | 80 |
44
+ | 12 | RealMLP | 1082 | +81 / −71 | 1103 | 1059 | 80 |
45
+ | 13 | LightGBM | 1058 | +63 / −61 | 1093 | 1013 | 80 |
46
+ | 14 | RandomForest | 1000 | anchor | 1000 | 1000 | 80 |
47
+ | 15 | XGBoost | 988 | +67 / −65 | 1024 | 936 | 80 |
48
+
49
+ Each model's runs are in `multabench/leaderboard/models/<model>.csv`, one row per (dataset, fold), failed
50
+ runs included. `python -m multabench.leaderboard.elo` prints the table, with win rate, average rank and
51
+ normalized error; `--modalities text` or `--modalities image,image-text` restricts it to those datasets.
52
+
53
+ Leaderboard runs pass `--official`, which also requires a clean checkout of a commit on `master`, so the `git`
54
+ column of every row names code anyone can check out. The image runs of the first 11 models are the one exception: they
55
+ ran at `7f28e55`, on the branch of #148, which was squash-merged unchanged.
56
+
57
+ ## Getting started
58
+
59
+ To use the datasets from another project, install the core, which needs neither torch nor any model package:
60
+
61
+ ```bash
62
+ pip install multabench
63
+ ```
64
+
65
+ ```python
66
+ from multabench import Size, load_embeddings, load_split
67
+ split = load_split("MUL_IMAGE_PETFINDER", fold=0, size=Size.TEN_K) # split.x_train, split.y_train, split.x_test, ...
68
+ embeddings = load_embeddings("MUL_TEXT_WINE_REVIEW", fold=0, size=Size.TEN_K, encoder="e5-small")
69
+ ```
70
+
71
+ To run the benchmark:
72
+
73
+ ```bash
74
+ source init.sh && source .venv/bin/activate
75
+ cp .env.example .env # Hugging Face token
76
+
77
+ python benchmark.py --model light --dataset_name MUL_IMAGE_PETFINDER --fold 0 --text_encoder e5-small --image_encoder dino-small
78
+ ```
79
+
80
+ `pyproject.toml` lists the core dependencies, a `bench` extra for `benchmark.py` and one extra per model package, so
81
+ `pip install -e ".[bench,catboost]"` installs what one model needs; each wrapper imports its package only when it runs.
82
+ ConTextTab and Kumo are pinned to a git commit, which PyPI doesn't allow in extras, so `requirements-git.txt` lists them.
83
+ `init.sh` sets up the shared `.venv` from `requirements.txt`, which locks the core, every extra and those two.
84
+
85
+ `benchmark.py` is the single entry point: it evaluates one model on one dataset fold and writes the
86
+ result to `runs/<model>_<dataset>_<text_encoder>_<image_encoder>_<size>_<fold>.json` (`--output_dir` to change), with
87
+ `none` for an encoder the dataset has no column for. A run
88
+ that fails still writes its JSON, with `"status": "error"` and the error, and exits non-zero; a successful one has
89
+ `"status": "ok"`. A run whose JSON already exists is skipped, so a stopped sweep can be relaunched as is; `--overwrite`
90
+ reruns it. `--help` lists the available models. `--text_encoder` picks how text columns are embedded: `tfidf`, frozen
91
+ `e5-small` (the default) or `e5-large`, or `e5-small-tar`, E5 fine-tuned on the task with LoRA (target-aware).
92
+ `--image_encoder` does the same for image columns: frozen `dino-small` (the default) or `dino-large`, or
93
+ `dino-small-tar`. Each applies only to datasets with that kind of column.
94
+
95
+ `sweep.py` runs `benchmark.py` over models × datasets × encoders × folds. It writes the runs that
96
+ don't have a result yet to a jobs file (`sweep_jobs_<timestamp>.txt`, or `--jobs_file`), one command per line,
97
+ which any scheduler can run. `--modalities text`, `image` or `image-text` keeps only those datasets. Text datasets
98
+ run once per text encoder and image datasets once per image encoder; image datasets that also have text columns
99
+ always embed their text with frozen `e5-small`. The default encoders include the fine-tuned `e5-small-tar` and
100
+ `dino-small-tar`, so pass `--text_encoders`/`--image_encoders` to run fewer. Each run is its own process, and a
101
+ failed run doesn't stop the others:
102
+
103
+ ```bash
104
+ python sweep.py --models light,cat --modalities text --text_encoders e5-small # only write the jobs file
105
+ python sweep.py --models light,cat --modalities text --text_encoders e5-small --launcher local # run them here, one after another
106
+ python sweep.py --models light,cat --modalities text --text_encoders e5-small --launcher slurm \
107
+ --sbatch="--partition=cpu --cpus-per-task=8 --mem=32G" --submit
108
+ python sweep.py --models tabm,real --modalities text --text_encoders e5-small --launcher slurm \
109
+ --sbatch="--partition=gpu --gres=gpu:1 --cpus-per-task=8 --mem=32G" --submit
110
+ ```
111
+
112
+ With `--launcher slurm`, each line becomes a task of a Slurm array; `--sbatch="..."` is passed to `sbatch`
113
+ as is (the `=` is needed, since the value starts with `--`), and without `--submit` the `sbatch` commands are only
114
+ printed. With a launcher, the sweep first downloads the runs' datasets and cached embeddings, since runs that
115
+ download the same files at once into a shared Hugging Face cache (e.g. on NFS) can fail. All the runs of a sweep get
116
+ the same `--sbatch`, so sweep the runs that need a GPU apart from those that don't (see below), as in the last two commands. Relaunching a sweep runs only what is still missing.
117
+
118
+ Every leaderboard run must use the same hardware, so their times compare: one NVIDIA H100 (80 GB), an Intel Xeon
119
+ Platinum 8481C with 8 CPU cores and 32 GB of RAM (`GPU_RUN_HARDWARE` in `multabench/utils/hardware.py`). Runs that need
120
+ no GPU, a CPU model (CatBoost, LightGBM, XGBoost, RandomForest) whose text is embedded with TF-IDF or read from the
121
+ cache, use an Intel Xeon Platinum 8581C with 8 CPU cores and 32 GB of RAM instead (`CPU_RUN_HARDWARE`). With
122
+ `--official` (`sweep.py --official` passes it to every run), `benchmark.py` checks the GPU, the processor, the CPUs it
123
+ may use and its memory limit, and raises before running if any differs; leaderboard runs always pass it. On Slurm,
124
+ request `--gres=gpu:1 --cpus-per-task=8 --mem=32G` on an H100 node, or `--cpus-per-task=8 --mem=32G` on an 8-core Xeon
125
+ 8581C node for CPU runs. The sweeps' Slurm jobs run under `srun`, so a run sees only the CPUs its job asked for, even
126
+ when Slurm hands it a whole machine.
127
+ The cached embeddings are computed on one NVIDIA A100, 6 CPU cores and 32 GB of RAM for now
128
+ (`EMBEDDING_HARDWARE`); the hardware they ran on is recorded with them.
129
+
130
+ `collect_results.py` gathers the run JSONs into one CSV per model in `multabench/leaderboard/models/`, one row per
131
+ run, failed runs included: model, dataset, encoders, size and fold, the status, the metric with `test_score` (higher
132
+ is better) and `test_error`, train and inference time per 1K rows apart from embedding, embedding time per 1K rows,
133
+ and the hardware. A run already in a model's file is replaced by its rerun; the other rows are kept.
134
+
135
+ ```bash
136
+ python collect_results.py --output_dir runs
137
+ ```
138
+
139
+ To use a benchmark split in your own code, `load_split` downloads the dataset and returns the same
140
+ train and test rows that `benchmark.py` evaluates on:
141
+
142
+ ```python
143
+ from multabench import Size, load_split
144
+
145
+ split = load_split("MUL_IMAGE_PETFINDER", fold=0, size=Size.TEN_K)
146
+ split.x_train, split.y_train, split.x_test, split.y_test, split.task_type, split.image_folder, split.image_column
147
+ ```
148
+
149
+ ## Cached embeddings
150
+
151
+ The frozen text and image embeddings are cached on Hugging Face, one public dataset repo per
152
+ encoder: `multabench/embeddings-e5-small` and `multabench/embeddings-dino-small`. A frozen encoder
153
+ doesn't depend on the split, so each distinct input of a dataset is embedded once, under
154
+ `<dataset>/`, in `embeddings.npz`, which maps every input, exactly as the encoder got it (e5's
155
+ `"passage: <column>: <value>"` after a run's missing-text fill, or the image file name), to its raw
156
+ encoder output (PCA is fitted per run, on the training rows), and `meta.json`, with the dataset's
157
+ rows and columns, the encoding time per feature, the hardware it ran on, the hardware it should run
158
+ on (`EMBEDDING_HARDWARE` in `multabench/utils/hardware.py`) and how they differ. A run looks its own
159
+ inputs up, so an embedding is never paired with another row's text, and an input missing from the
160
+ cache, e.g. after the dataset changed, raises. The cache is optional: `benchmark.py` reads it when
161
+ the dataset has embeddings for the encoder, and otherwise embeds live, so new encoders and datasets
162
+ need no cache to run. Only the frozen `e5-small` text embeddings are read so far. `load_embeddings`
163
+ returns them aligned with `load_split`'s rows:
164
+
165
+ ```python
166
+ from multabench import Size, load_embeddings
167
+
168
+ embeddings = load_embeddings("MUL_TEXT_WINE_REVIEW", fold=0, size=Size.TEN_K, encoder="e5-small")
169
+ embeddings.train["description"], embeddings.test["description"], embeddings.meta
170
+ ```
171
+
172
+ `python -m multabench.embeddings.embed` embeds one dataset, and `python -m
173
+ multabench.embeddings.embed_sweep` runs it over encoders × datasets, with the same `--launcher` and
174
+ `--sbatch` options as `sweep.py`, and uploads the result with `--upload`:
175
+
176
+ ```bash
177
+ python -m multabench.embeddings.embed_sweep --launcher slurm --sbatch="--partition=a100 --gres=gpu:1 --cpus-per-task=6 --mem=32G" --submit
178
+ python -m multabench.embeddings.embed_sweep --upload
179
+ ```
180
+
181
+ ## Datasets
182
+
183
+ Every dataset is a public dataset repo in the [`multabench`](https://huggingface.co/multabench)
184
+ Hugging Face organization, downloaded on demand. Repos are named
185
+ `<core|extended>-<img|text>-<reg|cls>-<name>`; `core` and `extended` are two slices of the
186
+ benchmark. Each holds `data.parquet` with explicit column types (datetime,
187
+ categorical, numeric, string), `metadata.json` with the target, and, for image datasets, the images
188
+ in `images-*.zip`.
189
+
190
+ The paper used the curated CSV copies on the [`chico89`](https://www.kaggle.com/chico89/datasets)
191
+ Kaggle account, which the Hugging Face datasets were converted from. The two may differ: the
192
+ Hugging Face datasets store dates, categories and numbers as typed columns where the CSVs hold
193
+ strings, and a few non-numeric values in numeric columns were mapped (DVM car's `"1 mile"` to 1,
194
+ Anime Planet's `"Unknown"` to missing). Scores on the two versions are not directly comparable.
195
+
196
+ | What you are looking for | Where it is |
197
+ |--------------------------|-------------|
198
+ | A dataset's Hugging Face repo | the `MulTaBenchDatasetID` values in `multabench/datasets/all_datasets.py` |
199
+ | Where a dataset originally comes from | `MULTABENCH_SOURCES` in `multabench/datasets/all_datasets.py` |
200
+ | How a dataset was curated | the recipes in `multabench/benchmark/datasets/` at the [`paper_version`](https://github.com/alanarazi7/MulTaBench/tree/paper_version) tag |
201
+ | Size, task and feature counts per dataset | `multabench/leaderboard/results/datasets_summary{,_extra}.csv` at the [`paper_version`](https://github.com/alanarazi7/MulTaBench/tree/paper_version) tag |
202
+
203
+ The 40 datasets released alongside the benchmark were admitted on a weaker criterion and carry no
204
+ tier name of their own, so their scores must not be pooled with the 40 MulTaBench datasets.
205
+
206
+ ## Protocol and TabArena
207
+
208
+ MulTaBench follows the [TabArena](https://tabarena.ai) protocol where doing so is cheap or affects
209
+ correctness, and deliberately keeps its own choices where full parity would multiply the compute.
210
+ MulTaBench is not an official TabArena leaderboard.
211
+
212
+ | | TabArena | MulTaBench | Status |
213
+ |---|---|---|---|
214
+ | Metrics | ROC AUC (binary), log loss (multiclass), RMSE (regression), via AutoGluon's scorers | Same | ✅ |
215
+ | Leaderboard | Elo via `bencheval`, RandomForest (default) anchored at 1000, missing results imputed with RandomForest and flagged, bootstrapped confidence intervals | Elo as in the paper, RandomForest anchored at 1000, bootstrapped confidence intervals; missing results are left out, neither imputed nor dropping the model, so a model is ranked on the datasets it ran on, which its Datasets column shows | |
216
+ | Splits | Fixed and part of the task definition | Same: a seeded function of the data | ✅ |
217
+ | Group and time structure | Audited per dataset; group-aware or forward-in-time splits where needed | Not audited: every dataset is split IID. Some datasets may have group or time structure, in which case IID splits can overestimate performance | |
218
+ | Runtime | Train and inference time per 1K rows | Same, with the time spent embedding text and images reported separately | ✅ |
219
+ | Hardware | Recorded per run; 8 CPU cores and 32 GB of RAM, with one NVIDIA L40S (48 GB) for GPU models | Recorded per run; every run uses 8 CPU cores and 32 GB of RAM, GPU models with one NVIDIA H100 (80 GB), checked by `benchmark.py --official` | |
220
+ | Outer splits | 3 folds, repeated 1–10 times depending on dataset size | 3 folds, repeated twice for every size | |
221
+ | Dataset size | Full size | `--size 10k` (the default, and the one the leaderboard reports) caps train and test at 10K and 5K rows; `--size full` keeps every row | |
222
+ | Inner validation | 8-fold bagging for every model | No bagging: one fit per split, and models that need validation hold out 10% of the training rows (at most 1,000). This favors models that ensemble internally (TabPFN, TabICL, TabDPT, TabM, RandomForest) over single GBDTs and RealMLP | |
223
+ | Hyperparameters | Default, tuned, and tuned + ensembled | Default only | |
224
+ | Model implementations | TabArena's model registry | MulTaBench's own wrappers, which add image and text embeddings | |
@@ -0,0 +1,2 @@
1
+ from multabench.benchmark.load import load_embeddings, load_split
2
+ from multabench.benchmark.splits import Size
File without changes