multabench 0.0.1__py3-none-any.whl

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 (86) hide show
  1. multabench/__init__.py +2 -0
  2. multabench/baselines/__init__.py +0 -0
  3. multabench/baselines/abstract_model.py +240 -0
  4. multabench/baselines/autogluon_mm.py +116 -0
  5. multabench/baselines/benchmarks/__init__.py +0 -0
  6. multabench/baselines/benchmarks/evaluate.py +109 -0
  7. multabench/baselines/catboost.py +40 -0
  8. multabench/baselines/contexttab.py +30 -0
  9. multabench/baselines/kumo_tabular.py +103 -0
  10. multabench/baselines/lgbm.py +28 -0
  11. multabench/baselines/preprocessing/__init__.py +0 -0
  12. multabench/baselines/preprocessing/categorical.py +24 -0
  13. multabench/baselines/preprocessing/dates.py +29 -0
  14. multabench/baselines/preprocessing/feature_types.py +48 -0
  15. multabench/baselines/preprocessing/image_embeddings.py +208 -0
  16. multabench/baselines/preprocessing/nulls.py +10 -0
  17. multabench/baselines/preprocessing/numerical.py +13 -0
  18. multabench/baselines/preprocessing/target.py +21 -0
  19. multabench/baselines/preprocessing/text_embeddings.py +271 -0
  20. multabench/baselines/preprocessing/validation.py +83 -0
  21. multabench/baselines/random_forest.py +35 -0
  22. multabench/baselines/realmlp.py +41 -0
  23. multabench/baselines/tabdpt.py +37 -0
  24. multabench/baselines/tabicl_v2.py +22 -0
  25. multabench/baselines/tabm.py +35 -0
  26. multabench/baselines/tabpfn.py +70 -0
  27. multabench/baselines/tabstar_v1.py +86 -0
  28. multabench/baselines/training/__init__.py +0 -0
  29. multabench/baselines/training/metrics.py +37 -0
  30. multabench/baselines/xgboost.py +38 -0
  31. multabench/benchmark/__init__.py +0 -0
  32. multabench/benchmark/collect.py +37 -0
  33. multabench/benchmark/load.py +129 -0
  34. multabench/benchmark/runs.py +35 -0
  35. multabench/benchmark/splits.py +53 -0
  36. multabench/benchmark/sweep.py +122 -0
  37. multabench/benchmark/utils/__init__.py +0 -0
  38. multabench/benchmark/utils/constants.py +2 -0
  39. multabench/benchmark/utils/curation.py +12 -0
  40. multabench/constants.py +10 -0
  41. multabench/datasets/__init__.py +0 -0
  42. multabench/datasets/all_datasets.py +241 -0
  43. multabench/datasets/hub.py +31 -0
  44. multabench/datasets/objects.py +37 -0
  45. multabench/dino/__init__.py +0 -0
  46. multabench/dino/constants.py +41 -0
  47. multabench/dino/dino_finetune.py +240 -0
  48. multabench/dino/image_loading.py +38 -0
  49. multabench/e5/__init__.py +0 -0
  50. multabench/e5/constants.py +61 -0
  51. multabench/e5/e5_finetune.py +317 -0
  52. multabench/embeddings/__init__.py +0 -0
  53. multabench/embeddings/compute.py +87 -0
  54. multabench/embeddings/embed.py +30 -0
  55. multabench/embeddings/embed_sweep.py +72 -0
  56. multabench/embeddings/hub.py +150 -0
  57. multabench/embeddings/jobs.py +32 -0
  58. multabench/finetune/train_args.py +29 -0
  59. multabench/finetune/utils.py +54 -0
  60. multabench/leaderboard/__init__.py +0 -0
  61. multabench/leaderboard/data/__init__.py +0 -0
  62. multabench/leaderboard/data/keys.py +45 -0
  63. multabench/leaderboard/data/loading.py +97 -0
  64. multabench/leaderboard/elo.py +112 -0
  65. multabench/leaderboard/large_results.py +193 -0
  66. multabench/leaderboard/paper_results.py +177 -0
  67. multabench/leaderboard/plots.py +159 -0
  68. multabench/leaderboard/utils.py +20 -0
  69. multabench/preprocessing/__init__.py +0 -0
  70. multabench/preprocessing/discretize.py +19 -0
  71. multabench/preprocessing/splits.py +89 -0
  72. multabench/result_keys.py +52 -0
  73. multabench/scripts/do_leaderboard.py +17 -0
  74. multabench/utils/__init__.py +0 -0
  75. multabench/utils/devices.py +23 -0
  76. multabench/utils/encoders.py +6 -0
  77. multabench/utils/hardware.py +143 -0
  78. multabench/utils/logging.py +23 -0
  79. multabench/utils/profiling.py +66 -0
  80. multabench/utils/timing.py +28 -0
  81. multabench/utils/warnings.py +97 -0
  82. multabench-0.0.1.dist-info/METADATA +277 -0
  83. multabench-0.0.1.dist-info/RECORD +86 -0
  84. multabench-0.0.1.dist-info/WHEEL +5 -0
  85. multabench-0.0.1.dist-info/licenses/LICENSE +21 -0
  86. multabench-0.0.1.dist-info/top_level.txt +1 -0
