From a58ecaf65e494a6a94788a8905469d20289cd558 Mon Sep 17 00:00:00 2001 From: till-m <36440677+till-m@users.noreply.github.com> Date: Wed, 24 Jan 2024 22:28:19 +0100 Subject: [PATCH] Domain reduction, Sphinx docs (#455) * Fixes issue-436: Constrained optimization does not allow duplicate points (#437) * Update docs of `bayesian_optimization.py` and `observer.py`. * Fix minor style issue in module docstring * Update docs of `__init__.py` and `events.py`. * Fix minor style issue in class docstring * Add workflow to check docstrings * Update bayes_opt/bayesian_optimization.py Co-authored-by: Leandro Braga <18340809+leandrobbraga@users.noreply.github.com> * Improve acq_max seeding of L-BFGS-B optimization (#297) * bounds_transformer could bypass global_bounds due to the test logic within _trim function in domain_reduction.py (#441) * Update trim bounds in domain_reduction.py Previously, when the new upper limit was less than the original lower limit, the new_bounds could bypass the global_bounds. * Update test_seq_domain_red.py Added test cases to catch an error when both bounds of new_bounds exceeded the global_bounds * Update domain_reduction.py _trim function now avoids an error when both bounds for a given parameter in new_bounds exceed the global_bounds * Update domain_reduction.py comments * fixed English in domain_reduction.py * use numpy to sort bounds, boundary exceeded warn. * simple sort test added * domain_red windows target_space to global_bounds Added windowing function to improve the convergence of optimizers that use domain_reduction. Improved comments and documentation. * target_space.max respects bounds; SDRT warnings * Remove unused function. This function was used to prototype a solution. It should not have been pushed and can be removed. * Updated target_space.py docstrings * Update tests/test_target_space.py Co-authored-by: till-m <36440677+till-m@users.noreply.github.com> * Added pbound warnings, updated various tests. * updated line spacing for consistency and style * added pbound test condition --------- Co-authored-by: till-m <36440677+till-m@users.noreply.github.com> * DomainReduction docs, docstyle * Add missing doc dependency --------- Co-authored-by: YoungJae Bae <57710489+YoungJaeBae@users.noreply.github.com> Co-authored-by: Leandro Braga <18340809+leandrobbraga@users.noreply.github.com> Co-authored-by: ptapping <63924582+ptapping@users.noreply.github.com> Co-authored-by: Edgar <50716923+perezed00@users.noreply.github.com> --- .github/workflows/build_docs.yml | 2 + .gitignore | 4 +- README.md | 137 +++++++++-------- bayes_opt/constraint.py | 2 +- bayes_opt/domain_reduction.py | 141 +++++++++++++----- bayes_opt/target_space.py | 129 ++++++++++------ bayes_opt/util.py | 3 +- docsrc/Makefile | 2 + docsrc/conf.py | 5 +- docsrc/func.ico | Bin 0 -> 136207 bytes docsrc/index.rst | 1 + docsrc/quickstart.rst | 2 + examples/domain_reduction.ipynb | 2 +- .../bayesian_optimization.gif | Bin {examples => static}/bo_example.png | Bin {examples => static}/func.png | Bin {examples => static}/sdr.png | Bin tests/test_logs_bounds.log | 5 + tests/test_seq_domain_red.py | 51 ++++++- tests/test_target_space.py | 38 ++++- tests/test_util.py | 22 ++- 21 files changed, 386 insertions(+), 160 deletions(-) create mode 100644 docsrc/func.ico create mode 100644 docsrc/quickstart.rst rename {examples => static}/bayesian_optimization.gif (100%) rename {examples => static}/bo_example.png (100%) rename {examples => static}/func.png (100%) rename {examples => static}/sdr.png (100%) create mode 100644 tests/test_logs_bounds.log diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 07282413b..a57745b01 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -29,6 +29,8 @@ jobs: pip install nbsphinx pip install sphinx_rtd_theme pip install jupyter + pip install numpydoc + pip install myst-parser - name: Install package run: | pip install -e . diff --git a/.gitignore b/.gitignore index 3f4f6d54e..6556c710e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ venv.bak/ docs/* docsrc/.ipynb_checkpoints/* -docsrc/*.ipynb \ No newline at end of file +docsrc/*.ipynb +docsrc/static/* +docsrc/README.md diff --git a/README.md b/README.md index ac94cc649..652dc9a18 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@
-

+

# Bayesian Optimization -![tests](https://github.com/fmfn/BayesianOptimization/actions/workflows/run_tests.yml/badge.svg) -[![Codecov](https://codecov.io/github/fmfn/BayesianOptimization/badge.svg?branch=master&service=github)](https://codecov.io/github/fmfn/BayesianOptimization?branch=master) +![tests](https://github.com/bayesian-optimization/BayesianOptimization/actions/workflows/run_tests.yml/badge.svg) +[![Codecov](https://codecov.io/github/bayesian-optimization/BayesianOptimization/badge.svg?branch=master&service=github)](https://codecov.io/github/bayesian-optimization/BayesianOptimization?branch=master) [![Pypi](https://img.shields.io/pypi/v/bayesian-optimization.svg)](https://pypi.python.org/pypi/bayesian-optimization) Pure Python implementation of bayesian global optimization with gaussian processes. +## Installation + * PyPI (pip): ```console @@ -30,25 +32,18 @@ suited for optimization of high cost functions, situations where the balance between exploration and exploitation is important. ## Quick Start -See below for a quick tour over the basics of the Bayesian Optimization package. More detailed information, other advanced features, and tips on usage/implementation can be found in the [examples](https://github.com/fmfn/BayesianOptimization/tree/master/examples) folder. I suggest that you: -- Follow the -[basic tour notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/basic-tour.ipynb) -to learn how to use the package's most important features. -- Take a look at the -[advanced tour notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/advanced-tour.ipynb) -to learn how to make the package more flexible, how to deal with categorical parameters, how to use observers, and more. -- Check out this -[notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/visualization.ipynb) -with a step by step visualization of how this method works. -- To understand how to use bayesian optimization when additional constraints are present, see the -[constrained optimization notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/constraints.ipynb). -- Explore this [notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/exploitation_vs_exploration.ipynb) +See below for a quick tour over the basics of the Bayesian Optimization package. More detailed information, other advanced features, and tips on usage/implementation can be found in the [examples](http://bayesian-optimization.github.io/BayesianOptimization/examples.html) folder. I suggest that you: +- Follow the [basic tour notebook](http://bayesian-optimization.github.io/BayesianOptimization/basic-tour.html) to learn how to use the package's most important features. +- Take a look at the [advanced tour notebook](http://bayesian-optimization.github.io/BayesianOptimization/advanced-tour.html) to learn how to make the package more flexible, how to deal with categorical parameters, how to use observers, and more. +- Check out this [notebook](http://bayesian-optimization.github.io/BayesianOptimization/visualization.html) with a step by step visualization of how this method works. +- To understand how to use bayesian optimization when additional constraints are present, see the [constrained optimization notebook](http://bayesian-optimization.github.io/BayesianOptimization/constraints.html). +- Explore this [notebook](http://bayesian-optimization.github.io/BayesianOptimization/exploitation_vs_exploration.html) exemplifying the balance between exploration and exploitation and how to control it. -- Go over this [script](https://github.com/fmfn/BayesianOptimization/blob/master/examples/sklearn_example.py) +- Go over this [script](https://github.com/bayesian-optimization/BayesianOptimization/blob/master/examples/sklearn_example.py) for examples of how to tune parameters of Machine Learning models using cross validation and bayesian optimization. -- Explore the [domain reduction notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/domain_reduction.ipynb) to learn more about how search can be sped up by dynamically changing parameters' bounds. -- Finally, take a look at this [script](https://github.com/fmfn/BayesianOptimization/blob/master/examples/async_optimization.py) +- Explore the [domain reduction notebook](http://bayesian-optimization.github.io/BayesianOptimization/domain_reduction.html) to learn more about how search can be sped up by dynamically changing parameters' bounds. +- Finally, take a look at this [script](https://github.com/bayesian-optimization/BayesianOptimization/blob/master/examples/async_optimization.py) for ideas on how to implement bayesian optimization in a distributed fashion using this package. @@ -56,11 +51,11 @@ for ideas on how to implement bayesian optimization in a distributed fashion usi Bayesian optimization works by constructing a posterior distribution of functions (gaussian process) that best describes the function you want to optimize. As the number of observations grows, the posterior distribution improves, and the algorithm becomes more certain of which regions in parameter space are worth exploring and which are not, as seen in the picture below. -![BayesianOptimization in action](./examples/bo_example.png) +![BayesianOptimization in action](./static/bo_example.png) As you iterate over and over, the algorithm balances its needs of exploration and exploitation taking into account what it knows about the target function. At each step a Gaussian Process is fitted to the known samples (points previously explored), and the posterior distribution, combined with a exploration strategy (such as UCB (Upper Confidence Bound), or EI (Expected Improvement)), are used to determine the next point that should be explored (see the gif below). -![BayesianOptimization in action](./examples/bayesian_optimization.gif) +![BayesianOptimization in action](./static/bayesian_optimization.gif) This process is designed to minimize the number of steps required to find a combination of parameters that are close to the optimal combination. To do so, this method uses a proxy optimization problem (finding the maximum of the acquisition function) that, albeit still a hard problem, is cheaper (in the computational sense) and common tools can be employed. Therefore Bayesian Optimization is most adequate for situations where sampling the function to be optimized is a very expensive endeavor. See the references for a proper discussion of this method. @@ -68,10 +63,9 @@ This project is under active development, if you find a bug, or anything that needs correction, please let me know. -Basic tour of the Bayesian Optimization package -=============================================== +## Basic tour of the Bayesian Optimization package -## 1. Specifying the function to be optimized +### 1. Specifying the function to be optimized This is a function optimization package, therefore the first and most important ingredient is, of course, the function to be optimized. @@ -89,7 +83,7 @@ def black_box_function(x, y): return -x ** 2 - (y - 1) ** 2 + 1 ``` -## 2. Getting Started +### 2. Getting Started All we need to get started is to instantiate a `BayesianOptimization` object specifying a function to be optimized `f`, and its parameters with their corresponding bounds, `pbounds`. This is a constrained optimization technique, so you must specify the minimum and maximum values that can be probed for each parameter in order for it to work @@ -160,7 +154,7 @@ for i, res in enumerate(optimizer.res): ``` -### 2.1 Changing bounds +#### 2.1 Changing bounds During the optimization process you may realize the bounds chosen for some parameters are not adequate. For these situations you can invoke the method `set_bounds` to alter them. You can pass any combination of **existing** parameters and their associated new bounds. @@ -183,17 +177,17 @@ optimizer.maximize( | 10 | -1.762 | 1.442 | 0.1735 | ================================================= -### 2.2 Sequential Domain Reduction +#### 2.2 Sequential Domain Reduction Sometimes the initial boundaries specified for a problem are too wide, and adding points to improve the response surface in regions of the solution domain is extraneous. Other times the cost function is very expensive to compute, and minimizing the number of calls is extremely beneficial. When it's worthwhile to converge on an optimal point quickly rather than try to find the optimal point, contracting the domain around the current optimal value as the search progresses can speed up the search progress considerably. Using the `SequentialDomainReductionTransformer` the bounds of the problem can be panned and zoomed dynamically in an attempt to improve convergence. -![sequential domain reduction](./examples/sdr.png) +![sequential domain reduction](./static/sdr.png) -An example of using the `SequentialDomainReductionTransformer` is shown in the [domain reduction notebook](https://github.com/fmfn/BayesianOptimization/blob/master/examples/domain_reduction.ipynb). More information about this method can be found in the paper ["On the robustness of a simple domain reduction scheme for simulation‐based optimization"](http://www.truegrid.com/srsm_revised.pdf). +An example of using the `SequentialDomainReductionTransformer` is shown in the [domain reduction notebook](http://bayesian-optimization.github.io/BayesianOptimization/domain_reduction.html). More information about this method can be found in the paper ["On the robustness of a simple domain reduction scheme for simulation‐based optimization"](http://www.truegrid.com/srsm_revised.pdf). -## 3. Guiding the optimization +### 3. Guiding the optimization It is often the case that we have an idea of regions of the parameter space where the maximum of our function might lie. For these situations the `BayesianOptimization` object allows the user to specify points to be probed. By default these will be explored lazily (`lazy=True`), meaning these points will be evaluated only the next time you call `maximize`. This probing process happens before the gaussian process takes over. @@ -221,11 +215,11 @@ optimizer.maximize(init_points=0, n_iter=0) ================================================= -## 4. Saving, loading and restarting +### 4. Saving, loading and restarting By default you can follow the progress of your optimization by setting `verbose>0` when instantiating the `BayesianOptimization` object. If you need more control over logging/alerting you will need to use an observer. For more information about observers checkout the advanced tour notebook. Here we will only see how to use the native `JSONLogger` object to save to and load progress from files. -### 4.1 Saving progress +#### 4.1 Saving progress ```python @@ -255,7 +249,7 @@ optimizer.maximize( By default the previous data in the json file is removed. If you want to keep working with the same logger, the `reset` parameter in `JSONLogger` should be set to False. -### 4.2 Loading progress +#### 4.2 Loading progress Naturally, if you stored progress you will be able to load that onto a new instance of `BayesianOptimization`. The easiest way to do it is by invoking the `load_logs` function, from the `util` submodule. @@ -277,54 +271,71 @@ load_logs(new_optimizer, logs=["./logs.log"]); ## Next Steps -This introduction covered the most basic functionality of the package. Checkout the [basic-tour](https://github.com/fmfn/BayesianOptimization/blob/master/examples/basic-tour.ipynb) and [advanced-tour](https://github.com/fmfn/BayesianOptimization/blob/master/examples/advanced-tour.ipynb) notebooks in the example folder, where you will find detailed explanations and other more advanced functionality. Also, browse the examples folder for implementation tips and ideas. - -Installation -============ - -### Installation - -The latest release can be obtained by two ways: - -* With PyPI (pip): - - pip install bayesian-optimization +This introduction covered the most basic functionality of the package. Checkout the [basic-tour](http://bayesian-optimization.github.io/BayesianOptimization/basic-tour.html) and [advanced-tour](http://bayesian-optimization.github.io/BayesianOptimization/advanced-tour.html), where you will find detailed explanations and other more advanced functionality. Also, browse the [examples](http://bayesian-optimization.github.io/BayesianOptimization/examples.html) for implementation tips and ideas. -* With conda (from conda-forge channel): - - conda install -c conda-forge bayesian-optimization - -The bleeding edge version can be installed with: - - pip install git+https://github.com/fmfn/BayesianOptimization.git +## Installing from GitHub +To install directly from master, simply run +``` +pip install https://github.com/bayesian-optimization/BayesianOptimization/archive/master.zip +``` +Please note that these builds are not necessarily stable. -If you prefer, you can clone it and run the setup.py file. Use the following -commands to get a copy from Github and install all dependencies: +If you prefer, you can also clone the repository and run `setup.py`: +``` +git clone https://github.com/bayesian-optimization/BayesianOptimization.git +cd BayesianOptimization +python setup.py install +``` + - git clone https://github.com/fmfn/BayesianOptimization.git - cd BayesianOptimization - python setup.py install +## Minutiae -Citation -============ +### Citation -If you used this package in your research and is interested in citing it here's how you do it: +If you used this package in your research, please cite it: ``` @Misc{, author = {Fernando Nogueira}, title = {{Bayesian Optimization}: Open source constrained global optimization tool for {Python}}, year = {2014--}, - url = " https://github.com/fmfn/BayesianOptimization" + url = " https://github.com/bayesian-optimization/BayesianOptimization" +} +``` +If you used any of the advanced functionalities, please additionally cite the corresponding publication: + +For the `SequentialDomainTransformer`: +``` +@article{ + author = {Stander, Nielen and Craig, Kenneth}, + year = {2002}, + month = {06}, + pages = {}, + title = {On the robustness of a simple domain reduction scheme for simulation-based optimization}, + volume = {19}, + journal = {International Journal for Computer-Aided Engineering and Software (Eng. Comput.)}, + doi = {10.1108/02644400210430190} +} +``` + +For constrained optimization: +``` +@inproceedings{gardner2014bayesian, + title={Bayesian optimization with inequality constraints.}, + author={Gardner, Jacob R and Kusner, Matt J and Xu, Zhixiang Eddie and Weinberger, Kilian Q and Cunningham, John P}, + booktitle={ICML}, + volume={2014}, + pages={937--945}, + year={2014} } ``` -# Dependencies +### Dependencies * Numpy * Scipy * Scikit-learn -# References: +### References: * http://papers.nips.cc/paper/4522-practical-bayesian-optimization-of-machine-learning-algorithms.pdf * http://arxiv.org/pdf/1012.2599v1.pdf * http://www.gaussianprocess.org/gpml/ diff --git a/bayes_opt/constraint.py b/bayes_opt/constraint.py index 5ea333525..10142e43b 100644 --- a/bayes_opt/constraint.py +++ b/bayes_opt/constraint.py @@ -81,7 +81,7 @@ def eval(self, **kwargs): Raises ------ TypeError - If the kwargs keys don't match the function argument names. + If the kwargs' keys don't match the function argument names. """ try: return self.fun(**kwargs) diff --git a/bayes_opt/domain_reduction.py b/bayes_opt/domain_reduction.py index f4b86ffc8..f1d31a418 100644 --- a/bayes_opt/domain_reduction.py +++ b/bayes_opt/domain_reduction.py @@ -1,26 +1,37 @@ +"""Implement domain transformation. + +In particular, this provides a base transformer class and a sequential domain +reduction transformer as based on Stander and Craig's "On the robustness of a +simple domain reduction scheme for simulation-based optimization" +""" from typing import Optional, Union, List import numpy as np from .target_space import TargetSpace +from warnings import warn class DomainTransformer(): - '''The base transformer class''' + """Base class.""" - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: + """To override with specific implementation.""" pass - def initialize(self, target_space: TargetSpace): + def initialize(self, target_space: TargetSpace) -> None: + """To override with specific implementation.""" raise NotImplementedError - def transform(self, target_space: TargetSpace): + def transform(self, target_space: TargetSpace) -> dict: + """To override with specific implementation.""" raise NotImplementedError class SequentialDomainReductionTransformer(DomainTransformer): - """ + """Reduce the searchable space. + A sequential domain reduction transformer based on the work by Stander, N. and Craig, K: - "On the robustness of a simple domain reduction scheme for simulation‐based optimization" + "On the robustness of a simple domain reduction scheme for simulation-based optimization" """ def __init__( @@ -36,7 +47,14 @@ def __init__( self.minimum_window_value = minimum_window def initialize(self, target_space: TargetSpace) -> None: - """Initialize all of the parameters""" + """Initialize all of the parameters. + + Parameters + ---------- + target_space : TargetSpace + TargetSpace this DomainTransformer operates on. + """ + # Set the original bounds self.original_bounds = np.copy(target_space.bounds) self.bounds = [self.original_bounds] @@ -47,6 +65,7 @@ def initialize(self, target_space: TargetSpace) -> None: else: self.minimum_window = [self.minimum_window_value] * len(target_space.bounds) + # Set initial values self.previous_optimal = np.mean(target_space.bounds, axis=1) self.current_optimal = np.mean(target_space.bounds, axis=1) self.r = target_space.bounds[:, 1] - target_space.bounds[:, 0] @@ -72,14 +91,12 @@ def initialize(self, target_space: TargetSpace) -> None: self._window_bounds_compatibility(self.original_bounds) def _update(self, target_space: TargetSpace) -> None: - + """Update contraction rate, window size, and window center.""" # setting the previous self.previous_optimal = self.current_optimal self.previous_d = self.current_d - - self.current_optimal = target_space.params[ - np.argmax(target_space.target) - ] + + self.current_optimal = target_space.params_to_array(target_space.max()['params']) self.current_d = 2.0 * (self.current_optimal - self.previous_optimal) / self.r @@ -97,32 +114,82 @@ def _update(self, target_space: TargetSpace) -> None: self.r = self.contraction_rate * self.r def _trim(self, new_bounds: np.array, global_bounds: np.array) -> np.array: - for i, variable in enumerate(new_bounds): - if variable[0] < global_bounds[i, 0]: - variable[0] = global_bounds[i, 0] - if variable[1] > global_bounds[i, 1]: - variable[1] = global_bounds[i, 1] - for i, entry in enumerate(new_bounds): - if entry[0] > entry[1]: - new_bounds[i, 0] = entry[1] - new_bounds[i, 1] = entry[0] - window_width = abs(entry[0] - entry[1]) - if window_width < self.minimum_window[i]: - dw = (self.minimum_window[i] - window_width) / 2.0 - left_expansion_space = abs(global_bounds[i, 0] - entry[0]) # should be non-positive - right_expansion_space = abs(global_bounds[i, 1] - entry[1]) # should be non-negative - # conservative - dw_l = min(dw, left_expansion_space) - dw_r = min(dw, right_expansion_space) - # this crawls towards the edge - ddw_r = dw_r + max(dw - dw_l, 0) - ddw_l = dw_l + max(dw - dw_r, 0) - new_bounds[i, 0] -= ddw_l - new_bounds[i, 1] += ddw_r + """ + Adjust the new_bounds and verify that they adhere to global_bounds and minimum_window. + + Parameters + ---------- + new_bounds : np.array + The proposed new_bounds that (may) need adjustment. + + global_bounds : np.array + The maximum allowable bounds for each parameter. + + Returns + ------- + new_bounds : np.array + The adjusted bounds after enforcing constraints. + """ + #sort bounds + new_bounds = np.sort(new_bounds) + + # Validate each parameter's bounds against the global_bounds + for i, pbounds in enumerate(new_bounds): + # If the one of the bounds is outside the global bounds, reset the bound to the global bound + # This is expected to happen when the window is near the global bounds, no warning is issued + if (pbounds[0] < global_bounds[i, 0]): + pbounds[0] = global_bounds[i, 0] + + if (pbounds[1] > global_bounds[i, 1]): + pbounds[1] = global_bounds[i, 1] + + # If a lower bound is greater than the associated global upper bound, reset it to the global lower bound + if (pbounds[0] > global_bounds[i, 1]): + pbounds[0] = global_bounds[i, 0] + warn("\nDomain Reduction Warning:\n"+ + "A parameter's lower bound is greater than the global upper bound."+ + "The offensive boundary has been reset."+ + "Be cautious of subsequent reductions.", stacklevel=2) + + # If an upper bound is less than the associated global lower bound, reset it to the global upper bound + if (pbounds[1] < global_bounds[i, 0]): + pbounds[1] = global_bounds[i, 1] + warn("\nDomain Reduction Warning:\n"+ + "A parameter's lower bound is greater than the global upper bound."+ + "The offensive boundary has been reset."+ + "Be cautious of subsequent reductions.", stacklevel=2) + + # Adjust new_bounds to ensure they respect the minimum window width for each parameter + for i, pbounds in enumerate(new_bounds): + current_window_width = abs(pbounds[0] - pbounds[1]) + + # If the window width is less than the minimum allowable width, adjust it + # Note that when minimum_window < width of the global bounds one side always has more space than required + if current_window_width < self.minimum_window[i]: + width_deficit = (self.minimum_window[i] - current_window_width) / 2.0 + available_left_space = abs(global_bounds[i, 0] - pbounds[0]) + available_right_space = abs(global_bounds[i, 1] - pbounds[1]) + + # determine how much to expand on the left and right + expand_left = min(width_deficit, available_left_space) + expand_right = min(width_deficit, available_right_space) + + # calculate the deficit on each side + expand_left_deficit = width_deficit - expand_left + expand_right_deficit = width_deficit - expand_right + + # shift the deficit to the side with more space + adjust_left = expand_left + max(expand_right_deficit, 0) + adjust_right = expand_right + max(expand_left_deficit, 0) + + # adjust the bounds + pbounds[0] -= adjust_left + pbounds[1] += adjust_right + return new_bounds def _window_bounds_compatibility(self, global_bounds: np.array) -> bool: - """Checks if global bounds are compatible with the minimum window sizes.""" + """Check if global bounds are compatible with the minimum window sizes.""" for i, entry in enumerate(global_bounds): global_window_width = abs(entry[1] - entry[0]) if global_window_width < self.minimum_window[i]: @@ -133,7 +200,7 @@ def _create_bounds(self, parameters: dict, bounds: np.array) -> dict: return {param: bounds[i, :] for i, param in enumerate(parameters)} def transform(self, target_space: TargetSpace) -> dict: - + """Transform the bounds of the target space.""" self._update(target_space) new_bounds = np.array( @@ -143,6 +210,6 @@ def transform(self, target_space: TargetSpace) -> dict: ] ).T - self._trim(new_bounds, self.original_bounds) + new_bounds = self._trim(new_bounds, self.original_bounds) self.bounds.append(new_bounds) return self._create_bounds(target_space.keys, new_bounds) diff --git a/bayes_opt/target_space.py b/bayes_opt/target_space.py index 195474cb2..bf727c5c2 100644 --- a/bayes_opt/target_space.py +++ b/bayes_opt/target_space.py @@ -1,6 +1,7 @@ """Manages the optimization domain and holds points.""" import numpy as np from colorama import Fore +from warnings import warn from .util import ensure_rng, NotUniqueError @@ -38,9 +39,11 @@ class TargetSpace(): >>> return p1 + p2 >>> pbounds = {'p1': (0, 1), 'p2': (1, 100)} >>> space = TargetSpace(target_func, pbounds, random_state=0) - >>> x = space.random_points(1)[0] - >>> y = space.register_point(x) - >>> assert self.max_point()['max_val'] == y + >>> x = np.array([4 , 5]) + >>> y = target_func(x) + >>> space.register(x, y) + >>> assert self.max()['target'] == 9 + >>> assert self.max()['params'] == {'p1': 1.0, 'p2': 2.0} """ def __init__(self, target_func, pbounds, constraint=None, random_state=None, @@ -178,6 +181,30 @@ def constraint_values(self): return self._constraint_values + @property + def mask(self): + """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 params_to_array(self, params): """Convert a dict representation of parameters into an array version. @@ -260,13 +287,14 @@ def register(self, params, target, constraint_value=None): Examples -------- + >>> target_func = lambda p1, p2: p1 + p2 >>> pbounds = {'p1': (0, 1), 'p2': (1, 100)} - >>> space = TargetSpace(lambda p1, p2: p1 + p2, pbounds) + >>> space = TargetSpace(target_func, pbounds) >>> len(space) 0 >>> x = np.array([0, 0]) >>> y = 1 - >>> space.add_observation(x, y) + >>> space.register(x, y) >>> len(space) 1 """ @@ -281,6 +309,11 @@ def register(self, params, target, constraint_value=None): raise NotUniqueError(f'Data point {x} is not unique. You can set' ' "allow_duplicate_points=True" to avoid this error') + # if x is not within the bounds of the parameter space, warn the user + if self._bounds is not None: + if 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) + self._params = np.concatenate([self._params, x.reshape(1, -1)]) self._target = np.concatenate([self._target, [target]]) @@ -313,6 +346,15 @@ def probe(self, params): ------- y : float target function value. + + Example + ------- + >>> target_func = lambda p1, p2: p1 + p2 + >>> pbounds = {'p1': (0, 1), 'p2': (1, 100)} + >>> space = TargetSpace(target_func, pbounds) + >>> space.probe([1, 5]) + >>> assert self.max()['target'] == 6 + >>> assert self.max()['params'] == {'p1': 1.0, 'p2': 5.0} """ x = self._as_array(params) params = dict(zip(self._keys, x)) @@ -327,19 +369,20 @@ def probe(self, params): return target, constraint_value def random_sample(self): - """Create random points within the bounds of the space. + """ + Sample a random point from within the bounds of the space. Returns ------- - data: np.ndarray - [num x dim] array points with dimensions corresponding to `self._keys` + data: ndarray + [1 x dim] array with dimensions corresponding to `self._keys` Examples -------- >>> target_func = lambda p1, p2: p1 + p2 >>> pbounds = {'p1': (0, 1), 'p2': (1, 100)} >>> space = TargetSpace(target_func, pbounds, random_state=0) - >>> space.random_points(1) + >>> space.random_sample() array([[ 55.33253689, 0.54488318]]) """ data = np.empty((1, self.dim)) @@ -348,61 +391,48 @@ def random_sample(self): return data.ravel() def _target_max(self): - """Get maximum target value found. + """Get the maximum target value within the current parameter bounds. If there is a constraint present, the maximum value that fulfills the - constraint is returned. + constraint within the parameter bounds is returned. - Returns ------- - dict | None - The maximum allowed point's target function value. - Returns None if there is no (allowed) maximum. + max: float + The maximum target value. + """ if len(self.target) == 0: return None - if self._constraint is None: - return self.target.max() - - allowed = self._constraint.allowed(self._constraint_values) - if allowed.any(): - return self.target[allowed].max() + if len(self.target[self.mask]) == 0: + return None - return None + return self.target[self.mask].max() def max(self): """Get maximum target value found and corresponding parameters. - + If there is a constraint present, the maximum value that fulfills the - constraint is returned. + constraint within the parameter bounds is returned. Returns ------- - dict | None - A dictionary containing the maximum allowed point's target function - value, parameters, and, if applicable, constraint function value. - Returns None if there is no (allowed) maximum. + res: dict + A dictionary with the keys 'target' and 'params'. The value of + 'target' is the maximum target value, and the value of 'params' is + a dictionary with the parameter names as keys and the parameter + values as values. + """ target_max = self._target_max() - if target_max is None: return None - if self._constraint is not None: - allowed = self._constraint.allowed(self._constraint_values) - - target = self.target[allowed] - params = self.params[allowed] - constraint_values = self.constraint_values[allowed] - else: - target = self.target - params = self.params - - target_max_idx = np.where(target == target_max)[0][0] - - + target = self.target[self.mask] + params = self.params[self.mask] + target_max_idx = np.argmax(target) + res = { 'target': target_max, 'params': dict( @@ -411,6 +441,7 @@ def max(self): } if self._constraint is not None: + constraint_values = self.constraint_values[self.mask] res['constraint'] = constraint_values[target_max_idx] return res @@ -420,9 +451,17 @@ def res(self): Returns ------- - dict - A dictionary containing the target function values, parameters, - and, if applicable, constraint function values and allowedness. + res: list + A list of dictionaries with the keys 'target', 'params', and + 'constraint'. The value of 'target' is the target value, the value + of 'params' is a dictionary with the parameter names as keys and the + parameter values as values, and the value of 'constraint' is the + constraint fulfillment. + + Notes + ----- + 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] diff --git a/bayes_opt/util.py b/bayes_opt/util.py index 417e307f7..a90f344d7 100644 --- a/bayes_opt/util.py +++ b/bayes_opt/util.py @@ -8,7 +8,7 @@ def acq_max(ac, gp, y_max, bounds, random_state, constraint=None, n_warmup=10000, n_iter=10, y_max_params=None): """Find the maximum of the acquisition function. - + It uses a combination of random sampling (cheap) and the 'L-BFGS-B' optimization method. First by sampling `n_warmup` (1e5) points at random, and then running L-BFGS-B from `n_iter` (10) random starting points. @@ -106,7 +106,6 @@ def adjusted_ac(x): x_seeds = random_state.uniform(bounds[:, 0], bounds[:, 1], size=(1+n_iter+int(not y_max_params is None), bounds.shape[0])) - x_seeds[0] = x_max if not y_max_params is None: # Add the provided best sample to the seeds so that the optimization diff --git a/docsrc/Makefile b/docsrc/Makefile index e0e46fb58..c3886de30 100644 --- a/docsrc/Makefile +++ b/docsrc/Makefile @@ -15,6 +15,8 @@ BUILDDIR = ../docs .PHONY: help Makefile github: + @cp ../README.md . + @cp -r ../static . @make html @cp -a ../docs/html/. ../docs diff --git a/docsrc/conf.py b/docsrc/conf.py index b758e36f4..159db2748 100644 --- a/docsrc/conf.py +++ b/docsrc/conf.py @@ -42,7 +42,9 @@ 'sphinx.ext.githubpages', 'nbsphinx', 'IPython.sphinxext.ipython_console_highlighting', - 'sphinx.ext.mathjax'] + 'sphinx.ext.mathjax', + 'myst_parser', + 'numpydoc'] source_suffix = { '.rst': 'restructuredtext', @@ -63,6 +65,7 @@ # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' +html_favicon = 'func.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docsrc/func.ico b/docsrc/func.ico new file mode 100644 index 0000000000000000000000000000000000000000..f294702610b6d5db3f5c55d0e2b71ea6ec497bc5 GIT binary patch literal 136207 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 aZ global_bounds[i, 1]): + test = False + if (pbounds[1] > global_bounds[i, 1] or pbounds[1] < global_bounds[i, 0]): + test = False + return test + + # test if the sorting of the bounds is correct + new_bounds = np.array( [[5, -5], [-10, 10]] ) + trimmed_bounds = bounds_transformer._trim(new_bounds, global_bounds) + assert (trimmed_bounds == np.array( [[-5, 5], [-10, 10]] )).all() + + # test if both (upper/lower) bounds for a parameter exceed the global bounds + new_bounds = np.array( [[-50, -20], [20, 50]] ) + with pytest.warns(UserWarning): + trimmed_bounds = bounds_transformer._trim(new_bounds, global_bounds) + assert verify_bounds_in_range(trimmed_bounds, global_bounds) + + # test if both (upper/lower) bounds for a parameter exceed the global bounds + # while they are out of order + new_bounds = np.array( [[-20, -50], [-10, 10]] ) + with pytest.warns(UserWarning): + trimmed_bounds = bounds_transformer._trim(new_bounds, global_bounds) + assert verify_bounds_in_range(trimmed_bounds, global_bounds) + +if __name__ == '__main__': + r""" + CommandLine: + python tests/test_seq_domain_red.py + """ + pytest.main([__file__]) \ No newline at end of file diff --git a/tests/test_target_space.py b/tests/test_target_space.py index c554dea0e..494b42495 100644 --- a/tests/test_target_space.py +++ b/tests/test_target_space.py @@ -74,6 +74,7 @@ def test_as_array(): def test_register(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} space = TargetSpace(target_func, PBOUNDS) assert len(space) == 0 @@ -96,6 +97,7 @@ def test_register(): def test_register_with_constraint(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} constraint = ConstraintModel(lambda x: x, -2, 2) space = TargetSpace(target_func, PBOUNDS, constraint=constraint) @@ -118,7 +120,16 @@ def test_register_with_constraint(): space.register(params={"p1": 2, "p2": 2}, target=3) +def test_register_point_beyond_bounds(): + PBOUNDS = {'p1': (0, 1), 'p2': (1, 10)} + space = TargetSpace(target_func, PBOUNDS) + + with pytest.warns(UserWarning): + space.register(params={"p1": 0.5, "p2": 20}, target=2.5) + + def test_probe(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} space = TargetSpace(target_func, PBOUNDS, allow_duplicate_points=True) assert len(space) == 0 @@ -166,12 +177,14 @@ def test_random_sample(): def test_y_max(): space = TargetSpace(target_func, PBOUNDS) assert space._target_max() == None - space.probe(params={"p1": 1, "p2": 2}) - space.probe(params={"p1": 5, "p2": 1}) + space.probe(params={"p1": 1, "p2": 7}) + space.probe(params={"p1": 0.5, "p2": 1}) space.probe(params={"p1": 0, "p2": 1}) - assert space._target_max() == 6 + assert space._target_max() == 8 + def test_y_max_with_constraint(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} constraint = ConstraintModel(lambda p1, p2: p1-p2, -2, 2) space = TargetSpace(target_func, PBOUNDS, constraint) assert space._target_max() == None @@ -181,10 +194,21 @@ def test_y_max_with_constraint(): assert space._target_max() == 3 +def test_y_max_within_pbounds(): + PBOUNDS = {'p1': (0, 2), 'p2': (1, 100)} + space = TargetSpace(target_func, PBOUNDS) + assert space._target_max() == None + space.probe(params={"p1": 1, "p2": 2}) + space.probe(params={"p1": 0, "p2": 1}) + with pytest.warns(UserWarning): + space.probe(params={"p1": 5, "p2": 1}) + assert space._target_max() == 3 + def test_max(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} space = TargetSpace(target_func, PBOUNDS) - + assert space.max() == None space.probe(params={"p1": 1, "p2": 2}) space.probe(params={"p1": 5, "p2": 4}) @@ -192,7 +216,9 @@ def test_max(): space.probe(params={"p1": 1, "p2": 6}) assert space.max() == {"params": {"p1": 5, "p2": 4}, "target": 9} + def test_max_with_constraint(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} constraint = ConstraintModel(lambda p1, p2: p1-p2, -2, 2) space = TargetSpace(target_func, PBOUNDS, constraint=constraint) @@ -203,7 +229,9 @@ def test_max_with_constraint(): space.probe(params={"p1": 1, "p2": 6}) # Unfeasible assert space.max() == {"params": {"p1": 2, "p2": 3}, "target": 5, "constraint": -1} + def test_max_with_constraint_identical_target_value(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} constraint = ConstraintModel(lambda p1, p2: p1-p2, -2, 2) space = TargetSpace(target_func, PBOUNDS, constraint=constraint) @@ -215,7 +243,9 @@ def test_max_with_constraint_identical_target_value(): space.probe(params={"p1": 1, "p2": 6}) # Unfeasible assert space.max() == {"params": {"p1": 2, "p2": 3}, "target": 5, "constraint": -1} + def test_res(): + PBOUNDS = {'p1': (0, 10), 'p2': (1, 100)} space = TargetSpace(target_func, PBOUNDS) assert space.res() == [] diff --git a/tests/test_util.py b/tests/test_util.py index b3a73668a..89b79c01e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -132,7 +132,7 @@ def f(x, y): optimizer = BayesianOptimization( f=f, - pbounds={"x": (-2, 2), "y": (-2, 2)} + pbounds={"x": (-200, 200), "y": (-200, 200)} ) assert len(optimizer.space) == 0 @@ -150,6 +150,21 @@ def f(x, y): load_logs(other_optimizer, [str(test_dir / "test_logs.log")]) +def test_logs_bounds(): + def f(x, y): + return x + y + + optimizer = BayesianOptimization( + f=f, + pbounds={"x": (-2, 2), "y": (-2, 2)} + ) + + with pytest.warns(UserWarning): + load_logs(optimizer, [str(test_dir / "test_logs_bounds.log")]) + + assert len(optimizer.space) == 5 + + def test_logs_constraint(): def f(x, y): @@ -162,7 +177,7 @@ def c(x, y): optimizer = BayesianOptimization( f=f, - pbounds={"x": (-2, 2), "y": (-2, 2)}, + pbounds={"x": (-200, 200), "y": (-200, 200)}, constraint=constraint ) @@ -179,5 +194,4 @@ def c(x, y): CommandLine: python tests/test_target_space.py """ - import pytest - pytest.main([__file__]) + pytest.main([__file__]) \ No newline at end of file