1
1
"""Bayesian Optimization implementation from the thesis by Willemsen."""
2
+
2
3
import itertools
3
4
import time
4
5
import warnings
13
14
# BO imports
14
15
from kernel_tuner .searchspace import Searchspace
15
16
from kernel_tuner .strategies .common import CostFunc
17
+ from kernel_tuner .util import StopCriterionReached
16
18
17
19
try :
18
20
from sklearn .gaussian_process import GaussianProcessRegressor
22
24
except ImportError :
23
25
bayes_opt_present = False
24
26
25
- from kernel_tuner import util
26
-
27
- supported_methods = ["poi" , "ei" , "lcb" , "lcb-srinivas" , "multi" , "multi-advanced" , "multi-fast" ]
27
+ supported_methods = ["poi" , "ei" , "lcb" , "lcb-srinivas" , "multi" , "multi-advanced" , "multi-fast" , "multi-ultrafast" ]
28
28
29
29
30
30
def generate_normalized_param_dicts (tune_params : dict , eps : float ) -> Tuple [dict , dict ]:
@@ -93,9 +93,6 @@ def tune(searchspace: Searchspace, runner, tuning_options):
93
93
94
94
"""
95
95
max_fevals = tuning_options .strategy_options .get ("max_fevals" , 100 )
96
- # limit max_fevals to max size of the parameter space
97
- max_fevals = min (searchspace .size , max_fevals )
98
-
99
96
prune_parameterspace = tuning_options .strategy_options .get ("pruneparameterspace" , True )
100
97
if not bayes_opt_present :
101
98
raise ImportError (
@@ -108,19 +105,8 @@ def tune(searchspace: Searchspace, runner, tuning_options):
108
105
_ , _ , eps = cost_func .get_bounds_x0_eps ()
109
106
110
107
# compute cartesian product of all tunable parameters
111
- parameter_space = itertools .product (* tune_params .values ())
112
-
113
- # check for search space restrictions
114
- if searchspace .restrictions is not None :
115
- tuning_options .verbose = False
116
- parameter_space = filter (lambda p : util .config_valid (p , tuning_options , runner .dev .max_threads ), parameter_space )
117
- parameter_space = list (parameter_space )
118
- if len (parameter_space ) < 1 :
119
- raise ValueError ("Empty parameterspace after restrictionscheck. Restrictionscheck is possibly too strict." )
120
- if len (parameter_space ) == 1 :
121
- raise ValueError (
122
- f"Only one configuration after restrictionscheck. Restrictionscheck is possibly too strict. Configuration: { parameter_space [0 ]} "
123
- )
108
+ # TODO actually use the Searchspace object properly throughout Bayesian Optimization
109
+ parameter_space = searchspace .list
124
110
125
111
# normalize search space to [0,1]
126
112
normalize_dict , denormalize_dict = generate_normalized_param_dicts (tune_params , eps )
@@ -138,18 +124,19 @@ def tune(searchspace: Searchspace, runner, tuning_options):
138
124
# initialize and optimize
139
125
try :
140
126
bo = BayesianOptimization (
141
- parameter_space , removed_tune_params , tuning_options , normalize_dict , denormalize_dict , cost_func
127
+ parameter_space , searchspace , removed_tune_params , tuning_options , normalize_dict , denormalize_dict , cost_func
142
128
)
143
- except util . StopCriterionReached as e :
144
- print (
129
+ except StopCriterionReached :
130
+ warnings . warn (
145
131
"Stop criterion reached during initialization, was popsize (default 20) greater than max_fevals or the alotted time?"
146
132
)
147
- raise e
133
+ return cost_func .results
134
+ # raise e
148
135
try :
149
136
if max_fevals - bo .fevals <= 0 :
150
137
raise ValueError ("No function evaluations left for optimization after sampling" )
151
138
bo .optimize (max_fevals )
152
- except util . StopCriterionReached as e :
139
+ except StopCriterionReached as e :
153
140
if tuning_options .verbose :
154
141
print (e )
155
142
@@ -165,7 +152,7 @@ def tune(searchspace: Searchspace, runner, tuning_options):
165
152
covariancelengthscale = ("The covariance length scale" , 1.5 ),
166
153
method = (
167
154
"The Bayesian Optimization method to use, choose any from " + ", " .join (supported_methods ),
168
- "multi-advanced " ,
155
+ "multi-ultrafast " ,
169
156
),
170
157
samplingmethod = (
171
158
"Method used for initial sampling the parameter space, either random or Latin Hypercube Sampling (LHS)" ,
@@ -179,6 +166,7 @@ class BayesianOptimization:
179
166
def __init__ (
180
167
self ,
181
168
searchspace : list ,
169
+ searchspace_obj : Searchspace ,
182
170
removed_tune_params : list ,
183
171
tuning_options : dict ,
184
172
normalize_dict : dict ,
@@ -202,7 +190,7 @@ def get_hyperparam(name: str, default, supported_values=list()):
202
190
# get hyperparameters
203
191
cov_kernel_name = get_hyperparam ("covariancekernel" , "matern32" , self .supported_cov_kernels )
204
192
cov_kernel_lengthscale = get_hyperparam ("covariancelengthscale" , 1.5 )
205
- acquisition_function = get_hyperparam ("method" , "multi-advanced " , self .supported_methods )
193
+ acquisition_function = get_hyperparam ("method" , "multi-ultrafast " , self .supported_methods )
206
194
acq = acquisition_function
207
195
acq_params = get_hyperparam ("methodparams" , {})
208
196
multi_af_names = get_hyperparam ("multi_af_names" , ["ei" , "poi" , "lcb" ])
@@ -256,6 +244,7 @@ def get_hyperparam(name: str, default, supported_values=list()):
256
244
257
245
# set remaining values
258
246
self .__searchspace = searchspace
247
+ self .__searchspace_obj = searchspace_obj
259
248
self .removed_tune_params = removed_tune_params
260
249
self .searchspace_size = len (self .searchspace )
261
250
self .num_dimensions = len (self .dimensions ())
@@ -345,6 +334,8 @@ def set_acquisition_function(self, acquisition_function: str):
345
334
self .optimize = self .__optimize_multi_advanced
346
335
elif acquisition_function == "multi-fast" :
347
336
self .optimize = self .__optimize_multi_fast
337
+ elif acquisition_function == "multi-ultrafast" :
338
+ self .optimize = self .__optimize_multi_ultrafast
348
339
else :
349
340
raise ValueError (
350
341
"Acquisition function must be one of {}, is {}" .format (self .supported_methods , acquisition_function )
@@ -461,7 +452,7 @@ def evaluate_objective_function(self, param_config: tuple) -> float:
461
452
"""Evaluates the objective function."""
462
453
param_config = self .unprune_param_config (param_config )
463
454
denormalized_param_config = self .denormalize_param_config (param_config )
464
- if not util . config_valid ( denormalized_param_config , self .tuning_options , self . max_threads ):
455
+ if not self .__searchspace_obj . is_param_config_valid ( denormalized_param_config ):
465
456
return self .invalid_value
466
457
val = self .cost_func (param_config )
467
458
self .fevals += 1
@@ -846,6 +837,44 @@ def __optimize_multi_fast(self, max_fevals):
846
837
self .update_after_evaluation (observation , candidate_index , candidate_params )
847
838
self .fit_observations_to_model ()
848
839
840
+ def __optimize_multi_ultrafast (self , max_fevals , predict_eval_ratio = 5 ):
841
+ """Optimize with a portfolio of multiple acquisition functions. Predictions are only taken once, or fewer if predictions take too long.
842
+
843
+ The `predict_eval_ratio` denotes the ratio between the duration of the predictions and the duration of evaluations, as updating the prediction every evaluation is not efficient when evaluation is quick.
844
+ Predictions are only updated if the previous evaluation took more than `predict_eval_ratio` * the last prediction duration, or the last prediction is more than `predict_eval_ratio` evaluations ago.
845
+ """
846
+ last_prediction_counter = 0
847
+ last_prediction_time = 0
848
+ last_eval_time = 0
849
+ while self .fevals < max_fevals :
850
+ aqfs = self .multi_afs
851
+ # if we take the prediction only once, we want to go from most exploiting to most exploring, because the more exploiting an AF is, the more it relies on non-stale information from the model
852
+ fit_observations = last_prediction_time * predict_eval_ratio <= last_eval_time or last_prediction_counter >= predict_eval_ratio
853
+ if fit_observations :
854
+ last_prediction_counter = 0
855
+ pred_start = time .perf_counter ()
856
+ if last_eval_time > 0.0 :
857
+ self .fit_observations_to_model ()
858
+ predictions , _ , std = self .predict_list (self .unvisited_cache )
859
+ last_prediction_time = time .perf_counter () - pred_start
860
+ else :
861
+ last_prediction_counter += 1
862
+ eval_start = time .perf_counter ()
863
+ hyperparam = self .contextual_variance (std )
864
+ if self .__visited_num >= self .searchspace_size :
865
+ raise ValueError (self .error_message_searchspace_fully_observed )
866
+ for af in aqfs :
867
+ if self .__visited_num >= self .searchspace_size or self .fevals >= max_fevals :
868
+ break
869
+ list_of_acquisition_values = af (predictions , hyperparam )
870
+ best_af = self .argopt (list_of_acquisition_values )
871
+ del predictions [best_af ] # to avoid going out of bounds
872
+ candidate_params = self .unvisited_cache [best_af ]
873
+ candidate_index = self .find_param_config_index (candidate_params )
874
+ observation = self .evaluate_objective_function (candidate_params )
875
+ self .update_after_evaluation (observation , candidate_index , candidate_params )
876
+ last_eval_time = time .perf_counter () - eval_start
877
+
849
878
def af_random (self , predictions = None , hyperparam = None ) -> list :
850
879
"""Acquisition function returning a randomly shuffled list for comparison."""
851
880
list_random = range (len (self .unvisited_cache ))
0 commit comments