multabench/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from multabench.benchmark.load import load_embeddings, load_split
2
+ from multabench.benchmark.splits import Size
File without changes
@@ -0,0 +1,240 @@
1
+ from typing import Tuple, Dict, Optional, Set, List, Any
2
+
3
+ import numpy as np
4
+ import torch
5
+ from pandas import DataFrame, Series
6
+ from sklearn.preprocessing import LabelEncoder
7
+ from skrub import DatetimeEncoder
8
+
9
+ from multabench.baselines.preprocessing.dates import fit_date_encoders, transform_date_features
10
+ from multabench.baselines.preprocessing.feature_types import detect_feature_types, transform_feature_types
11
+ from multabench.baselines.preprocessing.nulls import raise_if_null_target
12
+ from multabench.baselines.preprocessing.validation import split_to_val
13
+ from multabench.baselines.preprocessing.image_embeddings import (
14
+ transform_image_features,
15
+ fit_image_encoders,
16
+ )
17
+ from multabench.baselines.training.metrics import calculate_metric
18
+ from multabench.datasets.all_datasets import MulTaBenchDatasetID
19
+ from multabench.baselines.preprocessing.categorical import fit_categorical_encoders, transform_categorical_features
20
+ from multabench.baselines.preprocessing.numerical import fit_numerical_median, transform_numerical_features
21
+ from multabench.baselines.preprocessing.target import transform_preprocess_y, fit_preprocess_y
22
+ from multabench.dino.constants import DINOV3_SMALL
23
+ from multabench.e5.constants import E5_SMALL_V2
24
+ from multabench.baselines.preprocessing.text_embeddings import E5ColumnEncoder, fit_text_encoders, transform_text_features
25
+ from multabench.datasets.objects import SupervisedTask
26
+ from multabench.utils.warnings import silence_baselines_prints
27
+
28
+
29
+ class TabularModel:
30
+
31
+ MODEL_NAME: str
32
+ SHORT_NAME: str
33
+ USE_VAL_SPLIT: bool
34
+ USE_MEDIAN_FILLING: bool
35
+ USE_CATEGORICAL_ENCODING: bool
36
+ USE_TEXT_EMBEDDINGS: bool
37
+ USE_TARGET_ENCODER: bool
38
+ NEEDS_GPU = True
39
+
40
+ def __init__(self, problem_type: SupervisedTask, device: torch.device,
41
+ dataset: MulTaBenchDatasetID | None = None, verbose: bool = False, image_folder: str | None = None,
42
+ image_column: str | None = None,
43
+ tune_dino: bool = False,
44
+ dino_train_kwargs: Optional[Dict[str, Any]] = None,
45
+ dino_model_name: str = DINOV3_SMALL,
46
+ tune_e5: bool = False,
47
+ e5_train_kwargs: Optional[Dict[str, Any]] = None,
48
+ e5_model_name: str = E5_SMALL_V2,
49
+ **kwargs):
50
+ assert problem_type in {SupervisedTask.REGRESSION, SupervisedTask.BINARY, SupervisedTask.MULTICLASS}
51
+ self.problem_type = problem_type
52
+ self.dataset = dataset
53
+ self.is_cls = bool(problem_type in {SupervisedTask.BINARY, SupervisedTask.MULTICLASS})
54
+ self.device = device
55
+ self.best_val_loss: Optional[None] = None
56
+ self.verbose = verbose
57
+ self.d_output: int = 0
58
+ self.image_folder = image_folder
59
+ self.image_column = image_column
60
+ self.tune_dino = tune_dino
61
+ self.dino_train_kwargs = dino_train_kwargs or {}
62
+ self.dino_model_name = dino_model_name
63
+ self._tuned_dino_model = None
64
+ self._tuned_dino_processor = None
65
+ self.tune_e5 = tune_e5
66
+ self.e5_train_kwargs = e5_train_kwargs or {}
67
+ self.e5_model_name = e5_model_name
68
+ self.model_ = self.initialize_model()
69
+ self.target_transformer: Optional[LabelEncoder] = None
70
+ self.date_transformers: Dict[str, DatetimeEncoder] = {}
71
+ self.image_transformers: Dict[str, Any] = {} # PCA per image column
72
+ self.numerical_features: Set[str] = set()
73
+ self.numerical_medians: Dict[str, float] = {}
74
+ self.categorical_features: Set[str] = set()
75
+ self.categorical_indices: List[int] = []
76
+ self.categorical_encoders: Dict[str, LabelEncoder] = {}
77
+ self.text_features: Set[str] = set()
78
+ self.text_transformers: Dict[str, E5ColumnEncoder] = {}
79
+ self._train_embeddings: Dict[str, np.ndarray] = {}
80
+
81
+ def initialize_model(self):
82
+ raise NotImplementedError("Initialize model method not implemented yet")
83
+
84
+ def fit(self, x: DataFrame, y: Series):
85
+ x_train, y_train = x.copy(), y.copy()
86
+ if self.USE_VAL_SPLIT:
87
+ x_train, x_val, y_train, y_val = split_to_val(x=x, y=y, is_cls=self.is_cls)
88
+ else:
89
+ x_val, y_val = None, None
90
+ self.fit_preprocessor(x_train=x_train, y_train=y_train)
91
+ x_train, y_train = self.transform_preprocessor(x=x_train, y=y_train, train_embeddings=self._train_embeddings)
92
+ self._train_embeddings = {}
93
+ if x_val is not None and y_val is not None:
94
+ x_val, y_val = self.transform_preprocessor(x=x_val, y=y_val)
95
+ self._print_feature_summary(x_train)
96
+ with silence_baselines_prints():
97
+ self.fit_model(x_train=x_train, y_train=y_train, x_val=x_val, y_val=y_val)
98
+
99
+ def fit_preprocessor(self, x_train: DataFrame, y_train: Series):
100
+ x_train, y_train = self.do_model_agnostic_preprocessing(x=x_train, y=y_train)
101
+ if self.USE_MEDIAN_FILLING:
102
+ self.numerical_medians = fit_numerical_median(x=x_train, numerical_features=self.numerical_features)
103
+ if self.USE_CATEGORICAL_ENCODING:
104
+ self.categorical_encoders = fit_categorical_encoders(x=x_train, categorical_features=self.categorical_features)
105
+ if self.USE_TEXT_EMBEDDINGS:
106
+ self.text_transformers, text_embeddings = fit_text_encoders(
107
+ x=x_train,
108
+ text_features=self.text_features,
109
+ device=self.device,
110
+ y=y_train if self.tune_e5 else None,
111
+ tune_e5=self.tune_e5,
112
+ e5_train_kwargs=self.e5_train_kwargs if self.tune_e5 else None,
113
+ is_cls=self.is_cls,
114
+ d_output=self.d_output,
115
+ e5_model_name=self.e5_model_name,
116
+ dataset=self.dataset,
117
+ )
118
+ self._train_embeddings.update(text_embeddings)
119
+ self.vprint(f"📝 Detected {len(self.text_transformers)} text features: {sorted(self.text_transformers)}")
120
+ self.fit_internal_preprocessor(x=x_train, y=y_train)
121
+
122
+ def transform_preprocessor(self, x: DataFrame, y: Optional[Series],
123
+ train_embeddings: Optional[Dict[str, np.ndarray]] = None) -> Tuple[DataFrame, Optional[Series]]:
124
+ x = transform_date_features(x=x, date_transformers=self.date_transformers)
125
+ x = transform_image_features(
126
+ x=x,
127
+ image_transformers=self.image_transformers,
128
+ device=self.device,
129
+ image_folder=self.image_folder,
130
+ dino_model=getattr(self, "_tuned_dino_model", None),
131
+ dino_processor=getattr(self, "_tuned_dino_processor", None),
132
+ dino_model_name=self.dino_model_name,
133
+ train_embeddings=train_embeddings,
134
+ )
135
+ image_features = [f"{col}_img_pca_{i}" for col in self.image_transformers.keys() for i in range(self.image_transformers[col].n_components)]
136
+ x = transform_feature_types(x=x, numerical_features=self.numerical_features, image_features=image_features)
137
+ if y is not None:
138
+ raise_if_null_target(y)
139
+ if self.USE_TARGET_ENCODER:
140
+ y = transform_preprocess_y(y=y, scaler=self.target_transformer)
141
+ if self.USE_MEDIAN_FILLING:
142
+ x = transform_numerical_features(x=x, numerical_medians=self.numerical_medians)
143
+ if self.USE_CATEGORICAL_ENCODING:
144
+ x = transform_categorical_features(x=x, categorical_encoders=self.categorical_encoders)
145
+ if self.USE_TEXT_EMBEDDINGS:
146
+ x = transform_text_features(
147
+ x=x,
148
+ text_encoders=self.text_transformers,
149
+ device=self.device,
150
+ train_embeddings=train_embeddings,
151
+ )
152
+ return self.transform_internal_preprocessor(x=x, y=y)
153
+
154
+ def fit_internal_preprocessor(self, x: DataFrame, y: Series) -> Tuple[DataFrame, Series]:
155
+ pass
156
+
157
+ def transform_internal_preprocessor(self, x: DataFrame, y: Optional[Series]) -> Tuple[DataFrame, Optional[Series]]:
158
+ return x, y
159
+
160
+ def fit_model(self, x_train: DataFrame, y_train: Series, x_val: Optional[DataFrame], y_val: Optional[Series]):
161
+ raise NotImplementedError("Fit model method not implemented yet")
162
+
163
+ def do_model_agnostic_preprocessing(self, x: DataFrame, y: Series) -> Tuple[DataFrame, Series]:
164
+ raise_if_null_target(y)
165
+ feature_types = detect_feature_types(x, image_column=self.image_column)
166
+ self.date_transformers = fit_date_encoders(x=x, date_features=feature_types.date_features)
167
+ self.vprint(f"📅 Detected {len(self.date_transformers)} date features: {sorted(self.date_transformers)}")
168
+ # TODO: ConTextTab supports dates natively, perhaps we need to change this to "USE_DATE_TRANSFORMATION"
169
+ x = transform_date_features(x=x, date_transformers=self.date_transformers)
170
+ if self.is_cls:
171
+ self.d_output = len(set(y))
172
+ else:
173
+ self.d_output = 1
174
+ self.image_transformers, self._tuned_dino_model, self._tuned_dino_processor, self._train_embeddings = fit_image_encoders(
175
+ x=x,
176
+ image_features=feature_types.image_features,
177
+ device=self.device,
178
+ image_folder=self.image_folder,
179
+ y=y if self.tune_dino else None,
180
+ is_cls=self.is_cls,
181
+ d_output=self.d_output,
182
+ tune_dino=self.tune_dino,
183
+ dino_train_kwargs=self.dino_train_kwargs if self.tune_dino else None,
184
+ dino_model_name=self.dino_model_name,
185
+ dataset=self.dataset,
186
+ )
187
+ self.vprint(f"📷 Detected {len(self.image_transformers)} image features: {sorted(self.image_transformers)}")
188
+ non_image_columns = [col for col in x.columns if col not in self.image_transformers]
189
+ x = x[non_image_columns]
190
+ date_parts = {part for encoder in self.date_transformers.values() for part in encoder.get_feature_names_out()}
191
+ self.numerical_features = feature_types.numerical_features | date_parts
192
+ self.text_features = feature_types.text_features
193
+ self.categorical_features = feature_types.categorical_features
194
+ self.vprint(f"🔢 Detected {len(self.numerical_features)} numerical features: {sorted(self.numerical_features)}")
195
+ image_features = [f"{col}_img_pca_{i}" for col in self.image_transformers.keys() for i in range(self.image_transformers[col].n_components)]
196
+ x = transform_feature_types(x=x, numerical_features=self.numerical_features, image_features=image_features)
197
+ # Indices here are for the current x (no image/text PCA yet). After transform_preprocessor, column
198
+ # order changes (image cols → PCA, text cols → PCA), so indices no longer match. Models that need
199
+ # categorical columns at fit time (e.g. LightGBM) must resolve by name: [c for c in x.columns if c in self.categorical_features].
200
+ self.categorical_indices = [i for i, c in enumerate(x.columns) if c in self.categorical_features]
201
+ self.target_transformer = fit_preprocess_y(y=y, is_cls=self.is_cls)
202
+ # d_output already set above for fit_image_encoders
203
+ # TODO: drop constant columns, where constants means all values are the same (and no nulls)
204
+ return x, y
205
+
206
+ def predict(self, x: DataFrame) -> np.ndarray:
207
+ x, _ = self.transform_preprocessor(x=x, y=None)
208
+ return self.predict_from_processed(x=x)
209
+
210
+ def predict_from_processed(self, x: DataFrame) -> np.ndarray:
211
+ if not self.is_cls:
212
+ return self.model_.predict(x)
213
+ probs = self.model_.predict_proba(x)
214
+ if self.d_output == 2:
215
+ probs = probs[:, 1]
216
+ return probs
217
+
218
+ def score_all_metrics(self, X, y) -> Dict:
219
+ x = X.copy()
220
+ y = y.copy()
221
+ y_true = transform_preprocess_y(y=y, scaler=self.target_transformer)
222
+ y_pred = self.predict(x)
223
+ metrics = calculate_metric(y_true=y_true, y_pred=y_pred, d_output=self.d_output)
224
+ return metrics
225
+
226
+ def _print_feature_summary(self, x: DataFrame) -> None:
227
+ cols = list(x.columns)
228
+ n_img = sum(1 for c in cols if "_img_pca_" in c)
229
+ n_txt = sum(1 for c in cols if "_txt_pca_" in c)
230
+ n_tab = len(cols) - n_img - n_txt
231
+ parts = [f"tabular: {n_tab}"]
232
+ if n_img:
233
+ parts.append(f"img_emb: {n_img}")
234
+ if n_txt:
235
+ parts.append(f"txt_emb: {n_txt}")
236
+ print(f"🔭 {len(cols)} features → {self.SHORT_NAME} [{' | '.join(parts)}]")
237
+
238
+ def vprint(self, s: str):
239
+ if self.verbose:
240
+ print(s)
@@ -0,0 +1,116 @@
1
+ import hashlib
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import time
6
+
7
+ import numpy as np
8
+ from pandas import DataFrame, Series, notna
9
+
10
+
11
+ from multabench.baselines.preprocessing.target import fit_preprocess_y, transform_preprocess_y
12
+ from multabench.baselines.abstract_model import TabularModel
13
+ from multabench.baselines.preprocessing.feature_types import detect_feature_types
14
+ from multabench.datasets.objects import SupervisedTask
15
+ from multabench.e5.constants import E5_SMALL_V2
16
+
17
+ # DINOv2 (timm format) — DINOv3 is not supported by timm/AutoGluon as of Apr 2026
18
+ AGMM_IMAGE_MODEL = "vit_small_patch14_dinov2.lvd142m"
19
+ AGMM_LABEL = "__target__"
20
+
21
+ TASK2AG = {
22
+ SupervisedTask.BINARY: "binary",
23
+ SupervisedTask.MULTICLASS: "multiclass",
24
+ SupervisedTask.REGRESSION: "regression",
25
+ }
26
+
27
+ # Same metrics the benchmark scores with (baselines/training/metrics.py).
28
+ TASK2METRIC = {
29
+ SupervisedTask.BINARY: "roc_auc",
30
+ SupervisedTask.MULTICLASS: "log_loss",
31
+ SupervisedTask.REGRESSION: "root_mean_squared_error",
32
+ }
33
+
34
+
35
+ def unique_run_dir(dataset_name: str, x: DataFrame) -> str:
36
+ ts = int(time.time())
37
+ data_hash = hashlib.md5(x.head(5).to_csv().encode()).hexdigest()[:8]
38
+ return tempfile.mkdtemp(prefix=f"agmm_{dataset_name}_{ts}_{data_hash}_")
39
+
40
+
41
+ class AutoGluonMM(TabularModel):
42
+
43
+ MODEL_NAME = "AutoGluon-MM"
44
+ SHORT_NAME = "agmm"
45
+ USE_VAL_SPLIT = False
46
+ USE_MEDIAN_FILLING = False
47
+ USE_CATEGORICAL_ENCODING = False
48
+ USE_TEXT_EMBEDDINGS = False
49
+ USE_TARGET_ENCODER = True
50
+
51
+ def initialize_model(self):
52
+ return None
53
+
54
+ def fit_model(self, x_train: DataFrame, y_train: Series, x_val: DataFrame, y_val: Series):
55
+ raise NotImplementedError("AutoGluonMM uses a custom fit() — fit_model() is never called.")
56
+
57
+ def fit(self, x: DataFrame, y: Series):
58
+ from autogluon.multimodal import MultiModalPredictor
59
+ # Fit target encoder for consistent scoring with other baselines
60
+ self.target_transformer = fit_preprocess_y(y=y, is_cls=self.is_cls)
61
+ y_enc = transform_preprocess_y(y=y, scaler=self.target_transformer)
62
+ if self.is_cls:
63
+ self.d_output = len(set(y_enc))
64
+ else:
65
+ self.d_output = 1
66
+
67
+ # Detect modalities
68
+ feature_types = detect_feature_types(x, image_column=self.image_column)
69
+ image_cols = feature_types.image_features
70
+ text_cols = feature_types.text_features
71
+
72
+ # Expand image file paths
73
+ x_ag = x.copy()
74
+ if image_cols and self.image_folder:
75
+ for col in image_cols:
76
+ x_ag[col] = x_ag[col].apply(lambda f: os.path.join(self.image_folder, str(f)) if notna(f) else f)
77
+
78
+ # Build base model config based on detected modalities
79
+ has_image = bool(image_cols and self.image_folder)
80
+ has_text = bool(text_cols)
81
+ hparams = {}
82
+ if has_image:
83
+ hparams["model.timm_image.checkpoint_name"] = AGMM_IMAGE_MODEL
84
+ if has_text:
85
+ hparams["model.hf_text.checkpoint_name"] = E5_SMALL_V2
86
+
87
+ df_train = x_ag.copy()
88
+ df_train[AGMM_LABEL] = y_enc.values
89
+
90
+ output_dir = unique_run_dir(dataset_name=getattr(self.dataset, "name", "unknown"), x=x)
91
+ self.predictor_ = MultiModalPredictor(
92
+ label=AGMM_LABEL,
93
+ problem_type=TASK2AG[self.problem_type],
94
+ eval_metric=TASK2METRIC[self.problem_type],
95
+ path=output_dir,
96
+ pretrained=True,
97
+ )
98
+ self.predictor_.fit(df_train, hyperparameters=hparams, clean_ckpts=True)
99
+
100
+ def _prepare_x(self, x: DataFrame) -> DataFrame:
101
+ x_ag = x.copy()
102
+ if self.image_column and self.image_folder:
103
+ col = self.image_column
104
+ x_ag[col] = x_ag[col].apply(lambda f: os.path.join(self.image_folder, str(f)) if notna(f) else f)
105
+ return x_ag
106
+
107
+ def predict(self, x: DataFrame) -> np.ndarray:
108
+ x_ag = self._prepare_x(x)
109
+ if not self.is_cls:
110
+ result = self.predictor_.predict(x_ag).to_numpy()
111
+ else:
112
+ proba = self.predictor_.predict_proba(x_ag).to_numpy()
113
+ result = proba[:, 1] if self.d_output == 2 else proba
114
+ shutil.rmtree(self.predictor_.path, ignore_errors=True)
115
+ self.predictor_ = None
116
+ return result
File without changes
@@ -0,0 +1,109 @@
1
+ import time
2
+ from typing import Type, Dict
3
+
4
+ import torch
5
+
6
+ from multabench.datasets.all_datasets import MulTaBenchDatasetID
7
+ from multabench.datasets.objects import MultimodalDataset
8
+ from multabench.baselines.abstract_model import TabularModel
9
+ from multabench.benchmark.load import load_multabench_dataset, split_dataset
10
+ from multabench.benchmark.splits import Size
11
+ from multabench.utils.hardware import get_hardware_dict
12
+ from multabench.dino.constants import DINOV3_SMALL
13
+ from multabench.e5.constants import E5_SMALL_V2
14
+ from multabench.utils.logging import get_current_commit_hash
15
+ from multabench.result_keys import ResultKey
16
+ from multabench.utils.profiling import PeakMemoryTracker
17
+ from multabench.utils.timing import pop_embedding_seconds
18
+
19
+ def evaluate_on_loaded_dataset(model_cls: Type[TabularModel],
20
+ dataset: MultimodalDataset,
21
+ fold: int,
22
+ device: torch.device,
23
+ size: Size = Size.TEN_K,
24
+ verbose: bool = False,
25
+ tune_dino: bool = False,
26
+ dino_train_kwargs: dict | None = None,
27
+ dino_model_name: str = DINOV3_SMALL,
28
+ tune_e5: bool = False,
29
+ e5_train_kwargs: dict | None = None,
30
+ e5_model_name: str = E5_SMALL_V2) -> Dict:
31
+ start_time = time.time()
32
+ dataset_id = dataset.dataset_id
33
+ split = split_dataset(dataset, fold=fold, size=size)
34
+ x_train, y_train = split.x_train, split.y_train
35
+ x_test, y_test = split.x_test, split.y_test
36
+ kwargs = dict(problem_type=dataset.task_type, device=device, verbose=verbose, dataset=dataset_id,
37
+ image_folder=dataset.image_folder, image_column=dataset.image_column, tune_dino=tune_dino, dino_train_kwargs=dino_train_kwargs,
38
+ dino_model_name=dino_model_name,
39
+ tune_e5=tune_e5, e5_train_kwargs=e5_train_kwargs, e5_model_name=e5_model_name)
40
+ model = model_cls(**kwargs)
41
+ pop_embedding_seconds()
42
+ with PeakMemoryTracker(phase='train', device=device) as train_tracker:
43
+ model.fit(x_train, y_train)
44
+ train_embedding_s = pop_embedding_seconds()
45
+ with PeakMemoryTracker(phase='inference', device=device) as test_tracker:
46
+ metrics = model.score_all_metrics(X=x_test, y=y_test)
47
+ inference_embedding_s = pop_embedding_seconds()
48
+ runtime = time.time() - start_time
49
+ d_summary = {
50
+ ResultKey.TIMESTAMP: time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
51
+ ResultKey.GIT: get_current_commit_hash(),
52
+ ResultKey.MODEL: model_cls.MODEL_NAME,
53
+ ResultKey.DATASET: dataset_id.name,
54
+ ResultKey.TASK_TYPE: str(dataset_id.name)[:3],
55
+ ResultKey.TRAIN_TYPE: "benchmark",
56
+ ResultKey.FOLD: fold,
57
+ ResultKey.SIZE: size,
58
+ **metrics,
59
+ ResultKey.RUNTIME: runtime,
60
+ ResultKey.N_TRAIN: len(y_train),
61
+ ResultKey.N_TEST: len(y_test),
62
+ ResultKey.M_FEATURES: x_train.shape[1],
63
+ ResultKey.TUNE_DINO: tune_dino,
64
+ ResultKey.TUNE_E5: tune_e5,
65
+ ResultKey.E5_MODEL_NAME: e5_model_name,
66
+ ResultKey.BEST_VAL_LOSS: getattr(model, "best_val_loss", None),
67
+ **train_tracker.summary(),
68
+ **test_tracker.summary(),
69
+ ResultKey.TRAIN_TIME_PER_1K: (train_tracker.wall_time_s - train_embedding_s) / len(y_train) * 1000,
70
+ ResultKey.INFERENCE_TIME_PER_1K: (test_tracker.wall_time_s - inference_embedding_s) / len(y_test) * 1000,
71
+ ResultKey.TRAIN_EMBEDDING_TIME_PER_1K: train_embedding_s / len(y_train) * 1000,
72
+ ResultKey.INFERENCE_EMBEDDING_TIME_PER_1K: inference_embedding_s / len(y_test) * 1000,
73
+ **get_hardware_dict(device),
74
+ **(dino_train_kwargs or {}),
75
+ **(e5_train_kwargs or {}),
76
+ }
77
+ metric, error = metrics[ResultKey.METRIC], metrics[ResultKey.TEST_ERROR]
78
+ print(f"{dataset_id.name} fold {fold}: {metric} error {error:.4f} ({runtime:.0f}s)")
79
+ return d_summary
80
+
81
+
82
+ def evaluate_on_dataset(model_cls: Type[TabularModel],
83
+ dataset_id: MulTaBenchDatasetID,
84
+ fold: int,
85
+ device: torch.device,
86
+ size: Size = Size.TEN_K,
87
+ verbose: bool = False,
88
+ tune_dino: bool = False,
89
+ dino_train_kwargs: dict | None = None,
90
+ dino_model_name: str | None = None,
91
+ tune_e5: bool = False,
92
+ e5_train_kwargs: dict | None = None,
93
+ e5_model_name: str = E5_SMALL_V2) -> Dict:
94
+ print(f"Running model {model_cls.MODEL_NAME} over dataset {dataset_id} with fold {fold}")
95
+ dataset = load_multabench_dataset(dataset_id)
96
+ return evaluate_on_loaded_dataset(
97
+ model_cls=model_cls,
98
+ dataset=dataset,
99
+ fold=fold,
100
+ device=device,
101
+ size=size,
102
+ verbose=verbose,
103
+ tune_dino=tune_dino,
104
+ dino_train_kwargs=dino_train_kwargs,
105
+ dino_model_name=dino_model_name,
106
+ tune_e5=tune_e5,
107
+ e5_train_kwargs=e5_train_kwargs,
108
+ e5_model_name=e5_model_name,
109
+ )
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+
3
+ from pandas import DataFrame, Series
4
+
5
+ from multabench.constants import SEED
6
+ from multabench.utils.devices import CPU_CORES
7
+ from multabench.baselines.abstract_model import TabularModel
8
+
9
+
10
+ @dataclass
11
+ class CatBoostDefaultHyperparams:
12
+ # Using the "default" hyperparameters of the FT-Transformer paper: https://arxiv.org/pdf/2106.11959
13
+ early_stopping_rounds: int = 50
14
+ iterations: int = 2000
15
+ od_pval: float = 0.001
16
+ random_state: int = SEED
17
+ thread_count = CPU_CORES
18
+
19
+ class CatBoost(TabularModel):
20
+
21
+ MODEL_NAME = "CatBoost"
22
+ SHORT_NAME = "cat"
23
+ USE_VAL_SPLIT = True
24
+ USE_MEDIAN_FILLING = False
25
+ USE_CATEGORICAL_ENCODING = False
26
+ USE_TEXT_EMBEDDINGS = True
27
+ USE_TARGET_ENCODER = True
28
+ NEEDS_GPU = False
29
+
30
+ def initialize_model(self):
31
+ from catboost import CatBoostRegressor, CatBoostClassifier
32
+ model_cls = CatBoostClassifier if self.is_cls else CatBoostRegressor
33
+ params = vars(CatBoostDefaultHyperparams())
34
+ return model_cls(**params)
35
+
36
+ def fit_model(self, x_train: DataFrame, y_train: Series, x_val: DataFrame, y_val: Series):
37
+ cat_features = [i for i, c in enumerate(x_train.columns) if c in self.categorical_features]
38
+ logging_level = "Verbose" if self.verbose else "Silent"
39
+ self.model_.fit(x_train, y_train, eval_set=(x_val, y_val), use_best_model=True, cat_features=cat_features,
40
+ logging_level=logging_level)
@@ -0,0 +1,30 @@
1
+ import os
2
+
3
+ from pandas import DataFrame, Series
4
+
5
+ from multabench.baselines.abstract_model import TabularModel
6
+ from multabench.constants import HF_TOKEN
7
+
8
+
9
+ class ConTextTab(TabularModel):
10
+ MODEL_NAME = "ConTextTab"
11
+ SHORT_NAME = "ctx"
12
+ USE_VAL_SPLIT = False
13
+ USE_MEDIAN_FILLING = False
14
+ USE_CATEGORICAL_ENCODING = False
15
+ # TODO: Should we use text embeddings for ConTextTab?
16
+ USE_TEXT_EMBEDDINGS = False
17
+ USE_TARGET_ENCODER = False
18
+
19
+ def initialize_model(self):
20
+ from sap_rpt_oss import SAP_RPT_OSS_Classifier, SAP_RPT_OSS_Regressor
21
+ os.environ["HF_TOKEN"] = HF_TOKEN or ""
22
+ model_cls = SAP_RPT_OSS_Classifier if self.is_cls else SAP_RPT_OSS_Regressor
23
+ # Initialize the regressor, 8k context and 8-fold bagging gives best performance, reduce if running out of memory
24
+ model = model_cls(max_context_size=8192, bagging=8)
25
+ return model
26
+
27
+ def fit_model(self, x_train: DataFrame, y_train: Series, x_val: DataFrame, y_val: Series):
28
+ # TODO: ConTextTab natively supports dates, perhaps we need to process them differently
29
+ self.model_.fit(x_train, y_train)
30
+
@@ -0,0 +1,103 @@
1
+ import numpy as np
2
+ import torch
3
+ from pandas import DataFrame, Series
4
+
5
+ from multabench.baselines.abstract_model import TabularModel
6
+ from multabench.constants import SEED
7
+
8
+ TARGET = "__target__"
9
+
10
+
11
+ class KumoEstimator:
12
+ """Kumo Tabular on pandas, as TabArena and NVIDIA's adapter run it: fit caches the context, predict reads each
13
+ query row from it under float16 autocast."""
14
+
15
+ def __init__(self, task: str, size: str, num_estimators: int, n_classes: int, device: torch.device):
16
+ self.task = task
17
+ self.size = size
18
+ self.num_estimators = num_estimators
19
+ self.n_classes = n_classes
20
+ self.device = device
21
+
22
+ def _inference(self):
23
+ return torch.amp.autocast(self.device.type, dtype=torch.float16, enabled=self.device.type == "cuda")
24
+
25
+ def fit(self, x: DataFrame, y: Series):
26
+ import sdm
27
+ self.model = sdm.models.KumoTabular(task=self.task, size=self.size, device=self.device)
28
+ self.stypes = sdm.infer_stypes(x, _low_cardinality="infer")
29
+ x_context = sdm.TableTensor.from_pandas(df=x, stypes=self.stypes, device=self.device)
30
+ target_stype = "numerical" if self.task == "regression" else "categorical"
31
+ y_context = sdm.TableTensor.from_pandas(df=y.rename(TARGET).to_frame(), stypes={TARGET: target_stype},
32
+ device=self.device)
33
+ rows, cols = x_context.shape[-2:]
34
+ # NVIDIA's adapter fits one ensemble member at a time above these sizes, to bound GPU memory.
35
+ batch_size = 1 if rows > 2_000 or rows * cols >= 50_000 else self.num_estimators
36
+ generator = torch.Generator(self.device).manual_seed(SEED)
37
+ with torch.inference_mode(), self._inference():
38
+ self.model.fit(x=x_context, y=y_context, num_estimators=self.num_estimators,
39
+ estimator_batch_size=batch_size, generator=generator)
40
+ return self
41
+
42
+ def _forward(self, x: DataFrame):
43
+ import sdm
44
+ x_query = sdm.TableTensor.from_pandas(df=x, stypes=self.stypes, device=self.device)
45
+ with torch.inference_mode(), self._inference():
46
+ return self.model.predict(x_query)
47
+
48
+ def predict(self, x: DataFrame) -> np.ndarray:
49
+ """The mean of the predicted quantiles."""
50
+ return self._forward(x).numerical.float().mean(dim=-1).cpu().numpy()
51
+
52
+ def predict_proba(self, x: DataFrame) -> np.ndarray:
53
+ import sdm
54
+ out = self._forward(x)
55
+ probs = np.zeros((len(x), self.n_classes), dtype=np.float32)
56
+ probs[:, [int(label) for label in out.columns[sdm.Stype.numerical]]] = out.numerical.float().cpu().numpy()
57
+ return probs
58
+
59
+
60
+ class Kumo(TabularModel):
61
+ """NVIDIA's Kumo Tabular; its sizes differ only in the checkpoint (SIZE) and TabArena's NUM_ESTIMATORS."""
62
+
63
+ USE_VAL_SPLIT = False
64
+ USE_MEDIAN_FILLING = False
65
+ USE_CATEGORICAL_ENCODING = False
66
+ USE_TEXT_EMBEDDINGS = True
67
+ USE_TARGET_ENCODER = True
68
+
69
+ SIZE: str
70
+ NUM_ESTIMATORS: int
71
+
72
+ def initialize_model(self) -> None:
73
+ return None
74
+
75
+ def fit_model(self, x_train: DataFrame, y_train: Series, x_val: DataFrame, y_val: Series):
76
+ task = "classification" if self.is_cls else "regression"
77
+ self.model_ = KumoEstimator(task=task, size=self.SIZE, num_estimators=self.NUM_ESTIMATORS,
78
+ n_classes=self.d_output, device=self.device)
79
+ self.model_.fit(x_train, y_train)
80
+
81
+
82
+ class KumoTabular(Kumo):
83
+
84
+ MODEL_NAME = "Kumo-Tabular"
85
+ SHORT_NAME = "kumo"
86
+ SIZE = "large"
87
+ NUM_ESTIMATORS = 16
88
+
89
+
90
+ class KumoTabularMedium(Kumo):
91
+
92
+ MODEL_NAME = "Kumo-Tabular-Medium"
93
+ SHORT_NAME = "kumomedium"
94
+ SIZE = "medium"
95
+ NUM_ESTIMATORS = 8
96
+
97
+
98
+ class KumoTabularSmall(Kumo):
99
+
100
+ MODEL_NAME = "Kumo-Tabular-Small"
101
+ SHORT_NAME = "kumosmall"
102
+ SIZE = "small"
103
+ NUM_ESTIMATORS = 8
@@ -0,0 +1,28 @@
1
+ from pandas import DataFrame, Series
2
+
3
+ from multabench.constants import SEED
4
+ from multabench.baselines.abstract_model import TabularModel
5
+ from multabench.utils.devices import CPU_CORES
6
+
7
+
8
+ class LightGBM(TabularModel):
9
+
10
+ MODEL_NAME = "LightGBM"
11
+ SHORT_NAME = "light"
12
+ USE_VAL_SPLIT = True
13
+ USE_MEDIAN_FILLING = False
14
+ USE_CATEGORICAL_ENCODING = True
15
+ USE_TEXT_EMBEDDINGS = True
16
+ USE_TARGET_ENCODER = True
17
+ NEEDS_GPU = False
18
+
19
+ def initialize_model(self):
20
+ from lightgbm import LGBMClassifier, LGBMRegressor
21
+ model_cls = LGBMClassifier if self.is_cls else LGBMRegressor
22
+ params = {"verbose": -1, "random_state": SEED, "n_jobs": CPU_CORES}
23
+ return model_cls(**params)
24
+
25
+ def fit_model(self, x_train: DataFrame, y_train: Series, x_val: DataFrame, y_val: Series):
26
+ # Use column names so we only mark truly categorical columns; indices can point to PCA cols after transforms.
27
+ cat_cols = [c for c in x_train.columns if c in self.categorical_features]
28
+ self.model_.fit(x_train, y_train, eval_set=[(x_val, y_val)], categorical_feature=cat_cols)
File without changes