2
2
from __future__ import division
3
3
4
4
import numpy as np
5
+ import warnings
5
6
from sklearn .gaussian_process import GaussianProcessRegressor
6
7
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
8
10
9
11
10
12
class BayesianOptimization (object ):
@@ -25,27 +27,11 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
25
27
# Store the original dictionary
26
28
self .pbounds = pbounds
27
29
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 )
46
31
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 )
49
35
50
36
# Initialization flag
51
37
self .initialized = False
@@ -55,10 +41,6 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
55
41
self .x_init = []
56
42
self .y_init = []
57
43
58
- # Numpy array place holders
59
- self .X = None
60
- self .Y = None
61
-
62
44
# Counter of iterations
63
45
self .i = 0
64
46
@@ -73,7 +55,7 @@ def __init__(self, f, pbounds, random_state=None, verbose=1):
73
55
self .util = None
74
56
75
57
# PrintLog object
76
- self .plog = PrintLog (self .keys )
58
+ self .plog = PrintLog (self .space . keys )
77
59
78
60
# Output dictionary
79
61
self .res = {}
@@ -93,64 +75,50 @@ def init(self, init_points):
93
75
:param init_points:
94
76
Number of random points to probe.
95
77
"""
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
-
101
78
# Concatenate new random points to possible existing
102
79
# 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 )
108
82
109
- # Evaluate target function at all initialization
110
- # points (random + explore)
83
+ # Evaluate target function at all initialization points
111
84
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 )
114
86
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 )
125
95
126
96
# Updates the flag
127
97
self .initialized = True
128
98
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.
131
107
132
108
:param points_dict:
109
+ :param eager: if True, these points are evaulated immediately
133
110
"""
111
+ if eager :
112
+ self .plog .reset_timer ()
113
+ if self .verbose :
114
+ self .plog .print_header (initialization = True )
134
115
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 )
143
119
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
154
122
155
123
def initialize (self , points_dict ):
156
124
"""
@@ -174,7 +142,7 @@ def initialize(self, points_dict):
174
142
self .y_init .extend (points_dict ['target' ])
175
143
for i in range (len (points_dict ['target' ])):
176
144
all_points = []
177
- for key in self .keys :
145
+ for key in self .space . keys :
178
146
all_points .append (points_dict [key ][i ])
179
147
self .x_init .append (all_points )
180
148
@@ -202,7 +170,7 @@ def initialize_df(self, points_df):
202
170
self .y_init .append (points_df .loc [i , 'target' ])
203
171
204
172
all_points = []
205
- for key in self .keys :
173
+ for key in self .space . keys :
206
174
all_points .append (points_df .loc [i , key ])
207
175
208
176
self .x_init .append (all_points )
@@ -215,15 +183,9 @@ def set_bounds(self, new_bounds):
215
183
A dictionary with the parameter name and its new bounds
216
184
217
185
"""
218
-
219
186
# Update the internal object stored dict
220
187
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 )
227
189
228
190
def maximize (self ,
229
191
init_points = 5 ,
@@ -256,6 +218,13 @@ def maximize(self,
256
218
Returns
257
219
-------
258
220
: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)
259
228
"""
260
229
# Reset timer
261
230
self .plog .reset_timer ()
@@ -269,20 +238,19 @@ def maximize(self,
269
238
self .plog .print_header ()
270
239
self .init (init_points )
271
240
272
- y_max = self .Y .max ()
241
+ y_max = self .space . Y .max ()
273
242
274
243
# Set parameters if any was passed
275
244
self .gp .set_params (** gp_params )
276
245
277
246
# 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 )
280
248
281
249
# Finding argmax of the acquisition function.
282
250
x_max = acq_max (ac = self .util .utility ,
283
251
gp = self .gp ,
284
252
y_max = y_max ,
285
- bounds = self .bounds ,
253
+ bounds = self .space . bounds ,
286
254
random_state = self .random_state )
287
255
288
256
# Print new header
@@ -298,47 +266,37 @@ def maximize(self,
298
266
# Test if x_max is repeated, if it is, draw another one at random
299
267
# If it is repeated, print a warning
300
268
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 ]
307
271
pwarning = True
308
272
309
273
# 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 )
312
277
313
278
# 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 )))
316
285
317
286
# 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 ]
320
289
321
290
# Maximize acquisition function to find next probing point
322
291
x_max = acq_max (ac = self .util .utility ,
323
292
gp = self .gp ,
324
293
y_max = y_max ,
325
- bounds = self .bounds ,
294
+ bounds = self .space . bounds ,
326
295
random_state = self .random_state )
327
296
328
- # Print stuff
329
- if self .verbose :
330
- self .plog .print_step (self .X [- 1 ], self .Y [- 1 ], warning = pwarning )
331
-
332
297
# Keep track of total number of iterations
333
298
self .i += 1
334
299
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
-
342
300
# Print a final report if verbose active.
343
301
if self .verbose :
344
302
self .plog .print_summary ()
@@ -354,6 +312,38 @@ def points_to_csv(self, file_name):
354
312
:return: None
355
313
"""
356
314
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' ])
359
317
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