Skip to content

Commit 2c95f8c

Browse files
committed
working on adding TargetSpace abstraction for a simpler api
1 parent 087cdee commit 2c95f8c

File tree

1 file changed

+120
-84
lines changed

1 file changed

+120
-84
lines changed

bayes_opt/bayesian_optimization.py

Lines changed: 120 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,93 @@
77
from .helpers import UtilityFunction, unique_rows, PrintLog, acq_max
88

99

10+
class TargetSpace(object):
11+
"""
12+
Holds the param-space coordinates (X) and target values (Y)
13+
"""
14+
def __init__(self, f, pbounds, random_state):
15+
16+
self.random_state = random_state
17+
18+
# Function to be evaluate
19+
self.f = f
20+
21+
# Initialize bounds
22+
self.keys = list(pbounds.keys())
23+
self.bounds = np.array(list(pbounds.values()), dtype=np.float)
24+
self.dim = len(pbounds)
25+
26+
# Place to constant-time append new values
27+
self.new_Xs = []
28+
self.new_Ys = []
29+
30+
# Place to consolidate and concatanate all values
31+
self.X_arr = None
32+
self.Y_arr = None
33+
34+
self.plog = PrintLog(self.keys)
35+
36+
@property
37+
def X(self):
38+
self._consolidate()
39+
return self.X_arr
40+
41+
@property
42+
def Y(self):
43+
self._consolidate()
44+
return self.Y_arr
45+
46+
def unique_XY(self):
47+
X = self.X
48+
Y = self.Y
49+
ur = unique_rows(X)
50+
return X[ur], Y[ur]
51+
52+
def _consolidate(self):
53+
"""
54+
If there are any new values appends them to the contiguous array
55+
"""
56+
if self.new_Xs:
57+
assert len(self.new_Xs) == len(self.new_Ys)
58+
if self.X is None and self.Y is None:
59+
self.X = np.empty((0, self.bounds.shape[0]))
60+
self.Y = np.empty(0)
61+
self.new_Xs = np.vstack([self.X_arr] + self.new_Xs)
62+
self.new_Ys = np.hstack([self.Y_arr] + self.new_Ys)
63+
64+
def set_bounds(self, new_bounds):
65+
# Loop through the all bounds and reset the min-max bound matrix
66+
for row, key in enumerate(self.keys):
67+
if key in new_bounds:
68+
self.bounds[row] = new_bounds[key]
69+
70+
def add_observation(self, x, y, pwarning=False):
71+
np.asarray(x).reshape((1, -1))
72+
self.new_Xs.append(x)
73+
self.new_Ys.append(y)
74+
if self.verbose:
75+
self.plog.print_step(x, y, pwarning)
76+
77+
def observe_point(self, x, pwarning=False):
78+
"""
79+
Evaulates a single point x, to obtain the value y and then records them
80+
as observations.
81+
"""
82+
x = np.asarray(x).reshape((1, -1))
83+
y = self.f(**dict(zip(self.keys, x)))
84+
self.add_observation(x, y, pwarning)
85+
86+
def random_points(self, num):
87+
l = [self.random_state.uniform(x[0], x[1], size=num)
88+
for x in self.bounds]
89+
return list(map(list, zip(*l)))
90+
91+
def max_point(self):
92+
return {'max_val': self.Y.max(),
93+
'max_params': dict(zip(self.keys,
94+
self.X[self.Y.argmax()]))}
95+
96+
1097
class BayesianOptimization(object):
1198

1299
def __init__(self, f, pbounds, random_state=None, verbose=1):
@@ -55,9 +142,7 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
55142
self.x_init = []
56143
self.y_init = []
57144

58-
# Numpy array place holders
59-
self.X = None
60-
self.Y = None
145+
self.space = TargetSpace(f, pbounds, random_state)
61146

62147
# Counter of iterations
63148
self.i = 0
@@ -73,7 +158,7 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
73158
self.util = None
74159

75160
# PrintLog object
76-
self.plog = PrintLog(self.keys)
161+
self.plog = self.space.plog
77162

78163
# Output dictionary
79164
self.res = {}
@@ -92,70 +177,36 @@ def init(self, init_points):
92177
93178
:param init_points:
94179
Number of random points to probe.
95-
"""
96180
181+
Example:
182+
pbounds = {'p1': (0, 1), 'p2': (1, 100)}
183+
bounds = np.array(list(pbounds.values()))
184+
init_points = 10
185+
"""
97186
# Generate random points
98-
l = [self.random_state.uniform(x[0], x[1], size=init_points)
99-
for x in self.bounds]
100-
101187
# Concatenate new random points to possible existing
102188
# points from self.explore method.
103-
self.init_points += list(map(list, zip(*l)))
189+
rand_points = self.space.random_points(init_points)
190+
self.init_points += rand_points
104191

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)
192+
x_init = np.vstack(self.x_init)
193+
y_init = np.hstack(self.y_init)
194+
for x, y in zip(x_init, y_init):
195+
self.space.add_observation(x, y)
108196

109197
# Evaluate target function at all initialization
110198
# points (random + explore)
111199
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))))
114-
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))
200+
self.space.observe_point(x)
125201

126202
# Updates the flag
127203
self.initialized = True
128204

