Skip to content

Commit 3e55ac6

Browse files
authored
Merge pull request #4 from fmfn/master
update from main
2 parents 1e6279a + 33b99ec commit 3e55ac6

16 files changed

+503
-179
lines changed

.github/workflows/run_tests.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
22+
python-version: ["3.7", "3.8", "3.9", "3.10"]
2323

2424
steps:
2525
- uses: actions/checkout@v3
@@ -32,9 +32,11 @@ jobs:
3232
python -m pip install --upgrade pip
3333
pip install pytest
3434
pip install pytest-cov
35-
pip install codecov
35+
pip install coverage
3636
pip install -e .
37+
3738
- name: Test with pytest
3839
run: |
39-
pytest --cov-config .coveragerc --cov-report html --cov=bayes_opt
40-
codecov
40+
pytest --cov-report xml --cov=bayes_opt/
41+
- name: Upload coverage to Codecov
42+
uses: codecov/codecov-action@v3

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ env/
2727
venv/
2828
ENV/
2929
env.bak/
30-
venv.bak/
30+
venv.bak/
31+
*temp*

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ If you used this package in your research and is interested in citing it here's
323323
* Numpy
324324
* Scipy
325325
* Scikit-learn
326-
326+
327327
# References:
328328
* http://papers.nips.cc/paper/4522-practical-bayesian-optimization-of-machine-learning-algorithms.pdf
329329
* http://arxiv.org/pdf/1012.2599v1.pdf

bayes_opt/bayesian_optimization.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ class BayesianOptimization(Observable):
9191
bounds_transformer: DomainTransformer, optional(default=None)
9292
If provided, the transformation is applied to the bounds.
9393
94+
allow_duplicate_points: bool, optional (default=False)
95+
If True, the optimizer will allow duplicate points to be registered.
96+
This behavior may be desired in high noise situations where repeatedly probing
97+
the same point will give different answers. In other situations, the acquisition
98+
may occasionaly generate a duplicate point.
99+
94100
Methods
95101
-------
96102
probe()
@@ -111,9 +117,10 @@ def __init__(self,
111117
constraint=None,
112118
random_state=None,
113119
verbose=2,
114-
bounds_transformer=None):
120+
bounds_transformer=None,
121+
allow_duplicate_points=False):
115122
self._random_state = ensure_rng(random_state)
116-
123+
self._allow_duplicate_points = allow_duplicate_points
117124
self._queue = Queue()
118125

