API Reference / evaluation / tuning /

bayesian_search.py

Sequential model-based hyperparameter search using Gaussian Processes.

Where GridSearchCV enumerates and RandomSearchCV samples blindly, Bayesian optimization learns as it goes: it fits a cheap probabilistic surrogate to the (configuration, score) pairs observed so far and spends its next expensive model fit wherever that surrogate says the payoff is greatest.

The module supplies the three pieces of that loop:

  • GaussianProcess — the surrogate. A from-scratch GP regressor with
an RBF kernel that returns a posterior mean and standard deviation for any untried configuration.
  • AcquisitionFunction — the decision rule. Turns the surrogate's mean
and uncertainty into a single "how promising is this point" score, trading exploitation of good regions against exploration of uncertain ones.
  • BayesianSearchCV — the tuner. Warms up
with random configurations, then repeatedly refits the GP, maximizes the acquisition function with L-BFGS-B restarts, and cross-validates the winner.

Reach for this module when a single estimator fit is expensive enough that thinking about the next configuration is cheaper than trying another one at random — large models, big datasets, or budgets in the tens of evaluations rather than thousands. For cheap fits the GP overhead dominates and random search is the better tool.

Everything is implemented on NumPy and SciPy; no external Bayesian optimization library is required.

References

Snoek2012
Snoek, J., Larochelle, H., & Adams, R. P. (2012). Practical Bayesian Optimization of Machine Learning Algorithms. Advances in Neural Information Processing Systems (NeurIPS), 25, 2951-2959.
Brochu2010
Brochu, E., Cora, V. M., & de Freitas, N. (2010). A Tutorial on Bayesian Optimization of Expensive Cost Functions, with Application to Active User Modeling and Hierarchical Reinforcement Learning. arXiv:1012.2599. https://doi.org/10.48550/arXiv.1012.2599
Rasmussen2006
Rasmussen, C. E., & Williams, C. K. I. (2006). Gaussian Processes for Machine Learning. MIT Press.

Classes

GaussianProcess

class evaluation.tuning.bayesian_search.GaussianProcess

Gaussian Process surrogate model with an RBF kernel.

A GP places a distribution over functions and, conditioned on the points observed so far, returns for any new point both a predicted mean and an honest estimate of how uncertain that prediction is. That uncertainty is exactly what an AcquisitionFunction needs in order to decide where to sample next.
Constructor
__init__(
    self,
    kernel: str = 'rbf',
    length_scale: float = 1.0,
    noise: float = 1e-10,
)

Overview

  1. fit builds the kernel matrix K over the training inputs and
adds noise to its diagonal for numerical stability.
  1. It inverts that matrix once and caches the result in K_inv.
  2. predict forms the cross-kernel K_* between the query points
and the training inputs and applies the standard GP posterior formulas.

Theory

The squared-exponential (RBF) kernel with length scale \ell is

k(x, x') = \exp\!\left( -\frac{\lVert x - x' \rVert^{2}}{2\,\ell^{2}} \right)

Conditioning a zero-mean GP prior on observations (X, y) gives the posterior at test points X_*

\mu_* = K_*\,(K + \sigma_n^{2} I)^{-1}\, y
\Sigma_* = K_{**} - K_*\,(K + \sigma_n^{2} I)^{-1}\,K_*^{\top}

where K = k(X, X), K_* = k(X_*, X), K_{**} = k(X_*, X_*) and \sigma_n^{2} is noise. predict returns \mu_* and, on request, the square root of the diagonal of \Sigma_*, clipped at zero so round-off cannot produce a negative variance.

A small \ell makes the surrogate wiggly and trusts observations only very locally; a large \ell smooths the surface and lets a single observation influence distant regions.

Parameters

kernel
str = 'rbf'
Kernel type. Only 'rbf' (squared exponential) is implemented; the value is stored but not otherwise consulted.
length_scale
float = 1.0
Length scale :math:`\ell` of the RBF kernel. Inputs are used on their raw scale, so a single length scale is shared by all dimensions.
noise
float = 1e-10
Variance :math:`\sigma_n^{2}` added to the diagonal of :math:`K`. Acts as a jitter that keeps the inversion well conditioned; raise it if numpy.linalg.inv complains about a singular matrix.

