saealib.acquisition.smsego のソースコード

"""SMS-EGO acquisition function module."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import numpy as np
import numpy.typing as npt

from saealib.acquisition.base import PointwiseAcquisition, direction_to_minimize_sign
from saealib.surrogate.prediction import SurrogatePrediction
from saealib.utils.indicators import _non_dominated, hypervolume

if TYPE_CHECKING:
    from saealib.population import Archive


[ドキュメント] class SMSEGOAcquisition(PointwiseAcquisition): """ SMS-EGO acquisition function for multi-objective Bayesian optimisation. Scores each candidate by the hypervolume improvement (HVI) of its per-objective LCB prediction over the current Pareto front (Ponweiser 2008; LCB formula from mlr3mbo ``AcqFunctionSmsEgo.R``). LCB per objective:: y_hat_j(x) = mu_j(x) - lambda * sigma_j(x) Acquisition score:: SMS(x) = HV(P u {y_hat(x)}, r) - HV(P, r) ``archive.f``, ``reference_point``, and the predicted mean are internally converted to minimisation convention via ``direction_to_minimize_sign`` (see ``direction`` below) before the formulas above run. Parameters ---------- lam : float LCB width parameter λ. Default: 1.0 (mlr3mbo convention). reference_point : array-like or None Hypervolume reference point, given in raw objective space (i.e. in the same convention as ``problem.direction``, not necessarily minimisation). If None, auto-computed from the archive nadir with a 10 % margin. direction : np.ndarray or None Per-objective optimization direction (+1 = maximize, -1 = minimize). shape: (n_obj,). ``None`` (default) means already-minimize, so behaviour for existing minimize-only callers is unchanged; when unset, it is auto-injected from ``problem.direction`` at run start. """ requires_uncertainty: bool = True
[ドキュメント] def __init__( self, lam: float = 1.0, reference_point: npt.ArrayLike | None = None, direction: np.ndarray | None = None, ) -> None: self.lam = lam self.reference_point = ( np.asarray(reference_point, dtype=float) if reference_point is not None else None ) self.direction = direction
[ドキュメント] def compute_reference( self, archive: Archive, rng: np.random.Generator | None = None, ) -> tuple[np.ndarray, np.ndarray, float]: """ Extract the Pareto front and (if needed) compute the reference point. Parameters ---------- archive : Archive Archive of evaluated solutions. Returns ------- tuple ``(pareto_f, ref_point, base_hv)`` — non-dominated objective matrix, hypervolume reference point, and current hypervolume. """ s = direction_to_minimize_sign(self.direction) f = archive.f * s pareto_f = _non_dominated(f) if self.reference_point is not None: ref = self.reference_point * s else: nadir = f.max(axis=0) span = nadir - f.min(axis=0) ref = nadir + 0.1 * np.maximum(span, 1e-6) base_hv = hypervolume(pareto_f, ref) return pareto_f, ref, base_hv
[ドキュメント] def score( self, prediction: SurrogatePrediction, reference: Any, rng: np.random.Generator | None = None, ) -> np.ndarray: """ Compute hypervolume improvement of the LCB-predicted candidates. Parameters ---------- prediction : SurrogatePrediction Surrogate predictions. Must have std (has_uncertainty == True). reference : tuple ``(pareto_f, ref_point, base_hv)`` from ``compute_reference``. Returns ------- np.ndarray HVI scores. shape: (n_samples,). Higher is better. Raises ------ TypeError If prediction does not contain uncertainty estimates. """ if not prediction.has_uncertainty: raise TypeError( "SMSEGOAcquisition requires uncertainty estimates " "(prediction.std must not be None)." ) pareto_f, ref_point, base_hv = reference assert prediction.std is not None s = direction_to_minimize_sign(self.direction) lcb = prediction.value * s - self.lam * prediction.std # (n, n_obj) n = lcb.shape[0] hvi = np.zeros(n) for i in range(n): y = lcb[i] if np.any(y >= ref_point): continue combined = ( y[np.newaxis] if len(pareto_f) == 0 else np.vstack([pareto_f, y[np.newaxis]]) ) hvi[i] = max(hypervolume(combined, ref_point) - base_hv, 0.0) return hvi