119126
# Internal GP regressor
@@ -129,7 +136,8 @@ def __init__(self,
129136
# Data structure containing the function to be optimized, the
130137
# bounds of its domain, and a record of the evaluations we have
131138
# done so far
132-
self._space = TargetSpace(f, pbounds, random_state=random_state)
139+
self._space = TargetSpace(f, pbounds, random_state=random_state,
140+
allow_duplicate_points=self._allow_duplicate_points)
133141
self.is_constrained = False
134142
else:
135143
constraint_ = ConstraintModel(
@@ -242,12 +250,14 @@ def _prime_subscriptions(self):
242250
def maximize(self,
243251
init_points=5,
244252
n_iter=25,
245-
acq='ucb',
246-
kappa=2.576,
247-
kappa_decay=1,
248-
kappa_decay_delay=0,
249-
xi=0.0,
253+
acquisition_function=None,
254+
acq=None,
255+
kappa=None,
256+
kappa_decay=None,
257+
kappa_decay_delay=None,
258+
xi=None,
250259
**gp_params):
260+
251261
"""
252262
Probes the target space to find the parameters that yield the maximum
253263
value for the given function.
@@ -262,38 +272,33 @@ def maximize(self,
262272
Number of iterations where the method attempts to find the maximum
263273
value.
264274
265-
acq: {'ucb', 'ei', 'poi'}
266-
The acquisition method used.
267-
* 'ucb' stands for the Upper Confidence Bounds method
268-
* 'ei' is the Expected Improvement method
269-
* 'poi' is the Probability Of Improvement criterion.
270-
271-
kappa: float, optional(default=2.576)
272-
Parameter to indicate how closed are the next parameters sampled.
273-
Higher value = favors spaces that are least explored.
274-
Lower value = favors spaces where the regression function is
275-
the highest.
275+
acquisition_function: object, optional
276+
An instance of bayes_opt.util.UtilityFunction.
277+
If nothing is passed, a default using ucb is used
276278
277-
kappa_decay: float, optional(default=1)
278-
`kappa` is multiplied by this factor every iteration.
279-
280-
kappa_decay_delay: int, optional(default=0)
281-
Number of iterations that must have passed before applying the
282-
decay to `kappa`.
283-
284-
xi: float, optional(default=0.0)
285-
[unused]
279+
All other parameters are unused, and are only available to ensure backwards compatability - these
280+
will be removed in a future release
286281
"""
287282
self._prime_subscriptions()
288283
self.dispatch(Events.OPTIMIZATION_START)
289284
self._prime_queue(init_points)
290-
self.set_gp_params(**gp_params)
291285

292-
util = UtilityFunction(kind=acq,
293-
kappa=kappa,
294-
xi=xi,
295-
kappa_decay=kappa_decay,
296-
kappa_decay_delay=kappa_decay_delay)
286+
old_params_used = any([param is not None for param in [acq, kappa, kappa_decay, kappa_decay_delay, xi]])
287+
if old_params_used or gp_params:
288+
raise Exception('\nPassing acquisition function parameters or gaussian process parameters to maximize'
289+
'\nis no longer supported. Instead,please use the "set_gp_params" method to set'
290+
'\n the gp params, and pass an instance of bayes_opt.util.UtilityFunction'
291+
'\n using the acquisition_function argument\n')
292+
293+
if acquisition_function is None:
294+
util = UtilityFunction(kind='ucb',
295+
kappa=2.576,
296+
xi=0.0,
297+
kappa_decay=1,
298+
kappa_decay_delay=0)
299+
else:
300+
util = acquisition_function
301+
297302
iteration = 0
298303
while not self._queue.empty or iteration < n_iter:
299304
try:

bayes_opt/domain_reduction.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def initialize(self, target_space: TargetSpace) -> None:
6868

6969
self.r = self.contraction_rate * self.r
7070

71+
# check if the minimum window fits in the orignal bounds
72+
self._window_bounds_compatiblity(self.original_bounds)
73+
7174
def _update(self, target_space: TargetSpace) -> None:
7275

7376
# setting the previous
@@ -105,11 +108,27 @@ def _trim(self, new_bounds: np.array, global_bounds: np.array) -> np.array:
105108
new_bounds[i, 1] = entry[0]
106109
window_width = abs(entry[0] - entry[1])
107110
if window_width < self.minimum_window[i]:
108-
new_bounds[i, 0] -= (self.minimum_window[i] - window_width) / 2.0
109-
new_bounds[i, 1] += (self.minimum_window[i] - window_width) / 2.0
110-
111+
dw = (self.minimum_window[i] - window_width) / 2.0
112+
left_expansion_space = abs(global_bounds[i, 0] - entry[0]) # should be non-positive
113+
right_expansion_space = abs(global_bounds[i, 1] - entry[1]) # should be non-negative
114+
# conservative
115+
dw_l = min(dw, left_expansion_space)
116+
dw_r = min(dw, right_expansion_space)
117+
# this crawls towards the edge
118+
ddw_r = dw_r + max(dw - dw_l, 0)
119+
ddw_l = dw_l + max(dw - dw_r, 0)
120+
new_bounds[i, 0] -= ddw_l
121+
new_bounds[i, 1] += ddw_r
111122
return new_bounds
112123

124+
def _window_bounds_compatiblity(self, global_bounds: np.array) -> bool:
125+
"""Checks if global bounds are compatible with the minimum window sizes."""
126+
for i, entry in enumerate(global_bounds):
127+
global_window_width = abs(entry[1] - entry[0])
128+
if global_window_width < self.minimum_window[i]:
129+
raise ValueError(
130+
"Global bounds are not compatible with the minimum window size.")
131+
113132
def _create_bounds(self, parameters: dict, bounds: np.array) -> dict:
114133
return {param: bounds[i, :] for i, param in enumerate(parameters)}
115134

bayes_opt/target_space.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import warnings
2+
13
import numpy as np
24
from .util import ensure_rng, NotUniqueError
5+
from .util import Colours
36

47

58
def _hashable(x):
@@ -22,7 +25,9 @@ class TargetSpace(object):
2225
>>> y = space.register_point(x)
2326
>>> assert self.max_point()['max_val'] == y
2427
"""
25-
def __init__(self, target_func, pbounds, constraint=None, random_state=None):
28+
29+
def __init__(self, target_func, pbounds, constraint=None, random_state=None,
30+
allow_duplicate_points=False):
2631
"""
2732
Parameters
2833
----------
@@ -35,8 +40,16 @@ def __init__(self, target_func, pbounds, constraint=None, random_state=None):
3540
3641
random_state : int, RandomState, or None
3742
optionally specify a seed for a random number generator
43+
44+
allow_duplicate_points: bool, optional (default=False)
45+
If True, the optimizer will allow duplicate points to be registered.
46+
This behavior may be desired in high noise situations where repeatedly probing
47+
the same point will give different answers. In other situations, the acquisition
48+
may occasionaly generate a duplicate point.
3849
"""
3950
self.random_state = ensure_rng(random_state)
51+
self._allow_duplicate_points = allow_duplicate_points
52+
self.n_duplicate_points = 0
4053

4154
# The function to be optimized
4255
self.target_func = target_func
@@ -56,7 +69,6 @@ def __init__(self, target_func, pbounds, constraint=None, random_state=None):
5669
# keep track of unique points we have seen so far
5770
self._cache = {}
5871

59-
6072
self._constraint = constraint
6173

6274
if constraint is not None:
@@ -96,7 +108,7 @@ def keys(self):
96108
@property
97109
def bounds(self):
98110
return self._bounds
99-
111+
100112
@property
101113
def constraint(self):
102114
return self._constraint
@@ -176,8 +188,13 @@ def register(self, params, target, constraint_value=None):
176188
"""
177189
x = self._as_array(params)
178190
if x in self:
179-
raise NotUniqueError('Data point {} is not unique'.format(x))
180-
191+
if self._allow_duplicate_points:
192+
self.n_duplicate_points = self.n_duplicate_points + 1
193+
print(f'{Colours.RED}Data point {x} is not unique. {self.n_duplicate_points} duplicates registered.'
194+
f' Continuing ...{Colours.END}')
195+
else:
196+
raise NotUniqueError(f'Data point {x} is not unique. You can set "allow_duplicate_points=True" to '
197+
f'avoid this error')
181198

182199
self._params = np.concatenate([self._params, x.reshape(1, -1)])
183200
self._target = np.concatenate([self._target, [target]])
@@ -188,12 +205,12 @@ def register(self, params, target, constraint_value=None):
188205
else:
189206
if constraint_value is None:
190207
msg = ("When registering a point to a constrained TargetSpace" +
191-
" a constraint value needs to be present.")
208+
" a constraint value needs to be present.")
192209
raise ValueError(msg)
193210
# Insert data into unique dictionary
194211
self._cache[_hashable(x.ravel())] = (target, constraint_value)
195212
self._constraint_values = np.concatenate([self._constraint_values,
196-
[constraint_value]])
213+
[constraint_value]])
197214