Attributes

X_train
np.ndarray of shape (n_samples, n_features) or None
Training inputs retained from the last fit. None before fitting.
y_train
np.ndarray of shape (n_samples,) or None
Training targets retained from the last fit.
K_inv
np.ndarray of shape (n_samples, n_samples) or None
Cached inverse of the noise-augmented kernel matrix.

Notes

Complexity. fit is O(n^{3}) for the explicit matrix inversion and O(n^{2}) in memory; predict is O(m\,n) for the mean and O(m^{2} + m\,n^{2}) when standard deviations are requested. This is fine for the tens to low hundreds of observations a hyperparameter search produces, and unsuitable for large-scale regression.

When to use. As the surrogate inside BayesianSearchCV. It is a deliberately minimal implementation: no kernel hyperparameter marginal-likelihood optimization, no Cholesky solve, no per-dimension length scales.

References

Rasmussen2006
Rasmussen, C. E., & Williams, C. K. I. (2006). Gaussian Processes for Machine Learning, chapter 2. MIT Press.

Fit the surrogate to four observations and query an untried point:

python
>>> import numpy as np
>>> from tuiml.evaluation.tuning.bayesian_search import GaussianProcess
>>> X = np.array([[0.0], [1.0], [2.0], [3.0]])
>>> y = np.array([0.0, 0.8, 0.9, 0.1])
>>> gp = GaussianProcess(length_scale=1.0, noise=1e-6)
>>> gp.fit(X, y)
>>> mu, sigma = gp.predict(np.array([[1.5]]), return_std=True)
>>> round(float(mu[0]), 3)
1.019
>>> round(float(sigma[0]), 3)
0.1

The posterior interpolates the observations it has already seen:

python
>>> round(float(gp.predict(np.array([[1.0]]))[0]), 3)
0.8

Methods

get_parameter_schema (cls) -> dict

Return JSON Schema for constructor parameters.

fit (self, X: np.ndarray, y: np.ndarray)

Condition the Gaussian Process on observed points.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Observed inputs.
y
np.ndarray of shape (n_samples,)
Observed targets.
Returns
None
The model is updated in place; X_train, y_train and K_inv are set.
Raises
numpy.linalg.LinAlgError
If the kernel matrix is singular. Increase noise or drop duplicate rows from X.
predict (self, X: np.ndarray, return_std: bool=False) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]

Evaluate the GP posterior at new points.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Query points. Must have the same number of columns as the data passed to fit.
return_std
bool = False
If True, also return the posterior standard deviation, which is what acquisition functions use to quantify exploration value.
Returns
y_mean
np.ndarray of shape (n_samples,)
Posterior mean :math:`\mu_*`. Returned on its own when return_std=False.
y_std
np.ndarray of shape (n_samples,)
Posterior standard deviation, clipped at zero. Only returned when return_std=True, as the second element of a tuple.
Raises
ValueError
If fit has not been called yet.

AcquisitionFunction

class evaluation.tuning.bayesian_search.AcquisitionFunction

Acquisition functions that turn a GP posterior into a sampling decision.

A callable policy for Bayesian optimization. Given a fitted GaussianProcess and the best score observed so far, it scores candidate points so that higher is better; the optimizer then maximizes it to choose the next configuration to actually train. All three variants balance the same tension: the posterior mean \mu(x) pulls toward regions already known to be good (exploitation) while the posterior standard deviation \sigma(x) pulls toward regions the surrogate has not pinned down (exploration).
Constructor
__init__(
    self,
    kind: str = 'ei',
    xi: float = 0.01,
    kappa: float = 2.576,
)

Theory

Write y^{+} for the incumbent best observed score and

Z = \frac{\mu(x) - y^{+} - \xi}{\sigma(x)}

Expected Improvement (kind='ei') is the expected amount by which x beats the incumbent, which has the closed form

