From 0ef608f9d1ee3f6dbe5ff9bff0e65853f2593bc1 Mon Sep 17 00:00:00 2001 From: till-m <36440677+till-m@users.noreply.github.com> Date: Fri, 27 Dec 2024 09:03:19 +0100 Subject: [PATCH] Typed Optimization (#531) * WIP * Add ML example * Save for merge * Update * Parameter types more (#13) * fix: import error from exception module (#525) * fix: replace list with sequence (#524) * Fix min window type check (#523) * fix: replace dict with Mapping * fix: replace list with Sequence * fix: add type hint * fix: does not accept None * Change docs badge (#527) * fix: parameter, target_space * fix: constraint, bayesian_optimization * fix: ParamsType --------- Co-authored-by: till-m <36440677+till-m@users.noreply.github.com> * Use `.masks` not `._masks` * User `super` to call kernel * Update logging for parameters * Disable SDR when non-float parameters are present * Add demo script for typed optimization * Update parameters, testing * Remove sorting, gradient optimize only continuous params * Go back to `wrap_kernel` * Update code * Remove `tqdm` dependency, use EI acq * Add more text to typed optimization notebook. * Save files while moving device * Update with custom parameter type example * Mention that parameters are not sorted * Change array reg warning * Update Citations, parameter notebook --------- Co-authored-by: phi-friday --- README.md | 13 + bayes_opt/acquisition.py | 90 +-- bayes_opt/bayesian_optimization.py | 69 ++- bayes_opt/constraint.py | 5 +- bayes_opt/domain_reduction.py | 21 +- bayes_opt/logger.py | 19 +- bayes_opt/parameter.py | 506 ++++++++++++++++ bayes_opt/target_space.py | 300 ++++++++-- docsrc/index.rst | 22 +- docsrc/reference/parameter.rst | 5 + examples/advanced-tour.ipynb | 161 +---- examples/basic-tour.ipynb | 2 +- examples/parameter_types.ipynb | 756 ++++++++++++++++++++++++ examples/typed_hyperparameter_tuning.py | 94 +++ ruff.toml | 3 + scripts/format.sh | 2 +- tests/test_acceptance.py | 69 --- tests/test_acquisition.py | 2 +- tests/test_bayesian_optimization.py | 16 +- tests/test_observer.py | 10 - tests/test_parameter.py | 246 ++++++++ tests/test_seq_domain_red.py | 12 +- tests/test_target_space.py | 76 ++- tests/test_util.py | 8 - 24 files changed, 2082 insertions(+), 425 deletions(-) create mode 100644 bayes_opt/parameter.py create mode 100644 docsrc/reference/parameter.rst create mode 100644 examples/parameter_types.ipynb create mode 100644 examples/typed_hyperparameter_tuning.py delete mode 100644 tests/test_acceptance.py create mode 100644 tests/test_parameter.py diff --git a/README.md b/README.md index 37e7e5b4d..c485d11ca 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,16 @@ For constrained optimization: year={2014} } ``` + +For optimization over non-float parameters: +``` +@article{garrido2020dealing, + title={Dealing with categorical and integer-valued variables in bayesian optimization with gaussian processes}, + author={Garrido-Merch{\'a}n, Eduardo C and Hern{\'a}ndez-Lobato, Daniel}, + journal={Neurocomputing}, + volume={380}, + pages={20--35}, + year={2020}, + publisher={Elsevier} +} +``` diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index e17846758..167bd5dc0 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -127,7 +127,7 @@ def suggest( self._fit_gp(gp=gp, target_space=target_space) acq = self._get_acq(gp=gp, constraint=target_space.constraint) - return self._acq_min(acq, target_space.bounds, n_random=n_random, n_l_bfgs_b=n_l_bfgs_b) + return self._acq_min(acq, target_space, n_random=n_random, n_l_bfgs_b=n_l_bfgs_b) def _get_acq( self, gp: GaussianProcessRegressor, constraint: ConstraintModel | None = None @@ -182,7 +182,7 @@ def acq(x: NDArray[Float]) -> NDArray[Float]: def _acq_min( self, acq: Callable[[NDArray[Float]], NDArray[Float]], - bounds: NDArray[Float], + space: TargetSpace, n_random: int = 10_000, n_l_bfgs_b: int = 10, ) -> NDArray[Float]: @@ -197,10 +197,8 @@ def _acq_min( acq : Callable Acquisition function to use. Should accept an array of parameters `x`. - bounds : np.ndarray - Bounds of the search space. For `N` parameters this has shape - `(N, 2)` with `[i, 0]` the lower bound of parameter `i` and - `[i, 1]` the upper bound. + space : TargetSpace + The target space over which to optimize. n_random : int Number of random samples to use. @@ -217,15 +215,22 @@ def _acq_min( if n_random == 0 and n_l_bfgs_b == 0: error_msg = "Either n_random or n_l_bfgs_b needs to be greater than 0." raise ValueError(error_msg) - x_min_r, min_acq_r = self._random_sample_minimize(acq, bounds, n_random=n_random) - x_min_l, min_acq_l = self._l_bfgs_b_minimize(acq, bounds, n_x_seeds=n_l_bfgs_b) - # Either n_random or n_l_bfgs_b is not 0 => at least one of x_min_r and x_min_l is not None - if min_acq_r < min_acq_l: - return x_min_r - return x_min_l + x_min_r, min_acq_r, x_seeds = self._random_sample_minimize( + acq, space, n_random=max(n_random, n_l_bfgs_b), n_x_seeds=n_l_bfgs_b + ) + if n_l_bfgs_b: + x_min_l, min_acq_l = self._l_bfgs_b_minimize(acq, space, x_seeds=x_seeds) + # Either n_random or n_l_bfgs_b is not 0 => at least one of x_min_r and x_min_l is not None + if min_acq_r > min_acq_l: + return x_min_l + return x_min_r def _random_sample_minimize( - self, acq: Callable[[NDArray[Float]], NDArray[Float]], bounds: NDArray[Float], n_random: int + self, + acq: Callable[[NDArray[Float]], NDArray[Float]], + space: TargetSpace, + n_random: int, + n_x_seeds: int = 0, ) -> tuple[NDArray[Float] | None, float]: """Random search to find the minimum of `acq` function. @@ -234,14 +239,14 @@ def _random_sample_minimize( acq : Callable Acquisition function to use. Should accept an array of parameters `x`. - bounds : np.ndarray - Bounds of the search space. For `N` parameters this has shape - `(N, 2)` with `[i, 0]` the lower bound of parameter `i` and - `[i, 1]` the upper bound. + space : TargetSpace + The target space over which to optimize. n_random : int Number of random samples to use. + n_x_seeds : int + Number of top points to return, for use as starting points for L-BFGS-B. Returns ------- x_min : np.ndarray @@ -252,14 +257,22 @@ def _random_sample_minimize( """ if n_random == 0: return None, np.inf - x_tries = self.random_state.uniform(bounds[:, 0], bounds[:, 1], size=(n_random, bounds.shape[0])) + x_tries = space.random_sample(n_random, random_state=self.random_state) ys = acq(x_tries) x_min = x_tries[ys.argmin()] min_acq = ys.min() - return x_min, min_acq + if n_x_seeds != 0: + idxs = np.argsort(ys)[-n_x_seeds:] + x_seeds = x_tries[idxs] + else: + x_seeds = [] + return x_min, min_acq, x_seeds def _l_bfgs_b_minimize( - self, acq: Callable[[NDArray[Float]], NDArray[Float]], bounds: NDArray[Float], n_x_seeds: int = 10 + self, + acq: Callable[[NDArray[Float]], NDArray[Float]], + space: TargetSpace, + x_seeds: NDArray[Float] | None = None, ) -> tuple[NDArray[Float] | None, float]: """Random search to find the minimum of `acq` function. @@ -268,13 +281,11 @@ def _l_bfgs_b_minimize( acq : Callable Acquisition function to use. Should accept an array of parameters `x`. - bounds : np.ndarray - Bounds of the search space. For `N` parameters this has shape - `(N, 2)` with `[i, 0]` the lower bound of parameter `i` and - `[i, 1]` the upper bound. + space : TargetSpace + The target space over which to optimize. - n_x_seeds : int - Number of starting points for the L-BFGS-B optimizer. + x_seeds : int + Starting points for the L-BFGS-B optimizer. Returns ------- @@ -284,33 +295,44 @@ def _l_bfgs_b_minimize( min_acq : float Acquisition function value at `x_min` """ - if n_x_seeds == 0: - return None, np.inf - x_seeds = self.random_state.uniform(bounds[:, 0], bounds[:, 1], size=(n_x_seeds, bounds.shape[0])) + continuous_dimensions = space.continuous_dimensions + continuous_bounds = space.bounds[continuous_dimensions] + + if not continuous_dimensions.any(): + min_acq = np.inf + x_min = np.array([np.nan] * space.bounds.shape[0]) + return x_min, min_acq min_acq: float | None = None x_try: NDArray[Float] x_min: NDArray[Float] for x_try in x_seeds: - # Find the minimum of minus the acquisition function - res: OptimizeResult = minimize(acq, x_try, bounds=bounds, method="L-BFGS-B") + def continuous_acq(x: NDArray[Float], x_try=x_try) -> NDArray[Float]: + x_try[continuous_dimensions] = x + return acq(x_try) + + # Find the minimum of minus the acquisition function + res: OptimizeResult = minimize( + continuous_acq, x_try[continuous_dimensions], bounds=continuous_bounds, method="L-BFGS-B" + ) # See if success if not res.success: continue # Store it if better than previous minimum(maximum). if min_acq is None or np.squeeze(res.fun) >= min_acq: - x_min = res.x + x_try[continuous_dimensions] = res.x + x_min = x_try min_acq = np.squeeze(res.fun) if min_acq is None: min_acq = np.inf - x_min = np.array([np.nan] * bounds.shape[0]) + x_min = np.array([np.nan] * space.bounds.shape[0]) # Clip output to make sure it lies within the bounds. Due to floating # point technicalities this is not always the case. - return np.clip(x_min, bounds[:, 0], bounds[:, 1]), min_acq + return np.clip(x_min, space.bounds[:, 0], space.bounds[:, 1]), min_acq class UpperConfidenceBound(AcquisitionFunction): diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index b34a3246a..d7f2e4035 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -16,13 +16,15 @@ from bayes_opt import acquisition from bayes_opt.constraint import ConstraintModel +from bayes_opt.domain_reduction import DomainTransformer from bayes_opt.event import DEFAULT_EVENTS, Events from bayes_opt.logger import _get_default_logger +from bayes_opt.parameter import wrap_kernel from bayes_opt.target_space import TargetSpace from bayes_opt.util import ensure_rng if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Mapping, Sequence + from collections.abc import Callable, Iterable, Mapping from numpy.random import RandomState from numpy.typing import NDArray @@ -31,6 +33,7 @@ from bayes_opt.acquisition import AcquisitionFunction from bayes_opt.constraint import ConstraintModel from bayes_opt.domain_reduction import DomainTransformer + from bayes_opt.parameter import BoundsMapping, ParamsType Float = np.floating[Any] @@ -114,7 +117,7 @@ def __init__( ): self._random_state = ensure_rng(random_state) self._allow_duplicate_points = allow_duplicate_points - self._queue: deque[Mapping[str, float] | Sequence[float] | NDArray[Float]] = deque() + self._queue: deque[ParamsType] = deque() if acquisition_function is None: if constraint is None: @@ -128,15 +131,6 @@ def __init__( else: self._acquisition_function = acquisition_function - # Internal GP regressor - self._gp = GaussianProcessRegressor( - kernel=Matern(nu=2.5), - alpha=1e-6, - normalize_y=True, - n_restarts_optimizer=5, - random_state=self._random_state, - ) - if constraint is None: # Data structure containing the function to be optimized, the # bounds of its domain, and a record of the evaluations we have @@ -158,14 +152,22 @@ def __init__( ) self.is_constrained = True + # Internal GP regressor + self._gp = GaussianProcessRegressor( + kernel=wrap_kernel(Matern(nu=2.5), transform=self._space.kernel_transform), + alpha=1e-6, + normalize_y=True, + n_restarts_optimizer=5, + random_state=self._random_state, + ) + self._verbose = verbose self._bounds_transformer = bounds_transformer if self._bounds_transformer: - try: - self._bounds_transformer.initialize(self._space) - except (AttributeError, TypeError) as exc: - error_msg = "The transformer must be an instance of DomainTransformer" - raise TypeError(error_msg) from exc + if not isinstance(self._bounds_transformer, DomainTransformer): + msg = "The transformer must be an instance of DomainTransformer" + raise TypeError(msg) + self._bounds_transformer.initialize(self._space) self._sorting_warning_already_shown = False # TODO: remove in future version super().__init__(events=DEFAULT_EVENTS) @@ -204,10 +206,7 @@ def res(self) -> list[dict[str, Any]]: return self._space.res() def register( - self, - params: Mapping[str, float] | Sequence[float] | NDArray[Float], - target: float, - constraint_value: float | NDArray[Float] | None = None, + self, params: ParamsType, target: float, constraint_value: float | NDArray[Float] | None = None ) -> None: """Register an observation with known target. @@ -225,10 +224,10 @@ def register( # TODO: remove in future version if isinstance(params, np.ndarray) and not self._sorting_warning_already_shown: msg = ( - "You're attempting to register an np.ndarray. Currently, the optimizer internally sorts" - " parameters by key and expects any registered array to respect this order. In future" - " versions this behaviour will change and the order as given by the pbounds dictionary" - " will be used. If you wish to retain sorted parameters, please manually sort your pbounds" + "You're attempting to register an np.ndarray. In previous versions, the optimizer internally" + " sorted parameters by key and expected any registered array to respect this order." + " In the current and any future version the order as given by the pbounds dictionary will be" + " used. If you wish to retain sorted parameters, please manually sort your pbounds" " dictionary before constructing the optimizer." ) warn(msg, stacklevel=1) @@ -236,9 +235,7 @@ def register( self._space.register(params, target, constraint_value) self.dispatch(Events.OPTIMIZATION_STEP) - def probe( - self, params: Mapping[str, float] | Sequence[float] | NDArray[Float], lazy: bool = True - ) -> None: + def probe(self, params: ParamsType, lazy: bool = True) -> None: """Evaluate the function at the given points. Useful to guide the optimizer. @@ -255,10 +252,10 @@ def probe( # TODO: remove in future version if isinstance(params, np.ndarray) and not self._sorting_warning_already_shown: msg = ( - "You're attempting to register an np.ndarray. Currently, the optimizer internally sorts" - " parameters by key and expects any registered array to respect this order. In future" - " versions this behaviour will change and the order as given by the pbounds dictionary" - " will be used. If you wish to retain sorted parameters, please manually sort your pbounds" + "You're attempting to register an np.ndarray. In previous versions, the optimizer internally" + " sorted parameters by key and expected any registered array to respect this order." + " In the current and any future version the order as given by the pbounds dictionary will be" + " used. If you wish to retain sorted parameters, please manually sort your pbounds" " dictionary before constructing the optimizer." ) warn(msg, stacklevel=1) @@ -270,10 +267,10 @@ def probe( self._space.probe(params) self.dispatch(Events.OPTIMIZATION_STEP) - def suggest(self) -> dict[str, float]: + def suggest(self) -> dict[str, float | NDArray[Float]]: """Suggest a promising point to probe next.""" if len(self._space) == 0: - return self._space.array_to_params(self._space.random_sample()) + return self._space.array_to_params(self._space.random_sample(random_state=self._random_state)) # Finding argmax of the acquisition function. suggestion = self._acquisition_function.suggest(gp=self._gp, target_space=self._space, fit_gp=True) @@ -292,7 +289,7 @@ def _prime_queue(self, init_points: int) -> None: init_points = max(init_points, 1) for _ in range(init_points): - sample = self._space.random_sample() + sample = self._space.random_sample(random_state=self._random_state) self._queue.append(self._space.array_to_params(sample)) def _prime_subscriptions(self) -> None: @@ -344,7 +341,7 @@ def maximize(self, init_points: int = 5, n_iter: int = 25) -> None: self.dispatch(Events.OPTIMIZATION_END) - def set_bounds(self, new_bounds: Mapping[str, NDArray[Float] | Sequence[float]]) -> None: + def set_bounds(self, new_bounds: BoundsMapping) -> None: """Modify the bounds of the search space. Parameters @@ -356,4 +353,6 @@ def set_bounds(self, new_bounds: Mapping[str, NDArray[Float] | Sequence[float]]) def set_gp_params(self, **params: Any) -> None: """Set parameters of the internal Gaussian Process Regressor.""" + if "kernel" in params: + params["kernel"] = wrap_kernel(kernel=params["kernel"], transform=self._space.kernel_transform) self._gp.set_params(**params) diff --git a/bayes_opt/constraint.py b/bayes_opt/constraint.py index a8243a167..120169bdb 100644 --- a/bayes_opt/constraint.py +++ b/bayes_opt/constraint.py @@ -9,6 +9,8 @@ from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import Matern +from bayes_opt.parameter import wrap_kernel + if TYPE_CHECKING: from collections.abc import Callable @@ -55,6 +57,7 @@ def __init__( fun: Callable[..., float] | Callable[..., NDArray[Float]] | None, lb: float | NDArray[Float], ub: float | NDArray[Float], + transform: Callable[[Any], Any] | None = None, random_state: int | RandomState | None = None, ) -> None: self.fun = fun @@ -68,7 +71,7 @@ def __init__( self._model = [ GaussianProcessRegressor( - kernel=Matern(nu=2.5), + kernel=wrap_kernel(Matern(nu=2.5), transform) if transform is not None else Matern(nu=2.5), alpha=1e-6, normalize_y=True, n_restarts_optimizer=5, diff --git a/bayes_opt/domain_reduction.py b/bayes_opt/domain_reduction.py index c5be31951..243a51bd5 100644 --- a/bayes_opt/domain_reduction.py +++ b/bayes_opt/domain_reduction.py @@ -14,6 +14,7 @@ import numpy as np +from bayes_opt.parameter import FloatParameter from bayes_opt.target_space import TargetSpace if TYPE_CHECKING: @@ -62,22 +63,19 @@ class SequentialDomainReductionTransformer(DomainTransformer): def __init__( self, + parameters: Iterable[str] | None = None, gamma_osc: float = 0.7, gamma_pan: float = 1.0, eta: float = 0.9, minimum_window: NDArray[Float] | Sequence[float] | Mapping[str, float] | float = 0.0, ) -> None: + # TODO: Ensure that this is only applied to continuous parameters + self.parameters = parameters self.gamma_osc = gamma_osc self.gamma_pan = gamma_pan self.eta = eta - self.minimum_window_value: NDArray[Float] | Sequence[float] | float - if isinstance(minimum_window, Mapping): - self.minimum_window_value = [ - item[1] for item in sorted(minimum_window.items(), key=lambda x: x[0]) - ] - else: - self.minimum_window_value = minimum_window + self.minimum_window_value = minimum_window def initialize(self, target_space: TargetSpace) -> None: """Initialize all of the parameters. @@ -87,6 +85,15 @@ def initialize(self, target_space: TargetSpace) -> None: target_space : TargetSpace TargetSpace this DomainTransformer operates on. """ + if isinstance(self.minimum_window_value, Mapping): + self.minimum_window_value = [self.minimum_window_value[key] for key in target_space.keys] + else: + self.minimum_window_value = self.minimum_window_value + + any_not_float = any([not isinstance(p, FloatParameter) for p in target_space._params_config.values()]) + if any_not_float: + msg = "Domain reduction is only supported for all-FloatParameter optimization." + raise ValueError(msg) # Set the original bounds self.original_bounds = np.copy(target_space.bounds) self.bounds = [self.original_bounds] diff --git a/bayes_opt/logger.py b/bayes_opt/logger.py index a9756ca41..0f5cd41c2 100644 --- a/bayes_opt/logger.py +++ b/bayes_opt/logger.py @@ -127,19 +127,19 @@ def _format_bool(self, x: bool) -> str: x_ = ("T" if x else "F") if self._default_cell_size < 5 else str(x) return f"{x_:<{self._default_cell_size}}" - def _format_key(self, key: str) -> str: - """Format a key. + def _format_str(self, str_: str) -> str: + """Format a str. Parameters ---------- - key : string + str_ : str Value to format. Returns ------- A stringified, formatted version of `x`. """ - s = f"{key:^{self._default_cell_size}}" + s = f"{str_:^{self._default_cell_size}}" if len(s) > self._default_cell_size: return s[: self._default_cell_size - 3] + "..." return s @@ -168,7 +168,10 @@ def _step(self, instance: BayesianOptimization, colour: str = _colour_regular_me if self._is_constrained: cells[2] = self._format_bool(res["allowed"]) params = res.get("params", {}) - cells[3:] = [self._format_number(params.get(key, float("nan"))) for key in keys] + cells[3:] = [ + instance.space._params_config[key].to_string(val, self._default_cell_size) + for key, val in params.items() + ] return "| " + " | ".join(colour + x + self._colour_reset for x in cells if x is not None) + " |" @@ -188,10 +191,10 @@ def _header(self, instance: BayesianOptimization) -> str: # iter, target, allowed [, *params] cells: list[str | None] = [None] * (3 + len(keys)) - cells[:2] = self._format_key("iter"), self._format_key("target") + cells[:2] = self._format_str("iter"), self._format_str("target") if self._is_constrained: - cells[2] = self._format_key("allowed") - cells[3:] = [self._format_key(key) for key in keys] + cells[2] = self._format_str("allowed") + cells[3:] = [self._format_str(key) for key in keys] line = "| " + " | ".join(x for x in cells if x is not None) + " |" self._header_length = len(line) diff --git a/bayes_opt/parameter.py b/bayes_opt/parameter.py new file mode 100644 index 000000000..90fea618b --- /dev/null +++ b/bayes_opt/parameter.py @@ -0,0 +1,506 @@ +"""Parameter classes for Bayesian optimization.""" + +from __future__ import annotations + +import abc +from collections.abc import Sequence +from inspect import signature +from numbers import Number +from typing import TYPE_CHECKING, Any, Callable, Union + +import numpy as np +from sklearn.gaussian_process import kernels + +from bayes_opt.util import ensure_rng + +if TYPE_CHECKING: + from collections.abc import Mapping + + from numpy.typing import NDArray + + Float = np.floating[Any] + Int = np.integer[Any] + + FloatBoundsWithoutType = tuple[float, float] + FloatBoundsWithType = tuple[float, float, type[float]] + FloatBounds = Union[FloatBoundsWithoutType, FloatBoundsWithType] + IntBounds = tuple[Union[int, float], Union[int, float], type[int]] + CategoricalBounds = Sequence[Any] + Bounds = Union[FloatBounds, IntBounds, CategoricalBounds] + BoundsMapping = Mapping[str, Bounds] + + # FIXME: categorical parameters can be of any type. + # This will make static type checking for parameters difficult. + ParamsType = Union[Mapping[str, Any], Sequence[Any], NDArray[Float]] + + +def is_numeric(value: Any) -> bool: + """Check if a value is numeric.""" + return isinstance(value, Number) or ( + isinstance(value, np.generic) + and (np.isdtype(value.dtype, np.number) or np.issubdtype(value.dtype, np.number)) + ) + + +class BayesParameter(abc.ABC): + """Base class for Bayesian optimization parameters. + + Parameters + ---------- + name : str + The name of the parameter. + """ + + def __init__(self, name: str, bounds: NDArray[Any]) -> None: + self.name = name + self._bounds = bounds + + @property + def bounds(self) -> NDArray[Any]: + """The bounds of the parameter in float space.""" + return self._bounds + + @property + @abc.abstractmethod + def is_continuous(self) -> bool: + """Whether the parameter is continuous.""" + + def random_sample( + self, n_samples: int, random_state: np.random.RandomState | int | None + ) -> NDArray[Float]: + """Generate random samples from the parameter. + + Parameters + ---------- + n_samples : int + The number of samples to generate. + + random_state : np.random.RandomState | int | None + The random state to use for sampling. + + Returns + ------- + np.ndarray + The samples. + """ + random_state = ensure_rng(random_state) + return random_state.uniform(self.bounds[0], self.bounds[1], n_samples) + + @abc.abstractmethod + def to_float(self, value: Any) -> float | NDArray[Float]: + """Convert a parameter value to a float. + + Parameters + ---------- + value : Any + The value to convert, should be the canonical representation of the parameter. + """ + + @abc.abstractmethod + def to_param(self, value: float | NDArray[Float]) -> Any: + """Convert a float value to a parameter. + + Parameters + ---------- + value : np.ndarray + The value to convert, should be a float. + + Returns + ------- + Any + The canonical representation of the parameter. + """ + + @abc.abstractmethod + def kernel_transform(self, value: NDArray[Float]) -> NDArray[Float]: + """Transform a parameter value for use in a kernel. + + Parameters + ---------- + value : np.ndarray + The value(s) to transform, should be a float. + + Returns + ------- + np.ndarray + """ + + def to_string(self, value: Any, str_len: int) -> str: + """Represent a parameter value as a string. + + Parameters + ---------- + value : Any + The value to represent. + + str_len : int + The maximum length of the string representation. + + Returns + ------- + str + """ + s = f"{value!r:<{str_len}}" + + if len(s) > str_len: + return s[: str_len - 3] + "..." + return s + + @property + @abc.abstractmethod + def dim(self) -> int: + """The dimensionality of the parameter.""" + + +class FloatParameter(BayesParameter): + """A parameter with float values. + + Parameters + ---------- + name : str + The name of the parameter. + + bounds : tuple[float, float] + The bounds of the parameter. + """ + + def __init__(self, name: str, bounds: tuple[float, float]) -> None: + super().__init__(name, np.array(bounds)) + + @property + def is_continuous(self) -> bool: + """Whether the parameter is continuous.""" + return True + + def to_float(self, value: float) -> float: + """Convert a parameter value to a float. + + Parameters + ---------- + value : Any + The value to convert, should be the canonical representation of the parameter. + """ + return value + + def to_param(self, value: float | NDArray[Float]) -> float: + """Convert a float value to a parameter. + + Parameters + ---------- + value : np.ndarray + The value to convert, should be a float. + + Returns + ------- + Any + The canonical representation of the parameter. + """ + return value.flatten()[0] + + def to_string(self, value: float, str_len: int) -> str: + """Represent a parameter value as a string. + + Parameters + ---------- + value : Any + The value to represent. + + str_len : int + The maximum length of the string representation. + + Returns + ------- + str + """ + s = f"{value:<{str_len}.{str_len}}" + if len(s) > str_len: + if "." in s and "e" not in s: + return s[:str_len] + return s[: str_len - 3] + "..." + return s + + def kernel_transform(self, value: NDArray[Float]) -> NDArray[Float]: + """Transform a parameter value for use in a kernel. + + Parameters + ---------- + value : np.ndarray + The value(s) to transform, should be a float. + + Returns + ------- + np.ndarray + """ + return value + + @property + def dim(self) -> int: + """The dimensionality of the parameter.""" + return 1 + + +class IntParameter(BayesParameter): + """A parameter with int values. + + Parameters + ---------- + name : str + The name of the parameter. + + bounds : tuple[int, int] + The bounds of the parameter. + """ + + def __init__(self, name: str, bounds: tuple[int, int]) -> None: + super().__init__(name, np.array(bounds)) + + @property + def is_continuous(self) -> bool: + """Whether the parameter is continuous.""" + return False + + def random_sample( + self, n_samples: int, random_state: np.random.RandomState | int | None + ) -> NDArray[Float]: + """Generate random samples from the parameter. + + Parameters + ---------- + n_samples : int + The number of samples to generate. + + random_state : np.random.RandomState | int | None + The random state to use for sampling. + + Returns + ------- + np.ndarray + The samples. + """ + random_state = ensure_rng(random_state) + return random_state.randint(self.bounds[0], self.bounds[1] + 1, n_samples).astype(float) + + def to_float(self, value: int | float) -> float: + """Convert a parameter value to a float. + + Parameters + ---------- + value : Any + The value to convert, should be the canonical representation of the parameter. + """ + return float(value) + + def to_param(self, value: int | float | NDArray[Int] | NDArray[Float]) -> int: + """Convert a float value to a parameter. + + Parameters + ---------- + value : np.ndarray + The value to convert, should be a float. + + Returns + ------- + Any + The canonical representation of the parameter. + """ + return int(np.round(np.squeeze(value))) + + def kernel_transform(self, value: NDArray[Float]) -> NDArray[Float]: + """Transform a parameter value for use in a kernel. + + Parameters + ---------- + value : np.ndarray + The value(s) to transform, should be a float. + + Returns + ------- + np.ndarray + """ + return np.round(value) + + @property + def dim(self) -> int: + """The dimensionality of the parameter.""" + return 1 + + +class CategoricalParameter(BayesParameter): + """A parameter with categorical values. + + Parameters + ---------- + name : str + The name of the parameter. + + categories : Sequence[Any] + The categories of the parameter. + """ + + def __init__(self, name: str, categories: Sequence[Any]) -> None: + if len(categories) != len(set(categories)): + msg = "Categories must be unique." + raise ValueError(msg) + if len(categories) < 2: + msg = "At least two categories are required." + raise ValueError(msg) + + self.categories = categories + lower = np.zeros(self.dim) + upper = np.ones(self.dim) + bounds = np.vstack((lower, upper)).T + super().__init__(name, bounds) + + @property + def is_continuous(self) -> bool: + """Whether the parameter is continuous.""" + return False + + def random_sample( + self, n_samples: int, random_state: np.random.RandomState | int | None + ) -> NDArray[Float]: + """Generate random float-format samples from the parameter. + + Parameters + ---------- + n_samples : int + The number of samples to generate. + + random_state : np.random.RandomState | int | None + The random state to use for sampling. + + Returns + ------- + np.ndarray + The samples. + """ + random_state = ensure_rng(random_state) + res = random_state.randint(0, len(self.categories), n_samples) + one_hot = np.zeros((n_samples, len(self.categories))) + one_hot[np.arange(n_samples), res] = 1 + return one_hot.astype(float) + + def to_float(self, value: Any) -> NDArray[Float]: + """Convert a parameter value to a float. + + Parameters + ---------- + value : Any + The value to convert, should be the canonical representation of the parameter. + """ + res = np.zeros(len(self.categories)) + one_hot_index = [i for i, val in enumerate(self.categories) if val == value] + res[one_hot_index] = 1 + return res.astype(float) + + def to_param(self, value: float | NDArray[Float]) -> Any: + """Convert a float value to a parameter. + + Parameters + ---------- + value : np.ndarray + The value to convert, should be a float. + + Returns + ------- + Any + The canonical representation of the parameter. + """ + return self.categories[int(np.argmax(value))] + + def to_string(self, value: Any, str_len: int) -> str: + """Represent a parameter value as a string. + + Parameters + ---------- + value : Any + The value to represent. + + str_len : int + The maximum length of the string representation. + + Returns + ------- + str + """ + if not isinstance(value, str): + value = repr(value) + s = f"{value:<{str_len}}" + + if len(s) > str_len: + return s[: str_len - 3] + "..." + return s + + def kernel_transform(self, value: NDArray[Float]) -> NDArray[Float]: + """Transform a parameter value for use in a kernel. + + Parameters + ---------- + value : np.ndarray + The value(s) to transform, should be a float. + + Returns + ------- + np.ndarray + """ + value = np.atleast_2d(value) + res = np.zeros(value.shape) + res[:, np.argmax(value, axis=1)] = 1 + return res + + @property + def dim(self) -> int: + """The dimensionality of the parameter.""" + return len(self.categories) + + +def wrap_kernel(kernel: kernels.Kernel, transform: Callable[[Any], Any]) -> kernels.Kernel: + """Wrap a kernel to transform input data before passing it to the kernel. + + Parameters + ---------- + kernel : kernels.Kernel + The kernel to wrap. + + transform : Callable + The transformation function to apply to the input data. + + Returns + ------- + kernels.Kernel + The wrapped kernel. + + Notes + ----- + See https://arxiv.org/abs/1805.03463 for more information. + """ + kernel_type = type(kernel) + + class WrappedKernel(kernel_type): + @_copy_signature(getattr(kernel_type.__init__, "deprecated_original", kernel_type.__init__)) + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + def __call__(self, X: Any, Y: Any = None, eval_gradient: bool = False) -> Any: + X = transform(X) + Y = transform(Y) if Y is not None else None + return super().__call__(X, Y, eval_gradient) + + def __reduce__(self) -> str | tuple[Any, ...]: + return (wrap_kernel, (kernel, transform)) + + return WrappedKernel(**kernel.get_params()) + + +def _copy_signature(source_fct: Callable[..., Any]) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Clone a signature from a source function to a target function. + + via + https://stackoverflow.com/a/58989918/ + """ + + def copy(target_fct: Callable[..., Any]) -> Callable[..., Any]: + target_fct.__signature__ = signature(source_fct) + return target_fct + + return copy diff --git a/bayes_opt/target_space.py b/bayes_opt/target_space.py index 7dddbd515..39d1f9926 100644 --- a/bayes_opt/target_space.py +++ b/bayes_opt/target_space.py @@ -2,6 +2,7 @@ from __future__ import annotations +from copy import deepcopy from typing import TYPE_CHECKING, Any from warnings import warn @@ -9,17 +10,20 @@ from colorama import Fore from bayes_opt.exception import NotUniqueError +from bayes_opt.parameter import BayesParameter, CategoricalParameter, FloatParameter, IntParameter, is_numeric from bayes_opt.util import ensure_rng if TYPE_CHECKING: - from collections.abc import Callable, Mapping, Sequence + from collections.abc import Callable, Mapping from numpy.random import RandomState from numpy.typing import NDArray from bayes_opt.constraint import ConstraintModel + from bayes_opt.parameter import BoundsMapping, ParamsType Float = np.floating[Any] + Int = np.integer[Any] def _hashable(x: NDArray[Float]) -> tuple[float, ...]: @@ -66,7 +70,7 @@ class TargetSpace: def __init__( self, target_func: Callable[..., float] | None, - pbounds: Mapping[str, tuple[float, float]], + pbounds: BoundsMapping, constraint: ConstraintModel | None = None, random_state: int | RandomState | None = None, allow_duplicate_points: bool | None = False, @@ -79,11 +83,13 @@ def __init__( self.target_func = target_func # Get the name of the parameters - self._keys: list[str] = sorted(pbounds) - # Create an array with parameters bounds - self._bounds: NDArray[Float] = np.array( - [item[1] for item in sorted(pbounds.items(), key=lambda x: x[0])], dtype=float - ) + self._keys: list[str] = list(pbounds.keys()) + + self._params_config = self.make_params(pbounds) + self._dim = sum([self._params_config[key].dim for key in self._keys]) + + self._masks = self.make_masks() + self._bounds = self.calculate_bounds() # preallocated memory for X and Y points self._params: NDArray[Float] = np.empty(shape=(0, self.dim)) @@ -100,7 +106,9 @@ def __init__( if constraint.lb.size == 1: self._constraint_values = np.empty(shape=(0), dtype=float) else: - self._constraint_values = np.empty(shape=(0, constraint.lb.size), dtype=float) + self._constraint_values = np.empty(shape=(0, self._constraint.lb.size), dtype=float) + else: + self._constraint = None def __contains__(self, x: NDArray[Float]) -> bool: """Check if this parameter has already been registered. @@ -118,9 +126,6 @@ def __len__(self) -> int: ------- int """ - if len(self._params) != len(self._target): - error_msg = "The number of parameters and targets do not match." - raise ValueError(error_msg) return len(self._target) @property @@ -161,7 +166,7 @@ def dim(self) -> int: ------- int """ - return len(self._keys) + return self._dim @property def keys(self) -> list[str]: @@ -173,6 +178,11 @@ def keys(self) -> list[str]: """ return self._keys + @property + def params_config(self) -> dict[str, BayesParameter]: + """Get the parameters configuration.""" + return self._params_config + @property def bounds(self) -> NDArray[Float]: """Get the bounds of this TargetSpace. @@ -194,45 +204,101 @@ def constraint(self) -> ConstraintModel | None: return self._constraint @property - def constraint_values(self) -> NDArray[Float]: - """Get the constraint values registered to this TargetSpace. + def masks(self) -> dict[str, NDArray[np.bool_]]: + """Get the masks for the parameters. Returns ------- - np.ndarray + dict """ - if self._constraint is None: - error_msg = "TargetSpace belongs to an unconstrained optimization" - raise AttributeError(error_msg) - - return self._constraint_values + return self._masks @property - def mask(self) -> NDArray[np.bool_]: - """Return a boolean array of valid points. - - Points are valid if they satisfy both the constraint and boundary conditions. + def continuous_dimensions(self) -> NDArray[np.bool_]: + """Get the continuous parameters. Returns ------- - np.ndarray + dict """ - mask = np.ones_like(self.target, dtype=bool) + result = np.zeros(self.dim, dtype=bool) + masks = self.masks + for key in self.keys: + result[masks[key]] = self._params_config[key].is_continuous + return result - # mask points that don't satisfy the constraint - if self._constraint is not None: - mask &= self._constraint.allowed(self._constraint_values) + def make_params(self, pbounds: BoundsMapping) -> dict[str, BayesParameter]: + """Create a dictionary of parameters from a dictionary of bounds. - # mask points that are outside the bounds - if self._bounds is not None: - within_bounds = np.all( - (self._bounds[:, 0] <= self._params) & (self._params <= self._bounds[:, 1]), axis=1 + Parameters + ---------- + pbounds : dict + A dictionary with the parameter names as keys and a tuple with minimum + and maximum values. + + Returns + ------- + dict + A dictionary with the parameter names as keys and the corresponding + parameter objects as values. + """ + any_is_not_float = False # TODO: remove in an upcoming release + params: dict[str, BayesParameter] = {} + for key in pbounds: + pbound = pbounds[key] + + if isinstance(pbound, BayesParameter): + res = pbound + if not isinstance(pbound, FloatParameter): + any_is_not_float = True + elif (len(pbound) == 2 and is_numeric(pbound[0]) and is_numeric(pbound[1])) or ( + len(pbound) == 3 and pbound[-1] is float + ): + res = FloatParameter(name=key, bounds=(float(pbound[0]), float(pbound[1]))) + elif len(pbound) == 3 and pbound[-1] is int: + res = IntParameter(name=key, bounds=(int(pbound[0]), int(pbound[1]))) + any_is_not_float = True + else: + # assume categorical variable with pbound as list of possible values + res = CategoricalParameter(name=key, categories=pbound) + any_is_not_float = True + params[key] = res + if any_is_not_float: + msg = ( + "Non-float parameters are experimental and may not work as expected." + " Exercise caution when using them and please report any issues you encounter." ) - mask &= within_bounds + warn(msg, stacklevel=4) + return params - return mask + def make_masks(self) -> dict[str, NDArray[np.bool_]]: + """Create a dictionary of masks for the parameters. - def params_to_array(self, params: Mapping[str, float]) -> NDArray[Float]: + The mask can be used to select the corresponding parameters from an array. + + Returns + ------- + dict + A dictionary with the parameter names as keys and the corresponding + mask as values. + """ + masks = {} + pos = 0 + for key in self._keys: + mask = np.zeros(self._dim) + mask[pos : pos + self._params_config[key].dim] = 1 + masks[key] = mask.astype(bool) + pos = pos + self._params_config[key].dim + return masks + + def calculate_bounds(self) -> NDArray[Float]: + """Calculate the float bounds of the parameter space.""" + bounds = np.empty((self._dim, 2)) + for key in self._keys: + bounds[self.masks[key]] = self._params_config[key].bounds + return bounds + + def params_to_array(self, params: Mapping[str, float | NDArray[Float]]) -> NDArray[Float]: """Convert a dict representation of parameters into an array version. Parameters @@ -247,13 +313,35 @@ def params_to_array(self, params: Mapping[str, float]) -> NDArray[Float]: """ if set(params) != set(self.keys): error_msg = ( - f"Parameters' keys ({sorted(params)}) do " - f"not match the expected set of keys ({self.keys})." + f"Parameters' keys ({params}) do " f"not match the expected set of keys ({self.keys})." ) raise ValueError(error_msg) - return np.asarray([params[key] for key in self.keys]) + return self._to_float(params) + + @property + def constraint_values(self) -> NDArray[Float]: + """Get the constraint values registered to this TargetSpace. - def array_to_params(self, x: NDArray[Float]) -> dict[str, float]: + Returns + ------- + np.ndarray + """ + if self._constraint is None: + error_msg = "TargetSpace belongs to an unconstrained optimization" + raise AttributeError(error_msg) + + return self._constraint_values + + def kernel_transform(self, value: NDArray[Float]) -> NDArray[Float]: + """Transform floating-point suggestions to values used in the kernel. + + Vectorized. + """ + value = np.atleast_2d(value) + res = [self._params_config[p].kernel_transform(value[:, self.masks[p]]) for p in self._keys] + return np.hstack(res) + + def array_to_params(self, x: NDArray[Float]) -> dict[str, float | NDArray[Float]]: """Convert an array representation of parameters into a dict version. Parameters @@ -266,13 +354,56 @@ def array_to_params(self, x: NDArray[Float]) -> dict[str, float]: dict Representation of the parameters as dictionary. """ - if len(x) != len(self.keys): + if len(x) != self._dim: error_msg = ( f"Size of array ({len(x)}) is different than the " - f"expected number of parameters ({len(self.keys)})." + f"expected number of parameters ({self._dim})." ) raise ValueError(error_msg) - return dict(zip(self.keys, x)) + return self._to_params(x) + + def _to_float(self, value: Mapping[str, float | NDArray[Float]]) -> NDArray[Float]: + if set(value) != set(self.keys): + msg = f"Parameters' keys ({value}) do " f"not match the expected set of keys ({self.keys})." + raise ValueError(msg) + res = np.zeros(self._dim) + for key in self._keys: + p = self._params_config[key] + res[self.masks[key]] = p.to_float(value[key]) + return res + + def _to_params(self, value: NDArray[Float]) -> dict[str, float | NDArray[Float]]: + res: dict[str, float | NDArray[Float]] = {} + for key in self._keys: + p = self._params_config[key] + mask = self.masks[key] + res[key] = p.to_param(value[mask]) + return res + + @property + def mask(self) -> NDArray[np.bool_]: + """Return a boolean array of valid points. + + Points are valid if they satisfy both the constraint and boundary conditions. + + Returns + ------- + np.ndarray + """ + mask = np.ones_like(self.target, dtype=bool) + + # mask points that don't satisfy the constraint + if self._constraint is not None: + mask &= self._constraint.allowed(self._constraint_values) + + # mask points that are outside the bounds + if self._bounds is not None: + within_bounds = np.all( + (self._bounds[:, 0] <= self._params) & (self._params <= self._bounds[:, 1]), axis=1 + ) + mask &= within_bounds + + return mask def _as_array(self, x: Any) -> NDArray[Float]: try: @@ -282,18 +413,12 @@ def _as_array(self, x: Any) -> NDArray[Float]: x = x.ravel() if x.size != self.dim: - error_msg = ( - f"Size of array ({len(x)}) is different than the " - f"expected number of parameters ({len(self.keys)})." - ) - raise ValueError(error_msg) + msg = f"Size of array ({len(x)}) is different than the expected number of ({self.dim})." + raise ValueError(msg) return x def register( - self, - params: Mapping[str, float] | Sequence[float] | NDArray[Float], - target: float, - constraint_value: float | NDArray[Float] | None = None, + self, params: ParamsType, target: float, constraint_value: float | NDArray[Float] | None = None ) -> None: """Append a point and its target value to the known data. @@ -331,6 +456,7 @@ def register( 1 """ x = self._as_array(params) + if x in self: if self._allow_duplicate_points: self.n_duplicate_points = self.n_duplicate_points + 1 @@ -348,7 +474,16 @@ def register( # if x is not within the bounds of the parameter space, warn the user if self._bounds is not None and not np.all((self._bounds[:, 0] <= x) & (x <= self._bounds[:, 1])): - warn(f"\nData point {x} is outside the bounds of the parameter space. ", stacklevel=2) + for key in self.keys: + if not np.all( + (self._params_config[key].bounds[..., 0] <= x[self.masks[key]]) + & (x[self.masks[key]] <= self._params_config[key].bounds[..., 1]) + ): + msg = ( + f"\nData point {x} is outside the bounds of the parameter {key}." + f"\n\tBounds:\n{self._params_config[key].bounds}" + ) + warn(msg, stacklevel=2) # Make copies of the data, so as not to modify the originals incase something fails # during the registration process. This prevents out-of-sync data. @@ -378,9 +513,7 @@ def register( self._target = target_copy self._cache = cache_copy - def probe( - self, params: Mapping[str, float] | Sequence[float] | NDArray[Float] - ) -> float | tuple[float, float | NDArray[Float]]: + def probe(self, params: ParamsType) -> float | tuple[float, float | NDArray[Float]]: """Evaluate the target function on a point and register the result. Notes @@ -425,10 +558,22 @@ def probe( self.register(x, target, constraint_value) return target, constraint_value - def random_sample(self) -> NDArray[Float]: + def random_sample( + self, n_samples: int = 0, random_state: np.random.RandomState | int | None = None + ) -> NDArray[Float]: """ Sample a random point from within the bounds of the space. + Parameters + ---------- + n_samples : int, optional + Number of samples to draw. If 0, a single sample is drawn, + and a 1D array is returned. If n_samples > 0, an array of + shape (n_samples, dim) is returned. + + random_state : np.random.RandomState | int | None + The random state to use for sampling. + Returns ------- data: ndarray @@ -442,10 +587,16 @@ def random_sample(self) -> NDArray[Float]: >>> space.random_sample() array([[ 0.54488318, 55.33253689]]) """ - data = np.empty((1, self.dim)) - for col, (lower, upper) in enumerate(self._bounds): - data.T[col] = self.random_state.uniform(lower, upper, size=1) - return data.ravel() + random_state = ensure_rng(random_state) + flatten = n_samples == 0 + n_samples = max(1, n_samples) + data = np.empty((n_samples, self._dim)) + for key, mask in self.masks.items(): + smpl = self._params_config[key].random_sample(n_samples, random_state) + data[:, mask] = smpl.reshape(n_samples, self._params_config[key].dim) + if flatten: + return data.ravel() + return data def _target_max(self) -> float | None: """Get the maximum target value within the current parameter bounds. @@ -513,7 +664,7 @@ def res(self) -> list[dict[str, Any]]: Does not report if points are within the bounds of the parameter space. """ if self._constraint is None: - params = [dict(zip(self.keys, p)) for p in self.params] + params = [self.array_to_params(p) for p in self.params] return [{"target": target, "params": param} for target, param in zip(self.target, params)] @@ -529,7 +680,7 @@ def res(self) -> list[dict[str, Any]]: ) ] - def set_bounds(self, new_bounds: Mapping[str, NDArray[Float] | Sequence[float]]) -> None: + def set_bounds(self, new_bounds: BoundsMapping) -> None: """Change the lower and upper search bounds. Parameters @@ -537,6 +688,25 @@ def set_bounds(self, new_bounds: Mapping[str, NDArray[Float] | Sequence[float]]) new_bounds : dict A dictionary with the parameter name and its new bounds """ - for row, key in enumerate(self.keys): + new_params_config = self.make_params(new_bounds) + + dims = 0 + params_config = deepcopy(self._params_config) + for key in self.keys: if key in new_bounds: - self._bounds[row] = new_bounds[key] + if not isinstance(new_params_config[key], type(self._params_config[key])): + msg = ( + f"Parameter type {type(new_params_config[key])} of" + " new bounds does not match parameter type" + f" {type(self._params_config[key])} of old bounds" + ) + raise ValueError(msg) + params_config[key] = new_params_config[key] + dims = dims + params_config[key].dim + if dims != self.dim: + msg = ( + f"Dimensions of new bounds ({dims}) does not match" f" dimensions of old bounds ({self.dim})." + ) + raise ValueError(msg) + self._params_config = params_config + self._bounds = self.calculate_bounds() diff --git a/docsrc/index.rst b/docsrc/index.rst index ac664a582..5c198c6f2 100644 --- a/docsrc/index.rst +++ b/docsrc/index.rst @@ -11,6 +11,7 @@ Basic Tour Advanced Tour Constrained Bayesian Optimization + Parameter Types Sequential Domain Reduction Acquisition Functions Exploration vs. Exploitation @@ -26,6 +27,7 @@ reference/constraint reference/domain_reduction reference/target_space + reference/parameter reference/exception reference/other @@ -121,11 +123,13 @@ section. We suggest that you: to learn how to use the package's most important features. - Take a look at the `advanced tour notebook `__ - to learn how to make the package more flexible, how to deal with - categorical parameters, how to use observers, and more. + to learn how to make the package more flexible or how to use observers. - To learn more about acquisition functions, a central building block of bayesian optimization, see the `acquisition functions notebook `__ +- If you want to optimize over integer-valued or categorical + parameters, see the `parameter types + notebook `__. - Check out this `notebook `__ with a step by step visualization of how this method works. @@ -195,6 +199,20 @@ For constrained optimization: year={2014} } +For optimization over non-float parameters: + +:: + + @article{garrido2020dealing, + title={Dealing with categorical and integer-valued variables in bayesian optimization with gaussian processes}, + author={Garrido-Merch{\'a}n, Eduardo C and Hern{\'a}ndez-Lobato, Daniel}, + journal={Neurocomputing}, + volume={380}, + pages={20--35}, + year={2020}, + publisher={Elsevier} + } + .. |tests| image:: https://github.com/bayesian-optimization/BayesianOptimization/actions/workflows/run_tests.yml/badge.svg .. |Codecov| image:: https://codecov.io/github/bayesian-optimization/BayesianOptimization/badge.svg?branch=master&service=github :target: https://codecov.io/github/bayesian-optimization/BayesianOptimization?branch=master diff --git a/docsrc/reference/parameter.rst b/docsrc/reference/parameter.rst new file mode 100644 index 000000000..91b8f2e9a --- /dev/null +++ b/docsrc/reference/parameter.rst @@ -0,0 +1,5 @@ +:py:mod:`bayes_opt.parameter` +-------------------------------- + +.. automodule:: bayes_opt.parameter + :members: diff --git a/examples/advanced-tour.ipynb b/examples/advanced-tour.ipynb index dc72e40ed..9e93d09d7 100644 --- a/examples/advanced-tour.ipynb +++ b/examples/advanced-tour.ipynb @@ -96,7 +96,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Next point to probe is: {'x': -0.331911981189704, 'y': 1.3219469606529486}\n" + "Next point to probe is: {'x': np.float64(-0.331911981189704), 'y': np.float64(1.3219469606529486)}\n" ] } ], @@ -167,12 +167,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "-18.503835804889988 {'x': 1.953072105336, 'y': -2.9609778030491904}\n", - "-1.0819533157901717 {'x': 0.22703572807626315, 'y': 2.4249238905875123}\n", - "-6.50219704520679 {'x': -1.9991881984624875, 'y': 2.872282989383577}\n", - "-5.747604713731052 {'x': -1.994467585936897, 'y': -0.664242699361514}\n", - "-2.9682431497650823 {'x': 1.9737252084307952, 'y': 1.269540259274744}\n", - "{'target': 0.7861845912690544, 'params': {'x': -0.331911981189704, 'y': 1.3219469606529486}}\n" + "-18.707136686093495 {'x': np.float64(1.9261486197444082), 'y': np.float64(-2.9996360060323246)}\n", + "0.750594563473972 {'x': np.float64(-0.3763326769822668), 'y': np.float64(1.328297354179696)}\n", + "-6.559031075654336 {'x': np.float64(1.979183535803597), 'y': np.float64(2.9083667381450318)}\n", + "-6.915481333972961 {'x': np.float64(-1.9686133847781613), 'y': np.float64(-1.009985740060171)}\n", + "-6.8600832617014085 {'x': np.float64(-1.9763198875239296), 'y': np.float64(2.9885278383464513)}\n", + "{'target': np.float64(0.7861845912690544), 'params': {'x': np.float64(-0.331911981189704), 'y': np.float64(1.3219469606529486)}}\n" ] } ], @@ -190,112 +190,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Dealing with discrete parameters\n", - "\n", - "**There is no principled way of dealing with discrete parameters using this package.**\n", - "\n", - "Ok, now that we got that out of the way, how do you do it? You're bound to be in a situation where some of your function's parameters may only take on discrete values. Unfortunately, the nature of bayesian optimization with gaussian processes doesn't allow for an easy/intuitive way of dealing with discrete parameters - but that doesn't mean it is impossible. The example below showcases a simple, yet reasonably adequate, way to dealing with discrete parameters." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "def func_with_discrete_params(x, y, d):\n", - " # Simulate necessity of having d being discrete.\n", - " assert type(d) == int\n", - " \n", - " return ((x + y + d) // (1 + d)) / (1 + (x + y) ** 2)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "def function_to_be_optimized(x, y, w):\n", - " d = int(w)\n", - " return func_with_discrete_params(x, y, d)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "optimizer = BayesianOptimization(\n", - " f=function_to_be_optimized,\n", - " pbounds={'x': (-10, 10), 'y': (-10, 10), 'w': (0, 5)},\n", - " verbose=2,\n", - " random_state=1,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| iter | target | w | x | y |\n", - "-------------------------------------------------------------\n", - "| \u001b[30m1 | \u001b[30m-0.06199 | \u001b[30m2.085 | \u001b[30m4.406 | \u001b[30m-9.998 |\n", - "| \u001b[35m2 | \u001b[35m-0.0344 | \u001b[35m1.512 | \u001b[35m-7.065 | \u001b[35m-8.153 |\n", - "| \u001b[30m3 | \u001b[30m-0.2177 | \u001b[30m0.9313 | \u001b[30m-3.089 | \u001b[30m-2.065 |\n", - "| \u001b[35m4 | \u001b[35m0.1865 | \u001b[35m2.694 | \u001b[35m-1.616 | \u001b[35m3.704 |\n", - "| \u001b[30m5 | \u001b[30m-0.2187 | \u001b[30m1.022 | \u001b[30m7.562 | \u001b[30m-9.452 |\n", - "| \u001b[35m6 | \u001b[35m0.2488 | \u001b[35m2.684 | \u001b[35m-2.188 | \u001b[35m3.925 |\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| \u001b[35m7 | \u001b[35m0.2948 | \u001b[35m2.683 | \u001b[35m-2.534 | \u001b[35m4.08 |\n", - "| \u001b[35m8 | \u001b[35m0.3202 | \u001b[35m2.514 | \u001b[35m-3.83 | \u001b[35m5.287 |\n", - "| \u001b[30m9 | \u001b[30m0.0 | \u001b[30m4.057 | \u001b[30m-4.458 | \u001b[30m3.928 |\n", - "| \u001b[35m10 | \u001b[35m0.4802 | \u001b[35m2.296 | \u001b[35m-3.518 | \u001b[35m4.558 |\n", - "| \u001b[30m11 | \u001b[30m0.0 | \u001b[30m1.084 | \u001b[30m-3.737 | \u001b[30m4.472 |\n", - "| \u001b[30m12 | \u001b[30m0.0 | \u001b[30m2.649 | \u001b[30m-3.861 | \u001b[30m4.353 |\n", - "| \u001b[30m13 | \u001b[30m0.0 | \u001b[30m2.442 | \u001b[30m-3.658 | \u001b[30m4.599 |\n", - "| \u001b[30m14 | \u001b[30m-0.05801 | \u001b[30m1.935 | \u001b[30m-0.4758 | \u001b[30m-8.755 |\n", - "| \u001b[30m15 | \u001b[30m0.0 | \u001b[30m2.337 | \u001b[30m7.973 | \u001b[30m-8.96 |\n", - "| \u001b[30m16 | \u001b[30m0.07699 | \u001b[30m0.6926 | \u001b[30m5.59 | \u001b[30m6.854 |\n", - "| \u001b[30m17 | \u001b[30m-0.02025 | \u001b[30m3.534 | \u001b[30m-8.943 | \u001b[30m1.987 |\n", - "| \u001b[30m18 | \u001b[30m0.0 | \u001b[30m2.59 | \u001b[30m-7.339 | \u001b[30m5.941 |\n", - "| \u001b[30m19 | \u001b[30m0.0929 | \u001b[30m2.237 | \u001b[30m-4.535 | \u001b[30m9.065 |\n", - "| \u001b[30m20 | \u001b[30m0.1538 | \u001b[30m0.477 | \u001b[30m2.931 | \u001b[30m2.683 |\n", - "| \u001b[30m21 | \u001b[30m0.0 | \u001b[30m0.9999 | \u001b[30m4.397 | \u001b[30m-3.971 |\n", - "| \u001b[30m22 | \u001b[30m-0.01894 | \u001b[30m3.764 | \u001b[30m-7.043 | \u001b[30m-3.184 |\n", - "| \u001b[30m23 | \u001b[30m0.03683 | \u001b[30m1.851 | \u001b[30m5.783 | \u001b[30m7.966 |\n", - "| \u001b[30m24 | \u001b[30m-0.04359 | \u001b[30m1.615 | \u001b[30m-5.133 | \u001b[30m-6.556 |\n", - "| \u001b[30m25 | \u001b[30m0.02617 | \u001b[30m3.863 | \u001b[30m0.1052 | \u001b[30m8.579 |\n", - "| \u001b[30m26 | \u001b[30m-0.1071 | \u001b[30m0.8131 | \u001b[30m-0.7949 | \u001b[30m-9.292 |\n", - "| \u001b[30m27 | \u001b[30m0.0 | \u001b[30m4.969 | \u001b[30m8.778 | \u001b[30m-8.467 |\n", - "| \u001b[30m28 | \u001b[30m-0.1372 | \u001b[30m0.9475 | \u001b[30m-1.019 | \u001b[30m-7.018 |\n", - "| \u001b[30m29 | \u001b[30m0.08078 | \u001b[30m1.917 | \u001b[30m-0.2606 | \u001b[30m6.272 |\n", - "| \u001b[30m30 | \u001b[30m0.02003 | \u001b[30m4.278 | \u001b[30m3.8 | \u001b[30m8.398 |\n", - "=============================================================\n" - ] - } - ], - "source": [ - "optimizer.set_gp_params(alpha=1e-3)\n", - "optimizer.maximize()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Tuning the underlying Gaussian Process\n", + "## 2. Tuning the underlying Gaussian Process\n", "\n", "The bayesian optimization algorithm works by performing a gaussian process regression of the observed combination of parameters and their associated target values. The predicted parameter $\\rightarrow$ target hyper-surface (and its uncertainty) is then used to guide the next best point to probe." ] @@ -304,14 +199,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.1 Passing parameter to the GP\n", + "### 2.1 Passing parameter to the GP\n", "\n", "Depending on the problem it could be beneficial to change the default parameters of the underlying GP. You can use the `optimizer.set_gp_params` method to do this:" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -320,12 +215,12 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[30m1 | \u001b[30m0.7862 | \u001b[30m-0.3319 | \u001b[30m1.322 |\n", - "| \u001b[30m2 | \u001b[30m-18.19 | \u001b[30m1.957 | \u001b[30m-2.919 |\n", - "| \u001b[30m3 | \u001b[30m-12.05 | \u001b[30m-1.969 | \u001b[30m-2.029 |\n", - "| \u001b[30m4 | \u001b[30m-7.463 | \u001b[30m0.6032 | \u001b[30m-1.846 |\n", - "| \u001b[30m5 | \u001b[30m-1.093 | \u001b[30m1.444 | \u001b[30m1.096 |\n", - "| \u001b[35m6 | \u001b[35m0.8586 | \u001b[35m-0.2165 | \u001b[35m1.307 |\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m0.7862 \u001b[39m | \u001b[39m-0.331911\u001b[39m | \u001b[39m1.3219469\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-18.34 \u001b[39m | \u001b[39m1.9021640\u001b[39m | \u001b[39m-2.965222\u001b[39m |\n", + "| \u001b[35m3 \u001b[39m | \u001b[35m0.8731 \u001b[39m | \u001b[35m-0.298167\u001b[39m | \u001b[35m1.1948749\u001b[39m |\n", + "| \u001b[39m4 \u001b[39m | \u001b[39m-6.497 \u001b[39m | \u001b[39m1.9876938\u001b[39m | \u001b[39m2.8830942\u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m-4.286 \u001b[39m | \u001b[39m-1.995643\u001b[39m | \u001b[39m-0.141769\u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m-6.781 \u001b[39m | \u001b[39m-1.953302\u001b[39m | \u001b[39m2.9913127\u001b[39m |\n", "=================================================\n" ] } @@ -348,7 +243,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.2 Tuning the `alpha` parameter\n", + "### 2.2 Tuning the `alpha` parameter\n", "\n", "When dealing with functions with discrete parameters,or particularly erratic target space it might be beneficial to increase the value of the `alpha` parameter. This parameters controls how much noise the GP can handle, so increase it whenever you think that extra flexibility is needed." ] @@ -358,7 +253,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.3 Changing kernels\n", + "### 2.3 Changing kernels\n", "\n", "By default this package uses the Matern 2.5 kernel. Depending on your use case you may find that tuning the GP kernel could be beneficial. You're on your own here since these are very specific solutions to very specific problems. You should start with the [scikit learn docs](https://scikit-learn.org/stable/modules/gaussian_process.html#kernels-for-gaussian-processes)." ] @@ -376,7 +271,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -385,7 +280,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -399,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -411,7 +306,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -433,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -449,7 +344,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -476,7 +371,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -485,7 +380,7 @@ "['optimization:start', 'optimization:step', 'optimization:end']" ] }, - "execution_count": 20, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -497,7 +392,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "bayesian-optimization-t6LLJ9me-py3.10", "language": "python", "name": "python3" }, @@ -511,7 +406,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.1.undefined" + "version": "3.10.13" }, "nbdime-conflicts": { "local_diff": [ diff --git a/examples/basic-tour.ipynb b/examples/basic-tour.ipynb index 3cbcbd407..4ecd83296 100644 --- a/examples/basic-tour.ipynb +++ b/examples/basic-tour.ipynb @@ -252,7 +252,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Or as an iterable. Beware that the order has to be alphabetical. You can usee `optimizer.space.keys` for guidance" + "Or as an iterable. Beware that the order has to match the order of the initial `pbounds` dictionary. You can usee `optimizer.space.keys` for guidance" ] }, { diff --git a/examples/parameter_types.ipynb b/examples/parameter_types.ipynb new file mode 100644 index 000000000..3d668300a --- /dev/null +++ b/examples/parameter_types.ipynb @@ -0,0 +1,756 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Optimizing over non-float Parameters\n", + "\n", + "Sometimes, you need to optimize a target that is not just a function of floating-point values, but relies on integer or categorical parameters. This notebook shows how such problems are handled by following an approach from [\"Dealing with categorical and integer-valued variables in Bayesian Optimization with Gaussian processes\" by Garrido-Merchán and Hernández-Lobato](https://arxiv.org/abs/1805.03463). One simple way of handling an integer-valued parameter is to run the optimization as normal, but then round to the nearest integer after a point has been suggested. This method is similar, except that the rounding is performed in the _kernel_. Why does this matter? It means that the kernel is aware that two parameters, that map the to same point but are potentially distinct before this transformation are the same." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from bayes_opt import BayesianOptimization\n", + "from bayes_opt import acquisition\n", + "\n", + "from sklearn.gaussian_process.kernels import Matern\n", + "\n", + "# suppress warnings about this being an experimental feature\n", + "warnings.filterwarnings(action=\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Simple integer-valued function\n", + "Let's look at a simple, one-dimensional, integer-valued target function and compare a typed optimizer and a continuous optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+sAAAJOCAYAAADPppagAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hb1fnA8e/Vtrz3jkf2JCEhIWEkrLBH2aNAKNBSRuEXWgqUUTrYUKBQKBQKlF1m2SQhjJCQQCbZseO9l6xl7fv7Q7Zjx5a3E4/38zx5YktHV0e+0tV97znnfRVVVVWEEEIIIYQQQggxZGgOdgeEEEIIIYQQQgjRngTrQgghhBBCCCHEECPBuhBCCCGEEEIIMcRIsC6EEEIIIYQQQgwxEqwLIYQQQgghhBBDjATrQgghhBBCCCHEECPBuhBCCCGEEEIIMcRIsC6EEEIIIYQQQgwxEqwLIYQQQgghhBBDjATrQgghhGDRokUsWrToYHdDCCGEEM0kWBdCCCEOgvz8fH71q1+Rm5uLyWQiKiqKI444gscff5ympqZBec7t27fzxz/+kcLCwkHZvhBCCCEGjqKqqnqwOyGEEEKMJh9//DHnnXceRqORyy67jGnTpuHxeFi1ahXvvPMOS5Ys4dlnnx3w53377bc577zzWLlyZYdRdI/HA4DBYBjw5xVCCCFE7+kOdgeEEEKI0aSgoIALL7yQrKwsvvzyS1JTU1vvu+6668jLy+Pjjz8+4P2SIF0IIYQYWmQavBBCCHEAPfjgg9jtdp5//vl2gXqLcePGceONNwLg8/n485//zNixYzEajWRnZ3P77bfjdrvbPSY7O5vTTjuNVatWMXfuXEwmE7m5ubz88sutbV588UXOO+88AI455hgURUFRFL766iug45r1r776CkVReOutt/jrX/9KRkYGJpOJ4447jry8vA7Pv2TJkg6vpbN18NXV1Vx55ZUkJydjMpk45JBDeOmll9q1aXnulr61KCwsRFEUXnzxxdbbKisrueKKK8jIyMBoNJKamsqZZ54pU/2FEEIMezKyLoQQQhxAH374Ibm5uSxYsKDbtldddRUvvfQS5557LjfffDNr167lvvvuY8eOHbz33nvt2ubl5XHuuedy5ZVXcvnll/PCCy+wZMkSZs+ezdSpUzn66KP5zW9+wxNPPMHtt9/O5MmTAVr/D+X+++9Ho9Hw29/+lsbGRh588EEuueQS1q5d2+vX3tTUxKJFi8jLy+P6668nJyeH//73vyxZsgSLxdJ6kaI3zjnnHLZt28YNN9xAdnY21dXVLFu2jOLiYrKzs3u9PSGEEGKokGBdCCGEOECsVitlZWWceeaZ3bbdvHkzL730EldddRXPPfccANdeey1JSUk8/PDDrFy5kmOOOaa1/a5du/jmm2846qijADj//PPJzMzk3//+Nw8//DC5ubkcddRRPPHEE5xwwgk9zvzucrnYtGlT6zT52NhYbrzxRrZu3cq0adN69fqfffZZduzYwSuvvMIll1wCwDXXXMPChQu54447+MUvfkFkZGSPt2exWFi9ejUPPfQQv/3tb1tvv+2223rVLyGEEGIokmnwQgghxAFitVoBehSQfvLJJwAsXbq03e0333wzQId17VOmTGkN1AESExOZOHEie/fu7Vefr7jiinbr2Vueoy/b/eSTT0hJSeGiiy5qvU2v1/Ob3/wGu93O119/3avthYWFYTAY+Oqrr2hoaOh1f4QQQoihTIJ1IYQQ4gCJiooCwGazddu2qKgIjUbDuHHj2t2ekpJCTEwMRUVF7W4fM2ZMh23Exsb2O4jdf7uxsbEAfdpuUVER48ePR6Npf/rRMhV//9fUHaPRyAMPPMCnn35KcnIyRx99NA8++CCVlZW97psQQggx1EiwLoQQQhwgUVFRpKWlsXXr1h4/RlGUHrXTarWd3t7fCq092W6oPvr9/j49Z2+2d9NNN7F7927uu+8+TCYTd955J5MnT2bjxo19em4hhBBiqJBgXQghhDiATjvtNPLz81mzZk2X7bKysggEAuzZs6fd7VVVVVgsFrKysnr93D0N/HsrNjYWi8XS4fb9R8qzsrLYs2cPgUCg3e07d+5svb9le0CHbYYaeR87diw333wzX3zxBVu3bsXj8fDII4/05aUIIYQQQ4YE60IIIcQBdMsttxAeHs5VV11FVVVVh/vz8/N5/PHHOeWUUwB47LHH2t3/6KOPAnDqqaf2+rnDw8OBjkFwf40dO5bvv/8ej8fTettHH31ESUlJu3annHIKlZWVvPnmm623+Xw+/v73vxMREcHChQuBYNCu1Wr55ptv2j3+H//4R7vfnU4nLperQ18iIyM7lLcTQgghhhvJBi+EEEIcQGPHjuW1117jggsuYPLkyVx2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1omIiPygkToAIiIi6toll1yCc88913N92bJl2Lx5My677DL84he/wIEDBxAREQGNRgONpnc+3u12O5xOJ3Q6Xa88HxERUTjhyDoREZFCXXTRRbj77rtx/PhxvPbaawA6X7Oen5+PCy64ALGxsYiKisLAgQPx17/+tV2blpYW3HfffRgwYAAMBgPS0tIwZ84cHD58GABw7NgxCIKAlStXYtWqVejbty/0ej32798PADh48CDmzZuH+Ph4GAwGnHvuufjf//7nOf6aNWtw5ZVXAgAmT57smda/detWT5sNGzZgwoQJiIyMRHR0NC699FLs27cv4L83IiIiJeDIOhERkYJdd911+Otf/4qNGzdi8eLFHe7ft28fLrvsMgwbNgwrVqyAXq9HYWEhvvjiC08bh8OByy67DJs2bcLVV1+NW265BfX19cjPz8fevXvRt29fT9uXX34ZLS0tuOGGG6DX6xEfH499+/bh/PPPR0ZGBpYuXYrIyEi89dZbuOKKK/Duu+9i9uzZuPDCC/HHP/4Rjz/+OP76179i8ODBAOD5/9VXX8Wvf/1rTJ8+HQ8//DCamprwzDPP4IILLsB3332H3Nzc4P4iiYiIZIbJOhERkYJlZmYiJibGMwL+c/n5+bBardiwYQMSExM7bbN27Vps2rQJjzzyCG677TbP7UuXLoUoiu3aFhUVobCwEElJSZ7bLr74YmRnZ+Obb76BXq8HAPz+97/HBRdcgDvuuAOzZ89GXl4eJkyYgMcffxxTp07FpEmTPI9vaGjAH//4R/zmN7/Bc88957n917/+NQYOHIgHH3yw3e1EREThgNPgiYiIFC4qKsprVfjY2FgAwH//+184nc5O27z77rtITEz0FK5r6+dT6ufOndsuUa+ursbmzZtx1VVXob6+HpWVlaisrERVVRWmT5+OQ4cOobi42Gf8+fn5qK2txa9+9SvP4ysrK6FWqzFmzBhs2bLF5+OJiIhCEUfWiYiIFK6hoQHJycmd3vfLX/4SL7zwAn7zm99g6dKlmDJlCubMmYN58+ZBpXKdsz98+DAGDhzoV2G6Pn36tLteWFgIURRx99134+677+70MadOnUJGRobXYx46dAiAaw1+Z0wmU5dxERERhRom60RERApWVFSEuro69OvXr9P7IyIi8Nlnn2HLli348MMP8fHHH+PNN9/ERRddhI0bN0Kt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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def target_function_1d(x):\n", + " return np.sin(np.round(x)) - np.abs(np.round(x) / 5)\n", + "\n", + "c_pbounds = {'x': (-10, 10)}\n", + "bo_cont = BayesianOptimization(target_function_1d, c_pbounds, verbose=0, random_state=1)\n", + "\n", + "# one way of constructing an integer-valued parameter is to add a third element to the tuple\n", + "d_pbounds = {'x': (-10, 10, int)}\n", + "bo_disc = BayesianOptimization(target_function_1d, d_pbounds, verbose=0, random_state=1)\n", + "\n", + "fig, axs = plt.subplots(2, 1, figsize=(10, 6), sharex=True, sharey=True)\n", + "\n", + "bo_cont.maximize(init_points=2, n_iter=10)\n", + "bo_cont.acquisition_function._fit_gp(bo_cont._gp, bo_cont.space)\n", + "\n", + "y_mean, y_std = bo_cont._gp.predict(np.linspace(-10, 10, 1000).reshape(-1, 1), return_std=True)\n", + "axs[0].set_title('Continuous')\n", + "axs[0].plot(np.linspace(-10, 10, 1000), target_function_1d(np.linspace(-10, 10, 1000)), 'k--', label='True function')\n", + "axs[0].plot(np.linspace(-10, 10, 1000), y_mean, label='Predicted mean')\n", + "axs[0].fill_between(np.linspace(-10, 10, 1000), y_mean - y_std, y_mean + y_std, alpha=0.3, label='Predicted std')\n", + "axs[0].plot(bo_cont.space.params, bo_cont.space.target, 'ro')\n", + "\n", + "bo_disc.maximize(init_points=2, n_iter=10)\n", + "bo_disc.acquisition_function._fit_gp(bo_disc._gp, bo_disc.space)\n", + "\n", + "y_mean, y_std = bo_disc._gp.predict(np.linspace(-10, 10, 1000).reshape(-1, 1), return_std=True)\n", + "axs[1].set_title('Discrete')\n", + "axs[1].plot(np.linspace(-10, 10, 1000), target_function_1d(np.linspace(-10, 10, 1000)), 'k--', label='True function')\n", + "axs[1].plot(np.linspace(-10, 10, 1000), y_mean, label='Predicted mean')\n", + "axs[1].fill_between(np.linspace(-10, 10, 1000), y_mean - y_std, y_mean + y_std, alpha=0.3, label='Predicted std')\n", + "axs[1].plot(bo_disc.space.params, bo_disc.space.target, 'ro')\n", + "\n", + "for ax in axs:\n", + " ax.grid(True)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see, that the discrete optimizer is aware that the function is discrete and does not try to predict values between the integers. The continuous optimizer tries to predict values between the integers, despite the fact that these are known.\n", + "We can also see that the discrete optimizer predicts blocky mean and standard deviations, which is a result of the discrete nature of the function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Mixed-parameter optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def discretized_function(x, y):\n", + " y = np.round(y)\n", + " return (-1*np.cos(x)**np.abs(y) + -1*np.cos(y)) + 0.1 * (x + y) - 0.01 * (x**2 + y**2)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Bounded region of parameter space\n", + "c_pbounds = {'x': (-5, 5), 'y': (-5, 5)}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "labels = [\"All-float Optimizer\", \"Typed Optimizer\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "continuous_optimizer = BayesianOptimization(\n", + " f=discretized_function,\n", + " acquisition_function=acquisition.ExpectedImprovement(xi=0.01, random_state=1),\n", + " pbounds=c_pbounds,\n", + " verbose=2,\n", + " random_state=1,\n", + ")\n", + "\n", + "continuous_optimizer.set_gp_params(kernel=Matern(nu=2.5, length_scale=np.ones(2)))\n", + "\n", + "d_pbounds = {'x': (-5, 5), 'y': (-5, 5, int)}\n", + "discrete_optimizer = BayesianOptimization(\n", + " f=discretized_function,\n", + " acquisition_function=acquisition.ExpectedImprovement(xi=0.01, random_state=1),\n", + " pbounds=d_pbounds,\n", + " verbose=2,\n", + " random_state=1,\n", + ")\n", + "\n", + "discrete_optimizer.set_gp_params(kernel=Matern(nu=2.5, length_scale=np.ones(2)));" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================== All-float Optimizer ====================\n", + "\n", + "| iter | target | x | y |\n", + "-------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m0.03061 \u001b[39m | \u001b[39m-0.829779\u001b[39m | \u001b[39m2.2032449\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-0.6535 \u001b[39m | \u001b[39m-4.998856\u001b[39m | \u001b[39m-1.976674\u001b[39m |\n", + "| \u001b[35m3 \u001b[39m | \u001b[35m0.8025 \u001b[39m | \u001b[35m-0.829779\u001b[39m | \u001b[35m2.6549696\u001b[39m |\n", + "| \u001b[35m4 \u001b[39m | \u001b[35m0.9203 \u001b[39m | \u001b[35m-0.981065\u001b[39m | \u001b[35m2.6644394\u001b[39m |\n", + "| \u001b[35m5 \u001b[39m | \u001b[35m1.008 \u001b[39m | \u001b[35m-1.652553\u001b[39m | \u001b[35m2.7133425\u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m0.9926 \u001b[39m | \u001b[39m-1.119714\u001b[39m | \u001b[39m2.8358733\u001b[39m |\n", + "| \u001b[35m7 \u001b[39m | \u001b[35m1.322 \u001b[39m | \u001b[35m-2.418942\u001b[39m | \u001b[35m3.4600371\u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m-0.5063 \u001b[39m | \u001b[39m-3.092074\u001b[39m | \u001b[39m3.7368226\u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m-0.6432 \u001b[39m | \u001b[39m-4.089558\u001b[39m | \u001b[39m-0.560384\u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m1.267 \u001b[39m | \u001b[39m-2.360726\u001b[39m | \u001b[39m3.3725022\u001b[39m |\n", + "| \u001b[39m11 \u001b[39m | \u001b[39m0.4649 \u001b[39m | \u001b[39m-2.247113\u001b[39m | \u001b[39m3.7419056\u001b[39m |\n", + "| \u001b[39m12 \u001b[39m | \u001b[39m1.0 \u001b[39m | \u001b[39m-1.740988\u001b[39m | \u001b[39m3.4854116\u001b[39m |\n", + "| \u001b[39m13 \u001b[39m | \u001b[39m0.986 \u001b[39m | \u001b[39m1.2164322\u001b[39m | \u001b[39m4.4938459\u001b[39m |\n", + "| \u001b[39m14 \u001b[39m | \u001b[39m-2.27 \u001b[39m | \u001b[39m-2.213867\u001b[39m | \u001b[39m0.3585570\u001b[39m |\n", + "| \u001b[39m15 \u001b[39m | \u001b[39m-1.853 \u001b[39m | \u001b[39m1.7935035\u001b[39m | \u001b[39m-0.377351\u001b[39m |\n", + "=================================================\n", + "Max: 1.321554535694256\n", + "\n", + "\n", + "==================== Typed Optimizer ====================\n", + "\n", + "| iter | target | x | y |\n", + "-------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m0.8025 \u001b[39m | \u001b[39m-0.829779\u001b[39m | \u001b[39m3 \u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-2.75 \u001b[39m | \u001b[39m-4.998856\u001b[39m | \u001b[39m0 \u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m0.8007 \u001b[39m | \u001b[39m-0.827713\u001b[39m | \u001b[39m3 \u001b[39m |\n", + "| \u001b[39m4 \u001b[39m | \u001b[39m-0.749 \u001b[39m | \u001b[39m2.2682240\u001b[39m | \u001b[39m-5 \u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m0.3718 \u001b[39m | \u001b[39m-2.339072\u001b[39m | \u001b[39m4 \u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m0.2146 \u001b[39m | \u001b[39m4.9971028\u001b[39m | \u001b[39m5 \u001b[39m |\n", + "| \u001b[39m7 \u001b[39m | \u001b[39m0.7473 \u001b[39m | \u001b[39m4.9970839\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[35m8 \u001b[39m | \u001b[35m0.8275 \u001b[39m | \u001b[35m4.9986856\u001b[39m | \u001b[35m-3 \u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m0.3464 \u001b[39m | \u001b[39m4.9987136\u001b[39m | \u001b[39m-2 \u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m-0.7852 \u001b[39m | \u001b[39m4.9892216\u001b[39m | \u001b[39m-5 \u001b[39m |\n", + "| \u001b[39m11 \u001b[39m | \u001b[39m-0.6627 \u001b[39m | \u001b[39m-4.999635\u001b[39m | \u001b[39m-4 \u001b[39m |\n", + "| \u001b[39m12 \u001b[39m | \u001b[39m-0.1697 \u001b[39m | \u001b[39m-4.992664\u001b[39m | \u001b[39m-3 \u001b[39m |\n", + "| \u001b[35m13 \u001b[39m | \u001b[35m1.428 \u001b[39m | \u001b[35m4.9950290\u001b[39m | \u001b[35m3 \u001b[39m |\n", + "| \u001b[39m14 \u001b[39m | \u001b[39m1.137 \u001b[39m | \u001b[39m4.9970984\u001b[39m | \u001b[39m4 \u001b[39m |\n", + "| \u001b[35m15 \u001b[39m | \u001b[35m1.641 \u001b[39m | \u001b[35m4.0889271\u001b[39m | \u001b[35m3 \u001b[39m |\n", + "=================================================\n", + "Max: 1.6407143853831352\n", + "\n", + "\n" + ] + } + ], + "source": [ + "for lbl, optimizer in zip(labels, [continuous_optimizer, discrete_optimizer]):\n", + " print(f\"==================== {lbl} ====================\\n\")\n", + " optimizer.maximize(\n", + " init_points=2,\n", + " n_iter=13\n", + " )\n", + " print(f\"Max: {optimizer.max['target']}\\n\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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48qVK/D399crn//73/8QEBCgaFtQq1atEBAQgF27dqm81qtXLwQEBGD27Nn4888/8c033+Dx48f4+eefAQCrVq3C+++/jyZNmuCDDz4AAAQFBWncfnFxcejbty+io6PRv39/zJs3D507d8aiRYvwn//8B8OHDwcAzJ49G7169cL169fh4KD+dOro6Gi0a9dOadrvv/+ONWvWwMfHB8CrPQpdunTB0aNH8cEHH6BWrVq4ePEiFixYgL///lvl3Kf9+/djw4YNGDFiBLy8vHghlJYGDBiAn3/+Gbt370anTp0U0xMTE7F//36VPeaHDh3C+vXrMXLkSMhkMnz//feIiIjAqVOnEBISAuDVBXNNmzaFRCLBiBEj4O3tjd9++w1RUVHIyMjA6NGjAQDZ2dl48803cffuXYwcORL+/v5YtWoV9u/fr1XtzIiFZcSo3UQrdubMGQFA7N27VwghRF5enqhQoYIYNWqUUrtPP/1UAFB7vkD+3iNN5/Bou8fu5cuXIicnR6nN48ePha+vrxgyZIjSdGh5ThL+/5fC63TdY4cCv8qSk5OFTCYT48aNU0wzdBsV3GO3bds2AUDMnDlTqV2PHj2ERCIRcXFxSu/RyclJadr58+cFALFw4UKVdZEqbbMghGHn2IWHh4vatWsXucxu3boJJycnER8fr5j28OFD4e7uLlq1aqWY9vz5cyGXy5WWdevWLSGTycSMGTMU03Q5x+7JkycCgOjatavGdl26dBEAREZGhhDin1x16dJFqd3w4cMFAHH+/HnFtMLOH9KUwePHjyum7d69WwAQLi4u4s6dO4rpixcvVtnbUlje8924cUN4enqKt956S7x8+VII8ercMAcHB3HkyBGltosWLRIAxLFjxxTTAAgHBwdx+fLlQtdBrxTcYyeXy0WFChVE7969ldrNnz9fSCQScfPmTcU0/P8eozNnziim3blzRzg7O4t33nlHMS0qKkr4+fmJ1NRUpWW+9957wtPTU2RlZQkhhPjqq68EALFhwwZFm2fPnomqVasWuceOGbG8jPCq2P+3Zs0a+Pr6ok2bNgBeXcHUu3dvrFu3DnK5XNFu8+bNqFevHt555x2VZRjzEmmpVAonJycAr34NpKWl4eXLl2jcuDHOnj1rtPXoKjg4WOlXmbe3N2rUqIGbN28qphl7G/3666+QSqUYOXKk0vRx48ZB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55mZmWL58uUCgEhISBAJCQlCCCEiIyNF165dFe1Gjhwp/P39xa+//iouX74sIiMjRenSpcWjR4+EEELk5uaKTz/9VJw+fVrcvHlTrF69Wri6uor169cr1tOrVy8RERGhWE9OTo5i+b179xaRkZFG2jJERWN+iPTH/NgGduws0PDhw0X16tVF3759RZ06dcTz588Lbfv48WMBQBw+fFhp+tatW0XBfvvrwXr69KlwdHQUa9asUbyem5sr/P39xRdffFHo+mJiYkT37t3VLrOgMWPGiNatWxe6LCJTYH6I9Mf8WL8SZttVSIWaN28eQkJCsHHjRsTGxmo8PyE7OxsA4OzsrNM64uPj8eLFC6XzIhwdHdGkSRNcvXpVMe27777DsmXLcPfuXWRnZyM3Nxf169fXah0uLi7IysrSqS4iQzE/RPpjfqwfz7GzQPHx8Xj48CHy8vJw+/ZtjW3Lli0LiUSCx48fG72OdevW4aOPPkJUVBT27NmDc+fOYfDgwcjNzdVq/rS0NHh7exu9LiJNmB8i/TE/1o8dOwuTm5uL/v37o3fv3vjss8/w/vvvIzk5udD2Tk5OCA4OxpUrV3RaT1BQEJycnHDs2DHFtBcvXuD06dMIDg4GABw7dgxhYWEYPnw4GjRogKpVqyqudnp9/XK5XO06Ll26hAYNGuhUF5EhmB8i/TE/toEdOwvz8ccfIz09Hd988w0mTpyI6tWrY8iQIRrn6dChA44eParTekqWLIkPP/wQ48ePx++//44rV65g6NChyMrKQlRUFACgWrVqOHPmDHbv3o2///4bn3zyidJVSwAQEBCACxcu4Pr160hNTcWLFy8AAFlZWYiNjVUZtJLIlJgfIv0xPzbC3Cf50T8OHDggSpQoIY4cOaKYduvWLeHh4SG+//77Que7fPmycHFxEU+ePFFMK+rkVSGEyM7OFv/+97+Fl5eXkMlkonnz5uLUqVOK158/fy4GDRokPD09RalSpcSHH34oJk2aJOrVq6dok5ycLN566y3h5uYmAIgDBw4IIYRYu3atqFGjhp5bgkh3zA+R/pgf2yERQghzdizJOHr27ImGDRti8uTJ5i4FANC0aVOMHDkSffv2NXcpREVifoj0x/xYFh6KtRFz586Fm5ubucsA8Opefe+++y769Olj7lKItML8EOmP+bEs3GNHREREZCO4x46IiIjIRrBjR0RERGQj2LEjIiIishHs2BERERHZCHbsiIiIiGwEO3ZERERENoIdOyIiIiIbwY4dERERkY1gx46IiIjIRvwf6k1OaCtLhb8AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x = np.linspace(c_pbounds['x'][0], c_pbounds['x'][1], 1000)\n", + "y = np.linspace(c_pbounds['y'][0], c_pbounds['y'][1], 1000)\n", + "\n", + "X, Y = np.meshgrid(x, y)\n", + "\n", + "Z = discretized_function(X, Y)\n", + "\n", + "params = [{'x': x_i, 'y': y_j} for y_j in y for x_i in x]\n", + "array_params = [continuous_optimizer._space.params_to_array(p) for p in params]\n", + "c_pred = continuous_optimizer._gp.predict(array_params).reshape(X.shape)\n", + "d_pred = discrete_optimizer._gp.predict(array_params).reshape(X.shape)\n", + "\n", + "vmin = np.min([np.min(Z), np.min(c_pred), np.min(d_pred)])\n", + "vmax = np.max([np.max(Z), np.max(c_pred), np.max(d_pred)])\n", + "\n", + "fig, axs = plt.subplots(1, 3)\n", + "\n", + "axs[0].set_title('Actual function')\n", + "axs[0].contourf(X, Y, Z, cmap=plt.cm.coolwarm, vmin=vmin, vmax=vmax)\n", + "\n", + "\n", + "axs[1].set_title(labels[0])\n", + "axs[1].contourf(X, Y, c_pred, cmap=plt.cm.coolwarm, vmin=vmin, vmax=vmax)\n", + "axs[1].scatter(continuous_optimizer._space.params[:,0], continuous_optimizer._space.params[:,1], c='k')\n", + "\n", + "axs[2].set_title(labels[1])\n", + "axs[2].contourf(X, Y, d_pred, cmap=plt.cm.coolwarm, vmin=vmin, vmax=vmax)\n", + "axs[2].scatter(discrete_optimizer._space.params[:,0], discrete_optimizer._space.params[:,1], c='k')\n", + "\n", + "def make_plot_fancy(ax: plt.Axes):\n", + " ax.set_aspect(\"equal\")\n", + " ax.set_xlabel('x (float)')\n", + " ax.set_xticks([-5.0, -2.5, 0., 2.5, 5.0])\n", + " ax.set_ylabel('y (int)')\n", + " ax.set_yticks([-4, -2, 0, 2, 4])\n", + "\n", + "for ax in axs:\n", + " make_plot_fancy(ax)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Categorical variables\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also handle categorical variables! This is done under-the-hood by constructing parameters in a one-hot-encoding representation, with a transformation in the kernel rounding to the nearest one-hot representation. If you want to use this, you can specify a collection of strings as options.\n", + "\n", + "NB: As internally, the categorical variables are within a range of `[0, 1]` and the GP used for BO is by default isotropic, you might want to ensure your other features are similarly scaled to a range of `[0, 1]` or use an anisotropic GP." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def f1(x1, x2):\n", + " return -1*(x1 - np.sqrt(x1**2 + x2**2) * np.cos(np.sqrt(x1**2 + x2**2))**2 + 0.5 * np.sqrt(x1**2 + x2**2))\n", + "\n", + "def f2(x1, x2):\n", + " return -1*(x2 - np.sqrt(x1**2 + x2**2) * np.sin(np.sqrt(x1**2 + x2**2))**2 + 0.5 * np.sqrt(x1**2 + x2**2))\n", + "\n", + "def SPIRAL(x1, x2, k):\n", + " \"\"\"cf Ladislav-Luksan\n", + " \"\"\"\n", + " if k=='1':\n", + " return f1(10 * x1, 10 * x2)\n", + " elif k=='2':\n", + " return f2(10 * x1, 10 * x2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| iter | target | x1 | x2 | k |\n", + "-------------------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m-2.052 \u001b[39m | \u001b[39m-0.165955\u001b[39m | \u001b[39m0.4406489\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[35m2 \u001b[39m | \u001b[35m13.49 \u001b[39m | \u001b[35m-0.743751\u001b[39m | \u001b[35m0.9980810\u001b[39m | \u001b[35m1 \u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m-14.49 \u001b[39m | \u001b[39m-0.743433\u001b[39m | \u001b[39m0.9709879\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m4 \u001b[39m | \u001b[39m-13.33 \u001b[39m | \u001b[39m0.9950794\u001b[39m | \u001b[39m-0.352913\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m9.674 \u001b[39m | \u001b[39m0.5436849\u001b[39m | \u001b[39m-0.574376\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m9.498 \u001b[39m | \u001b[39m-0.218693\u001b[39m | \u001b[39m-0.709177\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m7 \u001b[39m | \u001b[39m11.43 \u001b[39m | \u001b[39m-0.918642\u001b[39m | \u001b[39m-0.648372\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m0.4882 \u001b[39m | \u001b[39m-0.218182\u001b[39m | \u001b[39m-0.012177\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m7.542 \u001b[39m | \u001b[39m-0.787692\u001b[39m | \u001b[39m0.3452580\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m-2.161 \u001b[39m | \u001b[39m0.1392349\u001b[39m | \u001b[39m-0.125728\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m11 \u001b[39m | \u001b[39m-0.8336 \u001b[39m | \u001b[39m0.1206357\u001b[39m | \u001b[39m-0.543264\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m12 \u001b[39m | \u001b[39m-8.413 \u001b[39m | \u001b[39m0.4981209\u001b[39m | \u001b[39m0.6434939\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m13 \u001b[39m | \u001b[39m6.372 \u001b[39m | \u001b[39m0.0587256\u001b[39m | \u001b[39m-0.892371\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m14 \u001b[39m | \u001b[39m-12.71 \u001b[39m | \u001b[39m0.7529885\u001b[39m | \u001b[39m-0.780621\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m15 \u001b[39m | \u001b[39m-1.521 \u001b[39m | \u001b[39m0.4118274\u001b[39m | \u001b[39m-0.517960\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m16 \u001b[39m | \u001b[39m11.88 \u001b[39m | \u001b[39m-0.755390\u001b[39m | \u001b[39m-0.533137\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[39m17 \u001b[39m | \u001b[39m0.6373 \u001b[39m | \u001b[39m0.2249733\u001b[39m | \u001b[39m-0.053787\u001b[39m | \u001b[39m2 \u001b[39m |\n", + "| \u001b[39m18 \u001b[39m | \u001b[39m2.154 \u001b[39m | \u001b[39m0.0583506\u001b[39m | \u001b[39m0.6550869\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "| \u001b[35m19 \u001b[39m | \u001b[35m13.69 \u001b[39m | \u001b[35m-0.741717\u001b[39m | \u001b[35m-0.820073\u001b[39m | \u001b[35m2 \u001b[39m |\n", + "| \u001b[39m20 \u001b[39m | \u001b[39m1.615 \u001b[39m | \u001b[39m-0.663312\u001b[39m | \u001b[39m-0.905925\u001b[39m | \u001b[39m1 \u001b[39m |\n", + "=============================================================\n" + ] + } + ], + "source": [ + "pbounds = {'x1': (-1, 1), 'x2': (-1, 1), 'k': ('1', '2')}\n", + "\n", + "categorical_optimizer = BayesianOptimization(\n", + " f=SPIRAL,\n", + " acquisition_function=acquisition.ExpectedImprovement(1e-2),\n", + " pbounds=pbounds,\n", + " verbose=2,\n", + " random_state=1,\n", + ")\n", + "discrete_optimizer.set_gp_params(alpha=1e-3)\n", + "\n", + "categorical_optimizer.maximize(\n", + " init_points=2,\n", + " n_iter=18,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "res = categorical_optimizer._space.res()\n", + "k1 = np.array([[p['params']['x1'], p['params']['x2']] for p in res if p['params']['k']=='1'])\n", + "k2 = np.array([[p['params']['x1'], p['params']['x2']] for p in res if p['params']['k']=='2'])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x1 = np.linspace(pbounds['x1'][0], pbounds['x1'][1], 1000)\n", + "x2 = np.linspace(pbounds['x2'][0], pbounds['x2'][1], 1000)\n", + "\n", + "X1, X2 = np.meshgrid(x1, x2)\n", + "Z1 = SPIRAL(X1, X2, '1')\n", + "Z2 = SPIRAL(X1, X2, '2')\n", + "\n", + "fig, axs = plt.subplots(1, 2)\n", + "\n", + "vmin = np.min([np.min(Z1), np.min(Z2)])\n", + "vmax = np.max([np.max(Z1), np.max(Z2)])\n", + "\n", + "axs[0].contourf(X1, X2, Z1, vmin=vmin, vmax=vmax)\n", + "axs[0].set_aspect(\"equal\")\n", + "axs[0].scatter(k1[:,0], k1[:,1], c='k')\n", + "axs[1].contourf(X1, X2, Z2, vmin=vmin, vmax=vmax)\n", + "axs[1].scatter(k2[:,0], k2[:,1], c='k')\n", + "axs[1].set_aspect(\"equal\")\n", + "axs[0].set_title('k=1')\n", + "axs[1].set_title('k=2')\n", + "fig.tight_layout()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Use in ML" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A typical usecase for integer and categorical parameters is optimizing the hyperparameters of a machine learning model. Below you can find an example where the hyperparameters of an SVM are optimized." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| iter | target | kernel | log10_C |\n", + "-------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m-0.2361 \u001b[39m | \u001b[39mpoly2 \u001b[39m | \u001b[39m0.9943696\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-0.2864 \u001b[39m | \u001b[39mrbf \u001b[39m | \u001b[39m-0.999771\u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m-0.2625 \u001b[39m | \u001b[39mpoly3 \u001b[39m | \u001b[39m0.7449728\u001b[39m |\n", + "| \u001b[35m4 \u001b[39m | \u001b[35m-0.2361 \u001b[39m | \u001b[35mpoly2 \u001b[39m | \u001b[35m0.9944598\u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m-0.298 \u001b[39m | \u001b[39mpoly3 \u001b[39m | \u001b[39m-0.999625\u001b[39m |\n", + "| \u001b[35m6 \u001b[39m | \u001b[35m-0.2361 \u001b[39m | \u001b[35mpoly2 \u001b[39m | \u001b[35m0.9945010\u001b[39m |\n", + "| \u001b[35m7 \u001b[39m | \u001b[35m-0.2152 \u001b[39m | \u001b[35mrbf \u001b[39m | \u001b[35m0.9928960\u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m-0.2153 \u001b[39m | \u001b[39mrbf \u001b[39m | \u001b[39m0.9917667\u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m-0.2362 \u001b[39m | \u001b[39mpoly2 \u001b[39m | \u001b[39m0.9897298\u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m-0.2362 \u001b[39m | \u001b[39mpoly2 \u001b[39m | \u001b[39m0.9874217\u001b[39m |\n", + "=================================================\n" + ] + } + ], + "source": [ + "from sklearn.datasets import load_breast_cancer\n", + "from sklearn.svm import SVC\n", + "from sklearn.metrics import log_loss\n", + "from sklearn.model_selection import train_test_split\n", + "from bayes_opt import BayesianOptimization\n", + "\n", + "data = load_breast_cancer()\n", + "X_train, y_train = data['data'], data['target']\n", + "X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2, random_state=1)\n", + "kernels = ['rbf', 'poly']\n", + "\n", + "def f_target(kernel, log10_C):\n", + " if kernel == 'poly2':\n", + " kernel = 'poly'\n", + " degree = 2\n", + " elif kernel == 'poly3':\n", + " kernel = 'poly'\n", + " degree = 3\n", + " elif kernel == 'rbf':\n", + " degree = 3 # not used, equal to default\n", + "\n", + " C = 10**log10_C\n", + "\n", + " model = SVC(C=C, kernel=kernel, degree=degree, probability=True, random_state=1)\n", + " model.fit(X_train, y_train)\n", + "\n", + " # Package looks for maximum, so we return -1 * log_loss\n", + " loss = -1 * log_loss(y_val, model.predict_proba(X_val))\n", + " return loss\n", + "\n", + "\n", + "params_svm ={\n", + " 'kernel': ['rbf', 'poly2', 'poly3'],\n", + " 'log10_C':(-1, +1),\n", + "}\n", + "\n", + "optimizer = BayesianOptimization(\n", + " f_target,\n", + " params_svm,\n", + " random_state=1,\n", + " verbose=2\n", + ")\n", + "\n", + "kernel = Matern(nu=2.5, length_scale=np.ones(optimizer.space.dim))\n", + "discrete_optimizer.set_gp_params(kernel=kernel)\n", + "optimizer.maximize(init_points=2, n_iter=8)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Defining your own Parameter\n", + "\n", + "Maybe you want to optimize over another form of parameters, which does not align with `float`, `int` or categorical. For this purpose, you can create your own, custom parameter. A simple example is a parameter that is discrete, but still admits a distance representation (like an integer) while not being uniformly spaced.\n", + "\n", + "However, you can go further even and encode constraints and even symmetries in your parameter. Let's consider the problem of finding a triangle which maximizes an area given its sides $a, b, c$ with a constraint that the perimeter is fixed, i.e. $a + b + c=s$.\n", + "\n", + "We will create a parameter that encodes such a triangle, and via it's kernel transform ensures that the sides sum to the required length $s$. As you might expect, the solution to this problem is an equilateral triangle, i.e. $a=b=c=s/3$.\n", + "\n", + "To define the parameter, we need to subclass `BayesParameter` and define a few important functions/properties.\n", + "\n", + "- `is_continuous` is a property which denotes whether a parameter is continuous. When optimizing the acquisition function, non-continuous parameters will not be optimized using gradient-based methods, but only via random sampling.\n", + "- `random_sample` is a function that samples randomly from the space of the parameter.\n", + "- `to_float` transforms the canonical representation of a parameter into float values for the target space to store. There is a one-to-one correspondence between valid float representations produced by this function and canonical representations of the parameter. This function is most important when working with parameters that use a non-numeric canonical representation, such as categorical parameters.\n", + "- `to_param` performs the inverse of `to_float`: Given a float-based representation, it creates a canonical representation. This function should perform binning whenever appropriate, e.g. in the case of the `IntParameter`, this function would round any float values supplied to it.\n", + "- `kernel_transform` is the most important function of the Parameter and defines how to represent a value in the kernel space. In contrast to `to_float`, this function expects both the input, as well as the output to be float-representations of the value.\n", + "- `to_string` produces a stringified version of the parameter, which allows users to define custom pretty-print rules for ththe ScreenLogger use.\n", + "- `dim` is a property which defines the dimensionality of the parameter. In most cases, this will be 1, but e.g. for categorical parameters it is equivalent to the cardinality of the category space. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from bayes_opt.logger import ScreenLogger\n", + "from bayes_opt.parameter import BayesParameter\n", + "from bayes_opt.event import Events\n", + "from bayes_opt.util import ensure_rng\n", + "\n", + "\n", + "class FixedPerimeterTriangleParameter(BayesParameter):\n", + " def __init__(self, name: str, bounds, perimeter) -> None:\n", + " super().__init__(name, bounds)\n", + " self.perimeter = perimeter\n", + "\n", + " @property\n", + " def is_continuous(self):\n", + " return True\n", + " \n", + " def random_sample(self, n_samples: int, random_state):\n", + " random_state = ensure_rng(random_state)\n", + " samples = []\n", + " while len(samples) < n_samples:\n", + " samples_ = random_state.dirichlet(np.ones(3), n_samples)\n", + " samples_ = samples_ * self.perimeter # scale samples by perimeter\n", + "\n", + " samples_ = samples_[np.all((self.bounds[:, 0] <= samples_) & (samples_ <= self.bounds[:, 1]), axis=-1)]\n", + " samples.extend(np.atleast_2d(samples_))\n", + " samples = np.array(samples[:n_samples])\n", + " return samples\n", + " \n", + " def to_float(self, value):\n", + " return value\n", + " \n", + " def to_param(self, value):\n", + " return value * self.perimeter / sum(value)\n", + "\n", + " def kernel_transform(self, value):\n", + " return value * self.perimeter / np.sum(value, axis=-1, keepdims=True)\n", + "\n", + " def to_string(self, value, str_len: int) -> str:\n", + " len_each = (str_len - 2) // 3\n", + " str_ = '|'.join([f\"{float(np.round(value[i], 4))}\"[:len_each] for i in range(3)])\n", + " return str_.ljust(str_len)\n", + "\n", + " @property\n", + " def dim(self):\n", + " return 3 # as we have three float values, each representing the length of one side.\n", + "\n", + "def area_of_triangle(sides):\n", + " a, b, c = sides\n", + " s = np.sum(sides, axis=-1) # perimeter\n", + " A = np.sqrt(s * (s-a) * (s-b) * (s-c))\n", + " return A\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| iter | target | sides |\n", + "-------------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m0.4572 \u001b[39m | \u001b[39m0.29|0.70|0.00 \u001b[39m |\n", + "| \u001b[35m2 \u001b[39m | \u001b[35m0.5096 \u001b[39m | \u001b[35m0.58|0.25|0.15 \u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m0.5081 \u001b[39m | \u001b[39m0.58|0.25|0.15 \u001b[39m |\n", + "| \u001b[35m4 \u001b[39m | \u001b[35m0.5386 \u001b[39m | \u001b[35m0.44|0.28|0.26 \u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m0.5279 \u001b[39m | \u001b[39m0.38|0.14|0.47 \u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m0.5328 \u001b[39m | \u001b[39m0.18|0.36|0.45 \u001b[39m |\n", + "| \u001b[39m7 \u001b[39m | \u001b[39m0.4366 \u001b[39m | \u001b[39m0.02|0.22|0.74 \u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m0.4868 \u001b[39m | \u001b[39m0.00|0.61|0.37 \u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m0.4977 \u001b[39m | \u001b[39m0.56|0.01|0.42 \u001b[39m |\n", + "| \u001b[35m10 \u001b[39m | \u001b[35m0.5418 \u001b[39m | \u001b[35m0.29|0.40|0.30 \u001b[39m |\n", + "| \u001b[39m11 \u001b[39m | \u001b[39m0.3361 \u001b[39m | \u001b[39m0.06|0.87|0.06 \u001b[39m |\n", + "| \u001b[39m12 \u001b[39m | \u001b[39m0.06468 \u001b[39m | \u001b[39m0.99|0.00|0.00 \u001b[39m |\n", + "| \u001b[39m13 \u001b[39m | \u001b[39m0.01589 \u001b[39m | \u001b[39m0.0|0.00|0.99 \u001b[39m |\n", + "| \u001b[39m14 \u001b[39m | \u001b[39m0.4999 \u001b[39m | \u001b[39m0.21|0.16|0.61 \u001b[39m |\n", + "| \u001b[39m15 \u001b[39m | \u001b[39m0.499 \u001b[39m | \u001b[39m0.53|0.46|0.00 \u001b[39m |\n", + "| \u001b[39m16 \u001b[39m | \u001b[39m0.4937 \u001b[39m | \u001b[39m0.00|0.41|0.58 \u001b[39m |\n", + "| \u001b[39m17 \u001b[39m | \u001b[39m0.5233 \u001b[39m | \u001b[39m0.33|0.51|0.14 \u001b[39m |\n", + "| \u001b[39m18 \u001b[39m | \u001b[39m0.5204 \u001b[39m | \u001b[39m0.17|0.54|0.28 \u001b[39m |\n", + "| \u001b[39m19 \u001b[39m | \u001b[39m0.5235 \u001b[39m | \u001b[39m0.51|0.15|0.32 \u001b[39m |\n", + "| \u001b[39m20 \u001b[39m | \u001b[39m0.5412 \u001b[39m | \u001b[39m0.31|0.27|0.41 \u001b[39m |\n", + "| \u001b[39m21 \u001b[39m | \u001b[39m0.4946 \u001b[39m | \u001b[39m0.41|0.00|0.57 \u001b[39m |\n", + "| \u001b[39m22 \u001b[39m | \u001b[39m0.5355 \u001b[39m | \u001b[39m0.41|0.39|0.19 \u001b[39m |\n", + "| \u001b[35m23 \u001b[39m | \u001b[35m0.5442 \u001b[39m | \u001b[35m0.35|0.32|0.32 \u001b[39m |\n", + "| \u001b[39m24 \u001b[39m | \u001b[39m0.5192 \u001b[39m | \u001b[39m0.16|0.28|0.54 \u001b[39m |\n", + "| \u001b[39m25 \u001b[39m | \u001b[39m0.5401 \u001b[39m | \u001b[39m0.39|0.23|0.36 \u001b[39m |\n", + "=======================================================\n" + ] + } + ], + "source": [ + "param = FixedPerimeterTriangleParameter(\n", + " name='sides',\n", + " bounds=np.array([[0., 1.], [0., 1.], [0., 1.]]),\n", + " perimeter=1.\n", + ")\n", + "\n", + "pbounds = {'sides': param}\n", + "optimizer = BayesianOptimization(\n", + " area_of_triangle,\n", + " pbounds,\n", + " random_state=1,\n", + ")\n", + "\n", + "# Increase the cell size to accommodate the three float values\n", + "logger = ScreenLogger(verbose=2, is_constrained=False)\n", + "logger._default_cell_size = 15\n", + "\n", + "for e in [Events.OPTIMIZATION_START, Events.OPTIMIZATION_STEP, Events.OPTIMIZATION_END]:\n", + " optimizer.subscribe(e, logger)\n", + "\n", + "optimizer.maximize(init_points=2, n_iter=23)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This seems to work decently well, but we can improve it significantly if we consider the symmetries inherent in the problem: This problem is permutation invariant, i.e. we do not care which side specifically is denoted as $a$, $b$ or $c$. Instead, we can, without loss of generality, decide that the shortest side will always be denoted as $a$, and the longest always as $c$. If we enhance our kernel transform with this symmetry, the performance improves significantly. This can be easily done by sub-classing the previously created triangle parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| iter | target | sides |\n", + "-------------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m0.4572 \u001b[39m | \u001b[39m0.00|0.29|0.70 \u001b[39m |\n", + "| \u001b[35m2 \u001b[39m | \u001b[35m0.5096 \u001b[39m | \u001b[35m0.15|0.25|0.58 \u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m0.498 \u001b[39m | \u001b[39m0.06|0.33|0.60 \u001b[39m |\n", + "| \u001b[35m4 \u001b[39m | \u001b[35m0.5097 \u001b[39m | \u001b[35m0.13|0.27|0.58 \u001b[39m |\n", + "| \u001b[35m5 \u001b[39m | \u001b[35m0.5358 \u001b[39m | \u001b[35m0.19|0.36|0.43 \u001b[39m |\n", + "| \u001b[35m6 \u001b[39m | \u001b[35m0.5443 \u001b[39m | \u001b[35m0.33|0.33|0.33 \u001b[39m |\n", + "| \u001b[39m7 \u001b[39m | \u001b[39m0.5405 \u001b[39m | \u001b[39m0.28|0.28|0.42 \u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m0.5034 \u001b[39m | \u001b[39m0.01|0.49|0.49 \u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m0.4977 \u001b[39m | \u001b[39m0.01|0.42|0.56 \u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m0.5427 \u001b[39m | \u001b[39m0.27|0.36|0.36 \u001b[39m |\n", + "=======================================================\n" + ] + } + ], + "source": [ + "class SortingFixedPerimeterTriangleParameter(FixedPerimeterTriangleParameter):\n", + " def __init__(self, name: str, bounds, perimeter) -> None:\n", + " super().__init__(name, bounds, perimeter)\n", + "\n", + " def to_param(self, value):\n", + " value = np.sort(value, axis=-1)\n", + " return super().to_param(value)\n", + "\n", + " def kernel_transform(self, value):\n", + " value = np.sort(value, axis=-1)\n", + " return super().kernel_transform(value)\n", + "\n", + "param = SortingFixedPerimeterTriangleParameter(\n", + " name='sides',\n", + " bounds=np.array([[0., 1.], [0., 1.], [0., 1.]]),\n", + " perimeter=1.\n", + ")\n", + "\n", + "pbounds = {'sides': param}\n", + "optimizer = BayesianOptimization(\n", + " area_of_triangle,\n", + " pbounds,\n", + " random_state=1,\n", + ")\n", + "\n", + "logger = ScreenLogger(verbose=2, is_constrained=False)\n", + "logger._default_cell_size = 15\n", + "\n", + "for e in [Events.OPTIMIZATION_START, Events.OPTIMIZATION_STEP, Events.OPTIMIZATION_END]:\n", + " optimizer.subscribe(e, logger)\n", + "\n", + "optimizer.maximize(init_points=2, n_iter=8)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bayesian-optimization-tb9vsVm6-py3.9", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/typed_hyperparameter_tuning.py b/examples/typed_hyperparameter_tuning.py new file mode 100644 index 000000000..1267a29e8 --- /dev/null +++ b/examples/typed_hyperparameter_tuning.py @@ -0,0 +1,94 @@ +import numpy as np +from bayes_opt import BayesianOptimization, acquisition +from sklearn.ensemble import GradientBoostingClassifier +from sklearn.datasets import load_digits +from sklearn.model_selection import KFold +from sklearn.metrics import log_loss +import matplotlib.pyplot as plt + +N_FOLDS = 10 +N_START = 2 +N_ITER = 25 - N_START +# Load data +data = load_digits() + + +# Define the hyperparameter space +continuous_pbounds = { + 'log_learning_rate': (-10, 0), + 'max_depth': (1, 6), + 'min_samples_split': (2, 6) +} + +discrete_pbounds = { + 'log_learning_rate': (-10, 0), + 'max_depth': (1, 6, int), + 'min_samples_split': (2, 6, int) +} + +kfold = KFold(n_splits=N_FOLDS, shuffle=True, random_state=42) + +res_continuous = [] +res_discrete = [] + +METRIC_SIGN = -1 + +for i, (train_idx, test_idx) in enumerate(kfold.split(data.data)): + print(f'Fold {i + 1}/{N_FOLDS}') + def gboost(log_learning_rate, max_depth, min_samples_split): + clf = GradientBoostingClassifier( + n_estimators=10, + max_depth=int(max_depth), + learning_rate=np.exp(log_learning_rate), + min_samples_split=int(min_samples_split), + random_state=42 + i + ) + clf.fit(data.data[train_idx], data.target[train_idx]) + #return clf.score(data.data[test_idx], data.target[test_idx]) + return METRIC_SIGN * log_loss(data.target[test_idx], clf.predict_proba(data.data[test_idx]), labels=list(range(10))) + + continuous_optimizer = BayesianOptimization( + f=gboost, + pbounds=continuous_pbounds, + acquisition_function=acquisition.ExpectedImprovement(xi=1e-2, random_state=42), + verbose=0, + random_state=42, + ) + + discrete_optimizer = BayesianOptimization( + f=gboost, + pbounds=discrete_pbounds, + acquisition_function=acquisition.ExpectedImprovement(xi=1e-2, random_state=42), + verbose=0, + random_state=42, + ) + continuous_optimizer.maximize(init_points=2, n_iter=N_ITER) + discrete_optimizer.maximize(init_points=2, n_iter=N_ITER) + res_continuous.append(METRIC_SIGN * continuous_optimizer.space.target) + res_discrete.append(METRIC_SIGN * discrete_optimizer.space.target) + +score_continuous = [] +score_discrete = [] + +for fold in range(N_FOLDS): + best_in_fold = min(np.min(res_continuous[fold]), np.min(res_discrete[fold])) + score_continuous.append(np.minimum.accumulate((res_continuous[fold] - best_in_fold))) + score_discrete.append(np.minimum.accumulate((res_discrete[fold] - best_in_fold))) + +mean_continuous = np.mean(score_continuous, axis=0) +quantiles_continuous = np.quantile(score_continuous, [0.1, 0.9], axis=0) +mean_discrete = np.mean(score_discrete, axis=0) +quantiles_discrete = np.quantile(score_discrete, [0.1, 0.9], axis=0) + + +plt.figure(figsize=(10, 5)) +plt.plot((mean_continuous), label='Continuous best seen') +plt.fill_between(range(N_ITER + N_START), quantiles_continuous[0], quantiles_continuous[1], alpha=0.3) +plt.plot((mean_discrete), label='Discrete best seen') +plt.fill_between(range(N_ITER + N_START), quantiles_discrete[0], quantiles_discrete[1], alpha=0.3) + +plt.xlabel('Number of iterations') +plt.ylabel('Score') +plt.legend(loc='best') +plt.grid() +plt.savefig('discrete_vs_continuous.png') diff --git a/ruff.toml b/ruff.toml index bb9ac4a9f..9c08e69ce 100644 --- a/ruff.toml +++ b/ruff.toml @@ -126,3 +126,6 @@ split-on-trailing-comma = false [lint.pydocstyle] convention = "numpy" + +[lint.flake8-pytest-style] +fixture-parentheses = false diff --git a/scripts/format.sh b/scripts/format.sh index 3f29d03e9..bff192c01 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -2,5 +2,5 @@ set -ex poetry run ruff format bayes_opt tests -poetry run ruff check bayes_opt tests --fix +poetry run ruff check bayes_opt --fix diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py deleted file mode 100644 index 56d1bbf92..000000000 --- a/tests/test_acceptance.py +++ /dev/null @@ -1,69 +0,0 @@ -# import numpy as np - -# from bayes_opt import BayesianOptimization -# from bayes_opt.util import ensure_rng - - -# def test_simple_optimization(): -# """ -# ... -# """ -# def f(x, y): -# return -x ** 2 - (y - 1) ** 2 + 1 - - -# optimizer = BayesianOptimization( -# f=f, -# pbounds={"x": (-3, 3), "y": (-3, 3)}, -# random_state=12356, -# verbose=0, -# ) - -# optimizer.maximize(init_points=0, n_iter=25) - -# max_target = optimizer.max["target"] -# max_x = optimizer.max["params"]["x"] -# max_y = optimizer.max["params"]["y"] - -# assert (1 - max_target) < 1e-3 -# assert np.abs(max_x - 0) < 1e-1 -# assert np.abs(max_y - 1) < 1e-1 - - -# def test_intermediate_optimization(): -# """ -# ... -# """ -# def f(x, y, z): -# x_factor = np.exp(-(x - 2) ** 2) + (1 / (x ** 2 + 1)) -# y_factor = np.exp(-(y - 6) ** 2 / 10) -# z_factor = (1 + 0.2 * np.cos(z)) / (1 + z ** 2) -# return (x_factor + y_factor) * z_factor - -# optimizer = BayesianOptimization( -# f=f, -# pbounds={"x": (-7, 7), "y": (-7, 7), "z": (-7, 7)}, -# random_state=56, -# verbose=0, -# ) - -# optimizer.maximize(init_points=0, n_iter=150) - -# max_target = optimizer.max["target"] -# max_x = optimizer.max["params"]["x"] -# max_y = optimizer.max["params"]["y"] -# max_z = optimizer.max["params"]["z"] - -# assert (2.640 - max_target) < 0 -# assert np.abs(2 - max_x) < 1e-1 -# assert np.abs(6 - max_y) < 1e-1 -# assert np.abs(0 - max_z) < 1e-1 - - -# if __name__ == '__main__': -# r""" -# CommandLine: -# python tests/test_bayesian_optimization.py -# """ -# import pytest -# pytest.main([__file__]) diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index e313f8dd0..1191976df 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -114,7 +114,7 @@ def fun(x): except IndexError: return np.nan - _, min_acq_l = acq._l_bfgs_b_minimize(fun, bounds=target_space.bounds, n_x_seeds=1) + _, min_acq_l = acq._l_bfgs_b_minimize(fun, space=target_space, x_seeds=np.array([[2.5, 0.5]])) assert min_acq_l == np.inf diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index d035f8b4e..48e1af115 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -38,7 +38,7 @@ def test_register(): assert len(optimizer.res) == 1 assert len(optimizer.space) == 1 - optimizer.space.register(params={"p1": 5, "p2": 4}, target=9) + optimizer.space.register(params=np.array([5, 4]), target=9) assert len(optimizer.res) == 2 assert len(optimizer.space) == 2 @@ -196,12 +196,12 @@ def test_set_bounds(): # Ignore unknown keys optimizer.set_bounds({"other": (7, 8)}) assert all(optimizer.space.bounds[:, 0] == np.array([0, 0, 0, 0])) - assert all(optimizer.space.bounds[:, 1] == np.array([1, 2, 3, 4])) + assert all(optimizer.space.bounds[:, 1] == np.array([1, 3, 2, 4])) # Update bounds accordingly optimizer.set_bounds({"p2": (1, 8)}) - assert all(optimizer.space.bounds[:, 0] == np.array([0, 1, 0, 0])) - assert all(optimizer.space.bounds[:, 1] == np.array([1, 8, 3, 4])) + assert all(optimizer.space.bounds[:, 0] == np.array([0, 0, 1, 0])) + assert all(optimizer.space.bounds[:, 1] == np.array([1, 3, 8, 4])) def test_set_gp_params(): @@ -333,11 +333,3 @@ def test_duplicate_points(): optimizer.register(params=next_point_to_probe, target=target) # and again (should throw warning) optimizer.register(params=next_point_to_probe, target=target) - - -if __name__ == "__main__": - r""" - CommandLine: - python tests/test_bayesian_optimization.py - """ - pytest.main([__file__]) diff --git a/tests/test_observer.py b/tests/test_observer.py index 8c8d54eb2..24b3e723f 100644 --- a/tests/test_observer.py +++ b/tests/test_observer.py @@ -114,13 +114,3 @@ def max(self): assert start_time == tracker._start_time if "win" not in sys.platform: assert previous_time < tracker._previous_time - - -if __name__ == "__main__": - r""" - CommandLine: - python tests/test_observer.py - """ - import pytest - - pytest.main([__file__]) diff --git a/tests/test_parameter.py b/tests/test_parameter.py new file mode 100644 index 000000000..b2394a454 --- /dev/null +++ b/tests/test_parameter.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import numpy as np +import pytest +from scipy.optimize import NonlinearConstraint +from sklearn.gaussian_process import GaussianProcessRegressor, kernels + +from bayes_opt import BayesianOptimization +from bayes_opt.parameter import CategoricalParameter, FloatParameter, IntParameter, wrap_kernel +from bayes_opt.target_space import TargetSpace + + +def test_float_parameters(): + def target_func(**kwargs): + # arbitrary target func + return sum(kwargs.values()) + + pbounds = {"p1": (0, 1), "p2": (1, 2)} + space = TargetSpace(target_func, pbounds) + + assert space.dim == len(pbounds) + assert space.empty + assert space.keys == ["p1", "p2"] + + assert isinstance(space._params_config["p1"], FloatParameter) + assert isinstance(space._params_config["p2"], FloatParameter) + + assert all(space.bounds[:, 0] == np.array([0, 1])) + assert all(space.bounds[:, 1] == np.array([1, 2])) + assert (space.bounds == space.bounds).all() + + point1 = {"p1": 0.2, "p2": 1.5} + target1 = 1.7 + space.probe(point1) + + point2 = {"p1": 0.5, "p2": 1.0} + target2 = 1.5 + space.probe(point2) + + assert (space.params[0] == np.fromiter(point1.values(), dtype=float)).all() + assert (space.params[1] == np.fromiter(point2.values(), dtype=float)).all() + + assert (space.target == np.array([target1, target2])).all() + + p1 = space._params_config["p1"] + assert p1.to_float(0.2) == 0.2 + assert p1.to_float(np.array(2.3)) == 2.3 + assert p1.to_float(3) == 3.0 + + +def test_int_parameters(): + def target_func(**kwargs): + assert [isinstance(kwargs[key], int) for key in kwargs] + # arbitrary target func + return sum(kwargs.values()) + + pbounds = {"p1": (0, 5, int), "p3": (-1, 3, int)} + space = TargetSpace(target_func, pbounds) + + assert space.dim == len(pbounds) + assert space.empty + assert space.keys == ["p1", "p3"] + + assert isinstance(space._params_config["p1"], IntParameter) + assert isinstance(space._params_config["p3"], IntParameter) + + point1 = {"p1": 2, "p3": 0} + target1 = 2 + space.probe(point1) + + point2 = {"p1": 1, "p3": -1} + target2 = 0 + space.probe(point2) + + assert (space.params[0] == np.fromiter(point1.values(), dtype=float)).all() + assert (space.params[1] == np.fromiter(point2.values(), dtype=float)).all() + + assert (space.target == np.array([target1, target2])).all() + + p1 = space._params_config["p1"] + assert p1.to_float(0) == 0.0 + assert p1.to_float(np.array(2)) == 2.0 + assert p1.to_float(3) == 3.0 + + assert p1.kernel_transform(0) == 0.0 + assert p1.kernel_transform(2.3) == 2.0 + assert p1.kernel_transform(np.array([1.3, 3.6, 7.2])) == pytest.approx(np.array([1, 4, 7])) + + +def test_cat_parameters(): + fruit_ratings = {"apple": 1.0, "banana": 2.0, "mango": 5.0, "honeydew melon": -10.0, "strawberry": np.pi} + + def target_func(fruit: str): + return fruit_ratings[fruit] + + fruits = ("apple", "banana", "mango", "honeydew melon", "strawberry") + pbounds = {"fruit": ("apple", "banana", "mango", "honeydew melon", "strawberry")} + space = TargetSpace(target_func, pbounds) + + assert space.dim == len(fruits) + assert space.empty + assert space.keys == ["fruit"] + + assert isinstance(space._params_config["fruit"], CategoricalParameter) + + assert space.bounds.shape == (len(fruits), 2) + assert (space.bounds[:, 0] == np.zeros(len(fruits))).all() + assert (space.bounds[:, 1] == np.ones(len(fruits))).all() + + point1 = {"fruit": "banana"} + target1 = 2.0 + space.probe(point1) + + point2 = {"fruit": "honeydew melon"} + target2 = -10.0 + space.probe(point2) + + assert (space.params[0] == np.array([0, 1, 0, 0, 0])).all() + assert (space.params[1] == np.array([0, 0, 0, 1, 0])).all() + + assert (space.target == np.array([target1, target2])).all() + + p1 = space._params_config["fruit"] + for i, fruit in enumerate(fruits): + assert (p1.to_float(fruit) == np.eye(5)[i]).all() + + assert (p1.kernel_transform(np.array([0.8, 0.2, 0.3, 0.5, 0.78])) == np.array([1, 0, 0, 0, 0])).all() + assert (p1.kernel_transform(np.array([0.78, 0.2, 0.3, 0.5, 0.8])) == np.array([0, 0, 0, 0, 1.0])).all() + + +def test_cateogrical_valid_bounds(): + pbounds = {"fruit": ("apple", "banana", "mango", "honeydew melon", "banana", "strawberry")} + with pytest.raises(ValueError): + TargetSpace(None, pbounds) + + pbounds = {"fruit": ("apple",)} + with pytest.raises(ValueError): + TargetSpace(None, pbounds) + + +def test_to_string(): + pbounds = {"p1": (0, 1), "p2": (1, 2)} + space = TargetSpace(None, pbounds) + + assert space._params_config["p1"].to_string(0.2, 5) == "0.2 " + assert space._params_config["p2"].to_string(1.5, 5) == "1.5 " + assert space._params_config["p1"].to_string(0.2, 3) == "0.2" + assert space._params_config["p2"].to_string(np.pi, 5) == "3.141" + assert space._params_config["p1"].to_string(1e-5, 6) == "1e-05 " + assert space._params_config["p2"].to_string(-1e-5, 6) == "-1e-05" + assert space._params_config["p1"].to_string(1e-15, 5) == "1e-15" + assert space._params_config["p1"].to_string(-1.2e-15, 7) == "-1.2..." + + pbounds = {"p1": (0, 5, int), "p3": (-1, 3, int)} + space = TargetSpace(None, pbounds) + + assert space._params_config["p1"].to_string(2, 5) == "2 " + assert space._params_config["p3"].to_string(0, 5) == "0 " + assert space._params_config["p1"].to_string(2, 3) == "2 " + assert space._params_config["p3"].to_string(-1, 5) == "-1 " + assert space._params_config["p1"].to_string(123456789, 6) == "123..." + + pbounds = {"fruit": ("apple", "banana", "mango", "honeydew melon", "strawberry")} + space = TargetSpace(None, pbounds) + + assert space._params_config["fruit"].to_string("apple", 5) == "apple" + assert space._params_config["fruit"].to_string("banana", 5) == "ba..." + assert space._params_config["fruit"].to_string("mango", 5) == "mango" + assert space._params_config["fruit"].to_string("honeydew melon", 10) == "honeyde..." + assert space._params_config["fruit"].to_string("strawberry", 10) == "strawberry" + + +def test_preconstructed_parameter(): + pbounds = {"p1": (0, 1), "p2": (1, 2), "p3": IntParameter("p3", (-1, 3))} + + def target_func(p1, p2, p3): + return p1 + p2 + p3 + + optimizer1 = BayesianOptimization(target_func, pbounds) + + pbounds = {"p1": (0, 1), "p2": (1, 2), "p3": (-1, 3, int)} + optimizer2 = BayesianOptimization(target_func, pbounds) + + assert optimizer1.space.keys == optimizer2.space.keys + assert (optimizer1.space.bounds == optimizer2.space.bounds).all() + assert optimizer1.space._params_config["p3"].to_float(2) == 2.0 + + +def test_integration_mixed_optimization(): + fruit_ratings = {"apple": 1.0, "banana": 2.0, "mango": 5.0, "honeydew melon": -10.0, "strawberry": np.pi} + + pbounds = { + "p1": (0, 1), + "p2": (1, 2), + "p3": (-1, 3, int), + "fruit": ("apple", "banana", "mango", "honeydew melon", "strawberry"), + } + + def target_func(p1, p2, p3, fruit): + return p1 + p2 + p3 + fruit_ratings[fruit] + + optimizer = BayesianOptimization(target_func, pbounds) + optimizer.maximize(init_points=2, n_iter=10) + + +def test_integration_mixed_optimization_with_constraints(): + fruit_ratings = {"apple": 1.0, "banana": 2.0, "mango": 5.0, "honeydew melon": -10.0, "strawberry": np.pi} + + pbounds = { + "p1": (0, 1), + "p2": (1, 2), + "p3": (-1, 3, int), + "fruit": ("apple", "banana", "mango", "honeydew melon", "strawberry"), + } + + def target_func(p1, p2, p3, fruit): + return p1 + p2 + p3 + fruit_ratings[fruit] + + def constraint_func(p1, p2, p3, fruit): + return (p1 + p2 + p3 - fruit_ratings[fruit]) ** 2 + + constraint = NonlinearConstraint(constraint_func, 0, 4.0) + + optimizer = BayesianOptimization(target_func, pbounds, constraint=constraint) + init_points = [ + {"p1": 0.5, "p2": 1.5, "p3": 1, "fruit": "banana"}, + {"p1": 0.5, "p2": 1.5, "p3": 2, "fruit": "mango"}, + ] + for p in init_points: + optimizer.register(p, target=target_func(**p), constraint_value=constraint_func(**p)) + optimizer.maximize(init_points=0, n_iter=2) + + +def test_wrapped_kernel_fit(): + pbounds = {"p1": (0, 1), "p2": (1, 10, int)} + space = TargetSpace(None, pbounds) + + space.register(space.random_sample(0), 1.0) + space.register(space.random_sample(1), 5.0) + + kernel = wrap_kernel(kernels.Matern(nu=2.5, length_scale=1e5), space.kernel_transform) + gp = GaussianProcessRegressor(kernel=kernel, alpha=1e-6, n_restarts_optimizer=5) + + gp.fit(space.params, space.target) + + assert gp.kernel_.length_scale != 1e5 diff --git a/tests/test_seq_domain_red.py b/tests/test_seq_domain_red.py index 265d3ee5b..c22dd0d1b 100644 --- a/tests/test_seq_domain_red.py +++ b/tests/test_seq_domain_red.py @@ -179,9 +179,9 @@ def test_minimum_window_dict_ordering(): ) -if __name__ == "__main__": - r""" - CommandLine: - python tests/test_seq_domain_red.py - """ - pytest.main([__file__]) +def test_mixed_parameters(): + """Ensure that the transformer errors when providing non-float parameters""" + pbounds = {"x": (-10, 10), "y": (-10, 10), "z": (1, 10, int)} + target_space = TargetSpace(target_func=black_box_function, pbounds=pbounds) + with pytest.raises(ValueError): + _ = SequentialDomainReductionTransformer().initialize(target_space) diff --git a/tests/test_target_space.py b/tests/test_target_space.py index bd957e068..c269569da 100644 --- a/tests/test_target_space.py +++ b/tests/test_target_space.py @@ -22,9 +22,9 @@ def test_keys_and_bounds_in_same_order(): assert space.dim == len(pbounds) assert space.empty - assert space.keys == ["p1", "p2", "p3", "p4"] + assert space.keys == ["p1", "p3", "p2", "p4"] assert all(space.bounds[:, 0] == np.array([0, 0, 0, 0])) - assert all(space.bounds[:, 1] == np.array([1, 2, 3, 4])) + assert all(space.bounds[:, 1] == np.array([1, 3, 2, 4])) def test_params_to_array(): @@ -50,25 +50,23 @@ def test_array_to_params(): space.array_to_params(np.array([2, 3, 5])) -def test_as_array(): +def test_to_float(): space = TargetSpace(target_func, PBOUNDS) - x = space._as_array([0, 1]) + x = space._to_float({"p2": 0, "p1": 1}) assert x.shape == (2,) - assert all(x == np.array([0, 1])) - - x = space._as_array({"p2": 1, "p1": 2}) - assert x.shape == (2,) - assert all(x == np.array([2, 1])) + assert all(x == np.array([1, 0])) with pytest.raises(ValueError): - x = space._as_array([2, 1, 7]) + x = space._to_float([0, 1]) + with pytest.raises(ValueError): + x = space._to_float([2, 1, 7]) with pytest.raises(ValueError): - x = space._as_array({"p2": 1, "p1": 2, "other": 7}) + x = space._to_float({"p2": 1, "p1": 2, "other": 7}) with pytest.raises(ValueError): - x = space._as_array({"p2": 1}) + x = space._to_float({"p2": 1}) with pytest.raises(ValueError): - x = space._as_array({"other": 7}) + x = space._to_float({"other": 7}) def test_register(): @@ -82,12 +80,18 @@ def test_register(): assert all(space.params[0] == np.array([1, 2])) assert all(space.target == np.array([3])) - # registering with array - space.register(params={"p1": 5, "p2": 4}, target=9) + # registering with dict out of order + space.register(params={"p2": 4, "p1": 5}, target=9) assert len(space) == 2 assert all(space.params[1] == np.array([5, 4])) assert all(space.target == np.array([3, 9])) + # registering with array + space.register(params=np.array([0, 1]), target=1) + assert len(space) == 3 + assert all(space.params[2] == np.array([0, 1])) + assert all(space.target == np.array([3, 9, 1])) + with pytest.raises(NotUniqueError): space.register(params={"p1": 1, "p2": 2}, target=3) with pytest.raises(NotUniqueError): @@ -95,8 +99,7 @@ def test_register(): def test_register_with_constraint(): - PBOUNDS = {"p1": (0, 10), "p2": (1, 100)} - constraint = ConstraintModel(lambda x: x, -2, 2) + constraint = ConstraintModel(lambda x: x, -2, 2, transform=lambda x: x) space = TargetSpace(target_func, PBOUNDS, constraint=constraint) assert len(space) == 0 @@ -108,14 +111,14 @@ def test_register_with_constraint(): assert all(space.constraint_values == np.array([0])) # registering with array - space.register(params={"p1": 5, "p2": 4}, target=9, constraint_value=2) + space.register(params={"p1": 0.5, "p2": 4}, target=4.5, constraint_value=2) assert len(space) == 2 - assert all(space.params[1] == np.array([5, 4])) - assert all(space.target == np.array([3, 9])) + assert all(space.params[1] == np.array([0.5, 4])) + assert all(space.target == np.array([3, 4.5])) assert all(space.constraint_values == np.array([0, 2])) with pytest.raises(ValueError): - space.register(params={"p1": 2, "p2": 2}, target=3) + space.register(params={"p1": 0.2, "p2": 2}, target=2.2) def test_register_point_beyond_bounds(): @@ -134,7 +137,7 @@ def test_probe(): # probing with dict space.probe(params={"p1": 1, "p2": 2}) assert len(space) == 1 - assert all(space.params[0] == np.array([1, 2])) + assert all(space.params[-1] == np.array([1, 2])) assert all(space.target == np.array([3])) # probing with array @@ -146,7 +149,7 @@ def test_probe(): # probing same point with dict space.probe(params={"p1": 1, "p2": 2}) assert len(space) == 3 - assert all(space.params[1] == np.array([5, 4])) + assert all(space.params[2] == np.array([1, 2])) assert all(space.target == np.array([3, 9, 3])) # probing same point with array @@ -277,12 +280,12 @@ def test_set_bounds(): # Ignore unknown keys space.set_bounds({"other": (7, 8)}) assert all(space.bounds[:, 0] == np.array([0, 0, 0, 0])) - assert all(space.bounds[:, 1] == np.array([1, 2, 3, 4])) + assert all(space.bounds[:, 1] == np.array([1, 3, 2, 4])) # Update bounds accordingly space.set_bounds({"p2": (1, 8)}) - assert all(space.bounds[:, 0] == np.array([0, 1, 0, 0])) - assert all(space.bounds[:, 1] == np.array([1, 8, 3, 4])) + assert all(space.bounds[:, 0] == np.array([0, 0, 1, 0])) + assert all(space.bounds[:, 1] == np.array([1, 3, 8, 4])) def test_no_target_func(): @@ -291,9 +294,18 @@ def test_no_target_func(): target_space.probe({"p1": 1, "p2": 2}) -if __name__ == "__main__": - r""" - CommandLine: - python tests/test_target_space.py - """ - pytest.main([__file__]) +def test_change_typed_bounds(): + pbounds = { + "p1": (0, 1), + "p2": (1, 2), + "p3": (-1, 3, int), + "fruit": ("apple", "banana", "mango", "honeydew melon", "strawberry"), + } + + space = TargetSpace(None, pbounds) + + with pytest.raises(ValueError): + space.set_bounds({"fruit": ("apple", "banana", "mango", "honeydew melon")}) + + with pytest.raises(ValueError): + space.set_bounds({"p3": (-1, 2, float)}) diff --git a/tests/test_util.py b/tests/test_util.py index 37bc52020..9a88262dc 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -98,11 +98,3 @@ def c(x, y): print(optimizer.space) assert len(optimizer.space) == 12 - - -if __name__ == "__main__": - r""" - CommandLine: - python tests/test_target_space.py - """ - pytest.main([__file__])