From f1d02d328b417a80638c5f0ce92b88b245a8dead Mon Sep 17 00:00:00 2001 From: Jannis Kastner Date: Tue, 23 Sep 2025 22:09:11 +0200 Subject: [PATCH 1/4] Readd UCB acquisition function (#1252) --- smac/acquisition/function/confidence_bound.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/smac/acquisition/function/confidence_bound.py b/smac/acquisition/function/confidence_bound.py index 211a08091..e6edaca6a 100644 --- a/smac/acquisition/function/confidence_bound.py +++ b/smac/acquisition/function/confidence_bound.py @@ -104,3 +104,94 @@ def _compute(self, X: np.ndarray) -> np.ndarray: beta_t = 2 * np.log((X.shape[1] * self._num_data**2) / self._beta) return -(m - np.sqrt(beta_t) * std) + + +class UCB(AbstractAcquisitionFunction): + r"""Computes the upper confidence bound for a given x over the best so far value as acquisition value. + + :math:`UCB(X) = \mu(\mathbf{X}) + \sqrt(\beta_t)\sigma(\mathbf{X})` [[SKKS10][SKKS10]] + + with + + :math:`\beta_t = 2 \log( |D| t^2 / \beta)` + + :math:`\text{Input space} D` + :math:`\text{Number of input dimensions} |D|` + :math:`\text{Number of data points} t` + :math:`\text{Exploration/exploitation tradeoff} \beta` + + Returns UCB(X) as the acquisition_function optimizer maximizes the acquisition value. + + Parameters + ---------- + beta : float, defaults to 1.0 + Controls the balance between exploration and exploitation of the acquisition function. + + Attributes + ---------- + _beta : float + Exploration-exploitation trade-off parameter. + _num_data : int + Number of data points seen so far. + """ + + def __init__(self, beta: float = 1.0) -> None: + super(UCB, self).__init__() + self._beta: float = beta + self._num_data: int | None = None + + @property + def name(self) -> str: # noqa: D102 + return "Upper Confidence Bound" + + @property + def meta(self) -> dict[str, Any]: # noqa: D102 + meta = super().meta + meta.update({"beta": self._beta}) + + return meta + + def _update(self, **kwargs: Any) -> None: + """Update acsquisition function attributes + + Parameters + ---------- + num_data : int + Number of data points + """ + assert "num_data" in kwargs + self._num_data = kwargs["num_data"] + + def _compute(self, X: np.ndarray) -> np.ndarray: + """Compute UCB acquisition value + + Parameters + ---------- + X : np.ndarray [N, D] + The input points where the acquisition function should be evaluated. The dimensionality of X is (N, D), + with N as the number of points to evaluate at and D is the number of dimensions of one X. + + Returns + ------- + np.ndarray [N,1] + Acquisition function values wrt X. + + Raises + ------ + ValueError + If `update` has not been called before. Number of data points is unspecified in this case. + """ + assert self._model is not None + if self._num_data is None: + raise ValueError( + "No current number of data points specified. Call `update` to inform the acquisition function." + ) + + if len(X.shape) == 1: + X = X[:, np.newaxis] + + m, var_ = self._model.predict_marginalized(X) + std = np.sqrt(var_) + beta_t = 2 * np.log((X.shape[1] * self._num_data**2) / self._beta) + + return m + np.sqrt(beta_t) * std From 4a7f48d3c815970bb4293bce94bfb2375ec6b413 Mon Sep 17 00:00:00 2001 From: Jannis Kastner Date: Tue, 23 Sep 2025 22:40:18 +0200 Subject: [PATCH 2/4] Fix formatting issue with pre-commit --- smac/acquisition/function/confidence_bound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smac/acquisition/function/confidence_bound.py b/smac/acquisition/function/confidence_bound.py index e6edaca6a..5c4894cd1 100644 --- a/smac/acquisition/function/confidence_bound.py +++ b/smac/acquisition/function/confidence_bound.py @@ -104,7 +104,7 @@ def _compute(self, X: np.ndarray) -> np.ndarray: beta_t = 2 * np.log((X.shape[1] * self._num_data**2) / self._beta) return -(m - np.sqrt(beta_t) * std) - + class UCB(AbstractAcquisitionFunction): r"""Computes the upper confidence bound for a given x over the best so far value as acquisition value. From 4e09dfab9ddb01fab4ebf2235bf9f3b0742c76f4 Mon Sep 17 00:00:00 2001 From: Jannis Kastner Date: Tue, 30 Sep 2025 11:13:04 +0200 Subject: [PATCH 3/4] Update confidence_bound.py from feature/sawei branch & fix LCB/UCB (#1252) --- smac/acquisition/function/confidence_bound.py | 159 +++++++++++------- 1 file changed, 96 insertions(+), 63 deletions(-) diff --git a/smac/acquisition/function/confidence_bound.py b/smac/acquisition/function/confidence_bound.py index 5c4894cd1..7a26c0eb7 100644 --- a/smac/acquisition/function/confidence_bound.py +++ b/smac/acquisition/function/confidence_bound.py @@ -1,5 +1,6 @@ from __future__ import annotations +from abc import abstractmethod from typing import Any import numpy as np @@ -9,16 +10,18 @@ ) from smac.utils.logging import get_logger -__copyright__ = "Copyright 2025, Leibniz University Hanover, Institute of AI" +__copyright__ = "Copyright 2022, automl.org" __license__ = "3-clause BSD" logger = get_logger(__name__) -class LCB(AbstractAcquisitionFunction): - r"""Computes the lower confidence bound for a given x over the best so far value as acquisition value. +class AbstractConfidenceBound(AbstractAcquisitionFunction): + r"""Computes the lower or upper confidence bound for a given x over the best so far value as acquisition value. + + Example for LCB (UCB adds the variance term instead of subtracting it): - :math:`LCB(X) = \mu(\mathbf{X}) - \sqrt(\beta_t)\sigma(\mathbf{X})` [[SKKS10][SKKS10]] + :math:`LCB(X) = \mu(\mathbf{X}) - \sqrt(\beta_t)\sigma(\mathbf{X})` [SKKS10]_ with @@ -42,26 +45,50 @@ class LCB(AbstractAcquisitionFunction): Exploration-exploitation trade-off parameter. _num_data : int Number of data points seen so far. + _bound_type: str + Type of Confidence Bound. Either UCB or LCB. Set in child class. + _update_beta : bool + Whether to update beta or not. + _beta_scaling_srinivas : bool + Whether to use the beta scaling according to [0, 1]. + + References + ---------- + [0] Srinivas, Niranjan, et al. "Gaussian process optimization in the bandit setting: No regret and experimental + design." arXiv preprint arXiv:0912.3995 (2009). or not. + [1] Makarova, Anastasia, et al. "Automatic Termination for Hyperparameter Optimization." First Conference on + Automated Machine Learning (Main Track). 2022. + """ - def __init__(self, beta: float = 1.0) -> None: - super(LCB, self).__init__() + def __init__( + self, beta: float = 1.0, nu: float = 1.0, update_beta: bool = True, beta_scaling_srinivas: bool = False + ) -> None: + super(AbstractConfidenceBound, self).__init__() self._beta: float = beta + self._nu: float = nu self._num_data: int | None = None + self._update_beta = update_beta + self._beta_scaling_srinivas = beta_scaling_srinivas + + @property + @abstractmethod + def bound_type(self) -> str: # noqa: D102 + ... @property def name(self) -> str: # noqa: D102 - return "Lower Confidence Bound" + return "Confidence Bound" @property def meta(self) -> dict[str, Any]: # noqa: D102 meta = super().meta - meta.update({"beta": self._beta}) + meta.update({"beta": self._beta, "nu": self._nu}) return meta def _update(self, **kwargs: Any) -> None: - """Update acsquisition function attributes + """Update acquisition function attributes Parameters ---------- @@ -82,8 +109,8 @@ def _compute(self, X: np.ndarray) -> np.ndarray: Returns ------- - np.ndarray [N,1] - Acquisition function values wrt X. + np.ndarray + Acquisition function values wrt X; shape [N,1]. Raises ------ @@ -91,6 +118,15 @@ def _compute(self, X: np.ndarray) -> np.ndarray: If `update` has not been called before. Number of data points is unspecified in this case. """ assert self._model is not None + + if self.bound_type == "LCB": + sign = -1 + elif self.bound_type == "UCB": + sign = 1 + else: + raise ValueError( + f"Which confidence bound is supposed to be used? Use LCB or UCB. bound_type is {self.bound_type}." + ) if self._num_data is None: raise ValueError( "No current number of data points specified. Call `update` to inform the acquisition function." @@ -101,15 +137,21 @@ def _compute(self, X: np.ndarray) -> np.ndarray: m, var_ = self._model.predict_marginalized(X) std = np.sqrt(var_) - beta_t = 2 * np.log((X.shape[1] * self._num_data**2) / self._beta) + if self._update_beta and not self._beta_scaling_srinivas: + beta_t = 2 * np.log((X.shape[1] * self._num_data**2) / self._beta) + elif self._update_beta and self._beta_scaling_srinivas: + beta_t = (2 * np.log((X.shape[1] * self._num_data**2 * np.pi**2) / (6 * self._beta))) / 5 + else: + beta_t = self._beta - return -(m - np.sqrt(beta_t) * std) + return sign * (m + sign * np.sqrt(self._nu * beta_t) * std) -class UCB(AbstractAcquisitionFunction): - r"""Computes the upper confidence bound for a given x over the best so far value as acquisition value. +# Order of parents is important (priorities). This way _bound_type is correctly overwritten by the Mixin +class LCB(AbstractConfidenceBound): + r"""Computes the lower confidence bound for a given x over the best so far value as acquisition value. - :math:`UCB(X) = \mu(\mathbf{X}) + \sqrt(\beta_t)\sigma(\mathbf{X})` [[SKKS10][SKKS10]] + :math:`LCB(X) = \mu(\mathbf{X}) - \sqrt(\beta_t)\sigma(\mathbf{X})` [SKKS10]_ with @@ -120,7 +162,7 @@ class UCB(AbstractAcquisitionFunction): :math:`\text{Number of data points} t` :math:`\text{Exploration/exploitation tradeoff} \beta` - Returns UCB(X) as the acquisition_function optimizer maximizes the acquisition value. + Returns -LCB(X) as the acquisition_function optimizer maximizes the acquisition value. Parameters ---------- @@ -133,65 +175,56 @@ class UCB(AbstractAcquisitionFunction): Exploration-exploitation trade-off parameter. _num_data : int Number of data points seen so far. + _bound_type: str + Type of Confidence Bound. Either UCB or LCB. + """ - def __init__(self, beta: float = 1.0) -> None: - super(UCB, self).__init__() - self._beta: float = beta - self._num_data: int | None = None + @property + def bound_type(self) -> str: # noqa: D102 + return "LCB" @property def name(self) -> str: # noqa: D102 - return "Upper Confidence Bound" + return "Lower Confidence Bound" - @property - def meta(self) -> dict[str, Any]: # noqa: D102 - meta = super().meta - meta.update({"beta": self._beta}) - return meta +class UCB(AbstractConfidenceBound): + r"""Computes the upper confidence bound for a given x over the best so far value as acquisition value. - def _update(self, **kwargs: Any) -> None: - """Update acsquisition function attributes + :math:`UCB(X) = \mu(\mathbf{X}) + \sqrt(\beta_t)\sigma(\mathbf{X})` [SKKS10]_ - Parameters - ---------- - num_data : int - Number of data points - """ - assert "num_data" in kwargs - self._num_data = kwargs["num_data"] + with - def _compute(self, X: np.ndarray) -> np.ndarray: - """Compute UCB acquisition value + :math:`\beta_t = 2 \log( |D| t^2 / \beta)` - Parameters - ---------- - X : np.ndarray [N, D] - The input points where the acquisition function should be evaluated. The dimensionality of X is (N, D), - with N as the number of points to evaluate at and D is the number of dimensions of one X. + :math:`\text{Input space} D` + :math:`\text{Number of input dimensions} |D|` + :math:`\text{Number of data points} t` + :math:`\text{Exploration/exploitation tradeoff} \beta` - Returns - ------- - np.ndarray [N,1] - Acquisition function values wrt X. + Returns -UCB(X) as the acquisition_function optimizer maximizes the acquisition value. - Raises - ------ - ValueError - If `update` has not been called before. Number of data points is unspecified in this case. - """ - assert self._model is not None - if self._num_data is None: - raise ValueError( - "No current number of data points specified. Call `update` to inform the acquisition function." - ) + Parameters + ---------- + beta : float, defaults to 1.0 + Controls the balance between exploration and exploitation of the acquisition function. - if len(X.shape) == 1: - X = X[:, np.newaxis] + Attributes + ---------- + _beta : float + Exploration-exploitation trade-off parameter. + _num_data : int + Number of data points seen so far. + _bound_type: str + Type of Confidence Bound. Either UCB or LCB. - m, var_ = self._model.predict_marginalized(X) - std = np.sqrt(var_) - beta_t = 2 * np.log((X.shape[1] * self._num_data**2) / self._beta) + """ - return m + np.sqrt(beta_t) * std + @property + def bound_type(self) -> str: # noqa: D102 + return "UCB" + + @property + def name(self) -> str: # noqa: D102 + return "Upper Confidence Bound" From 8341f23e12f0ecf0a5c6ea60715eb99053158408 Mon Sep 17 00:00:00 2001 From: Jannis Kastner Date: Mon, 6 Oct 2025 11:22:55 +0200 Subject: [PATCH 4/4] Fix sign error in confidence_bound/_compute --- smac/acquisition/function/confidence_bound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smac/acquisition/function/confidence_bound.py b/smac/acquisition/function/confidence_bound.py index 7a26c0eb7..3efc87591 100644 --- a/smac/acquisition/function/confidence_bound.py +++ b/smac/acquisition/function/confidence_bound.py @@ -144,7 +144,7 @@ def _compute(self, X: np.ndarray) -> np.ndarray: else: beta_t = self._beta - return sign * (m + sign * np.sqrt(self._nu * beta_t) * std) + return -(m + sign * np.sqrt(self._nu * beta_t) * std) # Order of parents is important (priorities). This way _bound_type is correctly overwritten by the Mixin