"""Scikit-learn surrogate adapter and convenience classes."""
from __future__ import annotations
from typing import Any
import numpy as np
from saealib.surrogate.base import ComparisonSurrogate, RegressionSurrogate
from saealib.surrogate.prediction import SurrogatePrediction
[docs]
class SklearnSurrogate(RegressionSurrogate):
"""
Surrogate adapter for scikit-learn compatible estimators.
Wraps any estimator that implements ``fit(X, y)`` and ``predict(X)``.
Multi-objective problems are handled by fitting one cloned estimator
per objective, following the same pattern as ``RBFSurrogate``.
Parameters
----------
estimator : sklearn estimator
A scikit-learn compatible regressor instance (e.g. ``SVR()``).
The estimator is cloned once per objective on the first ``fit`` call.
Raises
------
ImportError
If scikit-learn is not installed.
"""
[docs]
def __init__(self, estimator: object) -> None:
try:
import sklearn # noqa: F401
except ImportError as e:
raise ImportError(
"scikit-learn is required for SklearnSurrogate. "
"Install it with: pip install saealib[sklearn]"
) from e
self.estimator = estimator
self._models: list | None = None
self.n_obj: int | None = None
[docs]
def fit(self, train_x: np.ndarray, train_y: np.ndarray) -> None:
"""
Fit the surrogate model.
Parameters
----------
train_x : np.ndarray
Training input data. shape: (n_samples, n_features)
train_y : np.ndarray
Training output data. shape: (n_samples, n_obj) or (n_samples,).
"""
from sklearn.base import clone
arr = np.asarray(train_y, dtype=float)
if arr.ndim == 1:
arr = arr.reshape(-1, 1)
n_obj = arr.shape[1]
if self._models is None or n_obj != self.n_obj:
self.n_obj = n_obj
self._models = [clone(self.estimator) for _ in range(n_obj)]
for i, model in enumerate(self._models):
model.fit(train_x, arr[:, i])
[docs]
def predict(self, test_x: np.ndarray) -> SurrogatePrediction:
"""
Predict using the surrogate model.
Parameters
----------
test_x : np.ndarray
Input data. shape: (n_samples, n_features) or (n_features,)
Returns
-------
SurrogatePrediction
prediction.value shape: (n_samples, n_obj)
prediction.std is None
"""
test = np.asarray(test_x)
if test.ndim == 1:
test = test.reshape(1, -1)
assert self._models is not None
preds = [m.predict(test) for m in self._models]
value = np.column_stack(preds)
return SurrogatePrediction.objective(value=value)
[docs]
class SklearnSVMSurrogate(SklearnSurrogate):
"""
Support Vector Regression surrogate.
Convenience wrapper around ``SklearnSurrogate`` using
``sklearn.svm.SVR``.
Parameters
----------
**kwargs
Keyword arguments forwarded to ``sklearn.svm.SVR``.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
from sklearn.svm import SVR
super().__init__(SVR(**kwargs))
[docs]
class SklearnNNSurrogate(SklearnSurrogate):
"""
Multi-layer Perceptron surrogate.
Convenience wrapper around ``SklearnSurrogate`` using
``sklearn.neural_network.MLPRegressor``.
Parameters
----------
**kwargs
Keyword arguments forwarded to ``sklearn.neural_network.MLPRegressor``.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
from sklearn.neural_network import MLPRegressor
super().__init__(MLPRegressor(**kwargs))
[docs]
class SklearnRFRSurrogate(SklearnSurrogate):
"""
Random Forest Regressor surrogate.
Convenience wrapper around ``SklearnSurrogate`` using
``sklearn.ensemble.RandomForestRegressor``.
Parameters
----------
**kwargs
Keyword arguments forwarded to
``sklearn.ensemble.RandomForestRegressor``.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
from sklearn.ensemble import RandomForestRegressor
super().__init__(RandomForestRegressor(**kwargs))
[docs]
class SklearnGPRSurrogate(SklearnSurrogate):
"""
Gaussian Process Regressor surrogate.
Convenience wrapper around ``SklearnSurrogate`` using
``sklearn.gaussian_process.GaussianProcessRegressor``.
Provides uncertainty estimates via ``SurrogatePrediction.std``,
enabling compatibility with acquisition functions such as EI, LCB,
PoF, and MaxUncertainty.
Parameters
----------
**kwargs
Keyword arguments forwarded to
``sklearn.gaussian_process.GaussianProcessRegressor``
(e.g. ``kernel``, ``alpha``, ``n_restarts_optimizer``).
References
----------
:cite:`sacks1989dace`: Sacks, J., Welch, W. J., Mitchell, T. J., & Wynn,
H. P. (1989). Design and analysis of computer experiments. *Statistical
Science*, 4(4), 409-423.
:cite:`rasmussen2006gpml`: Rasmussen, C. E., & Williams, C. K. I.
(2006). *Gaussian Processes for Machine Learning*. MIT Press.
"""
provides_uncertainty: bool = True
[docs]
def __init__(self, **kwargs: object) -> None:
from sklearn.gaussian_process import GaussianProcessRegressor
super().__init__(GaussianProcessRegressor(**kwargs))
[docs]
def predict(self, test_x: np.ndarray) -> SurrogatePrediction:
"""
Predict mean and standard deviation using the GP models.
Parameters
----------
test_x : np.ndarray
Input data. shape: (n_samples, n_features) or (n_features,)
Returns
-------
SurrogatePrediction
prediction.value shape: (n_samples, n_obj)
prediction.std shape: (n_samples, n_obj)
"""
test = np.asarray(test_x)
if test.ndim == 1:
test = test.reshape(1, -1)
assert self._models is not None
means, stds = [], []
for m in self._models:
mu, sigma = m.predict(test, return_std=True)
means.append(mu)
stds.append(sigma)
value = np.column_stack(means)
std = np.column_stack(stds)
return SurrogatePrediction.objective(value=value, std=std)
[docs]
class SklearnXGBSurrogate(SklearnSurrogate):
"""
XGBoost surrogate.
Convenience wrapper around ``SklearnSurrogate`` using
``xgboost.XGBRegressor``.
Parameters
----------
**kwargs
Keyword arguments forwarded to ``xgboost.XGBRegressor``.
Raises
------
ImportError
If xgboost is not installed.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
try:
from xgboost import XGBRegressor
except ImportError as e:
raise ImportError(
"xgboost is required for SklearnXGBSurrogate. "
"Install it with: pip install saealib[xgboost]"
) from e
super().__init__(XGBRegressor(**kwargs))
[docs]
class SklearnLGBMSurrogate(SklearnSurrogate):
"""
LightGBM surrogate.
Convenience wrapper around ``SklearnSurrogate`` using
``lightgbm.LGBMRegressor``.
Parameters
----------
**kwargs
Keyword arguments forwarded to ``lightgbm.LGBMRegressor``.
Raises
------
ImportError
If lightgbm is not installed.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
try:
from lightgbm import LGBMRegressor
except ImportError as e:
raise ImportError(
"lightgbm is required for SklearnLGBMSurrogate. "
"Install it with: pip install saealib[lightgbm]"
) from e
super().__init__(LGBMRegressor(**kwargs)) # type: ignore # LightGBM stubs mistype __init__ kwargs
[docs]
class SklearnClassificationSurrogate(ComparisonSurrogate):
"""
Surrogate adapter for scikit-learn compatible binary classifiers.
Wraps any estimator that implements ``fit(X, y)`` and
``predict_proba(X)``. ``train_y`` must be binary labels ``{0, 1}``.
Parameters
----------
estimator : sklearn estimator
A scikit-learn compatible classifier with ``predict_proba`` support
(e.g. ``SVC(probability=True)`` or ``RandomForestClassifier()``).
Raises
------
ImportError
If scikit-learn is not installed.
"""
[docs]
def __init__(self, estimator: object) -> None:
try:
import sklearn # noqa: F401
except ImportError as e:
raise ImportError(
"scikit-learn is required for SklearnClassificationSurrogate. "
"Install it with: pip install saealib[sklearn]"
) from e
self.estimator = estimator
self._model: Any = None
[docs]
def fit(self, train_x: np.ndarray, train_y: np.ndarray) -> None:
"""
Fit the classifier.
Parameters
----------
train_x : np.ndarray
Training input data. shape: (n_samples, n_features)
train_y : np.ndarray
Binary labels. shape: (n_samples,), values in ``{0, 1}``.
"""
from sklearn.base import clone
labels = np.asarray(train_y, dtype=float).ravel()
if self._model is None:
self._model = clone(self.estimator)
self._model.fit(train_x, labels)
[docs]
def predict_proba(self, test_x: np.ndarray) -> SurrogatePrediction:
"""
Return class-1 probability estimates.
Parameters
----------
test_x : np.ndarray
Input data. shape: (n_samples, n_features) or (n_features,)
Returns
-------
SurrogatePrediction
``value`` shape: ``(n_samples, 1)``, values in ``[0, 1]``.
"""
test = np.asarray(test_x)
if test.ndim == 1:
test = test.reshape(1, -1)
assert self._model is not None
proba = self._model.predict_proba(test)
return SurrogatePrediction.objective(value=proba[:, 1:2])
[docs]
class SklearnSVCClassificationSurrogate(SklearnClassificationSurrogate):
"""
Support Vector Classification surrogate.
Convenience wrapper around ``SklearnClassificationSurrogate`` using
``sklearn.svm.SVC(probability=True)``.
Parameters
----------
**kwargs
Keyword arguments forwarded to ``sklearn.svm.SVC``.
``probability=True`` is always set.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
from sklearn.svm import SVC
kwargs["probability"] = True
super().__init__(SVC(**kwargs))
[docs]
class SklearnRFCClassificationSurrogate(SklearnClassificationSurrogate):
"""
Random Forest Classification surrogate.
Convenience wrapper around ``SklearnClassificationSurrogate`` using
``sklearn.ensemble.RandomForestClassifier``.
Parameters
----------
**kwargs
Keyword arguments forwarded to
``sklearn.ensemble.RandomForestClassifier``.
"""
[docs]
def __init__(self, **kwargs: object) -> None:
from sklearn.ensemble import RandomForestClassifier
super().__init__(RandomForestClassifier(**kwargs))