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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Set, Dict
|
|
2
|
+
|
|
3
|
+
from pandas import Series, DataFrame
|
|
4
|
+
from sklearn.preprocessing import LabelEncoder
|
|
5
|
+
|
|
6
|
+
from multabench.baselines.preprocessing.nulls import MISSING_VALUE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def fit_categorical_encoders(x: DataFrame, categorical_features: Set[str]) -> Dict[str, LabelEncoder]:
|
|
10
|
+
return {col: fit_encode_categorical(s=x[col]) for col in categorical_features}
|
|
11
|
+
|
|
12
|
+
def fit_encode_categorical(s: Series) -> LabelEncoder:
|
|
13
|
+
encoder = LabelEncoder()
|
|
14
|
+
train_values = set(s).union({MISSING_VALUE})
|
|
15
|
+
encoder.fit(list(train_values))
|
|
16
|
+
return encoder
|
|
17
|
+
|
|
18
|
+
def transform_categorical_features(x: DataFrame, categorical_encoders: Dict[str, LabelEncoder]) -> DataFrame:
|
|
19
|
+
x = x.copy()
|
|
20
|
+
for col, encoder in categorical_encoders.items():
|
|
21
|
+
s = x[col].apply(lambda v: v if v in encoder.classes_ else MISSING_VALUE)
|
|
22
|
+
s = encoder.transform(s).astype(int)
|
|
23
|
+
x[col] = s
|
|
24
|
+
return x
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Datetime columns expanded into numeric parts (copied from tabstar 1.1.15, tabstar/preprocessing/dates.py)."""
|
|
2
|
+
from typing import Dict, Set
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
from skrub import DatetimeEncoder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def transform_date_features(x: DataFrame, date_transformers: Dict[str, DatetimeEncoder]) -> DataFrame:
|
|
10
|
+
rows = x.shape[0]
|
|
11
|
+
for col, dt_encoder in date_transformers.items():
|
|
12
|
+
dt_df = dt_encoder.transform(x[col])
|
|
13
|
+
dt_df.index = x.index
|
|
14
|
+
x = x.drop(columns=[col])
|
|
15
|
+
x = pd.concat([x, dt_df], axis=1)
|
|
16
|
+
if x.shape[0] != rows:
|
|
17
|
+
raise ValueError(f"Row mismatch after transforming date column {col}: expected {rows}, got {x.shape}")
|
|
18
|
+
return x
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fit_date_encoders(x: DataFrame, date_features: Set[str]) -> Dict[str, DatetimeEncoder]:
|
|
22
|
+
date_encoders = {}
|
|
23
|
+
for col in [c for c in x.columns if c in date_features]:
|
|
24
|
+
# Adds: "year", "month", "day", "hour", "total_seconds", "weekday"
|
|
25
|
+
encoder = DatetimeEncoder(add_weekday=True, add_total_seconds=True)
|
|
26
|
+
encoder.fit(x[col])
|
|
27
|
+
date_encoders[col] = encoder
|
|
28
|
+
return date_encoders
|
|
29
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Feature types, read from the stored dtypes and the dataset's image column, and the dtype conversion per type."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Optional, Set
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pandas import DataFrame
|
|
7
|
+
from pandas.api.types import is_bool_dtype, is_datetime64_any_dtype, is_numeric_dtype
|
|
8
|
+
|
|
9
|
+
from multabench.baselines.preprocessing.nulls import MISSING_VALUE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class FeatureTypes:
|
|
14
|
+
numerical_features: Set[str] = field(default_factory=set)
|
|
15
|
+
categorical_features: Set[str] = field(default_factory=set)
|
|
16
|
+
text_features: Set[str] = field(default_factory=set)
|
|
17
|
+
date_features: Set[str] = field(default_factory=set)
|
|
18
|
+
image_features: Set[str] = field(default_factory=set)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def detect_feature_types(x: DataFrame, image_column: Optional[str]) -> FeatureTypes:
|
|
22
|
+
result = FeatureTypes()
|
|
23
|
+
for col in x.columns:
|
|
24
|
+
if col == image_column:
|
|
25
|
+
result.image_features.add(col)
|
|
26
|
+
elif is_datetime64_any_dtype(x[col]):
|
|
27
|
+
result.date_features.add(col)
|
|
28
|
+
elif isinstance(x[col].dtype, pd.CategoricalDtype):
|
|
29
|
+
result.categorical_features.add(col)
|
|
30
|
+
elif is_numeric_dtype(x[col]) or is_bool_dtype(x[col]):
|
|
31
|
+
result.numerical_features.add(col)
|
|
32
|
+
else:
|
|
33
|
+
result.text_features.add(col)
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def transform_feature_types(x: DataFrame, numerical_features: set[str], image_features: set[str]) -> DataFrame:
|
|
38
|
+
new_x = {}
|
|
39
|
+
for col in x.columns:
|
|
40
|
+
if col in numerical_features:
|
|
41
|
+
new_x[col] = x[col].astype(float)
|
|
42
|
+
elif col in image_features:
|
|
43
|
+
new_x[col] = x[col]
|
|
44
|
+
else:
|
|
45
|
+
new_x[col] = x[col].astype(object).fillna(MISSING_VALUE).astype(str)
|
|
46
|
+
new_x = DataFrame(new_x, index=x.index)
|
|
47
|
+
ordered_x = new_x[x.columns]
|
|
48
|
+
return ordered_x
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PIL import Image
|
|
3
|
+
from typing import Dict, Tuple, Optional, List, Any, Set
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import torch
|
|
7
|
+
from pandas import DataFrame, Series
|
|
8
|
+
from sklearn.decomposition import PCA
|
|
9
|
+
from sklearn.preprocessing import StandardScaler
|
|
10
|
+
from multabench.constants import SEED
|
|
11
|
+
from multabench.datasets.all_datasets import MulTaBenchDatasetID
|
|
12
|
+
from transformers import AutoImageProcessor, AutoModel, DINOv3ViTImageProcessorFast, DINOv3ViTModel
|
|
13
|
+
|
|
14
|
+
from multabench.dino.constants import DINOV3_SMALL
|
|
15
|
+
from multabench.dino.dino_finetune import finetune_dino_with_lora
|
|
16
|
+
from multabench.dino.image_loading import load_images
|
|
17
|
+
from multabench.embeddings.hub import CachedEmbeddings, cached_embeddings, cached_encoder
|
|
18
|
+
from multabench.preprocessing.discretize import discretize_numerical
|
|
19
|
+
from multabench.utils.warnings import suppress_channel_dimension_warning
|
|
20
|
+
from multabench.preprocessing.splits import split_to_val
|
|
21
|
+
from multabench.utils.timing import embedding_step
|
|
22
|
+
|
|
23
|
+
# Alternatives: facebook/dinov3-vits16plus-pretrain-lvd1689m, facebook/dinov3-convnext-tiny-pretrain-lvd1689m
|
|
24
|
+
|
|
25
|
+
PCA_COMPONENTS = 30
|
|
26
|
+
IMAGE_BATCH_SIZE = 64
|
|
27
|
+
|
|
28
|
+
IMAGE_ENCODER = {"encoder": None, "processor": None}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _ScaledPCA:
|
|
32
|
+
"""StandardScaler then PCA; exposes .transform(X) and .n_components for drop-in use. Holds the column's cached
|
|
33
|
+
embeddings when it has them."""
|
|
34
|
+
def __init__(self, scaler: StandardScaler, pca: PCA, cache: Optional[CachedEmbeddings] = None):
|
|
35
|
+
self.scaler = scaler
|
|
36
|
+
self.pca = pca
|
|
37
|
+
self.n_components = pca.n_components
|
|
38
|
+
self.cache = cache
|
|
39
|
+
|
|
40
|
+
def transform(self, X: np.ndarray) -> np.ndarray:
|
|
41
|
+
return self.pca.transform(self.scaler.transform(X))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fit_image_encoders(
|
|
45
|
+
x: DataFrame,
|
|
46
|
+
image_features: Set[str],
|
|
47
|
+
device: torch.device,
|
|
48
|
+
image_folder: Optional[str],
|
|
49
|
+
is_cls: bool,
|
|
50
|
+
d_output: int,
|
|
51
|
+
y: Optional[Series] = None,
|
|
52
|
+
tune_dino: bool = False,
|
|
53
|
+
dino_train_kwargs: Optional[Dict[str, Any]] = None,
|
|
54
|
+
dino_model_name: str = DINOV3_SMALL,
|
|
55
|
+
dataset: Optional[MulTaBenchDatasetID] = None,
|
|
56
|
+
) -> Tuple[Dict[str, Any], Optional[Any], Optional[DINOv3ViTImageProcessorFast], Dict[str, np.ndarray]]:
|
|
57
|
+
"""
|
|
58
|
+
Fit PCA per image column. If tune_dino and y is provided,
|
|
59
|
+
finetune DINO with LoRA on train/val split then use the best model for encoding.
|
|
60
|
+
Otherwise columns with cached embeddings on the Hub are read from there instead of being encoded.
|
|
61
|
+
Returns (image_encoders, tuned_dino_model_or_none, tuned_processor_or_none, train_embeddings per column).
|
|
62
|
+
"""
|
|
63
|
+
image_encoders: Dict[str, Any] = {}
|
|
64
|
+
train_embeddings: Dict[str, np.ndarray] = {}
|
|
65
|
+
image_features = [c for c in x.columns if c in image_features]
|
|
66
|
+
tuned_model = None
|
|
67
|
+
tuned_processor: Optional[DINOv3ViTImageProcessorFast] = None
|
|
68
|
+
|
|
69
|
+
if not image_features:
|
|
70
|
+
return image_encoders, tuned_model, tuned_processor, train_embeddings
|
|
71
|
+
if image_folder is None:
|
|
72
|
+
raise ValueError(f"Image columns {image_features} need an image_folder")
|
|
73
|
+
|
|
74
|
+
if tune_dino and y is not None:
|
|
75
|
+
if not is_cls:
|
|
76
|
+
y = discretize_numerical(y, n_bins=20)
|
|
77
|
+
x_tr, x_val, y_tr, y_val = split_to_val(x=x, y=y, is_cls=True)
|
|
78
|
+
first_col = image_features[0]
|
|
79
|
+
train_imgs = load_images(s=x_tr[first_col], image_folder=image_folder)
|
|
80
|
+
val_imgs = load_images(s=x_val[first_col], image_folder=image_folder)
|
|
81
|
+
kwargs = dino_train_kwargs or {}
|
|
82
|
+
tuned_model, tuned_processor = finetune_dino_with_lora(
|
|
83
|
+
train_images=train_imgs,
|
|
84
|
+
train_y=y_tr.values,
|
|
85
|
+
val_images=val_imgs,
|
|
86
|
+
val_y=y_val.values,
|
|
87
|
+
device=device,
|
|
88
|
+
processor=AutoImageProcessor.from_pretrained(dino_model_name, use_fast=True),
|
|
89
|
+
model_name=dino_model_name,
|
|
90
|
+
**kwargs,
|
|
91
|
+
)
|
|
92
|
+
tuned_model.to(device)
|
|
93
|
+
dino_model, img_processor = tuned_model, tuned_processor
|
|
94
|
+
caches = {} if tune_dino else _caches_per_column(dino_model_name, dataset)
|
|
95
|
+
if not tune_dino and not set(caches).issuperset(image_features):
|
|
96
|
+
dino_model, img_processor = get_image_encoder(model_name=dino_model_name)
|
|
97
|
+
dino_model.to(device)
|
|
98
|
+
|
|
99
|
+
for col in image_features:
|
|
100
|
+
cache = caches.get(col)
|
|
101
|
+
if cache is None:
|
|
102
|
+
images = load_images(s=x[col], image_folder=image_folder)
|
|
103
|
+
embeddings = encode_images_in_batches(images=images, processor=img_processor, model=dino_model)
|
|
104
|
+
else:
|
|
105
|
+
embeddings = cache.lookup(x[col].astype(str).tolist(), feature=col)
|
|
106
|
+
scaler = StandardScaler()
|
|
107
|
+
scaler.fit(embeddings)
|
|
108
|
+
scaled = scaler.transform(embeddings)
|
|
109
|
+
pca_col = PCA(n_components=PCA_COMPONENTS, random_state=SEED)
|
|
110
|
+
pca_col.fit(scaled)
|
|
111
|
+
image_encoders[col] = _ScaledPCA(scaler, pca_col, cache=cache)
|
|
112
|
+
train_embeddings[col] = embeddings
|
|
113
|
+
|
|
114
|
+
return image_encoders, tuned_model, tuned_processor, train_embeddings
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _caches_per_column(dino_model_name: str, dataset: Optional[MulTaBenchDatasetID]) -> Dict[str, CachedEmbeddings]:
|
|
118
|
+
"""The cached embeddings of each image column that has them."""
|
|
119
|
+
encoder = cached_encoder(dino_model_name, tuned=False)
|
|
120
|
+
if encoder is None or dataset is None:
|
|
121
|
+
return {}
|
|
122
|
+
cache = cached_embeddings(encoder, dataset)
|
|
123
|
+
print(f"Reading the image embeddings of {cache.meta['features']} from the cached {encoder} embeddings")
|
|
124
|
+
return {col: cache for col in cache.meta["features"]}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def transform_image_features(
|
|
128
|
+
x: DataFrame,
|
|
129
|
+
image_transformers: Dict[str, Any],
|
|
130
|
+
device: torch.device,
|
|
131
|
+
image_folder: str,
|
|
132
|
+
dino_model: Optional[DINOv3ViTModel] = None,
|
|
133
|
+
dino_processor: Optional[DINOv3ViTImageProcessorFast] = None,
|
|
134
|
+
dino_model_name: str = DINOV3_SMALL,
|
|
135
|
+
train_embeddings: Optional[Dict[str, np.ndarray]] = None,
|
|
136
|
+
) -> DataFrame:
|
|
137
|
+
# TODO: In realistic scenarios where we tune the model repeatedly, the efficient way would be to fit the encoder once,
|
|
138
|
+
# cache the embeddings, and then apply PCA on the cached embeddings over and over for every train split in every run.
|
|
139
|
+
if not image_transformers:
|
|
140
|
+
return x
|
|
141
|
+
train_embeddings = train_embeddings or {}
|
|
142
|
+
for image_col, image_pca in image_transformers.items():
|
|
143
|
+
assert image_col in x.columns, f"Image column {image_col} not found in DataFrame"
|
|
144
|
+
s = x[image_col]
|
|
145
|
+
embeddings = train_embeddings.get(image_col)
|
|
146
|
+
if embeddings is None and image_pca.cache is not None:
|
|
147
|
+
embeddings = image_pca.cache.lookup(s.astype(str).tolist(), feature=image_col)
|
|
148
|
+
if embeddings is None:
|
|
149
|
+
if dino_model is None or dino_processor is None:
|
|
150
|
+
dino_model, dino_processor = get_image_encoder(model_name=dino_model_name)
|
|
151
|
+
dino_model.to(device)
|
|
152
|
+
embeddings = image_urls_to_embeddings(s=s, image_folder=image_folder, processor=dino_processor, model=dino_model)
|
|
153
|
+
assert len(embeddings) == len(x)
|
|
154
|
+
pca_vec = image_pca.transform(embeddings).astype(np.float32)
|
|
155
|
+
pca_cols = [f"{image_col}_img_pca_{i}" for i in range(pca_vec.shape[1])]
|
|
156
|
+
pca_df = pd.DataFrame(pca_vec, index=s.index, columns=pca_cols)
|
|
157
|
+
x = x.drop(columns=[image_col])
|
|
158
|
+
x = pd.concat([x, pca_df], axis=1)
|
|
159
|
+
return x
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@embedding_step
|
|
163
|
+
def get_image_encoder(model_name: str) -> Tuple[DINOv3ViTModel, DINOv3ViTImageProcessorFast]:
|
|
164
|
+
if IMAGE_ENCODER["encoder"] is None:
|
|
165
|
+
model = AutoModel.from_pretrained(model_name)
|
|
166
|
+
model.eval()
|
|
167
|
+
IMAGE_ENCODER["encoder"] = model
|
|
168
|
+
IMAGE_ENCODER["processor"] = AutoImageProcessor.from_pretrained(model_name, use_fast=True)
|
|
169
|
+
dino_model = IMAGE_ENCODER["encoder"]
|
|
170
|
+
img_processor = IMAGE_ENCODER["processor"]
|
|
171
|
+
if not isinstance(dino_model, DINOv3ViTModel):
|
|
172
|
+
raise TypeError(f"Expected DINOv3ViTModel, got {type(dino_model)}")
|
|
173
|
+
if not isinstance(img_processor, (DINOv3ViTImageProcessorFast)):
|
|
174
|
+
raise TypeError(f"Expected BitImageProcessor, got {type(img_processor)}")
|
|
175
|
+
return dino_model, img_processor
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def image_urls_to_embeddings(
|
|
179
|
+
s: Series,
|
|
180
|
+
image_folder: str,
|
|
181
|
+
processor: DINOv3ViTImageProcessorFast,
|
|
182
|
+
model: DINOv3ViTModel,
|
|
183
|
+
) -> np.ndarray:
|
|
184
|
+
images = load_images(s=s, image_folder=image_folder)
|
|
185
|
+
embeddings = encode_images_in_batches(images=images, processor=processor, model=model)
|
|
186
|
+
return embeddings
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@embedding_step
|
|
190
|
+
def encode_images_in_batches(
|
|
191
|
+
images: List[Image.Image],
|
|
192
|
+
processor: DINOv3ViTImageProcessorFast,
|
|
193
|
+
model: DINOv3ViTModel,
|
|
194
|
+
) -> np.ndarray:
|
|
195
|
+
embeddings = []
|
|
196
|
+
for i in range(0, len(images), IMAGE_BATCH_SIZE):
|
|
197
|
+
batch_images = images[i:i + IMAGE_BATCH_SIZE]
|
|
198
|
+
with suppress_channel_dimension_warning():
|
|
199
|
+
inputs = processor(images=batch_images, return_tensors="pt").to(model.device)
|
|
200
|
+
with torch.no_grad():
|
|
201
|
+
outputs = model(**inputs)
|
|
202
|
+
last_hidden = outputs["last_hidden_state"] if isinstance(outputs, dict) else outputs.last_hidden_state
|
|
203
|
+
batch_embeddings = last_hidden[:, 0, :].cpu().numpy()
|
|
204
|
+
embeddings.append(batch_embeddings)
|
|
205
|
+
embeddings = np.concatenate(embeddings, axis=0)
|
|
206
|
+
d_model = model.config.hidden_size
|
|
207
|
+
assert embeddings.shape == (len(images), d_model), f"Expected ({len(images)}, {d_model}), got {embeddings.shape}"
|
|
208
|
+
return embeddings
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Missing values (copied from tabstar 1.1.15, tabstar/preprocessing/nulls.py)."""
|
|
2
|
+
from pandas import Series
|
|
3
|
+
|
|
4
|
+
MISSING_VALUE = "Unknown Value"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def raise_if_null_target(y: Series):
|
|
8
|
+
y_missing = y.isnull().sum()
|
|
9
|
+
if y_missing > 0:
|
|
10
|
+
raise ValueError(f"Target variable {y.name} has {y_missing} null values, please handle them before training.")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Set, Dict
|
|
2
|
+
|
|
3
|
+
from pandas import DataFrame
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def fit_numerical_median(x: DataFrame, numerical_features: Set[str]) -> Dict[str, float]:
|
|
7
|
+
return {col: x[col].median() for col in numerical_features}
|
|
8
|
+
|
|
9
|
+
def transform_numerical_features(x: DataFrame, numerical_medians: Dict[str, float]) -> DataFrame:
|
|
10
|
+
x = x.copy()
|
|
11
|
+
for col, train_median in numerical_medians.items():
|
|
12
|
+
x[col] = x[col].fillna(train_median)
|
|
13
|
+
return x
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pandas import Series
|
|
5
|
+
from sklearn.preprocessing import LabelEncoder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fit_preprocess_y(y: Series, is_cls: bool) -> Optional[LabelEncoder]:
|
|
9
|
+
if not is_cls:
|
|
10
|
+
return None
|
|
11
|
+
label_encoder = LabelEncoder()
|
|
12
|
+
label_encoder.fit(y)
|
|
13
|
+
return label_encoder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def transform_preprocess_y(y: Series | np.ndarray, scaler: Optional[LabelEncoder]) -> Series:
|
|
17
|
+
if scaler is None:
|
|
18
|
+
return y
|
|
19
|
+
assert isinstance(scaler, LabelEncoder), f"Scaler must be LabelEncoder if not None, but got {type(scaler)}"
|
|
20
|
+
y = y.copy()
|
|
21
|
+
return Series(scaler.transform(y), name=y.name, index=y.index)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from sklearn.decomposition import PCA
|
|
4
|
+
|
|
5
|
+
from multabench.constants import SEED
|
|
6
|
+
|
|
7
|
+
from multabench.preprocessing.discretize import discretize_numerical
|
|
8
|
+
|
|
9
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false" # Suppresses warning, avoids deadlock
|
|
10
|
+
from typing import Dict, Set, Optional, Any, Tuple
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from pandas import DataFrame, Series
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from multabench.datasets.all_datasets import MulTaBenchDatasetID
|
|
18
|
+
from multabench.e5.constants import E5_SMALL_V2, TF_IDF
|
|
19
|
+
from multabench.e5.e5_finetune import encode_texts_with_e5, get_vanilla_e5
|
|
20
|
+
from multabench.embeddings.hub import CachedEmbeddings, cached_embeddings, cached_encoder, e5_passages
|
|
21
|
+
from multabench.utils.timing import embedding_step
|
|
22
|
+
|
|
23
|
+
PCA_COMPONENTS = 30
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SkrubColumnEncoder:
|
|
27
|
+
"""Per-column text encoder using skrub.StringEncoder (TF-IDF + TruncatedSVD). CPU-only, no tokenizer needed."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, string_encoder: Any, col_name: str, n_components: int):
|
|
30
|
+
self.string_encoder = string_encoder
|
|
31
|
+
self.col_name = col_name
|
|
32
|
+
self.n_components = n_components
|
|
33
|
+
self.encoder = self # so wrapper.encoder.transform(X) delegates to self.transform(X)
|
|
34
|
+
|
|
35
|
+
@embedding_step
|
|
36
|
+
def encode_texts(self, texts: list[str], device) -> np.ndarray:
|
|
37
|
+
"""Encode texts with skrub StringEncoder (device ignored — CPU-only)."""
|
|
38
|
+
result = self.string_encoder.transform(pd.Series(texts, dtype=str))
|
|
39
|
+
return np.asarray(result)
|
|
40
|
+
|
|
41
|
+
def embed(self, x: DataFrame, device) -> np.ndarray:
|
|
42
|
+
return self.encode_texts(x[self.col_name].astype(str).fillna("").tolist(), device)
|
|
43
|
+
|
|
44
|
+
def transform(self, X: np.ndarray) -> np.ndarray:
|
|
45
|
+
return X # encode_texts already returns final (N, n_components) array
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class E5ColumnEncoder:
|
|
49
|
+
"""Per-column text encoder: holds E5 model, tokenizer (processor), and PCA (or identity). Uses passage: col_name: col_val format."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, model: Any, tokenizer: Any, encoder: Any, col_name: str,
|
|
52
|
+
cache: Optional[CachedEmbeddings] = None):
|
|
53
|
+
self.model = model
|
|
54
|
+
self.tokenizer = tokenizer
|
|
55
|
+
self.encoder = encoder
|
|
56
|
+
self.col_name = col_name
|
|
57
|
+
self.n_components = encoder.n_components
|
|
58
|
+
self.cache = cache
|
|
59
|
+
|
|
60
|
+
def encode_texts(self, texts: list[str], device: torch.device) -> np.ndarray:
|
|
61
|
+
"""Encode texts with this column's E5 model and tokenizer (passage: col_name: col_val)."""
|
|
62
|
+
return encode_texts_with_e5(
|
|
63
|
+
texts=texts,
|
|
64
|
+
model=self.model,
|
|
65
|
+
tokenizer=self.tokenizer,
|
|
66
|
+
device=device,
|
|
67
|
+
col_name=self.col_name,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def embed(self, x: DataFrame, device: torch.device) -> np.ndarray:
|
|
71
|
+
"""The embeddings of x's texts, looked up in the cache when there is one."""
|
|
72
|
+
texts = x[self.col_name].astype(str).fillna("").tolist()
|
|
73
|
+
if self.cache is None:
|
|
74
|
+
return self.encode_texts(texts, device)
|
|
75
|
+
return self.cache.lookup(e5_passages(self.col_name, texts), feature=self.col_name)
|
|
76
|
+
|
|
77
|
+
def transform(self, X: np.ndarray) -> np.ndarray:
|
|
78
|
+
return self.encoder.transform(X)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@embedding_step
|
|
82
|
+
def fit_text_encoders_skrub(
|
|
83
|
+
x: DataFrame,
|
|
84
|
+
text_features_list: list[str],
|
|
85
|
+
) -> Tuple[Dict[str, SkrubColumnEncoder], Dict[str, np.ndarray]]:
|
|
86
|
+
"""Fit one SkrubColumnEncoder per column using skrub.StringEncoder (TF-IDF + TruncatedSVD)."""
|
|
87
|
+
from skrub import StringEncoder
|
|
88
|
+
text_encoders: Dict[str, SkrubColumnEncoder] = {}
|
|
89
|
+
for col in text_features_list:
|
|
90
|
+
col_series = x[col].astype(str).fillna("")
|
|
91
|
+
print(f"Fitting SkrubStringEncoder for column {col} with n_components={PCA_COMPONENTS} for {len(col_series)} texts")
|
|
92
|
+
string_enc = StringEncoder(n_components=PCA_COMPONENTS, random_state=SEED)
|
|
93
|
+
string_enc.fit(col_series)
|
|
94
|
+
text_encoders[str(col)] = SkrubColumnEncoder(
|
|
95
|
+
string_encoder=string_enc,
|
|
96
|
+
col_name=str(col),
|
|
97
|
+
n_components=PCA_COMPONENTS,
|
|
98
|
+
)
|
|
99
|
+
return text_encoders, {}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def fit_text_encoders_vanilla(
|
|
103
|
+
x: DataFrame,
|
|
104
|
+
text_features_list: list[str],
|
|
105
|
+
device: torch.device,
|
|
106
|
+
e5_model_name: str = E5_SMALL_V2,
|
|
107
|
+
dataset: Optional[MulTaBenchDatasetID] = None,
|
|
108
|
+
) -> Tuple[Dict[str, E5ColumnEncoder], Dict[str, np.ndarray]]:
|
|
109
|
+
"""Fit one E5ColumnEncoder per column using shared vanilla E5 + PCA per column. Uses passage: col_name: col_val format.
|
|
110
|
+
Columns with cached embeddings on the Hub are read from there instead of being encoded."""
|
|
111
|
+
caches = _caches_per_column(e5_model_name, dataset)
|
|
112
|
+
model, tokenizer = None, None
|
|
113
|
+
if not set(caches).issuperset(text_features_list):
|
|
114
|
+
model, tokenizer = get_vanilla_e5(device, model_name=e5_model_name)
|
|
115
|
+
text_encoders: Dict[str, E5ColumnEncoder] = {}
|
|
116
|
+
train_embeddings: Dict[str, np.ndarray] = {}
|
|
117
|
+
for col in text_features_list:
|
|
118
|
+
pca = PCA(n_components=PCA_COMPONENTS, random_state=SEED)
|
|
119
|
+
wrapper = E5ColumnEncoder(model=model, tokenizer=tokenizer, encoder=pca, col_name=str(col), cache=caches.get(col))
|
|
120
|
+
col_embeddings = wrapper.embed(x, device)
|
|
121
|
+
pca.fit(col_embeddings)
|
|
122
|
+
text_encoders[str(col)] = wrapper
|
|
123
|
+
train_embeddings[str(col)] = col_embeddings
|
|
124
|
+
return text_encoders, train_embeddings
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _caches_per_column(e5_model_name: str, dataset: Optional[MulTaBenchDatasetID]) -> Dict[str, CachedEmbeddings]:
|
|
128
|
+
"""The cached embeddings of each text column that has them."""
|
|
129
|
+
encoder = cached_encoder(e5_model_name, tuned=False)
|
|
130
|
+
if encoder is None or dataset is None:
|
|
131
|
+
return {}
|
|
132
|
+
cache = cached_embeddings(encoder, dataset)
|
|
133
|
+
print(f"Reading the text embeddings of {cache.meta['features']} from the cached {encoder} embeddings")
|
|
134
|
+
return {col: cache for col in cache.meta["features"]}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def fit_text_encoders_tuned(
|
|
138
|
+
x: DataFrame,
|
|
139
|
+
text_features_list: list[str],
|
|
140
|
+
device: torch.device,
|
|
141
|
+
y: Series,
|
|
142
|
+
e5_train_kwargs: Dict[str, Any],
|
|
143
|
+
is_cls: bool,
|
|
144
|
+
d_output: int,
|
|
145
|
+
e5_model_name: str = E5_SMALL_V2,
|
|
146
|
+
) -> Tuple[Dict[str, E5ColumnEncoder], Dict[str, np.ndarray]]:
|
|
147
|
+
"""Fit a single E5 model for all text columns with passage: col_name: col_val format. Each column gets an E5ColumnEncoder sharing the same tuned model."""
|
|
148
|
+
from transformers import AutoTokenizer
|
|
149
|
+
|
|
150
|
+
from multabench.e5.e5_finetune import finetune_e5_with_lora
|
|
151
|
+
from multabench.preprocessing.splits import split_to_val
|
|
152
|
+
|
|
153
|
+
if not is_cls:
|
|
154
|
+
y = discretize_numerical(y, n_bins=20)
|
|
155
|
+
x_tr, x_val, y_tr, y_val = split_to_val(x=x, y=y, is_cls=True)
|
|
156
|
+
kwargs = e5_train_kwargs or {}
|
|
157
|
+
|
|
158
|
+
# Build combined train/val: for each row, for each text col, create (col_name: col_val, label)
|
|
159
|
+
train_texts: list[str] = []
|
|
160
|
+
train_y: list = []
|
|
161
|
+
for idx in x_tr.index:
|
|
162
|
+
row_y = y_tr.loc[idx]
|
|
163
|
+
for col in text_features_list:
|
|
164
|
+
val = str(x_tr.loc[idx, col]).strip() if pd.notna(x_tr.loc[idx, col]) else ""
|
|
165
|
+
train_texts.append(f"{col}: {val}")
|
|
166
|
+
train_y.append(row_y)
|
|
167
|
+
|
|
168
|
+
val_texts: list[str] = []
|
|
169
|
+
val_y: list = []
|
|
170
|
+
for idx in x_val.index:
|
|
171
|
+
row_y = y_val.loc[idx]
|
|
172
|
+
for col in text_features_list:
|
|
173
|
+
val = str(x_val.loc[idx, col]).strip() if pd.notna(x_val.loc[idx, col]) else ""
|
|
174
|
+
val_texts.append(f"{col}: {val}")
|
|
175
|
+
val_y.append(row_y)
|
|
176
|
+
|
|
177
|
+
train_y_arr = np.array(train_y)
|
|
178
|
+
val_y_arr = np.array(val_y)
|
|
179
|
+
|
|
180
|
+
tokenizer = AutoTokenizer.from_pretrained(e5_model_name)
|
|
181
|
+
print(f"Finetuning single E5 ({e5_model_name}) for {len(text_features_list)} columns with {len(train_texts)} train examples (passage: col_name: col_val)")
|
|
182
|
+
tuned_model, tuned_tokenizer = finetune_e5_with_lora(
|
|
183
|
+
train_texts=train_texts,
|
|
184
|
+
train_y=train_y_arr,
|
|
185
|
+
val_texts=val_texts,
|
|
186
|
+
val_y=val_y_arr,
|
|
187
|
+
device=device,
|
|
188
|
+
tokenizer=tokenizer,
|
|
189
|
+
model_name=e5_model_name,
|
|
190
|
+
**kwargs,
|
|
191
|
+
)
|
|
192
|
+
tuned_model.to(device)
|
|
193
|
+
|
|
194
|
+
text_encoders: Dict[str, E5ColumnEncoder] = {}
|
|
195
|
+
train_embeddings: Dict[str, np.ndarray] = {}
|
|
196
|
+
for col in text_features_list:
|
|
197
|
+
texts = x[col].astype(str).fillna("").tolist()
|
|
198
|
+
col_embeddings = encode_texts_with_e5(texts=texts, model=tuned_model, tokenizer=tuned_tokenizer, device=device, col_name=str(col))
|
|
199
|
+
encoder = PCA(n_components=PCA_COMPONENTS, random_state=SEED)
|
|
200
|
+
encoder.fit(col_embeddings)
|
|
201
|
+
text_encoders[col] = E5ColumnEncoder(model=tuned_model, tokenizer=tuned_tokenizer, encoder=encoder, col_name=str(col))
|
|
202
|
+
train_embeddings[col] = col_embeddings
|
|
203
|
+
return text_encoders, train_embeddings
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def fit_text_encoders(
|
|
207
|
+
x: DataFrame,
|
|
208
|
+
text_features: Set[str],
|
|
209
|
+
device: torch.device,
|
|
210
|
+
y: Optional[Series] = None,
|
|
211
|
+
tune_e5: bool = False,
|
|
212
|
+
e5_train_kwargs: Optional[Dict[str, Any]] = None,
|
|
213
|
+
is_cls: bool = True,
|
|
214
|
+
d_output: int = 2,
|
|
215
|
+
e5_model_name: str = E5_SMALL_V2,
|
|
216
|
+
dataset: Optional[MulTaBenchDatasetID] = None,
|
|
217
|
+
) -> Tuple[Dict[str, E5ColumnEncoder], Dict[str, np.ndarray]]:
|
|
218
|
+
"""
|
|
219
|
+
Fit one E5 model per text column (or vanilla E5 shared across columns when not tuning).
|
|
220
|
+
Each column gets an E5ColumnEncoder wrapper holding model, tokenizer, and PCA.
|
|
221
|
+
Returns text_encoders mapping column -> E5ColumnEncoder, and the training rows' embeddings per column.
|
|
222
|
+
"""
|
|
223
|
+
text_features_list = sorted(text_features)
|
|
224
|
+
if not text_features_list:
|
|
225
|
+
return {}, {}
|
|
226
|
+
if e5_model_name == TF_IDF:
|
|
227
|
+
return fit_text_encoders_skrub(
|
|
228
|
+
x=x,
|
|
229
|
+
text_features_list=text_features_list,
|
|
230
|
+
)
|
|
231
|
+
if tune_e5 and y is not None:
|
|
232
|
+
return fit_text_encoders_tuned(
|
|
233
|
+
x=x,
|
|
234
|
+
text_features_list=text_features_list,
|
|
235
|
+
device=device,
|
|
236
|
+
y=y,
|
|
237
|
+
e5_train_kwargs=e5_train_kwargs or {},
|
|
238
|
+
is_cls=is_cls,
|
|
239
|
+
d_output=d_output,
|
|
240
|
+
e5_model_name=e5_model_name,
|
|
241
|
+
)
|
|
242
|
+
return fit_text_encoders_vanilla(
|
|
243
|
+
x=x,
|
|
244
|
+
text_features_list=text_features_list,
|
|
245
|
+
device=device,
|
|
246
|
+
e5_model_name=e5_model_name,
|
|
247
|
+
dataset=dataset,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def transform_text_features(
|
|
252
|
+
x: DataFrame,
|
|
253
|
+
text_encoders: Dict[str, E5ColumnEncoder],
|
|
254
|
+
device: torch.device,
|
|
255
|
+
train_embeddings: Optional[Dict[str, np.ndarray]] = None,
|
|
256
|
+
) -> DataFrame:
|
|
257
|
+
train_embeddings = train_embeddings or {}
|
|
258
|
+
for text_col, wrapper in text_encoders.items():
|
|
259
|
+
embeddings = train_embeddings.get(text_col)
|
|
260
|
+
if embeddings is None:
|
|
261
|
+
embeddings = wrapper.embed(x, device)
|
|
262
|
+
assert len(embeddings) == len(x)
|
|
263
|
+
n_components = wrapper.n_components
|
|
264
|
+
pca_vec = wrapper.encoder.transform(embeddings)
|
|
265
|
+
pca_cols = [f"{text_col}_txt_pca_{i}" for i in range(n_components)]
|
|
266
|
+
pca_df = pd.DataFrame(pca_vec, index=x.index, columns=pca_cols)
|
|
267
|
+
cols_before = len(x.columns)
|
|
268
|
+
x = x.drop(columns=[text_col])
|
|
269
|
+
x = pd.concat([x, pca_df], axis=1)
|
|
270
|
+
assert len(x.columns) == cols_before + n_components - 1
|
|
271
|
+
return x
|