22from __future__ import division
33
44import numpy as np
5+ import warnings
56from sklearn .gaussian_process import GaussianProcessRegressor
67from 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
1012class 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