Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ on:
env:
package-name: smac
test-dir: tests
extra-requires: "[gpytorch,dev]"
extra-requires: "[gpytorch,dev,tabpfn]"

# Arguments used for pytest
pytest-args: >-
Expand Down
99 changes: 99 additions & 0 deletions examples/4_advanced_optimizer/5_tabPFN_surrogate_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Support Vector Machine with Cross-Validation
# Flags: doc-Runnable

An example of optimizing a simple support vector machine on the IRIS dataset. We use the
hyperparameter optimization facade, which uses a random forest as its surrogate model. It is able to
scale to higher evaluation budgets and a higher number of dimensions. Also, you can use mixed data
types as well as conditional hyperparameters.
"""

import numpy as np
from ConfigSpace import Categorical, Configuration, ConfigurationSpace, Float, Integer
from ConfigSpace.conditions import InCondition
from sklearn import datasets, svm
from sklearn.model_selection import cross_val_score
from smac.model.tabPFNv2 import TabPFNModel

from smac import HyperparameterOptimizationFacade, Scenario

__copyright__ = "Copyright 2025, Leibniz University Hanover, Institute of AI"
__license__ = "3-clause BSD"


# We load the iris-dataset (a widely used benchmark)
iris = datasets.load_iris()


class SVM:
@property
def configspace(self) -> ConfigurationSpace:
# Build Configuration Space which defines all parameters and their ranges
cs = ConfigurationSpace(seed=0)

# First we create our hyperparameters
kernel = Categorical("kernel", ["linear", "poly", "rbf", "sigmoid"], default="poly")
C = Float("C", (0.001, 1000.0), default=1.0, log=True)
shrinking = Categorical("shrinking", [True, False], default=True)
degree = Integer("degree", (1, 5), default=3)
coef = Float("coef0", (0.0, 10.0), default=0.0)
gamma = Categorical("gamma", ["auto", "value"], default="auto")
gamma_value = Float("gamma_value", (0.0001, 8.0), default=1.0, log=True)

# Then we create dependencies
use_degree = InCondition(child=degree, parent=kernel, values=["poly"])
use_coef = InCondition(child=coef, parent=kernel, values=["poly", "sigmoid"])
use_gamma = InCondition(child=gamma, parent=kernel, values=["rbf", "poly", "sigmoid"])
use_gamma_value = InCondition(child=gamma_value, parent=gamma, values=["value"])

# Add hyperparameters and conditions to our configspace
cs.add([kernel, C, shrinking, degree, coef, gamma, gamma_value])
cs.add([use_degree, use_coef, use_gamma, use_gamma_value])

return cs

def train(self, config: Configuration, seed: int = 0) -> float:
"""Creates a SVM based on a configuration and evaluates it on the
iris-dataset using cross-validation."""
config_dict = dict(config)
if "gamma" in config:
config_dict["gamma"] = config_dict["gamma_value"] if config_dict["gamma"] == "value" else "auto"
config_dict.pop("gamma_value", None)

classifier = svm.SVC(**config_dict, random_state=seed)
scores = cross_val_score(classifier, iris.data, iris.target, cv=5)
cost = 1 - np.mean(scores)

return cost


if __name__ == "__main__":
classifier = SVM()

# Next, we create an object, holding general information about the run
scenario = Scenario(
classifier.configspace,
n_trials=50, # We want to run max 50 trials (combination of config and seed)
)

# We want to run the facade's default initial design, but we want to change the number
# of initial configs to 5.
initial_design = HyperparameterOptimizationFacade.get_initial_design(scenario, n_configs=5)

# Now we use SMAC to find the best hyperparameters
smac = HyperparameterOptimizationFacade(
scenario,
classifier.train,
initial_design=initial_design,
overwrite=True, # If the run exists, we overwrite it; alternatively, we can continue from last state
model=TabPFNModel(configspace=scenario.configspace, seed=scenario.seed), # use TabPFN as surrogate model
)

incumbent = smac.optimize()

# Get cost of default configuration
default_cost = smac.validate(classifier.configspace.get_default_configuration())
print(f"Default cost: {default_cost}")

# Let's calculate the cost of the incumbent
incumbent_cost = smac.validate(incumbent)
print(f"Incumbent cost: {incumbent_cost}")
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ def read_file(filepath: str) -> str:
"black", # This allows mkdocstrings to format signatures in the docs
"pytest",
"pytest-coverage",
"pytest-cases",
"pytest-cases"
],
"tabpfn":["tabpfn"]
}

setuptools.setup(
Expand Down Expand Up @@ -85,7 +86,7 @@ def read_file(filepath: str) -> str:
"dask_jobqueue>=0.8.2",
"emcee>=3.0.0",
"regex",
"pyyaml",
"pyyaml"
],
extras_require=extras_require,
test_suite="pytest",
Expand Down
13 changes: 8 additions & 5 deletions smac/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
from smac.model.multi_objective_model import MultiObjectiveModel
from smac.model.random_model import RandomModel

__all__ = [
"AbstractModel",
"MultiObjectiveModel",
"RandomModel",
]
__all__ = ["AbstractModel", "MultiObjectiveModel", "RandomModel"]

try:
from smac.model.tabPFNv2 import TabPFNModel

__all__ = ["AbstractModel", "MultiObjectiveModel", "RandomModel", "TabPFNModel"]
except ImportError:
pass
132 changes: 132 additions & 0 deletions smac/model/tabPFNv2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from __future__ import annotations

from typing import Any

import numpy as np
from ConfigSpace import ConfigurationSpace
from ConfigSpace.hyperparameters import CategoricalHyperparameter
from tabpfn import TabPFNRegressor

from smac.model.abstract_model import AbstractModel
from smac.utils.logging import get_logger

__copyright__ = "Copyright 2025, Leibniz University Hanover, Institute of AI"
__license__ = "3-clause BSD"

logger = get_logger(__name__)


class TabPFNModel(AbstractModel):
"""TabPFNModel, for more details check: https://github.com/PriorLabs/TabPFN.

