77from .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+
1097class 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