198215
def probe(self, params):
199216
"""
@@ -215,21 +232,16 @@ def probe(self, params):
215232
target function value.
216233
"""
217234
x = self._as_array(params)
235+
params = dict(zip(self._keys, x))
236+
target = self.target_func(**params)
218237

219-
try:
220-
return self._cache[_hashable(x)]
221-
except KeyError:
222-
params = dict(zip(self._keys, x))
223-
target = self.target_func(**params)
224-
225-
if self._constraint is None:
226-
self.register(x, target)
227-
return target
228-
else:
229-
constraint_value = self._constraint.eval(**params)
230-
self.register(x, target, constraint_value)
231-
return target, constraint_value
232-
238+
if self._constraint is None:
239+
self.register(x, target)
240+
return target
241+
else:
242+
constraint_value = self._constraint.eval(**params)
243+
self.register(x, target, constraint_value)
244+
return target, constraint_value
233245

234246
def random_sample(self):
235247
"""
@@ -317,7 +329,7 @@ def res(self):
317329
self._constraint_values,
318330
params,
319331
self._constraint.allowed(self._constraint_values)
320-
)
332+
)
321333
]
322334

323335
def set_bounds(self, new_bounds):

bayes_opt/util.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import numpy as np
33
from scipy.stats import norm
44
from scipy.optimize import minimize
5+
from colorama import just_fix_windows_console
56

67

78
def acq_max(ac, gp, y_max, bounds, random_state, constraint=None, n_warmup=10000, n_iter=10):
@@ -96,9 +97,29 @@ def to_minimize(x):
9697
class UtilityFunction(object):
9798
"""
9899
An object to compute the acquisition functions.
100+
101+
kind: {'ucb', 'ei', 'poi'}
102+
* 'ucb' stands for the Upper Confidence Bounds method
103+
* 'ei' is the Expected Improvement method
104+
* 'poi' is the Probability Of Improvement criterion.
105+
106+
kappa: float, optional(default=2.576)
107+
Parameter to indicate how closed are the next parameters sampled.
108+
Higher value = favors spaces that are least explored.
109+
Lower value = favors spaces where the regression function is
110+
the highest.
111+
112+
kappa_decay: float, optional(default=1)
113+
`kappa` is multiplied by this factor every iteration.
114+
115+
kappa_decay_delay: int, optional(default=0)
116+
Number of iterations that must have passed before applying the
117+
decay to `kappa`.
118+
119+
xi: float, optional(default=0.0)
99120
"""
100121

101-
def __init__(self, kind, kappa, xi, kappa_decay=1, kappa_decay_delay=0):
122+
def __init__(self, kind='ucb', kappa=2.576, xi=0, kappa_decay=1, kappa_decay_delay=0):
102123

103124
self.kappa = kappa
104125
self._kappa_decay = kappa_decay
@@ -275,3 +296,6 @@ def underline(cls, s):
275296
def yellow(cls, s):
276297
"""Wrap text in yellow."""
277298
return cls._wrap_colour(s, cls.YELLOW)
299+
300+
301+
just_fix_windows_console()

0 commit comments

Comments
 (0)