\text{EI}(x) = \left(\mu(x) - y^{+} - \xi\right)\,\Phi(Z) + \sigma(x)\,\phi(Z)

where \Phi and \phi are the standard normal CDF and PDF. The first term rewards a high mean, the second rewards uncertainty, and \xi sets how much improvement must be expected before a point counts as promising. EI is the usual default because it is scale-aware: it answers "by how much", not merely "how likely".

Probability of Improvement (kind='poi') keeps only the probability that the incumbent is beaten,

\text{PI}(x) = P\!\left(f(x) \geq y^{+} + \xi\right) = \Phi(Z)

which ignores the size of the gain and therefore exploits greedily unless \xi is raised.

Upper Confidence Bound (kind='ucb') is an explicit optimistic bound and the only variant that does not reference y^{+},

\text{UCB}(x) = \mu(x) + \kappa\,\sigma(x)

with \kappa directly dialing exploration. The default \kappa = 2.576 is the two-sided 99% normal quantile.

Parameters

kind
{'ei', 'ucb', 'poi'} = 'ei'
Which policy to evaluate: 'ei' for Expected Improvement, 'ucb' for Upper Confidence Bound, 'poi' for Probability of Improvement. Any other value raises at call time, not at construction.
xi
float = 0.01
Minimum improvement margin :math:`\xi` for 'ei' and 'poi'. Larger values explore more; 0.0 is purely greedy. Ignored by 'ucb'.
kappa
float = 2.576
Exploration weight :math:`\kappa` for 'ucb'; larger values favor uncertain regions. Ignored by 'ei' and 'poi'.

Notes

Complexity. One GP prediction with standard deviations plus O(m) arithmetic for m candidate points, so the cost is dominated by predict.

When to use. Keep 'ei' unless you have a reason not to. Choose 'ucb' when you want a single, interpretable exploration knob, and 'poi' when any improvement at all is what matters. Standard deviations are floored at 1e-9 before division, so already-observed points where \sigma \to 0 yield a near-zero score rather than a divide-by-zero.

References

Jones1998
Jones, D. R., Schonlau, M., & Welch, W. J. (1998). Efficient Global Optimization of Expensive Black-Box Functions. Journal of Global Optimization, 13(4), 455-492. https://doi.org/10.1023/A:1008306431147
Srinivas2010
Srinivas, N., Krause, A., Kakade, S., & Seeger, M. (2010). Gaussian Process Optimization in the Bandit Setting: No Regret and Experimental Design. ICML, 1015-1022.

Score two candidate points against a surrogate whose incumbent is 0.9:

python
>>> import numpy as np
>>> from tuiml.evaluation.tuning.bayesian_search import (
...     AcquisitionFunction, GaussianProcess)
>>> X = np.array([[0.0], [1.0], [2.0], [3.0]])
>>> y = np.array([0.0, 0.8, 0.9, 0.1])
>>> gp = GaussianProcess(length_scale=1.0, noise=1e-6)
>>> gp.fit(X, y)
>>> candidates = np.array([[1.5], [2.5]])
>>> ei = AcquisitionFunction(kind='ei', xi=0.01)
>>> [round(float(v), 4) for v in ei(candidates, gp, y_best=0.9)]
[0.1157, 0.0]

UCB and POI rank the same candidates with different appetites for risk:

python
>>> ucb = AcquisitionFunction(kind='ucb', kappa=2.576)
>>> [round(float(v), 4) for v in ucb(candidates, gp, y_best=0.9)]
[1.2752, 0.8316]
>>> poi = AcquisitionFunction(kind='poi')
>>> [round(float(v), 4) for v in poi(candidates, gp, y_best=0.9)]
[0.8626, 0.0006]

Methods

get_parameter_schema (cls) -> dict

Return JSON Schema for constructor parameters.

__call__ (self, X: np.ndarray, gp: GaussianProcess, y_best: float) -> np.ndarray