129-
def eval_points(self, points):
130-
"""
131-
Evaulates specific points and adds them to the set of known
132-
observations self.X and self.Y
133-
"""
134-
if self.X is None and self.Y is None:
135-
self.X = np.empty((0, self.bounds.shape[0]))
136-
self.Y = np.empty(0)
137-
138-
new_Xs = []
139-
new_Ys = []
140-
141-
for x in points:
142-
x = np.asarray(x).reshape((1, -1))
143-
y = self.f(**dict(zip(self.keys, x)))
144-
145-
new_Xs.append(x)
146-
new_Ys.append(y)
147-
148-
if self.verbose:
149-
self.plog.print_step(x, self.Y[-1])
150-
151-
# Append the evaluated points
152-
self.X = np.vstack([self.X] + new_Xs)
153-
self.Y = np.hstack([self.Y] + new_Ys)
154-
155-
156205
def explore(self, points_dict):
157206
"""Method to explore user defined points
158207
208+
This is executed lazily.
209+
159210
:param points_dict:
160211
"""
161212

@@ -242,15 +293,14 @@ def set_bounds(self, new_bounds):
242293
A dictionary with the parameter name and its new bounds
243294
244295
"""
296+
self.space.set_bounds(new_bounds)
245297

246298
# Update the internal object stored dict
247299
self.pbounds.update(new_bounds)
248-
249-
# Loop through the all bounds and reset the min-max bound matrix
250-
for row, key in enumerate(self.pbounds.keys()):
251-
252-
# Reset all entries, even if the same.
253-
self.bounds[row] = self.pbounds[key]
300+
# # Loop through the all bounds and reset the min-max bound matrix
301+
# for row, key in enumerate(self.pbounds.keys()):
302+
# # Reset all entries, even if the same.
303+
# self.bounds[row] = self.pbounds[key]
254304

255305
def maximize(self,
256306
init_points=5,
@@ -296,14 +346,13 @@ def maximize(self,
296346
self.plog.print_header()
297347
self.init(init_points)
298348

299-
y_max = self.Y.max()
349+
y_max = self.space.Y.max()
300350

301351
# Set parameters if any was passed
302352
self.gp.set_params(**gp_params)
303353

304354
# Find unique rows of X to avoid GP from breaking
305-
ur = unique_rows(self.X)
306-
self.gp.fit(self.X[ur], self.Y[ur])
355+
self.gp.fit(*self.space.unique_XY())
307356

308357
# Finding argmax of the acquisition function.
309358
x_max = acq_max(ac=self.util.utility,
@@ -325,25 +374,19 @@ def maximize(self,
325374
# Test if x_max is repeated, if it is, draw another one at random
326375
# If it is repeated, print a warning
327376
pwarning = False
328-
if np.any((self.X - x_max).sum(axis=1) == 0):
329-
330-
x_max = self.random_state.uniform(self.bounds[:, 0],
331-
self.bounds[:, 1],
332-
size=self.bounds.shape[0])
333-
377+
if np.any((self.space.X - x_max).sum(axis=1) == 0):
378+
x_max = self.space.random_points(1)[0]
334379
pwarning = True
335380

336381
# Append most recently generated values to X and Y arrays
337-
self.X = np.vstack((self.X, x_max.reshape((1, -1))))
338-
self.Y = np.append(self.Y, self.f(**dict(zip(self.keys, x_max))))
382+
self.space.observe_point(x_max, pwarning)
339383

340384
# Updating the GP.
341-
ur = unique_rows(self.X)
342-
self.gp.fit(self.X[ur], self.Y[ur])
385+
self.gp.fit(*self.space.unique_XY())
343386

344387
# Update maximum value to search for next probe point.
345-
if self.Y[-1] > y_max:
346-
y_max = self.Y[-1]
388+
if self.space.Y[-1] > y_max:
389+
y_max = self.space.Y[-1]
347390

348391
# Maximize acquisition function to find next probing point
349392
x_max = acq_max(ac=self.util.utility,
@@ -352,19 +395,12 @@ def maximize(self,
352395
bounds=self.bounds,
353396
random_state=self.random_state)
354397

355-
# Print stuff
356-
if self.verbose:
357-
self.plog.print_step(self.X[-1], self.Y[-1], warning=pwarning)
358-
359398
# Keep track of total number of iterations
360399
self.i += 1
361400

362-
self.res['max'] = {'max_val': self.Y.max(),
363-
'max_params': dict(zip(self.keys,
364-
self.X[self.Y.argmax()]))
365-
}
366-
self.res['all']['values'].append(self.Y[-1])
367-
self.res['all']['params'].append(dict(zip(self.keys, self.X[-1])))
401+
self.res['max'] = self.space.max_point()
402+
self.res['all']['values'].append(self.space.Y[-1])
403+
self.res['all']['params'].append(dict(zip(self.keys, self.space.X[-1])))
368404

369405
# Print a final report if verbose active.
370406
if self.verbose:
@@ -381,6 +417,6 @@ def points_to_csv(self, file_name):
381417
:return: None
382418
"""
383419

384-
points = np.hstack((self.X, np.expand_dims(self.Y, axis=1)))
420+
points = np.hstack((self.space.X, np.expand_dims(self.space.Y, axis=1)))
385421
header = ', '.join(self.keys + ['target'])
386422
np.savetxt(file_name, points, header=header, delimiter=',')

0 commit comments

Comments
 (0)