Skip to content

Commit 2b32e9f

Browse files
committed
v1.5.0: new features for HPO, ensembling, etc.
1 parent 630470d commit 2b32e9f

18 files changed

+719
-201
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@ on our benchmarks.
1515

1616
![Meta-test benchmark results](./figures/meta-test_benchmark_results.png)
1717

18+
## When (not) to use pytabkit
19+
20+
- **To get the best possible results**:
21+
- Generally we recommend AutoGluon for the best possible results,
22+
though it does not include all the models from pytabkit.
23+
It will probably include RealMLP in the upcoming 1.4 version.
24+
- To get the best possible results from `pytabkit`,
25+
we recommend using
26+
`Ensemble_HPO_Classifier(n_cv=8, use_full_caruana_ensembling=True, use_tabarena_spaces=True, n_hpo_steps=50)`
27+
with a `val_metric_name` corresponding to your target metric
28+
(e.g., `class_error`, `cross_entropy`, `brier`, `1-auc_ovr`), or the corresponding `Regressor`.
29+
(This might take very long to fit.)
30+
- For only a single model, we recommend using
31+
`RealMLP_HPO_Classifier(n_cv=8, hpo_space_name='tabarena', use_caruana_ensembling=True, n_hyperopt_steps=50)`,
32+
also with `val_metric_name` as above, or the corresponding `Regressor`.
33+
- **Models**: [TabArena](https://github.com/AutoGluon/tabrepo)
34+
also includes some newer models like RealMLP and TabM
35+
with more general preprocessing (missing numericals, text, etc.),
36+
as well as very good boosted tree implementations.
37+
`pytabkit` is currently still easier to use
38+
and supports vectorized cross-validation for RealMLP,
39+
which can significantly speed up the training.
40+
- **Benchmarking**: While pytabkit can be good for quick benchmarking for development,
41+
for method evaluation we recommend [TabArena](https://github.com/AutoGluon/tabrepo).
42+
1843
## Installation (new in 1.4.0: optional model dependencies)
1944

2045
```bash
@@ -171,6 +196,20 @@ and https://docs.ray.io/en/latest/cluster/vms/user-guides/community/slurm.html
171196

172197
## Releases (see git tags)
173198

199+
- v1.5.0:
200+
- added `n_repeats` parameter to scikit-learn interfaces for repeated cross-validation
201+
- HPO sklearn interfaces (the ones using random search)
202+
can now do weighted ensembling instead by setting `use_caruana_ensembling=True`.
203+
Removed the `RealMLP_Ensemble_Classifier` and `RealMLP_Ensemble_Regressor` from v1.4.2
204+
since they are now redundant through this feature.
205+
- renamed `space` parameter of GBDT HPO interface
206+
to `hpo_space_name` so now it also works with non-TPE versions.
207+
- Added new [TabArena](https://tabarena.ai) search spaces for boosted trees (not TPE),
208+
which should be almost equivalent to the ones from TabArena
209+
except for the early stopping logic.
210+
- TabM now supports `val_metric_name` for early stopping on different metrics.
211+
- fixed issues #20 and #21 regarding HPO
212+
- small updates for the ["Rethinking Early Stopping" paper](https://arxiv.org/abs/2501.19195)
174213
- v1.4.2:
175214
- fixed handling of custom `val_metric_name` HPO models and `Ensemble_TD_Regressor`.
176215
- if `tmp_folder` is specified in HPO models,
@@ -246,7 +285,7 @@ and https://docs.ray.io/en/latest/cluster/vms/user-guides/community/slurm.html
246285
Add time limit for RealMLP,
247286
add support for `lightning` (but also still allowing `pytorch-lightning`),
248287
making skorch a lazy import, removed msgpack\_numpy dependency.
249-
- v1.0.0: Release for the NeurIPS version and arXiv v2.
288+
- v1.0.0: Release for the NeurIPS version and arXiv v2+v3.
250289
- More baselines (MLP-PLR, FT-Transformer, TabR-HPO, RF-HPO),
251290
also some un-polished internal interfaces for other methods,
252291
esp. the ones in AutoGluon.

pytabkit/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
__version__ = "1.4.2"
5+
__version__ = "1.5.0"

pytabkit/models/alg_interfaces/alg_interfaces.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ def get_refit_interface(self, n_refit: int, fit_params: Optional[List[Dict]] = N
347347
else:
348348
assert self.fit_params is not None
349349
fit_params = self.fit_params
350+
# print(f'{fit_params=}')
350351
alg_interface = self.create_alg_interface(n_refit,
351352
**utils.join_dicts(self.config, fit_params[0]['hyper_fit_params']))
352353
# the alg_interface itself may have other hypers that have been fit

pytabkit/models/alg_interfaces/catboost_interfaces.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ def __init__(self, space=None, n_hyperopt_steps: int = 50, **config):
324324
# 'used_ram_limit': hp.choice('used_ram_limit', [100000000000]),
325325
# }
326326
# need to add defaults as well
327-
327+
if space is None:
328+
space = config.get('hpo_space_name', None)
328329
if space == 'NODE' or space == 'popov':
329330
# space from NODE paper:
330331
# Popov, Morozov, and Babenko, Neural oblivious decision ensembles for deep learning on tabular data
@@ -721,6 +722,31 @@ def _sample_params(self, is_classification: bool, seed: int, n_train: int):
721722
'one_hot_max_size': rng.choice([2, 3, 5, 10]),
722723
'grow_policy': rng.choice(['SymmetricTree', 'Depthwise']),
723724
}
725+
elif hpo_space_name == 'tabarena':
726+
space = {
727+
'n_estimators': 10_000,
728+
'early_stopping_rounds': 300, # probably not exactly equivalent to TabArena
729+
'learning_rate': np.exp(rng.uniform(np.log(5e-3), np.log(1e-1))),
730+
731+
'bootstrap_type': 'Bernoulli',
732+
'subsample': rng.uniform(0.7, 1.0), # can only be used with Bernoulli (or Poisson)!
733+
734+
'grow_policy': rng.choice(['SymmetricTree', 'Depthwise']),
735+
'max_depth': rng.integers(4, 8, endpoint=True),
736+
737+
'colsample_bylevel': rng.uniform(0.85, 1.0),
738+
'l2_leaf_reg': np.exp(rng.uniform(np.log(1e-4), np.log(5.0))),
739+
740+
'leaf_estimation_iterations': np.floor(np.exp(rng.uniform(np.log(1.0), np.log(21.0)))),
741+
742+
# categorical features
743+
'one_hot_max_size': np.floor(np.exp(rng.uniform(np.log(8.0), np.log(101.0)))),
744+
'model_size_reg': np.exp(rng.uniform(np.log(0.1), np.log(1.5))),
745+
'max_ctr_complexity': rng.integers(2, 5, endpoint=True), # shrunk
746+
747+
'boosting_type': 'Plain', # avoid Ordered as the default on GPU
748+
'max_bin': 254, # added this to be sure
749+
}
724750
else:
725751
raise ValueError()
726752
return space

pytabkit/models/alg_interfaces/ensemble_interfaces.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,11 @@ def __init__(self, alg_interfaces: List[AlgInterface], fit_params: Optional[List
175175

176176
def get_refit_interface(self, n_refit: int, fit_params: Optional[List[Dict]] = None) -> 'AlgInterface':
177177
# todo: could use sub_fit_params
178-
return AlgorithmSelectionAlgInterface([alg_interface.get_refit_interface(n_refit=n_refit)
179-
for alg_interface in self.alg_interfaces],
178+
refit_interfaces = []
179+
for alg_context in self.alg_contexts_:
180+
with alg_context as alg_interface:
181+
refit_interfaces.append(alg_interface.get_refit_interface(n_refit=n_refit))
182+
return AlgorithmSelectionAlgInterface(refit_interfaces,
180183
fit_params=fit_params or self.fit_params)
181184

182185
def fit(self, ds: DictDataset, idxs_list: List[SplitIdxs], interface_resources: InterfaceResources,

pytabkit/models/alg_interfaces/lightgbm_interfaces.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ def __init__(self, space=None, n_hyperopt_steps: int = 50, opt_method: str = 'hy
276276
from hyperopt import hp
277277
default_config = {}
278278
max_config = dict()
279+
if space is None:
280+
space = config.get('hpo_space_name', None)
279281
if space == 'catboost_quality_benchmarks':
280282
# space from catboost quality benchmarks,
281283
# https://github.com/catboost/benchmarks/blob/master/quality_benchmarks/lightgbm_experiment.py
@@ -672,6 +674,26 @@ def _sample_params(self, is_classification: bool, seed: int, n_train: int):
672674
'num_leaves': rng.integers(16, 255, endpoint=True),
673675
'extra_trees': rng.choice([False, True]),
674676
}
677+
elif hpo_space_name == 'tabarena':
678+
space = {
679+
'early_stopping_rounds': 300, # not exactly equivalent, probably
680+
'n_estimators': 10_000,
681+
'learning_rate': np.exp(rng.uniform(np.log(5e-3), np.log(1e-1))),
682+
'feature_fraction': rng.uniform(0.4, 1),
683+
'bagging_fraction': rng.uniform(0.7, 1),
684+
'bagging_freq': 1, # already the default here but not in original LightGBM
685+
'num_leaves': np.floor(np.exp(rng.uniform(np.log(2.0), np.log(201)))),
686+
'min_data_in_leaf': np.floor(np.exp(rng.uniform(np.log(1.0), np.log(65)))),
687+
'extra_trees': rng.choice([False, True]),
688+
689+
'min_data_per_group': np.floor(np.exp(rng.uniform(np.log(2.0), np.log(101)))),
690+
'cat_l2': np.exp(rng.uniform(np.log(5e-3), np.log(2.0))),
691+
'cat_smooth': np.exp(rng.uniform(np.log(1e-3), np.log(100.0))),
692+
'max_cat_to_onehot': np.floor(np.exp(rng.uniform(np.log(8.0), np.log(101.0)))),
693+
694+
'lambda_l1': np.exp(rng.uniform(np.log(1e-5), np.log(1.0))),
695+
'lambda_l2': np.exp(rng.uniform(np.log(1e-5), np.log(2.0))),
696+
}
675697
else:
676698
raise ValueError()
677699
return space

pytabkit/models/alg_interfaces/nn_interfaces.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,9 +644,9 @@ def sample_params(self, seed: int) -> Dict[str, Any]:
644644

645645
if rng.uniform(0.0, 1.0) > 0.5:
646646
# large configs
647-
params['plr_hidden_1'] = rng.choice([8, 16, 32, 64])
648-
params['plr_hidden_2'] = rng.choice([8, 16, 32, 64])
649-
params['n_epochs'] = rng.choice([256, 512])
647+
params['plr_hidden_1'] = rng.choice([8, 16, 32, 64]).item()
648+
params['plr_hidden_2'] = rng.choice([8, 16, 32, 64]).item()
649+
params['n_epochs'] = rng.choice([256, 512]).item()
650650
params['use_early_stopping'] = True
651651

652652
# set in the defaults of RealMLP in TabArena

pytabkit/models/alg_interfaces/sub_split_interfaces.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ def get_refit_interface(self, n_refit: int, fit_params: Optional[List[Dict]] = N
3838

3939
config = utils.join_dicts(self.sub_split_interfaces[0].config, self.config)
4040
if config.get('use_best_mean_iteration_for_refit', True):
41+
sub_fit_params = [utils.update_dict(fit_params[0], remove_keys='sub_fit_params')]
4142
return SingleSplitWrapperAlgInterface(
42-
[self.sub_split_interfaces[0].get_refit_interface(n_refit=1, fit_params=fit_params) for i in
43+
[self.sub_split_interfaces[0].get_refit_interface(
44+
n_refit=1, fit_params=sub_fit_params)
45+
for i in
4346
range(n_refit)], fit_params=fit_params)
4447
else:
4548
if n_refit != len(self.sub_split_interfaces):
@@ -114,7 +117,9 @@ def fit(self, ds: DictDataset, idxs_list: List[SplitIdxs], interface_resources:
114117
for ssi in self.sub_split_interfaces:
115118
ssi.fit_params = self.fit_params
116119
else:
117-
self.fit_params = [dict(sub_fit_params=[(ssi.fit_params[0] if ssi.fit_params is not None else None) for ssi in self.sub_split_interfaces])]
120+
self.fit_params = [dict(
121+
sub_fit_params=[(ssi.fit_params[0] if ssi.fit_params is not None else None) for ssi in
122+
self.sub_split_interfaces])]
118123

119124
return None
120125

@@ -338,7 +343,7 @@ def fit(self, ds: DictDataset, idxs_list: List[SplitIdxs], interface_resources:
338343
self.fit_params = [dict(n_estimators=len(val_errors))]
339344

340345
if isinstance(val_errors, dict):
341-
return None # not implemented
346+
return None # not implemented
342347
else:
343348
return [[[(dict(n_estimators=i + 1), err) for i, err in enumerate(val_errors)]]]
344349

0 commit comments

Comments
 (0)