Source code for smt_optim.acquisition_functions.ehvi

from functools import partial
from typing import Callable

import numpy as np

import scipy.stats as stats

from smt_optim.utils.multi_obj import get_pf_from_dataset


[docs] def psi( a: float | np.ndarray, b: float | np.ndarray, mu: float | np.ndarray, s: float | np.ndarray, ) -> float | np.ndarray: """ Helper function to be used with the EHVI (`ehvi_2o`) acquisition function. Parameters ---------- a : float or np.array b : float or np.array mu : float or np.array s : float or np.array Returns ------- float or np.ndarray """ z = (b - mu) / s return s * stats.norm.pdf(z) + (a - mu) * stats.norm.cdf(z)
[docs] def ehvi_2o(mu: np.ndarray, s: np.ndarray, Y: np.ndarray) -> float: """ Compute the Expected Hypervolume Improvement (EHVI) for bi-objective optimization. Parameters ---------- mu : np.ndarray of shape (2,) Predictive mean of the two objective functions. s : np.ndarray of shape (2,) Predictive standard deviation of the two objective functions. Y : np.ndarray of shape (n, 2) Augmented Pareto front, sorted in ascending order with respect to the second objective. Returns ------- float The expected hypervolume improvement. Notes ----- Follows the implementation discussed in: Yang, K., Emmerich, M., Deutz, A., & Bäck, T. (2019). Multi-objective Bayesian global optimization using expected hypervolume improvement gradient. Swarm and evolutionary computation, 44, 945-956. """ # term1 is truncated because: np.inf * 0. = np.nan term1 = ( (Y[:-1, 0] - Y[1:, 0])[:-1] * stats.norm.cdf((Y[1:, 0] - mu[0]) / s[0])[:-1] * psi(Y[1:, 1], Y[1:, 1], mu[1], s[1])[:-1] ) term2 = ( psi(Y[:-1, 0], Y[:-1, 0], mu[0], s[0]) - psi(Y[:-1, 0], Y[1:, 0], mu[0], s[0]) ) * psi(Y[1:, 1], Y[1:, 1], mu[1], s[1]) return term1.sum() + term2.sum()
[docs] def init_ehvi_2o(state) -> Callable: """ Initialize the Expected Hypervolume Improvement (EHVI) acquisition function for bi-objective Bayesian optimization. The returned callable evaluates EHVI using the objective surrogate models stored in `state`. The Pareto front is extracted from the scaled dataset and augmented with reference points required by the two-objective EHVI computation (`ehvi_2o`). Parameters ---------- state : State Optimization state containing the problem definition, scaled dataset, and trained objective surrogate models. The problem must have exactly two objectives. The scaled dataset must contain objective evaluations from which the current Pareto front can be computed. Returns ------- Callable EHVI acquisition function with signature:: acquisition(x) -> float where `x` is a candidate point and the returned value is the expected hypervolume improvement at `x`. Raises ------ ValueError If the optimization problem is not bi-objective. Notes ----- The Pareto front is augmented with two additional points and sorted by the second objective before being passed to the EHVI computation. Follows the implementation discussed in: Yang, K., Emmerich, M., Deutz, A., & Bäck, T. (2019). Multi-objective Bayesian global optimization using expected hypervolume improvement gradient. Swarm and evolutionary computation, 44, 945-956. """ if state.problem.num_obj != 2: raise ValueError( f"EHVI (2-objective) only supports bi-objective optimization, but got " f"{state.problem.num_obj} objectives. Use a compatible acquisition function." ) data = state.scaled_dataset.export_as_dict() obj = data["obj"] pf = get_pf_from_dataset(state.scaled_dataset) r1 = obj[:, 0].max() r2 = obj[:, 1].max() # augment PF by 2 points augmented_pf = np.vstack((np.array([[r1, -np.inf]]), pf, np.array([[-np.inf, r2]]))) # sort augmented PF by the second objective indices = np.argsort(augmented_pf[:, 1]) Y = augmented_pf[indices, :] def ehvi_wrapper(x: np.ndarray, models: list, Y: np.ndarray) -> float: mu = np.empty(2) s = np.empty(2) for i in range(2): mu[i] = models[i].predict_values(x).item() s2_obj = models[i].predict_variances(x).item() s[i] = np.sqrt(np.maximum(s2_obj, 1e-16)) return ehvi_2o(mu, s, Y) return partial(ehvi_wrapper, models=state.obj_models, Y=Y)