Skip to content

Commit 6ea60e0

Browse files
authored
Merge pull request #75 from Erotemic/eval_points
explore_eager and new data structure for efficient appends
2 parents 42eaf5c + eb4a6d4 commit 6ea60e0

9 files changed

+701
-117
lines changed

.travis.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
language: python
2+
sudo: false
3+
4+
cache:
5+
apt: true
6+
directories:
7+
- $HOME/.cache/pip
8+
- $HOME/download
9+
python:
10+
- "2.7"
11+
- "3.4"
12+
- "3.5"
13+
- "3.6"
14+
before_install:
15+
- pip install pip -U
16+
- pip install pytest -U
17+
- pip install pytest-cov -U
18+
- pip install codecov -U
19+
#- pip install xdoctest -U
20+
# - pip install delorean
21+
install:
22+
#- travis_retry python setup.py build develop
23+
- travis_retry pip install -e .
24+
script:
25+
- travis_wait pytest --cov-config .coveragerc --cov-report html --cov bayes_opt bayes_opt
26+
#-p no:doctest --xdoctest --cov=ubelt ubelt
27+
after_success:
28+
#- coveralls || echo "Coveralls upload failed"
29+
- codecov
30+
#after_failure:
31+
# - cat failed_doctests.txt
32+
cache:
33+
apt: true
34+
directories:
35+
- $HOME/.pip-cache

bayes_opt/bayesian_optimization.py

Lines changed: 99 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from __future__ import division
33

44
import numpy as np
5+
import warnings
56
from sklearn.gaussian_process import GaussianProcessRegressor
67
from sklearn.gaussian_process.kernels import Matern
7-
from .helpers import UtilityFunction, unique_rows, PrintLog, acq_max
8+
from .helpers import (UtilityFunction, PrintLog, acq_max, ensure_rng)
9+
from .target_space import TargetSpace
810

911

1012
class BayesianOptimization(object):
@@ -25,27 +27,11 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
2527
# Store the original dictionary
2628
self.pbounds = pbounds
2729

28-
if random_state is None:
29-
self.random_state = np.random.RandomState()
30-
elif isinstance(random_state, int):
31-
self.random_state = np.random.RandomState(random_state)
32-
else:
33-
self.random_state = random_state
34-
35-
# Get the name of the parameters
36-
self.keys = list(pbounds.keys())
37-
38-
# Find number of parameters
39-
self.dim = len(pbounds)
40-
41-
# Create an array with parameters bounds
42-
self.bounds = []
43-
for key in self.pbounds.keys():
44-
self.bounds.append(self.pbounds[key])
45-
self.bounds = np.asarray(self.bounds)
30+
self.random_state = ensure_rng(random_state)
4631

47-
# Some function to be optimized
48-
self.f = f
32+
# Data structure containing the function to be optimized, the bounds of
33+
# its domain, and a record of the evaluations we have done so far
34+
self.space = TargetSpace(f, pbounds, random_state)
4935

5036
# Initialization flag
5137
self.initialized = False
@@ -55,10 +41,6 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
5541
self.x_init = []
5642
self.y_init = []
5743

58-
# Numpy array place holders
59-
self.X = None
60-
self.Y = None
61-
6244
# Counter of iterations
6345
self.i = 0
6446

@@ -73,7 +55,7 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
7355
self.util = None
7456

7557
# PrintLog object
76-
self.plog = PrintLog(self.keys)
58+
self.plog = PrintLog(self.space.keys)
7759