Parameters
----------
instance_features : dict[str, list[int | float]] | None, defaults to None
Features (list of int or floats) of the instances (str). The features are incorporated into the X data,
on which the model is trained on.
pca_components : float, defaults to 7
Number of components to keep when using PCA to reduce dimensionality of instance features.
seed : int
n_estimators : int, defaults to 8
The number of estimators in the TabPFN ensemble.
softmax_temperature : float, defaults to 0.9
The temperature for the softmax function.
"""

def __init__(
self,
configspace: ConfigurationSpace,
instance_features: dict[str, list[int | float]] | None = None,
pca_components: int | None = 7,
seed: int = 0,
n_estimators: int = 8,
softmax_temperature: float = 0.9,
) -> None:
super().__init__(
configspace=configspace,
instance_features=instance_features,
pca_components=pca_components,
seed=seed,
)

self._tabpfn = None
self.n_estimators = n_estimators
self.categorical_features_indices = [
i for i, hp in enumerate(list(configspace.values())) if isinstance(hp, CategoricalHyperparameter)
]
self.softmax_temperature = softmax_temperature
self.random_state = seed

@property
def meta(self) -> dict[str, Any]:
"""Returns the metadata of the model.

Returns
-------
dict[str, Any]: meta data
"""
meta = super().meta
meta.update(
{
"pca_components": self._pca_components,
}
)
return meta

def _train(self, X: np.ndarray, y: np.ndarray) -> TabPFNModel:
y = y.flatten()

self._tabpfn = self._get_tabpfn()
if self._tabpfn is None:
raise AssertionError("TabPFNRegressor is not initialized properly!")
self._tabpfn.fit(X, y)

# Set the flag
self._is_trained = True

return self

def _predict(
self,
X: np.ndarray,
covariance_type: str | None = "diagonal",
) -> tuple[np.ndarray, np.ndarray | None]:
if len(X.shape) != 2:
raise ValueError("Expected 2d array, got %dd array!" % len(X.shape))

if X.shape[1] != len(self._types):
raise ValueError("Rows in X should have %d entries but have %d!" % (len(self._types), X.shape[1]))

if covariance_type != "diagonal":
raise ValueError("`covariance_type` can only take `diagonal` for this model.")

assert self._tabpfn is not None
# X = self._impute_inactive(X)

out_dict = self._tabpfn.predict(X, output_type="full")

# Variance estimation is difficult with TabPFN, it can have very large variances
var = out_dict["criterion"].variance(out_dict["logits"]).cpu().detach().numpy()
var = var.flatten()
var = np.clip(var, np.percentile(var, 5), np.percentile(var, 95))
if np.isclose(var.min(), var.max()):
var = np.zeros_like(var)
else:
var = (var - var.min()) / (var.max() - var.min())
var = var + 1e-6 # Avoid zero variance
return out_dict["mean"], var

def _get_tabpfn(self) -> TabPFNRegressor:
"""Return a TabPFNRegressor instance with the specified parameters.
The fit_mode is set to 'low_memory' because the model is often retrained.

Returns
-------
TabPFNRegressor: TabPFNRegressor.
"""
return TabPFNRegressor(
n_estimators=self.n_estimators,
categorical_features_indices=self.categorical_features_indices,
softmax_temperature=self.softmax_temperature,
fit_mode="low_memory",
)
Loading
Loading