Surrogate#
saealib restricts the responsibility of the predictive model approximating the objective function to Surrogate, a swappable component.
Surrogate knows only fit/predict — it knows nothing about how a prediction is converted into a score (AcquisitionFunction), or where training data comes from (TrainingSet, via SurrogateManager).
Surrogate’s role#
Surrogate requires two methods to be implemented.
fit(train_x, train_y) -> None: Trains the model on inputs of shape (n_samples, n_features) and outputs of shape (n_samples, n_obj) (or (n_samples,) for single-objective).
predict(test_x) -> SurrogatePrediction: Returns predictions for inputs of shape (n_samples, n_features).
The class attribute provides_uncertainty: bool = False indicates whether predictions carry uncertainty (standard deviation).
The default is False; only Gaussian Process implementations override it to True.
There are two marker base classes between Surrogate and predict().
RegressionSurrogate: A marker for regression surrogates where train_y is a real-valued objective function output.
ComparisonSurrogate: For comparison surrogates where train_y is a {0, 1}-valued binary comparison label.
predict_proba(test_x) -> SurrogatePrediction (values are win probabilities in [0, 1]) is the primary interface, with predict() delegating to predict_proba() by default.
SurrogatePrediction#
predict()’s return value is SurrogatePrediction, a unified dataclass.
Field |
Content |
|---|---|
|
The predicted value. Shape |
|
Uncertainty (standard deviation). |
|
Class labels, only present for classification models |
|
A |
value and std are convenience properties for the objective channel.
When a surrogate’s prediction represents a quantity that isn’t the objective value (such as a novelty score), this mechanism prevents contaminating things like pbest.
Archive-based criteria such as NoveltyAcquisition instead receive candidate and archive design points directly.
Built-in Surrogates#
RBFSurrogate(kernel, polynomial_degree="auto", solver="lstsq", alpha=1e-8): A surrogate using RBF interpolation [16, 17, 18] (the origin of RBF interpolation itself is Hardy, 1971).
kernel is a required RBFKernel object with no default (built-in kernels are listed below); a custom kernel is a subclass implementing evaluate(r).
predict() explicitly returns std=None (RBF interpolation provides no uncertainty).
Parameter |
Values |
Role |
|---|---|---|
|
|
Augments the system with a polynomial term (from |
|
|
How the (possibly augmented) linear system is solved: least squares (tolerant of rank-deficient systems) / direct / ridge-regularized via |
|
|
Ridge regularization strength, used only when |
kernel and polynomial_degree are the values as configured; resolved_kernel and resolved_polynomial_degree expose what a fit() call actually resolves and uses, keeping the configured RBFKernel instance itself immutable.
Reassigning kernel/polynomial_degree/solver/alpha invalidates any fitted state; predict() then raises RuntimeError until the next fit().
Kernels carrying a length_scale field (in the same distance units as the input data) resolve it automatically to the median pairwise training-point distance when left None.
Each kernel declares two independent degrees. min_polynomial_degree is the lowest degree the kernel accepts — passing anything below it, None included, raises ValidationError. auto_polynomial_degree is what polynomial_degree="auto" resolves to, which may be higher than the minimum.
Kernel |
Formula |
|
|
|---|---|---|---|
|
\(\exp\left(-\dfrac{r^2}{2\,\ell^2}\right)\) |
none |
|
|
\(r^2 \log r\) (no |
|
|
|
\(r\) (no |
|
|
|
\(r^3\) (no |
|
|
|
\(\sqrt{r^2 + \ell^2}\) |
|
|
|
Matérn covariance, |
none |
|
All six kernels and all three solver values are supported: each is covered by tests and usable as shipped. Passing polynomial_degree=None to GaussianKernel or MaternKernel is likewise supported, and drops the constant term that "auto" would have added.
PerObjectiveSurrogate(surrogates): A RegressionSurrogate subclass, a composite class that assigns a different surrogate per objective.
Raises ValueError at fit time if train_y’s column count doesn’t match len(surrogates).
provides_uncertainty is a composite judgment, returning True only if every constituent surrogate is True.
External library adapters#
Regression surrogates via a scikit-learn-compatible API carry a Sklearn prefix.
Class |
Model |
|---|---|
|
Gaussian Process [18, 19]. The sole implementation with |
|
Random Forest regression |
|
SVM |
|
MLP |
|
XGBoost (the |
|
LightGBM (the |
|
PyTorch-based models (the |
SklearnGPRSurrogate computes standard deviation from the GP kernel via return_std=True, and returns provides_uncertainty=True.
The following classes are classification surrogates aimed at feasibility prediction.
Class |
Model |
|---|---|
|
Classification models compatible with scikit-learn in general |
|
Random Forest classification |
|
SVM classification |
See TrainingSet’s FeasibilityClassificationSet for how training data is extracted for these classification surrogates.
For pairwise comparison, use the dedicated ComparisonSurrogate-family implementations rather than these classification surrogates, paired with SurrogateManager’s PairwiseSurrogateManager and PairwiseComparisonSet.
See Installation for how to install each extra.
Note
Adapters for BoTorch/SMT surrogates, beyond scikit-learn/XGBoost/LightGBM/PyTorch, are not currently implemented in saealib.
pyproject.toml has no corresponding extra either.
pymoo has adapters, but at the Problem/Crossover/Mutation/Algorithm level, not as a Surrogate — pymoo doesn’t provide surrogate models.
Extension hooks#
If you just want to add post-fit processing, you can add it to an existing Surrogate instance with with_post_fit(fn) instead of creating a new subclass.
with_post_fit doesn’t modify the original instance — it returns a copy with fn added.
from saealib import GaussianKernel, RBFSurrogate
def log_fit(train_x, train_y, ctx=None):
print(f"fit on {len(train_x)} samples")
base = RBFSurrogate(kernel=GaussianKernel())
logged = base.with_post_fit(log_fit)
fn’s signature is fn(train_x, train_y, ctx) -> None.
Implementing a custom Surrogate#
If you need a custom predictive model, subclass Surrogate and implement fit()/predict().
Choose RegressionSurrogate for regression, or ComparisonSurrogate (implementing predict_proba()) for comparison, as the class to subclass.
The following example is a simple regression surrogate that returns the nearest training point’s objective value directly as the prediction.
import numpy as np
from saealib import RegressionSurrogate, SurrogatePrediction
class NearestNeighborSurrogate(RegressionSurrogate):
"""A simple surrogate that returns the nearest point's objective value directly as the prediction."""
def fit(self, train_x, train_y):
self.train_x = np.asarray(train_x, dtype=float)
self.train_y = np.asarray(train_y, dtype=float)
def predict(self, test_x):
test_x = np.atleast_2d(test_x)
dists = np.linalg.norm(self.train_x[None, :, :] - test_x[:, None, :], axis=2)
nearest = dists.argmin(axis=1)
value = self.train_y[nearest]
return SurrogatePrediction(value=value)
Uncertainty support table#
To use an uncertainty-based AcquisitionFunction, Surrogate needs to return std.
Class |
|
|---|---|
|
|
|
|
|
|
Optimizer.validate() detects and warns about a mismatch between AcquisitionFunction’s requires_uncertainty and Surrogate’s provides_uncertainty.