7860
# Output dictionary
7961
self.res = {}
@@ -93,64 +75,50 @@ def init(self, init_points):
9375
:param init_points:
9476
Number of random points to probe.
9577
"""
96-
97-
# Generate random points
98-
l = [self.random_state.uniform(x[0], x[1], size=init_points)
99-
for x in self.bounds]
100-
10178
# Concatenate new random points to possible existing
10279
# points from self.explore method.
103-
self.init_points += list(map(list, zip(*l)))
104-
105-
# Create empty arrays to store the new points and values of the function.
106-
self.X = np.empty((0, self.bounds.shape[0]))
107-
self.Y = np.empty(0)
80+
rand_points = self.space.random_points(init_points)
81+
self.init_points.extend(rand_points)
10882

109-
# Evaluate target function at all initialization
110-
# points (random + explore)
83+
# Evaluate target function at all initialization points
11184
for x in self.init_points:
112-
self.X = np.vstack((self.X, np.asarray(x).reshape((1, -1))))
113-
self.Y = np.append(self.Y, self.f(**dict(zip(self.keys, x))))
85+
y = self._observe_point(x)
11486

115-
if self.verbose:
116-
self.plog.print_step(x, self.Y[-1])
117-
118-
# Append any other points passed by the self.initialize method (these
119-
# also have a corresponding target value passed by the user).
120-
self.init_points += self.x_init
121-
self.X = np.vstack((self.X, np.asarray(self.x_init).reshape(-1, self.X.shape[1])))
122-
123-
# Append the target value of self.initialize method.
124-
self.Y = np.concatenate((self.Y, self.y_init))
87+
# Add the points from `self.initialize` to the observations
88+
if self.x_init:
89+
x_init = np.vstack(self.x_init)
90+
y_init = np.hstack(self.y_init)
91+
for x, y in zip(x_init, y_init):
92+
self.space.add_observation(x, y)
93+
if self.verbose:
94+
self.plog.print_step(x, y)
12595

12696
# Updates the flag
12797
self.initialized = True
12898

129-
def explore(self, points_dict):
130-
"""Method to explore user defined points
99+
def _observe_point(self, x):
100+
y = self.space.observe_point(x)
101+
if self.verbose:
102+
self.plog.print_step(x, y)
103+
return y
104+
105+
def explore(self, points_dict, eager=False):
106+
"""Method to explore user defined points.
131107
132108
:param points_dict:
109+
:param eager: if True, these points are evaulated immediately
133110
"""
111+
if eager:
112+
self.plog.reset_timer()
113+
if self.verbose:
114+
self.plog.print_header(initialization=True)
134115

135-
# Consistency check
136-
param_tup_lens = []
137-
138-
for key in self.keys:
139-
param_tup_lens.append(len(list(points_dict[key])))
140-
141-
if all([e == param_tup_lens[0] for e in param_tup_lens]):
142-
pass
116+
points = self.space._dict_to_points(points_dict)
117+
for x in points:
118+
self._observe_point(x)
143119
else:
144-
raise ValueError('The same number of initialization points '
145-
'must be entered for every parameter.')
146-
147-
# Turn into list of lists
148-
all_points = []
149-
for key in self.keys:
150-
all_points.append(points_dict[key])
151-
152-
# Take transpose of list
153-
self.init_points = list(map(list, zip(*all_points)))
120+
points = self.space._dict_to_points(points_dict)
121+
self.init_points = points
154122

155123
def initialize(self, points_dict):
156124
"""
@@ -174,7 +142,7 @@ def initialize(self, points_dict):
174142
self.y_init.extend(points_dict['target'])
175143
for i in range(len(points_dict['target'])):
176144
all_points = []
177-
for key in self.keys:
145+
for key in self.space.keys:
178146
all_points.append(points_dict[key][i])
179147
self.x_init.append(all_points)
180148

@@ -202,7 +170,7 @@ def initialize_df(self, points_df):
202170
self.y_init.append(points_df.loc[i, 'target'])
203171

204172
all_points = []
205-
for key in self.keys:
173+
for key in self.space.keys:
206174
all_points.append(points_df.loc[i, key])
207175

208176
self.x_init.append(all_points)
@@ -215,15 +183,9 @@ def set_bounds(self, new_bounds):
215183
A dictionary with the parameter name and its new bounds
216184
217185
"""
218-
219186
# Update the internal object stored dict
220187
self.pbounds.update(new_bounds)
221-
222-
# Loop through the all bounds and reset the min-max bound matrix
223-
for row, key in enumerate(self.pbounds.keys()):
224-
225-
# Reset all entries, even if the same.
226-
self.bounds[row] = self.pbounds[key]
188+
self.space.set_bounds(new_bounds)
227189

