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
@@ -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