44from scipy .optimize import minimize
55
66from tensorflow .keras .losses import BinaryCrossentropy
7- # from tensorflow.keras.initializers import GlorotUniform
87
98from .types import DenseConfigurationSpace , DenseConfiguration
109from .models import DenseSequential
1110from .decorators import unbatch , value_and_gradient , numpy_io
1211from .optimizers import multi_start
1312
14- # from hpbandster.core.master import Master
1513from hpbandster .optimizers .hyperband import HyperBand
1614from hpbandster .core .base_config_generator import base_config_generator
1715
1816
17+ minimize_multi_start = multi_start (minimizer_fn = minimize )
18+
19+
1920def is_duplicate (x , xs , rtol = 1e-5 , atol = 1e-8 ):
2021 # Clever ways of doing this would involve data structs. like KD-trees
2122 # or locality sensitive hashing (LSH), but these are premature
@@ -97,14 +98,15 @@ def __init__(self, config_space, gamma=1/3, num_random_init=10,
9798 self .logit = self ._build_compile_network (num_layers , num_units ,
9899 activation , optimizer )
99100 self .loss = self ._build_loss (self .logit , normalize = normalize )
100- self .minimizer = self ._build_minimizer (num_restarts = num_restarts ,
101- method = method , ftol = ftol ,
102- max_iter = max_iter )
103101
104102 self .gamma = gamma
105103 self .num_random_init = num_random_init
106104 self .random_rate = random_rate
105+
107106 self .num_restarts = num_restarts
107+ self .method = method
108+ self .ftol = ftol
109+ self .max_iter = max_iter
108110
109111 self .batch_size = batch_size
110112 self .num_steps_per_iter = num_steps_per_iter
@@ -115,6 +117,35 @@ def __init__(self, config_space, gamma=1/3, num_random_init=10,
115117 self .seed = seed
116118 self .random_state = np .random .RandomState (seed )
117119
120+ def _array_from_dict (self , dct ):
121+ config = DenseConfiguration (self .config_space , values = dct )
122+ return config .to_array ()
123+
124+ def _dict_from_array (self , array ):
125+ config = DenseConfiguration .from_array (self .config_space ,
126+ array_dense = array )
127+ return config .get_dictionary ()
128+
129+ def _get_dataset_size (self ):
130+ return len (self .config_arrs )
131+
132+ def _load_data (self ):
133+ X = np .vstack (self .config_arrs )
134+ y = np .hstack (self .losses )
135+ return X , y
136+
137+ def _load_labels (self , y ):
138+ # TODO(LT): we can use clever data structures like heaps to make this
139+ # labelling constant-time, but this is probably a premature
140+ # optimization at this time...
141+ tau = np .quantile (y , q = self .gamma )
142+ return np .less (y , tau )
143+
144+ def _get_steps_per_epoch (self , dataset_size ):
145+ steps_per_epoch = int (np .ceil (np .true_divide (dataset_size ,
146+ self .batch_size )))
147+ return steps_per_epoch
148+
118149 @staticmethod
119150 def _build_compile_network (num_layers , num_units , activation , optimizer ):
120151
@@ -142,52 +173,12 @@ def loss(x):
142173
143174 return loss
144175
145- @staticmethod
146- def _build_minimizer (num_restarts , method = "L-BFGS-B" , max_iter = 100 ,
147- ftol = 1e-2 ):
148-
149- @multi_start (num_restarts = num_restarts )
150- def multi_start_minimizer (fn , x0 , bounds ):
151- return minimize (fn , x0 = x0 , method = method , jac = True , bounds = bounds ,
152- options = dict (maxiter = max_iter , ftol = ftol ))
153-
154- return multi_start_minimizer
155-
156- def _load_data (self ):
157- X = np .vstack (self .config_arrs )
158- y = np .hstack (self .losses )
159- return X , y
160-
161- def _load_labels (self , y ):
162- tau = np .quantile (y , q = self .gamma )
163- return np .less (y , tau )
164-
165- def _get_steps_per_epoch (self , dataset_size ):
166- steps_per_epoch = int (np .ceil (np .true_divide (dataset_size ,
167- self .batch_size )))
168- return steps_per_epoch
169-
170- def get_config (self , budget ):
171-
172- dataset_size = len (self .config_arrs )
173-
174- config_random = self .config_space .sample_configuration ()
175- config_random_dict = config_random .get_dictionary ()
176-
177- if dataset_size < self .num_random_init :
178- self .logger .debug (f"Completed { dataset_size } /{ self .num_random_init } "
179- " initial runs. Returning random candidate..." )
180- return (config_random_dict , {})
181-
182- if self .random_state .binomial (p = self .random_rate , n = 1 ):
183- self .logger .info ("[Glob. maximum: skipped "
184- f"(prob={ self .random_rate :.2f} )] "
185- "Returning random candidate ..." )
186- return (config_random_dict , {})
176+ def _update_model (self ):
187177
188178 X , y = self ._load_data ()
189179 z = self ._load_labels (y )
190180
181+ dataset_size = self ._get_dataset_size ()
191182 steps_per_epoch = self ._get_steps_per_epoch (dataset_size )
192183 num_epochs = self .num_steps_per_iter // steps_per_epoch
193184
@@ -203,15 +194,21 @@ def get_config(self, budget):
203194 f"num steps per iter: { self .num_steps_per_iter } , "
204195 f"num epochs: { num_epochs } " )
205196
206- # Maximize acquisition function
197+ def _get_maximum (self ):
198+
207199 self .logger .debug ("Beginning multi-start maximization with "
208200 f"{ self .num_restarts } starts..." )
209201
210- results = self .minimizer (self .loss , self .bounds ,
211- random_state = self .random_state )
202+ results = minimize_multi_start (self .loss , self .bounds ,
203+ num_restarts = self .num_restarts ,
204+ method = self .method , jac = True ,
205+ options = dict (maxiter = self .max_iter ,
206+ ftol = self .ftol ),
207+ random_state = self .random_state )
212208
213209 res_best = None
214210 for i , res in enumerate (results ):
211+ # TODO(LT): This currently assumes `normalize=False`
215212 self .logger .debug (f"[Maximum { i + 1 :02d} /{ self .num_restarts :02d} : "
216213 f"logit={ - res .fun :.3f} ] success: { res .success } , "
217214 f"iterations: { res .nit :02d} , status: { res .status } "
@@ -225,7 +222,32 @@ def get_config(self, budget):
225222 if res_best is None or res .fun < res_best .fun :
226223 res_best = res
227224
228- if res_best is None :
225+ return res_best
226+
227+ def get_config (self , budget ):
228+
229+ dataset_size = self ._get_dataset_size ()
230+
231+ config_random = self .config_space .sample_configuration ()
232+ config_random_dict = config_random .get_dictionary ()
233+
234+ if dataset_size < self .num_random_init :
235+ self .logger .debug (f"Completed { dataset_size } /{ self .num_random_init } "
236+ " initial runs. Returning random candidate..." )
237+ return (config_random_dict , {})
238+
239+ if self .random_state .binomial (p = self .random_rate , n = 1 ):
240+ self .logger .info ("[Glob. maximum: skipped "
241+ f"(prob={ self .random_rate :.2f} )] "
242+ "Returning random candidate ..." )
243+ return (config_random_dict , {})
244+
245+ # Update model
246+ self ._update_model ()
247+
248+ # Maximize acquisition function
249+ opt = self ._get_maximum ()
250+ if opt is None :
229251 # TODO(LT): It's actually important to report what one of these
230252 # occurred...
231253 self .logger .warn ("[Glob. maximum: not found!] Either optimization "
@@ -234,29 +256,28 @@ def get_config(self, budget):
234256 " Returning random candidate..." )
235257 return (config_random_dict , {})
236258
237- self .logger .info (f"[Glob. maximum: logit={ - res_best .fun :.3f} , "
238- f"prob={ tf .sigmoid (- res_best .fun ):.3f} , "
239- f"rel. ratio={ tf .sigmoid (- res_best .fun )/ self .gamma :.3f} ] "
240- f"x={ res_best .x } " )
259+ # TODO(LT): This currently assumes `normalize=False`
260+ self .logger .info (f"[Glob. maximum: logit={ - opt .fun :.3f} , "
261+ f"prob={ tf .sigmoid (- opt .fun ):.3f} , "
262+ f"rel. ratio={ tf .sigmoid (- opt .fun )/ self .gamma :.3f} ] "
263+ f"x={ opt .x } " )
241264
242- config_opt_arr = res_best .x
243- config_opt = DenseConfiguration .from_array (self .config_space ,
244- array_dense = config_opt_arr )
245- config_opt_dict = config_opt .get_dictionary ()
265+ config_opt_arr = opt .x
266+ config_opt_dict = self ._dict_from_array (config_opt_arr )
246267
247268 return (config_opt_dict , {})
248269
249270 def new_result (self , job , update_model = True ):
250271
251272 super (DRE , self ).new_result (job )
252273
253- # TODO: ignoring this right now
274+ # TODO(LT): support multi-fidelity
254275 budget = job .kwargs ["budget" ]
255276
256- loss = job .result ["loss" ]
257277 config_dict = job .kwargs ["config" ]
258- config = DenseConfiguration (self .config_space , values = config_dict )
259- config_arr = config .to_array ()
278+ config_arr = self ._array_from_dict (config_dict )
279+
280+ loss = job .result ["loss" ]
260281
261- self .losses .append (loss )
262282 self .config_arrs .append (config_arr )
283+ self .losses .append (loss )
0 commit comments