Score candidate points with the configured acquisition policy.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Candidate points to score.
gp
GaussianProcess
Surrogate already fitted on the observations so far.
y_best
float
Incumbent best observed score :math:`y^{+}`. Used by 'ei' and 'poi'; ignored by 'ucb'.
Returns
values
np.ndarray of shape (n_samples,)
Acquisition values, higher meaning more promising. Not comparable across different kind settings.
Raises
ValueError
If self.kind is not one of 'ei', 'ucb' or 'poi'.

BayesianSearchCV

class evaluation.tuning.bayesian_search.BayesianSearchCV(BaseTuner)

Bayesian optimization search for hyperparameter tuning.

Uses Gaussian Process regression to model the objective function and acquisition functions to select promising hyperparameters.
Constructor
__init__(
    self,
    estimator,
    param_space: Dict,
    n_iterations: int = 50,
    acquisition: str = 'ei',
    n_random_starts: int = 10,
    xi: float = 0.01,
    kappa: float = 2.576,
    scoring: Union[str, Callable] = 'accuracy',
    cv: int = 5,
    refit: bool = True,
    verbose: int = 0,
    n_jobs: int = 1,
    random_seed: Optional[int] = None,
    progress_callback: Optional[Callable] = None,
    **kwargs,
)

Parameters

estimator
object
Estimator to tune. Must have fit() and predict() methods.
param_space
dict

Dictionary with parameters as keys and search spaces as values. Values can be:

  • List of values: discrete search
  • Tuple (min, max): continuous search
  • Tuple (min, max, 'int'): integer search
n_iterations
int = 50
Number of iterations to run.
acquisition
str = 'ei'
Acquisition function: 'ei' (Expected Improvement), 'ucb' (Upper Confidence Bound), or 'poi' (Probability of Improvement).
n_random_starts
int = 10
Number of random evaluations before using GP.
xi
float = 0.01
Exploration parameter for EI and POI.
kappa
float = 2.576
Exploration parameter for UCB.
scoring
str or callable = 'accuracy'
Scoring metric.
cv
int = 5
Number of cross-validation folds.
refit
bool = True
Refit estimator with best parameters on full data.
verbose
int = 0
Verbosity level.
random_state
int
Random seed.

Attributes

best_params_
dict
Best parameters found.
best_score_
float
Best cross-validation score.
best_estimator_
object
Estimator fitted with best parameters.
cv_results_
dict
Cross-validation results for all evaluated parameters.
gp_
GaussianProcess
Fitted Gaussian Process model.
python
>>> from tuiml.algorithms.ensemble import BaggingClassifier
>>> from tuiml.datasets import load_iris
>>> from tuiml.evaluation.splitting import train_test_split
>>> from tuiml.evaluation.tuning import BayesianSearchCV
>>> X, y = load_iris()
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

A search space maps each parameter to a (low, high) range for floats

or a (low, high, 'int') triple for integers:

python
>>> param_space = {
...     'n_estimators': (5, 20, 'int'),
...     'bag_size_percent': (50.0, 100.0),
... }
>>> search = BayesianSearchCV(
...     estimator=BaggingClassifier(),
...     param_space=param_space,
...     n_iterations=5,
...     cv=2,
... )
>>> search = search.fit(X_train, y_train)              # doctest: +SKIP
>>> sorted(search.best_params_)                        # doctest: +SKIP
['bag_size_percent', 'n_estimators']

Methods

get_parameter_schema (cls) -> dict

Return JSON Schema for BayesianSearchCV parameters.

Returns
dict
JSON Schema describing all __init__ parameters.
fit (self, X: np.ndarray, y: np.ndarray) -> 'BayesianSearchCV'

Run Bayesian optimization to find best parameters.

Parameters
X
ndarray of shape (n_samples, n_features)
Training features.
y
ndarray of shape (n_samples,)
Target values.
Returns
self
get_results (self) -> TuningResult

Get tuning results as TuningResult object.

Returns
result
TuningResult
__repr__ (self) -> str

Return a short constructor-like summary of the search.

Returns
repr
str
"BayesianSearchCV(estimator=..., n_iterations=..., acquisition='...')", showing the wrapped estimator's class name and the two settings that most affect how the search behaves.