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.
- multabench/__init__.py +2 -0
- multabench/baselines/__init__.py +0 -0
- multabench/baselines/abstract_model.py +240 -0
- multabench/baselines/autogluon_mm.py +116 -0
- multabench/baselines/benchmarks/__init__.py +0 -0
- multabench/baselines/benchmarks/evaluate.py +109 -0
- multabench/baselines/catboost.py +40 -0
- multabench/baselines/contexttab.py +30 -0
- multabench/baselines/kumo_tabular.py +103 -0
- multabench/baselines/lgbm.py +28 -0
- multabench/baselines/preprocessing/__init__.py +0 -0
- multabench/baselines/preprocessing/categorical.py +24 -0
- multabench/baselines/preprocessing/dates.py +29 -0
- multabench/baselines/preprocessing/feature_types.py +48 -0
- multabench/baselines/preprocessing/image_embeddings.py +208 -0
- multabench/baselines/preprocessing/nulls.py +10 -0
- multabench/baselines/preprocessing/numerical.py +13 -0
- multabench/baselines/preprocessing/target.py +21 -0
- multabench/baselines/preprocessing/text_embeddings.py +271 -0
- multabench/baselines/preprocessing/validation.py +83 -0
- multabench/baselines/random_forest.py +35 -0
- multabench/baselines/realmlp.py +41 -0
- multabench/baselines/tabdpt.py +37 -0
- multabench/baselines/tabicl_v2.py +22 -0
- multabench/baselines/tabm.py +35 -0
- multabench/baselines/tabpfn.py +70 -0
- multabench/baselines/tabstar_v1.py +86 -0
- multabench/baselines/training/__init__.py +0 -0
- multabench/baselines/training/metrics.py +37 -0
- multabench/baselines/xgboost.py +38 -0
- multabench/benchmark/__init__.py +0 -0
- multabench/benchmark/collect.py +37 -0
- multabench/benchmark/load.py +129 -0
- multabench/benchmark/runs.py +35 -0
- multabench/benchmark/splits.py +53 -0
- multabench/benchmark/sweep.py +122 -0
- multabench/benchmark/utils/__init__.py +0 -0
- multabench/benchmark/utils/constants.py +2 -0
- multabench/benchmark/utils/curation.py +12 -0
- multabench/constants.py +10 -0
- multabench/datasets/__init__.py +0 -0
- multabench/datasets/all_datasets.py +241 -0
- multabench/datasets/hub.py +31 -0
- multabench/datasets/objects.py +37 -0
- multabench/dino/__init__.py +0 -0
- multabench/dino/constants.py +41 -0
- multabench/dino/dino_finetune.py +240 -0
- multabench/dino/image_loading.py +38 -0
- multabench/e5/__init__.py +0 -0
- multabench/e5/constants.py +61 -0
- multabench/e5/e5_finetune.py +317 -0
- multabench/embeddings/__init__.py +0 -0
- multabench/embeddings/compute.py +87 -0
- multabench/embeddings/embed.py +30 -0
- multabench/embeddings/embed_sweep.py +72 -0
- multabench/embeddings/hub.py +150 -0
- multabench/embeddings/jobs.py +32 -0
- multabench/finetune/train_args.py +29 -0
- multabench/finetune/utils.py +54 -0
- multabench/leaderboard/__init__.py +0 -0
- multabench/leaderboard/data/__init__.py +0 -0
- multabench/leaderboard/data/keys.py +45 -0
- multabench/leaderboard/data/loading.py +97 -0
- multabench/leaderboard/elo.py +112 -0
- multabench/leaderboard/large_results.py +193 -0
- multabench/leaderboard/paper_results.py +177 -0
- multabench/leaderboard/plots.py +159 -0
- multabench/leaderboard/utils.py +20 -0
- multabench/preprocessing/__init__.py +0 -0
- multabench/preprocessing/discretize.py +19 -0
- multabench/preprocessing/splits.py +89 -0
- multabench/result_keys.py +52 -0
- multabench/scripts/do_leaderboard.py +17 -0
- multabench/utils/__init__.py +0 -0
- multabench/utils/devices.py +23 -0
- multabench/utils/encoders.py +6 -0
- multabench/utils/hardware.py +143 -0
- multabench/utils/logging.py +23 -0
- multabench/utils/profiling.py +66 -0
- multabench/utils/timing.py +28 -0
- multabench/utils/warnings.py +97 -0
- multabench-0.0.1.dist-info/METADATA +277 -0
- multabench-0.0.1.dist-info/RECORD +86 -0
- multabench-0.0.1.dist-info/WHEEL +5 -0
- multabench-0.0.1.dist-info/licenses/LICENSE +21 -0
- multabench-0.0.1.dist-info/top_level.txt +1 -0
multabench/__init__.py
ADDED
|
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
|