228190
def maximize(self,
229191
init_points=5,
@@ -256,6 +218,13 @@ def maximize(self,
256218
Returns
257219
-------
258220
:return: Nothing
221+
222+
Example:
223+
>>> xs = np.linspace(-2, 10, 10000)
224+
>>> f = np.exp(-(xs - 2)**2) + np.exp(-(xs - 6)**2/10) + 1/ (xs**2 + 1)
225+
>>> bo = BayesianOptimization(f=lambda x: f[int(x)],
226+
>>> pbounds={"x": (0, len(f)-1)})
227+
>>> bo.maximize(init_points=2, n_iter=25, acq="ucb", kappa=1)
259228
"""
260229
# Reset timer
261230
self.plog.reset_timer()
@@ -269,20 +238,19 @@ def maximize(self,
269238
self.plog.print_header()
270239
self.init(init_points)
271240

272-
y_max = self.Y.max()
241+
y_max = self.space.Y.max()
273242

274243
# Set parameters if any was passed
275244
self.gp.set_params(**gp_params)
276245

277246
# Find unique rows of X to avoid GP from breaking
278-
ur = unique_rows(self.X)
279-
self.gp.fit(self.X[ur], self.Y[ur])
247+
self.gp.fit(self.space.X, self.space.Y)
280248

281249
# Finding argmax of the acquisition function.
282250
x_max = acq_max(ac=self.util.utility,
283251
gp=self.gp,
284252
y_max=y_max,
285-
bounds=self.bounds,
253+
bounds=self.space.bounds,
286254
random_state=self.random_state)
287255

288256
# Print new header
@@ -298,47 +266,37 @@ def maximize(self,
298266
# Test if x_max is repeated, if it is, draw another one at random
299267
# If it is repeated, print a warning
300268
pwarning = False
301-
if np.any((self.X - x_max).sum(axis=1) == 0):
302-
303-
x_max = self.random_state.uniform(self.bounds[:, 0],
304-
self.bounds[:, 1],
305-
size=self.bounds.shape[0])
306-
269+
while x_max in self.space:
270+
x_max = self.space.random_points(1)[0]
307271
pwarning = True
308272

309273
# Append most recently generated values to X and Y arrays
310-
self.X = np.vstack((self.X, x_max.reshape((1, -1))))
311-
self.Y = np.append(self.Y, self.f(**dict(zip(self.keys, x_max))))
274+
y = self.space.observe_point(x_max)
275+
if self.verbose:
276+
self.plog.print_step(x_max, y, pwarning)
312277

313278
# Updating the GP.
314-
ur = unique_rows(self.X)
315-
self.gp.fit(self.X[ur], self.Y[ur])
279+
self.gp.fit(self.space.X, self.space.Y)
280+
281+
# Update the best params seen so far
282+
self.res['max'] = self.space.max_point()
283+
self.res['all']['values'].append(y)
284+
self.res['all']['params'].append(dict(zip(self.space.keys, x_max)))
316285

317286
# Update maximum value to search for next probe point.
318-
if self.Y[-1] > y_max:
319-
y_max = self.Y[-1]
287+
if self.space.Y[-1] > y_max:
288+
y_max = self.space.Y[-1]
320289

321290
# Maximize acquisition function to find next probing point
322291
x_max = acq_max(ac=self.util.utility,
323292
gp=self.gp,
324293
y_max=y_max,
325-
bounds=self.bounds,
294+
bounds=self.space.bounds,
326295
random_state=self.random_state)
327296

328-
# Print stuff
329-
if self.verbose:
330-
self.plog.print_step(self.X[-1], self.Y[-1], warning=pwarning)
331-
332297
# Keep track of total number of iterations
333298
self.i += 1
334299

335-
self.res['max'] = {'max_val': self.Y.max(),
336-
'max_params': dict(zip(self.keys,
337-
self.X[self.Y.argmax()]))
338-
}
339-
self.res['all']['values'].append(self.Y[-1])
340-
self.res['all']['params'].append(dict(zip(self.keys, self.X[-1])))
341-
342300
# Print a final report if verbose active.
343301
if self.verbose:
344302
self.plog.print_summary()
@@ -354,6 +312,38 @@ def points_to_csv(self, file_name):
354312
:return: None
355313
"""
356314

357-
points = np.hstack((self.X, np.expand_dims(self.Y, axis=1)))
358-
header = ', '.join(self.keys + ['target'])
315+
points = np.hstack((self.space.X, np.expand_dims(self.space.Y, axis=1)))
316+
header = ', '.join(self.space.keys + ['target'])
359317
np.savetxt(file_name, points, header=header, delimiter=',')
318+
319+
# --- API compatibility ---
320+
321+
@property
322+
def X(self):
323+
warnings.warn("use self.space.X instead", DeprecationWarning)
324+
return self.space.X
325+
326+
@property
327+
def Y(self):
328+
warnings.warn("use self.space.Y instead", DeprecationWarning)
329+
return self.space.Y
330+
331+
@property
332+
def keys(self):
333+
warnings.warn("use self.space.keys instead", DeprecationWarning)
334+
return self.space.keys
335+
336+
@property
337+
def f(self):
338+
warnings.warn("use self.space.target_func instead", DeprecationWarning)
339+
return self.space.target_func
340+
341+
@property
342+
def bounds(self):
343+
warnings.warn("use self.space.dim instead", DeprecationWarning)
344+
return self.space.bounds
345+
346+
@property
347+
def dim(self):
348+
warnings.warn("use self.space.dim instead", DeprecationWarning)
349+
return self.space.dim

0 commit comments

Comments
 (0)