tabdpt 1.3.0__tar.gz → 1.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tabdpt-1.3.0 → tabdpt-1.3.2}/PKG-INFO +3 -1
- {tabdpt-1.3.0 → tabdpt-1.3.2}/pyproject.toml +5 -1
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/__init__.py +2 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/classifier.py +35 -66
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/estimator.py +101 -35
- tabdpt-1.3.2/src/tabdpt/explain.py +134 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/regressor.py +31 -61
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/utils.py +4 -2
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt.egg-info/PKG-INFO +3 -1
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt.egg-info/SOURCES.txt +2 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt.egg-info/requires.txt +3 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/tests/test_estimator.py +31 -0
- tabdpt-1.3.2/tests/test_explain.py +110 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/tests/test_inference.py +18 -5
- {tabdpt-1.3.0 → tabdpt-1.3.2}/LICENSE +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/README.md +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/setup.cfg +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/bar_distribution.py +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt/model.py +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt.egg-info/dependency_links.txt +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/src/tabdpt.egg-info/top_level.txt +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/tests/test_cls.py +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/tests/test_reg.py +0 -0
- {tabdpt-1.3.0 → tabdpt-1.3.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabdpt
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: TabDPT: Scaling Tabular Foundation Models on Real Data
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Repository, https://github.com/layer6ai-labs/TabDPT-inference
|
|
@@ -31,6 +31,8 @@ Requires-Dist: openml==0.15.1; extra == "reproduce-results"
|
|
|
31
31
|
Requires-Dist: jupyter==1.1.1; extra == "reproduce-results"
|
|
32
32
|
Requires-Dist: tenacity==9.1.2; extra == "reproduce-results"
|
|
33
33
|
Requires-Dist: gdown==5.2.0; extra == "reproduce-results"
|
|
34
|
+
Provides-Extra: explain
|
|
35
|
+
Requires-Dist: captum~=0.9.0; extra == "explain"
|
|
34
36
|
Dynamic: license-file
|
|
35
37
|
|
|
36
38
|
<div align="center">
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tabdpt"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.2"
|
|
8
8
|
description = "TabDPT: Scaling Tabular Foundation Models on Real Data"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -37,6 +37,9 @@ reproduce-results = [
|
|
|
37
37
|
"tenacity==9.1.2",
|
|
38
38
|
"gdown==5.2.0",
|
|
39
39
|
]
|
|
40
|
+
explain = [
|
|
41
|
+
"captum~=0.9.0"
|
|
42
|
+
]
|
|
40
43
|
|
|
41
44
|
[project.urls]
|
|
42
45
|
Repository = "https://github.com/layer6ai-labs/TabDPT-inference"
|
|
@@ -54,6 +57,7 @@ show_missing = true
|
|
|
54
57
|
|
|
55
58
|
[dependency-groups]
|
|
56
59
|
dev = [
|
|
60
|
+
"tabdpt[explain]",
|
|
57
61
|
"pytest>=8.4.1",
|
|
58
62
|
"pytest-cov>=5.0.0,<8.0",
|
|
59
63
|
"rliable>=1.2.0,<2.0",
|
|
@@ -8,6 +8,7 @@ from .bar_distribution import (
|
|
|
8
8
|
)
|
|
9
9
|
from .classifier import TabDPTClassifier
|
|
10
10
|
from .regressor import OutputType, TabDPTRegressor
|
|
11
|
+
from .explain import integrated_gradients
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"TabDPTClassifier",
|
|
@@ -19,4 +20,5 @@ __all__ = [
|
|
|
19
20
|
"distribution_mode",
|
|
20
21
|
"distribution_quantiles",
|
|
21
22
|
"distribution_sample",
|
|
23
|
+
"integrated_gradients",
|
|
22
24
|
]
|
|
@@ -7,7 +7,7 @@ from scipy.special import softmax
|
|
|
7
7
|
from sklearn.base import ClassifierMixin
|
|
8
8
|
|
|
9
9
|
from .estimator import TabDPTEstimator
|
|
10
|
-
from .utils import generate_random_permutation
|
|
10
|
+
from .utils import generate_random_permutation
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class TabDPTClassifier(TabDPTEstimator, ClassifierMixin):
|
|
@@ -99,81 +99,50 @@ class TabDPTClassifier(TabDPTEstimator, ClassifierMixin):
|
|
|
99
99
|
Returns:
|
|
100
100
|
A class probability array of size `n_instances` x `n_classes`.
|
|
101
101
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
prep = self._preprocess(X, context_size=context_size, seed=seed, class_perm=class_perm)
|
|
103
|
+
X_ctx, y_ctx, X_query = prep.X_ctx, prep.y_ctx, prep.X_query
|
|
104
|
+
num_features = torch.tensor([X_ctx.shape[-1]], dtype=torch.long, device=X_ctx.device)
|
|
104
105
|
|
|
105
106
|
if context_size is None:
|
|
106
107
|
context_size = np.inf
|
|
107
|
-
|
|
108
108
|
if batch_size is None:
|
|
109
|
-
|
|
110
|
-
batch_size = 4096
|
|
111
|
-
else:
|
|
112
|
-
batch_size = 128*1024
|
|
113
|
-
|
|
114
|
-
if seed is not None:
|
|
115
|
-
feat_perm = generate_random_permutation(train_x.shape[1], seed)
|
|
116
|
-
train_x = train_x[:, feat_perm]
|
|
117
|
-
test_x = test_x[:, feat_perm]
|
|
109
|
+
batch_size = self._default_batch_size()
|
|
118
110
|
|
|
119
111
|
pred_list = []
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if not return_logits:
|
|
139
|
-
pred = pred[..., :self.num_classes] / temperature
|
|
140
|
-
pred = torch.nn.functional.softmax(pred.float(), dim=-1)
|
|
141
|
-
pred_list.append(pred.squeeze(1))
|
|
142
|
-
else:
|
|
143
|
-
for b in range(math.ceil(len(self.X_test) / batch_size)):
|
|
144
|
-
start = b * batch_size
|
|
145
|
-
end = min(len(self.X_test), (b + 1) * batch_size)
|
|
146
|
-
|
|
147
|
-
indices_nni = self._get_context_indices(
|
|
148
|
-
self.X_test[start:end], context_size=context_size, seed=seed
|
|
149
|
-
)
|
|
150
|
-
X_nni = train_x[torch.tensor(indices_nni)]
|
|
151
|
-
y_nni = train_y[torch.tensor(indices_nni)]
|
|
152
|
-
|
|
153
|
-
X_nni, y_nni = (
|
|
154
|
-
pad_x(torch.Tensor(X_nni), self.max_features).to(self.device),
|
|
155
|
-
torch.Tensor(y_nni).to(self.device),
|
|
112
|
+
for b in range(math.ceil(len(self.X_test) / batch_size)):
|
|
113
|
+
start = b * batch_size
|
|
114
|
+
end = min(len(self.X_test), (b + 1) * batch_size)
|
|
115
|
+
|
|
116
|
+
if self._uses_stacked_context(context_size):
|
|
117
|
+
X_query_batch = X_query[:, start:end]
|
|
118
|
+
X_ctx_batch = X_ctx
|
|
119
|
+
y_ctx_batch = y_ctx
|
|
120
|
+
else:
|
|
121
|
+
X_query_batch = X_query[start:end]
|
|
122
|
+
X_ctx_batch = X_ctx[prep.indices_nni[start:end]]
|
|
123
|
+
y_ctx_batch = y_ctx[prep.indices_nni[start:end]]
|
|
124
|
+
|
|
125
|
+
if self.num_classes <= self.max_num_classes:
|
|
126
|
+
pred = self.model(
|
|
127
|
+
x_src=torch.cat([X_ctx_batch, X_query_batch], dim=1),
|
|
128
|
+
y_src=y_ctx_batch,
|
|
129
|
+
is_cls=True
|
|
156
130
|
)
|
|
157
|
-
|
|
158
|
-
|
|
131
|
+
pred = torch.nn.functional.log_softmax(pred[..., : self.num_classes].float(), dim=-1)
|
|
132
|
+
else:
|
|
133
|
+
pred = self._predict_large_cls(X_ctx_batch, X_query_batch, y_ctx_batch, num_features)
|
|
159
134
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
y_src=y_nni,
|
|
164
|
-
is_cls=True
|
|
165
|
-
)
|
|
166
|
-
pred = torch.nn.functional.log_softmax(pred[..., : self.num_classes].float(), dim=-1)
|
|
167
|
-
else:
|
|
168
|
-
pred = self._predict_large_cls(X_nni, X_eval, y_nni, num_features)
|
|
135
|
+
if not return_logits:
|
|
136
|
+
pred = pred[..., :self.num_classes] / temperature
|
|
137
|
+
pred = torch.nn.functional.softmax(pred.float(), dim=-1)
|
|
169
138
|
|
|
170
|
-
|
|
139
|
+
if self._uses_stacked_context(context_size):
|
|
140
|
+
pred_list.append(pred.squeeze(1))
|
|
141
|
+
else:
|
|
171
142
|
if not return_logits:
|
|
172
|
-
|
|
173
|
-
pred
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
pred_list.append(pred.squeeze(dim=0))
|
|
143
|
+
# numerical stability - not sure if needed now, but from legacy code
|
|
144
|
+
pred /= pred.sum(axis=-1, keepdims=True)
|
|
145
|
+
pred_list.append(pred.squeeze(0))
|
|
177
146
|
|
|
178
147
|
pred_val = torch.cat(pred_list, dim=0).detach().cpu().float().numpy()
|
|
179
148
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import json
|
|
2
3
|
from typing import Literal
|
|
3
|
-
import
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
import torch
|
|
@@ -13,7 +14,7 @@ from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler, Po
|
|
|
13
14
|
from sklearn.utils.validation import check_is_fitted
|
|
14
15
|
|
|
15
16
|
from .model import TabDPTModel
|
|
16
|
-
from .utils import convert_to_torch_tensor, Log1pScaler, generate_random_permutation, pad_x
|
|
17
|
+
from .utils import convert_to_torch_tensor, Log1pScaler, generate_random_permutation, pad_x, normalize_data
|
|
17
18
|
|
|
18
19
|
# Constants for model caching and download
|
|
19
20
|
_VERSION = "1_3"
|
|
@@ -90,21 +91,10 @@ class TabDPTEstimator(BaseEstimator):
|
|
|
90
91
|
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
|
91
92
|
self.use_flash = use_flash and self.device == "cuda"
|
|
92
93
|
self.missing_indicators = missing_indicators
|
|
94
|
+
self.clip_sigma = clip_sigma
|
|
95
|
+
self.model_weight_path = model_weight_path
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
self.path = model_weight_path
|
|
96
|
-
else:
|
|
97
|
-
self.path = self.download_weights()
|
|
98
|
-
|
|
99
|
-
with safe_open(self.path, framework="pt", device=self.device) as f:
|
|
100
|
-
meta = f.metadata()
|
|
101
|
-
cfg_dict = json.loads(meta["cfg"])
|
|
102
|
-
cfg = OmegaConf.create(cfg_dict)
|
|
103
|
-
model_state = {k: f.get_tensor(k) for k in f.keys()}
|
|
104
|
-
|
|
105
|
-
cfg.env.device = self.device
|
|
106
|
-
self.model = TabDPTModel.load(model_state=model_state, config=cfg, use_flash=self.use_flash, clip_sigma=clip_sigma)
|
|
107
|
-
self.model.eval()
|
|
97
|
+
self._load_model()
|
|
108
98
|
|
|
109
99
|
self.max_features = self.model.num_features
|
|
110
100
|
self.max_num_classes = self.model.n_out
|
|
@@ -151,6 +141,32 @@ class TabDPTEstimator(BaseEstimator):
|
|
|
151
141
|
|
|
152
142
|
self.projection = None
|
|
153
143
|
|
|
144
|
+
def _load_model(self) -> None:
|
|
145
|
+
"""Read the checkpoint and build ``self.model``; the constructor calls this once.
|
|
146
|
+
|
|
147
|
+
Separated out so a caller can share one network across several estimators built with the
|
|
148
|
+
same ``model_weight_path`` (or the same default download), ``use_flash`` and ``clip_sigma``:
|
|
149
|
+
build one estimator, then for the next skip its own load by overriding or monkeypatching
|
|
150
|
+
this method to hand back the cached module instead of rebuilding it. Downloads from
|
|
151
|
+
Hugging Face when ``model_weight_path`` is unset.
|
|
152
|
+
"""
|
|
153
|
+
if self.model_weight_path:
|
|
154
|
+
self.path = self.model_weight_path
|
|
155
|
+
else:
|
|
156
|
+
self.path = self.download_weights()
|
|
157
|
+
|
|
158
|
+
with safe_open(self.path, framework="pt", device=self.device) as f:
|
|
159
|
+
meta = f.metadata()
|
|
160
|
+
cfg_dict = json.loads(meta["cfg"])
|
|
161
|
+
cfg = OmegaConf.create(cfg_dict)
|
|
162
|
+
model_state = {k: f.get_tensor(k) for k in f.keys()}
|
|
163
|
+
|
|
164
|
+
cfg.env.device = self.device
|
|
165
|
+
self.model = TabDPTModel.load(
|
|
166
|
+
model_state=model_state, config=cfg, use_flash=self.use_flash, clip_sigma=self.clip_sigma
|
|
167
|
+
)
|
|
168
|
+
self.model.eval()
|
|
169
|
+
|
|
154
170
|
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
155
171
|
assert isinstance(X, np.ndarray), "X must be a numpy array"
|
|
156
172
|
assert isinstance(y, np.ndarray), "y must be a numpy array"
|
|
@@ -268,8 +284,39 @@ class TabDPTEstimator(BaseEstimator):
|
|
|
268
284
|
y_ctx = ctx_y[None, :].float()
|
|
269
285
|
return X_ctx, y_ctx
|
|
270
286
|
|
|
271
|
-
def
|
|
287
|
+
def _get_ensemble_iterator(self, n_ensembles: int, seed: int | None = None):
|
|
288
|
+
generator = np.random.SeedSequence(seed)
|
|
289
|
+
ensemble_iterator = generator.generate_state(n_ensembles)
|
|
290
|
+
if self.verbose:
|
|
291
|
+
from tqdm import tqdm
|
|
292
|
+
ensemble_iterator = tqdm(ensemble_iterator, desc="ensembles")
|
|
293
|
+
return ensemble_iterator
|
|
294
|
+
|
|
295
|
+
@dataclass
|
|
296
|
+
class PreprocessResult:
|
|
297
|
+
X_ctx: torch.Tensor
|
|
298
|
+
y_ctx: torch.Tensor
|
|
299
|
+
X_query: torch.Tensor
|
|
300
|
+
feat_perm: torch.Tensor
|
|
301
|
+
mean_y: torch.Tensor | None
|
|
302
|
+
std_y: torch.Tensor | None
|
|
303
|
+
indices_nni: torch.Tensor | None
|
|
304
|
+
|
|
305
|
+
def _preprocess(
|
|
306
|
+
self,
|
|
307
|
+
X: np.ndarray,
|
|
308
|
+
context_size: int | None = None,
|
|
309
|
+
seed: int | None = None,
|
|
310
|
+
class_perm: np.ndarray | None = None,
|
|
311
|
+
normalize_y_ctx: bool = False
|
|
312
|
+
) -> TabDPTEstimator.PreprocessResult:
|
|
313
|
+
"""
|
|
314
|
+
Do all operations requred to go from a predict function input to tensors for the model
|
|
315
|
+
"""
|
|
316
|
+
|
|
272
317
|
check_is_fitted(self)
|
|
318
|
+
if context_size is None:
|
|
319
|
+
context_size = np.inf
|
|
273
320
|
|
|
274
321
|
if self.missing_indicators:
|
|
275
322
|
inds = np.isnan(X)[:, self.has_missing_indicator].astype(float)
|
|
@@ -280,35 +327,54 @@ class TabDPTEstimator(BaseEstimator):
|
|
|
280
327
|
if self.normalizer == 'quantile-uniform':
|
|
281
328
|
self.X_test = 2*self.X_test - 1
|
|
282
329
|
|
|
283
|
-
|
|
330
|
+
X_ctx, y_ctx, X_query = (
|
|
284
331
|
convert_to_torch_tensor(self.X_train).to(self.device).float(),
|
|
285
332
|
convert_to_torch_tensor(self.y_train).to(self.device).float(),
|
|
286
333
|
convert_to_torch_tensor(self.X_test).to(self.device).float(),
|
|
287
334
|
)
|
|
288
335
|
|
|
289
336
|
# Apply PCA/subsampling to reduce the number of features if necessary
|
|
337
|
+
feat_perm = None
|
|
290
338
|
if self.n_features > self.max_features:
|
|
291
339
|
if self.feature_reduction == "pca":
|
|
292
|
-
|
|
293
|
-
|
|
340
|
+
X_ctx = X_ctx @ self.projection
|
|
341
|
+
X_query = X_query @ self.projection
|
|
294
342
|
elif self.feature_reduction == "subsample":
|
|
295
|
-
feat_perm = generate_random_permutation(
|
|
296
|
-
|
|
297
|
-
|
|
343
|
+
feat_perm = generate_random_permutation(X_ctx.shape[1], seed)
|
|
344
|
+
X_ctx = X_ctx[:, feat_perm][:, :self.max_features]
|
|
345
|
+
X_query = X_query[:, feat_perm][:, :self.max_features]
|
|
298
346
|
|
|
299
347
|
if class_perm is not None:
|
|
300
348
|
assert self.mode == "cls", "class_perm only makes sense for classification"
|
|
301
349
|
inv_perm = np.argsort(class_perm)
|
|
302
|
-
|
|
303
|
-
inv_perm = torch.as_tensor(inv_perm, device=
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return train_x, train_y, test_x
|
|
350
|
+
y_ctx = y_ctx.to(torch.long)
|
|
351
|
+
inv_perm = torch.as_tensor(inv_perm, device=y_ctx.device)
|
|
352
|
+
y_ctx = inv_perm[y_ctx].to(torch.float)
|
|
307
353
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
354
|
+
if feat_perm is None:
|
|
355
|
+
if seed is not None:
|
|
356
|
+
feat_perm = generate_random_permutation(X_ctx.shape[1], seed)
|
|
357
|
+
X_ctx = X_ctx[:, feat_perm]
|
|
358
|
+
X_query = X_query[:, feat_perm]
|
|
359
|
+
else:
|
|
360
|
+
feat_perm = torch.arange(X_ctx.shape[1])
|
|
361
|
+
|
|
362
|
+
if normalize_y_ctx:
|
|
363
|
+
y_ctx, mean_y, std_y = normalize_data(y_ctx, return_mean_std=True)
|
|
364
|
+
else:
|
|
365
|
+
mean_y, std_y = None, None
|
|
366
|
+
|
|
367
|
+
if not self._uses_stacked_context(context_size):
|
|
368
|
+
indices_nni = self._get_context_indices(
|
|
369
|
+
self.X_test, context_size=context_size, seed=seed
|
|
370
|
+
)
|
|
371
|
+
X_ctx = pad_x(X_ctx, self.max_features)
|
|
372
|
+
X_query = pad_x(X_query.unsqueeze(1), self.max_features).to(self.device)
|
|
373
|
+
return self.PreprocessResult(X_ctx, y_ctx, X_query, feat_perm, mean_y, std_y, torch.tensor(indices_nni))
|
|
374
|
+
|
|
375
|
+
X_ctx, y_ctx = self._prepare_stacked_context(X_ctx, y_ctx, context_size, seed)
|
|
376
|
+
X_query = pad_x(X_query[None, :, :], self.max_features).to(self.device)
|
|
377
|
+
return self.PreprocessResult(X_ctx, y_ctx, X_query, feat_perm, mean_y, std_y, None)
|
|
378
|
+
|
|
379
|
+
def _default_batch_size(self):
|
|
380
|
+
return 4096 if self.device == "cpu" else 128*1024
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import warnings
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from .classifier import TabDPTClassifier
|
|
7
|
+
from .regressor import TabDPTRegressor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def integrated_gradients(
|
|
11
|
+
fitted_model: TabDPTRegressor | TabDPTClassifier,
|
|
12
|
+
X_test: np.ndarray,
|
|
13
|
+
class_index: int = 0,
|
|
14
|
+
batch_size: int | None = None,
|
|
15
|
+
n_steps: int = 50,
|
|
16
|
+
context_size: int | None = None,
|
|
17
|
+
seed: int | None = None,
|
|
18
|
+
class_perm: np.ndarray | None = None
|
|
19
|
+
) -> np.ndarray:
|
|
20
|
+
"""
|
|
21
|
+
Runs integrated gradients for one output and returns local attributions.
|
|
22
|
+
|
|
23
|
+
Attributions for classification are relative to logit space (normalized by log_softmax), and attributions for
|
|
24
|
+
regression are relative to output values after normalization by target mean/std.
|
|
25
|
+
|
|
26
|
+
Options for baselines are not included because by default, mean/std scaling is applied, meaning that a zero baseline
|
|
27
|
+
is reasonable, and using the model without similar scaling would be an uncommon use case.
|
|
28
|
+
|
|
29
|
+
Retrieval is not supported since the context will no longer be relevant when testing points along the integration
|
|
30
|
+
path. Support for high numbers of classes is not yet implemented, although integrated gradients does not scale well
|
|
31
|
+
to high numbers of classes anyway.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
fitted_model: TabDPT model after running .fit
|
|
35
|
+
X_test: Model inputs to attribute, of size `n_test` x `n_features`
|
|
36
|
+
class_index: For classifiers, the index of the class to produce attributions for. Logits are normalized using
|
|
37
|
+
log_softmax, so for the binary classification case, only explaining either class index 0 or 1 should suffice,
|
|
38
|
+
but multiple runs could be required for >2 classes.
|
|
39
|
+
batch_size: Internal test batch size for forward and backward passes. The context is shared and batching
|
|
40
|
+
across the sequence dimension is used. Note that IG requires `n_steps*X_test.shape[0]` actual test points, so
|
|
41
|
+
larger batch sizes than the size of `X_test` can be used if memory permits.
|
|
42
|
+
n_steps: Number of steps to integrate over
|
|
43
|
+
context_size, seed, class_perm: Same meaning as in predict functions
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Local attributions of size `n_test` x `n_features`
|
|
47
|
+
"""
|
|
48
|
+
from captum.attr import IntegratedGradients
|
|
49
|
+
|
|
50
|
+
if context_size is not None and not fitted_model._uses_stacked_context(context_size):
|
|
51
|
+
raise NotImplementedError(
|
|
52
|
+
"Integrated gradients only supports shared contexts for test points. Note that IG with retrieval is not "
|
|
53
|
+
"well-defined since the context depends on the test point values."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
is_cls = isinstance(fitted_model, TabDPTClassifier)
|
|
57
|
+
if is_cls and fitted_model.num_classes > fitted_model.max_num_classes:
|
|
58
|
+
raise NotImplementedError(
|
|
59
|
+
"Integrated gradients not implemented for high numbers of classes, since this requires multiple forward "
|
|
60
|
+
"passes internally."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if fitted_model.scaler is None:
|
|
64
|
+
warnings.warn(
|
|
65
|
+
"Using integrated gradients with no scaler will likely produce invalid results since we assume a zero baseline"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if batch_size is None:
|
|
69
|
+
batch_size = fitted_model._default_batch_size()
|
|
70
|
+
|
|
71
|
+
prep = fitted_model._preprocess(
|
|
72
|
+
X_test,
|
|
73
|
+
context_size=context_size,
|
|
74
|
+
seed=seed,
|
|
75
|
+
class_perm=class_perm,
|
|
76
|
+
normalize_y_ctx=not is_cls
|
|
77
|
+
)
|
|
78
|
+
X_ctx, y_ctx, X_query, feat_perm = prep.X_ctx, prep.y_ctx, prep.X_query, prep.feat_perm
|
|
79
|
+
feat_perm = feat_perm.cpu().numpy()
|
|
80
|
+
|
|
81
|
+
def cls_wrapped_forward(X_query):
|
|
82
|
+
x_src = torch.cat([X_ctx, X_query.unsqueeze(0)], dim=1)
|
|
83
|
+
pred = fitted_model.model(x_src=x_src, y_src=y_ctx, is_cls=True)
|
|
84
|
+
return torch.nn.functional.log_softmax(pred[..., :fitted_model.num_classes], dim=-1).squeeze(1)
|
|
85
|
+
|
|
86
|
+
def reg_wrapped_forward(X_query):
|
|
87
|
+
x_src = torch.cat([X_ctx, X_query.unsqueeze(0)], dim=1)
|
|
88
|
+
pred = fitted_model.model(x_src=x_src, y_src=y_ctx, is_cls=False)
|
|
89
|
+
logits = pred.squeeze(1)[:, fitted_model.max_num_classes:].float()
|
|
90
|
+
ret = fitted_model._expectation_from_regression_logits(logits)
|
|
91
|
+
return ret
|
|
92
|
+
|
|
93
|
+
forward = cls_wrapped_forward if is_cls else reg_wrapped_forward
|
|
94
|
+
ig = IntegratedGradients(forward)
|
|
95
|
+
|
|
96
|
+
attrs = []
|
|
97
|
+
for b in range(math.ceil(X_query.shape[1] / batch_size)):
|
|
98
|
+
attrs.append(ig.attribute(
|
|
99
|
+
X_query.squeeze(0)[b*batch_size : (b+1)*batch_size],
|
|
100
|
+
internal_batch_size=batch_size,
|
|
101
|
+
n_steps=n_steps,
|
|
102
|
+
target=class_index if is_cls else None
|
|
103
|
+
).cpu().numpy())
|
|
104
|
+
attrs = np.concatenate(attrs)
|
|
105
|
+
|
|
106
|
+
if fitted_model.n_features > fitted_model.max_features and fitted_model.feature_reduction == "pca":
|
|
107
|
+
# In the case of PCA projection, IG is done with respect to the principal components: u = x @ V, so divide by u
|
|
108
|
+
# to get the integral, multiply by V.T to make the inner derivative in the IG formula be with respect to x
|
|
109
|
+
# (chain rule), then multiply by x.
|
|
110
|
+
pca_X = X_query.squeeze(0).cpu().numpy()
|
|
111
|
+
integral = np.divide(attrs, pca_X, out=np.zeros_like(attrs), where=pca_X!=0) # Avoid divide by zero
|
|
112
|
+
inv_perm = np.argsort(feat_perm)
|
|
113
|
+
integral = integral[:, inv_perm]
|
|
114
|
+
proj = fitted_model.projection.cpu().numpy()
|
|
115
|
+
# Padding to max_features in _preprocess can lead to X_query/integral being larger than projection matrix
|
|
116
|
+
attrs = integral[:, :proj.shape[1]] @ proj.T * fitted_model.X_test
|
|
117
|
+
else:
|
|
118
|
+
inv_perm = np.argsort(feat_perm)
|
|
119
|
+
# If feature subsampling is used, feat_perm will be longer than attrs, so we need to add zero attributions
|
|
120
|
+
if attrs.shape[1] < len(inv_perm):
|
|
121
|
+
attrs = np.pad(attrs, ((0, 0), (0, len(inv_perm) - attrs.shape[1])))
|
|
122
|
+
attrs = attrs[:, inv_perm]
|
|
123
|
+
|
|
124
|
+
# Add back any columns removed by SimpleImputer
|
|
125
|
+
for i in np.nonzero(np.isnan(fitted_model.imputer.statistics_))[0]:
|
|
126
|
+
attrs = np.insert(attrs, i, np.zeros_like(attrs[:, 0]), axis=1)
|
|
127
|
+
|
|
128
|
+
if fitted_model.missing_indicators and any(fitted_model.has_missing_indicator):
|
|
129
|
+
n_missing_indicators = np.sum(fitted_model.has_missing_indicator)
|
|
130
|
+
feat_attrs = attrs[:, :-n_missing_indicators]
|
|
131
|
+
feat_attrs[:, fitted_model.has_missing_indicator] += attrs[:, -n_missing_indicators:]
|
|
132
|
+
return feat_attrs
|
|
133
|
+
|
|
134
|
+
return attrs
|
|
@@ -7,7 +7,6 @@ from sklearn.base import RegressorMixin
|
|
|
7
7
|
|
|
8
8
|
from .bar_distribution import FullPrediction
|
|
9
9
|
from .estimator import TabDPTEstimator
|
|
10
|
-
from .utils import generate_random_permutation, pad_x, normalize_data
|
|
11
10
|
|
|
12
11
|
OutputType = Literal["mean", "full"]
|
|
13
12
|
|
|
@@ -74,73 +73,44 @@ class TabDPTRegressor(TabDPTEstimator, RegressorMixin):
|
|
|
74
73
|
seed: int | None = None,
|
|
75
74
|
return_logits: bool = False,
|
|
76
75
|
):
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
prep = self._preprocess(X, context_size=context_size, seed=seed, normalize_y_ctx=True)
|
|
77
|
+
X_ctx, y_ctx, X_query, mean_y, std_y = prep.X_ctx, prep.y_ctx, prep.X_query, prep.mean_y, prep.std_y
|
|
79
78
|
|
|
80
79
|
if context_size is None:
|
|
81
80
|
context_size = np.inf
|
|
82
|
-
|
|
83
81
|
if batch_size is None:
|
|
84
|
-
|
|
85
|
-
batch_size = 4096
|
|
86
|
-
else:
|
|
87
|
-
batch_size = 128*1024
|
|
88
|
-
|
|
89
|
-
if seed is not None:
|
|
90
|
-
feat_perm = generate_random_permutation(train_x.shape[1], seed)
|
|
91
|
-
train_x = train_x[:, feat_perm]
|
|
92
|
-
test_x = test_x[:, feat_perm]
|
|
93
|
-
|
|
94
|
-
train_y, mean_y, std_y = normalize_data(train_y, return_mean_std=True)
|
|
82
|
+
batch_size = self._default_batch_size()
|
|
95
83
|
|
|
96
84
|
pred_list = []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
indices_nni = self._get_context_indices(
|
|
122
|
-
self.X_test[start:end], context_size=context_size, seed=seed
|
|
123
|
-
)
|
|
124
|
-
X_nni = train_x[torch.tensor(indices_nni)]
|
|
125
|
-
y_nni = train_y[torch.tensor(indices_nni)]
|
|
85
|
+
for b in range(math.ceil(len(self.X_test) / batch_size)):
|
|
86
|
+
start = b * batch_size
|
|
87
|
+
end = min(len(self.X_test), (b + 1) * batch_size)
|
|
88
|
+
|
|
89
|
+
if self._uses_stacked_context(context_size):
|
|
90
|
+
X_query_batch = X_query[:, start:end]
|
|
91
|
+
X_ctx_batch = X_ctx
|
|
92
|
+
y_ctx_batch = y_ctx
|
|
93
|
+
else:
|
|
94
|
+
X_query_batch = X_query[start:end]
|
|
95
|
+
X_ctx_batch = X_ctx[prep.indices_nni[start:end]]
|
|
96
|
+
y_ctx_batch = y_ctx[prep.indices_nni[start:end]]
|
|
97
|
+
|
|
98
|
+
pred = self.model(
|
|
99
|
+
x_src=torch.cat([X_ctx_batch, X_query_batch], dim=1),
|
|
100
|
+
y_src=y_ctx_batch,
|
|
101
|
+
is_cls=False
|
|
102
|
+
)
|
|
103
|
+
if self._uses_stacked_context(context_size):
|
|
104
|
+
pred = pred.squeeze(1)
|
|
105
|
+
else:
|
|
106
|
+
pred = pred.squeeze(0)
|
|
107
|
+
logits = pred[:, self.max_num_classes:].float()
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
X_eval = pad_x(X_eval.unsqueeze(1), self.max_features).to(self.device)
|
|
133
|
-
pred = self.model(
|
|
134
|
-
x_src=torch.cat([X_nni, X_eval], dim=1),
|
|
135
|
-
y_src=y_nni,
|
|
136
|
-
is_cls=False
|
|
137
|
-
)
|
|
138
|
-
logits = pred.squeeze(0)[:, self.max_num_classes:].float()
|
|
139
|
-
if return_logits:
|
|
140
|
-
pred_list.append(logits)
|
|
141
|
-
else:
|
|
142
|
-
y_hat = self._expectation_from_regression_logits(logits)
|
|
143
|
-
pred_list.append(y_hat * std_y + mean_y)
|
|
109
|
+
if return_logits:
|
|
110
|
+
pred_list.append(logits)
|
|
111
|
+
else:
|
|
112
|
+
y_hat = self._expectation_from_regression_logits(logits)
|
|
113
|
+
pred_list.append(y_hat * std_y + mean_y)
|
|
144
114
|
|
|
145
115
|
preds = torch.cat(pred_list, dim=0)
|
|
146
116
|
if return_logits:
|
|
@@ -40,13 +40,14 @@ def maskmean(x, mask, dim):
|
|
|
40
40
|
|
|
41
41
|
def maskstd(x, mask, dim=0):
|
|
42
42
|
num = mask.sum(dim=dim, keepdim=True)
|
|
43
|
-
mean = maskmean(x, mask, dim=
|
|
43
|
+
mean = maskmean(x, mask, dim=dim)
|
|
44
44
|
diffs = torch.where(mask, mean - x, 0)
|
|
45
|
-
return ((diffs**2).sum(dim=
|
|
45
|
+
return ((diffs**2).sum(dim=dim, keepdim=True) / (num - 1)) ** 0.5
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def normalize_data(data, eval_pos=-1, dim=0, return_mean_std: bool = False):
|
|
49
49
|
X = data[:eval_pos] if eval_pos > 0 else data
|
|
50
|
+
X = X.detach() # Allow gradient computations wrt inputs
|
|
50
51
|
mask = ~torch.isnan(X)
|
|
51
52
|
mean = maskmean(X, mask, dim=dim)
|
|
52
53
|
std = maskstd(X, mask, dim=dim) + 1e-6
|
|
@@ -58,6 +59,7 @@ def normalize_data(data, eval_pos=-1, dim=0, return_mean_std: bool = False):
|
|
|
58
59
|
|
|
59
60
|
def clip_outliers(data, eval_pos=-1, n_sigma=4, dim=0):
|
|
60
61
|
X = data[:eval_pos] if eval_pos > 0 else data
|
|
62
|
+
X = X.detach() # Allow gradient computations wrt inputs
|
|
61
63
|
mask = ~torch.isnan(X)
|
|
62
64
|
mean = maskmean(X, mask, dim=dim)
|
|
63
65
|
cutoff = n_sigma * maskstd(X, mask, dim=dim)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabdpt
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: TabDPT: Scaling Tabular Foundation Models on Real Data
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Repository, https://github.com/layer6ai-labs/TabDPT-inference
|
|
@@ -31,6 +31,8 @@ Requires-Dist: openml==0.15.1; extra == "reproduce-results"
|
|
|
31
31
|
Requires-Dist: jupyter==1.1.1; extra == "reproduce-results"
|
|
32
32
|
Requires-Dist: tenacity==9.1.2; extra == "reproduce-results"
|
|
33
33
|
Requires-Dist: gdown==5.2.0; extra == "reproduce-results"
|
|
34
|
+
Provides-Extra: explain
|
|
35
|
+
Requires-Dist: captum~=0.9.0; extra == "explain"
|
|
34
36
|
Dynamic: license-file
|
|
35
37
|
|
|
36
38
|
<div align="center">
|
|
@@ -5,6 +5,7 @@ src/tabdpt/__init__.py
|
|
|
5
5
|
src/tabdpt/bar_distribution.py
|
|
6
6
|
src/tabdpt/classifier.py
|
|
7
7
|
src/tabdpt/estimator.py
|
|
8
|
+
src/tabdpt/explain.py
|
|
8
9
|
src/tabdpt/model.py
|
|
9
10
|
src/tabdpt/regressor.py
|
|
10
11
|
src/tabdpt/utils.py
|
|
@@ -15,6 +16,7 @@ src/tabdpt.egg-info/requires.txt
|
|
|
15
16
|
src/tabdpt.egg-info/top_level.txt
|
|
16
17
|
tests/test_cls.py
|
|
17
18
|
tests/test_estimator.py
|
|
19
|
+
tests/test_explain.py
|
|
18
20
|
tests/test_inference.py
|
|
19
21
|
tests/test_reg.py
|
|
20
22
|
tests/test_utils.py
|
|
@@ -82,3 +82,34 @@ def test_invalid_feature_reduction_raises():
|
|
|
82
82
|
def test_invalid_faiss_metric_raises():
|
|
83
83
|
with pytest.raises(AssertionError):
|
|
84
84
|
build(faiss_metric="bad")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- Separable model loading ---
|
|
88
|
+
|
|
89
|
+
def test_load_model_is_separately_callable():
|
|
90
|
+
"""``_load_model`` can be re-run (or overridden) after construction to swap the network."""
|
|
91
|
+
est = build()
|
|
92
|
+
first_model = est.model
|
|
93
|
+
est._load_model()
|
|
94
|
+
assert est.model is not first_model # a fresh module, same checkpoint
|
|
95
|
+
assert est.max_features == MAX_FEATURES # unaffected: derived once, from the first load
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_load_model_can_be_overridden_to_share_a_network():
|
|
99
|
+
"""A caller can skip the per-instance download/build entirely by overriding ``_load_model``."""
|
|
100
|
+
donor = build()
|
|
101
|
+
|
|
102
|
+
class _SharedWeightEstimator(TabDPTEstimator):
|
|
103
|
+
def _load_model(self) -> None:
|
|
104
|
+
self.model = donor.model # reuse the donor's network instead of loading one
|
|
105
|
+
|
|
106
|
+
est = _SharedWeightEstimator(mode="cls", device=DEVICE)
|
|
107
|
+
assert est.model is donor.model
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_get_params_reads_every_constructor_argument():
|
|
111
|
+
"""``clip_sigma`` and ``model_weight_path`` are stored on ``self``, as sklearn's contract requires."""
|
|
112
|
+
est = build(clip_sigma=4.0)
|
|
113
|
+
params = est.get_params()
|
|
114
|
+
assert params["clip_sigma"] == 4.0
|
|
115
|
+
assert params["model_weight_path"] is None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from tabdpt import TabDPTClassifier, TabDPTRegressor
|
|
6
|
+
from tabdpt.explain import integrated_gradients
|
|
7
|
+
|
|
8
|
+
from device_utils import pick_device, seed_everything
|
|
9
|
+
|
|
10
|
+
DEVICE = pick_device()
|
|
11
|
+
N_TRAIN = 2000
|
|
12
|
+
N_TEST = 100
|
|
13
|
+
N_COL = 4
|
|
14
|
+
|
|
15
|
+
class TestIntegratedGradients(unittest.TestCase):
|
|
16
|
+
@classmethod
|
|
17
|
+
def setUpClass(cls):
|
|
18
|
+
seed_everything(0)
|
|
19
|
+
cls.gen = np.random.default_rng(0)
|
|
20
|
+
cls.X_train = cls.gen.normal(size=(N_TRAIN, N_COL))
|
|
21
|
+
cls.X_test = cls.gen.normal(size=(N_TEST, N_COL))
|
|
22
|
+
# Expected attributions: feature 1 > feature 2, feature 0 and 3 uninmportant
|
|
23
|
+
cls.y_reg = cls.X_train[:,1] + 0.1*cls.X_train[:,2]
|
|
24
|
+
cls.y_cls = cls.y_reg > 0
|
|
25
|
+
|
|
26
|
+
def validate_attrs(self, attrs):
|
|
27
|
+
self.assertEqual(attrs.shape, (N_TEST, N_COL))
|
|
28
|
+
agg = np.mean(np.abs(attrs), axis=0)
|
|
29
|
+
self.assertGreater(agg[1], agg[2])
|
|
30
|
+
self.assertGreater(agg[2], agg[0])
|
|
31
|
+
self.assertGreater(agg[2], agg[3])
|
|
32
|
+
self.assertGreater(agg[1]/10, agg[0], "most important feature should be clearly above unimportant ones")
|
|
33
|
+
self.assertGreater(agg[1]/10, agg[3], "most important feature should be clearly above unimportant ones")
|
|
34
|
+
|
|
35
|
+
def test_regression(self):
|
|
36
|
+
model = TabDPTRegressor(device=DEVICE)
|
|
37
|
+
model.fit(self.X_train, self.y_reg)
|
|
38
|
+
attrs = integrated_gradients(model, self.X_test)
|
|
39
|
+
self.validate_attrs(attrs)
|
|
40
|
+
|
|
41
|
+
def test_classification(self):
|
|
42
|
+
model = TabDPTClassifier(device=DEVICE)
|
|
43
|
+
model.fit(self.X_train, self.y_cls)
|
|
44
|
+
attrs = integrated_gradients(model, self.X_test, class_index=1)
|
|
45
|
+
self.validate_attrs(attrs)
|
|
46
|
+
|
|
47
|
+
def test_no_retrieval(self):
|
|
48
|
+
model = TabDPTClassifier(context_reduction="retrieval", device=DEVICE)
|
|
49
|
+
model.fit(self.X_train, self.y_cls)
|
|
50
|
+
with self.assertRaises(NotImplementedError):
|
|
51
|
+
integrated_gradients(model, self.X_test, context_size=10)
|
|
52
|
+
|
|
53
|
+
def test_no_large_cls(self):
|
|
54
|
+
model = TabDPTClassifier(device=DEVICE)
|
|
55
|
+
n_classes = model.max_num_classes + 2
|
|
56
|
+
y = np.tile(np.arange(n_classes), len(self.y_cls) // n_classes + 1)[:len(self.y_cls)]
|
|
57
|
+
model.fit(self.X_train, y)
|
|
58
|
+
with self.assertRaises(NotImplementedError):
|
|
59
|
+
integrated_gradients(model, self.X_test)
|
|
60
|
+
|
|
61
|
+
def test_pca(self):
|
|
62
|
+
model = TabDPTRegressor(device=DEVICE)
|
|
63
|
+
n_features = model.max_features + 10
|
|
64
|
+
X_train = np.hstack((self.X_train, self.gen.normal(size=(N_TRAIN, n_features-N_COL))))
|
|
65
|
+
X_test = np.hstack((self.X_test, self.gen.normal(size=(N_TEST, n_features-N_COL))))
|
|
66
|
+
model.fit(X_train, self.y_reg)
|
|
67
|
+
attrs = integrated_gradients(model, X_test)
|
|
68
|
+
self.assertEqual(attrs.shape, (N_TEST, n_features))
|
|
69
|
+
agg = np.mean(np.abs(attrs), axis=0)
|
|
70
|
+
self.assertGreater(agg[1], agg[2])
|
|
71
|
+
self.assertGreater(agg[2], max(agg[0], np.max(agg[3:])))
|
|
72
|
+
|
|
73
|
+
def test_feature_subsample(self):
|
|
74
|
+
model = TabDPTRegressor(feature_reduction="subsample", device=DEVICE, compile=False, verbose=False)
|
|
75
|
+
n_features = model.max_features + 10
|
|
76
|
+
X_train = np.hstack((self.X_train, self.gen.normal(size=(N_TRAIN, n_features-N_COL))))
|
|
77
|
+
X_test = np.hstack((self.X_test, self.gen.normal(size=(N_TEST, n_features-N_COL))))
|
|
78
|
+
model.fit(X_train, self.y_reg)
|
|
79
|
+
attrs = integrated_gradients(model, X_test)
|
|
80
|
+
self.assertEqual(attrs.shape, (N_TEST, n_features))
|
|
81
|
+
# Can't assert feature ordering because relevant features might not have been sampled
|
|
82
|
+
|
|
83
|
+
def test_missing_indicators(self):
|
|
84
|
+
model = TabDPTRegressor(missing_indicators=True, device=DEVICE)
|
|
85
|
+
X_train = self.X_train.copy()
|
|
86
|
+
X_train[self.gen.uniform(size=X_train.shape[0]) > 0.9, 1] = np.nan
|
|
87
|
+
X_train[self.gen.uniform(size=X_train.shape[0]) > 0.5, 3] = np.nan
|
|
88
|
+
X_test = self.X_test.copy()
|
|
89
|
+
X_test[self.gen.uniform(size=X_test.shape[0]) > 0.9, 1] = np.nan
|
|
90
|
+
X_test[self.gen.uniform(size=X_test.shape[0]) > 0.5, 3] = np.nan
|
|
91
|
+
model.fit(X_train, self.y_reg)
|
|
92
|
+
attrs = integrated_gradients(model, X_test)
|
|
93
|
+
self.validate_attrs(attrs)
|
|
94
|
+
|
|
95
|
+
def test_batching(self):
|
|
96
|
+
model = TabDPTRegressor(device=DEVICE)
|
|
97
|
+
model.fit(self.X_train, self.y_reg)
|
|
98
|
+
attrs = integrated_gradients(model, self.X_test, batch_size=N_TEST // 10)
|
|
99
|
+
self.validate_attrs(attrs)
|
|
100
|
+
|
|
101
|
+
def test_nan_cols(self):
|
|
102
|
+
model = TabDPTClassifier(device=DEVICE)
|
|
103
|
+
X_train = self.X_train.copy()
|
|
104
|
+
X_train[:, 0] = np.nan
|
|
105
|
+
X_train[:, N_COL-1] = np.nan
|
|
106
|
+
model.fit(X_train, self.y_cls)
|
|
107
|
+
attrs = integrated_gradients(model, self.X_test)
|
|
108
|
+
self.assertEqual(attrs.shape, (N_TEST, N_COL))
|
|
109
|
+
self.assertTrue(np.all(attrs[:, 0] == 0))
|
|
110
|
+
self.assertTrue(np.all(attrs[:, N_COL-1] == 0))
|
|
@@ -26,11 +26,20 @@ class TestClassifier(unittest.TestCase):
|
|
|
26
26
|
self.assertEqual(p.shape, (N_TRAIN, N_CLASSES))
|
|
27
27
|
np.testing.assert_allclose(p.sum(axis=1), 1.0, atol=1e-4)
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
p = self.model.predict_proba(self.X,
|
|
29
|
+
def test_batching(self):
|
|
30
|
+
p = self.model.predict_proba(self.X, batch_size=N_TRAIN//4)
|
|
31
31
|
self.assertEqual(p.shape, (N_TRAIN, N_CLASSES))
|
|
32
32
|
np.testing.assert_allclose(p.sum(axis=1), 1.0, atol=1e-4)
|
|
33
33
|
|
|
34
|
+
def test_context_reduction(self):
|
|
35
|
+
for context_reduction in ("retrieval", "subsample", "subsample-balanced"):
|
|
36
|
+
model = TabDPTClassifier(device=DEVICE, context_reduction=context_reduction)
|
|
37
|
+
model.fit(self.X, self.y)
|
|
38
|
+
for batch_size in (None, N_TRAIN//4):
|
|
39
|
+
p = model.predict_proba(self.X, context_size=20, batch_size=batch_size)
|
|
40
|
+
self.assertEqual(p.shape, (N_TRAIN, N_CLASSES))
|
|
41
|
+
np.testing.assert_allclose(p.sum(axis=1), 1.0, atol=1e-4)
|
|
42
|
+
|
|
34
43
|
def test_return_logits(self):
|
|
35
44
|
logits = self.model.predict_proba(self.X, context_size=2048, return_logits=True)
|
|
36
45
|
self.assertEqual(logits.shape[0], N_TRAIN)
|
|
@@ -97,9 +106,13 @@ class TestRegressor(unittest.TestCase):
|
|
|
97
106
|
pred = self.model.predict(self.X, n_ensembles=1, context_size=2048)
|
|
98
107
|
self.assertEqual(pred.shape, (N_TRAIN,))
|
|
99
108
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
def test_context_reduction(self):
|
|
110
|
+
for context_reduction in ("retrieval", "subsample"):
|
|
111
|
+
model = TabDPTRegressor(device=DEVICE, verbose=False)
|
|
112
|
+
model.fit(self.X, self.y)
|
|
113
|
+
for batch_size in (None, N_TRAIN//4):
|
|
114
|
+
pred = model.predict(self.X, n_ensembles=1, context_size=20, batch_size=batch_size)
|
|
115
|
+
self.assertEqual(pred.shape, (N_TRAIN,))
|
|
103
116
|
|
|
104
117
|
def test_ensemble(self):
|
|
105
118
|
pred = self.model.predict(self.X, n_ensembles=3, context_size=2048, seed=0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|