GPopt.GPOpt
GPOpt class.
1"""GPOpt class.""" 2 3# Authors: T. Moudiki 4# 5# License: BSD 3 Clause Clear 6 7import copy 8import nnetsauce as ns 9import numpy as np 10import pandas as pd 11import pickle 12import shelve 13from collections import namedtuple 14from functools import partial 15 16try: 17 from sklearn.utils.discovery import all_estimators 18except: 19 from sklearn.utils import all_estimators 20from sklearn.base import RegressorMixin 21from sklearn.gaussian_process import GaussianProcessRegressor 22from sklearn.ensemble import RandomForestRegressor 23from sklearn.gaussian_process.kernels import Matern 24from sklearn.model_selection import GridSearchCV, RandomizedSearchCV 25 26import scipy.stats as st 27from joblib import Parallel, delayed 28from time import time 29from tqdm import tqdm 30from .config import REGRESSORS, REMOVED_REGRESSORS 31from .utils import generate_sobol2 32from .utils import Progbar 33 34 35class GPOpt: 36 """Class GPOpt. 37 38 # Arguments: 39 40 lower_bound: a numpy array; 41 lower bound for researched minimum 42 43 upper_bound: a numpy array; 44 upper bound for researched minimum 45 46 objective_func: a function; 47 the objective function to be minimized 48 49 params_names: a list; 50 names of the parameters of the objective function (optional) 51 52 surrogate_obj: an object; 53 An ML model for estimating the uncertainty around the objective function 54 Must be nnetsauce.CustomRegressor or nnetsauce.PredictionInterval 55 56 x_init: 57 initial setting of points where `objective_func` is evaluated (optional) 58 59 y_init: 60 initial setting values at points where `objective_func` is evaluated (optional) 61 62 n_init: an integer; 63 number of points in the initial setting, when `x_init` and `y_init` are not provided 64 65 n_choices: an integer; 66 number of points for the calculation of expected improvement 67 68 n_iter: an integer; 69 number of iterations of the minimization algorithm 70 71 alpha: a float; 72 Value added to the diagonal of the kernel matrix during fitting (for Matern 5/2 kernel) 73 74 n_restarts_optimizer: an integer; 75 The number of restarts of the optimizer for finding the kernel’s parameters which maximize the log-marginal likelihood. 76 77 seed: an integer; 78 reproducibility seed 79 80 save: a string; 81 Specifies where to save the optimizer in its current state 82 83 n_jobs: an integer; 84 number of jobs for parallel computing on initial setting or method `lazyoptimize` (can be -1) 85 86 acquisition: a string; 87 acquisition function: "ei" (expected improvement) or "ucb" (upper confidence bound) 88 89 method: an str; 90 "bayesian" (default) for Gaussian posteriors, "mc" for Monte Carlo posteriors, 91 "splitconformal" (only for acquisition = "ucb") for conformalized surrogates 92 93 min_value: a float; 94 minimum value of the objective function (default is None). For example, 95 if objective function is accuracy, will be 1, and the algorithm will stop 96 97 per_second: a boolean; 98 __experimental__, default is False (leave to default for now) 99 100 log_scale: a boolean; 101 __experimental__, default is False (leave to default for now) 102 103 see also [Bayesian Optimization with GPopt](https://thierrymoudiki.github.io/blog/2021/04/16/python/misc/gpopt) 104 or [Hyperparameters tuning with GPopt](https://thierrymoudiki.github.io/blog/2021/06/11/python/misc/hyperparam-tuning-gpopt) [Agnostic BayesOpt](https://thierrymoudiki.github.io/blog/2024/12/09/python/bayesconfoptim) 105 106 """ 107 108 def __init__( 109 self, 110 lower_bound, 111 upper_bound, 112 objective_func=None, 113 params_names=None, 114 surrogate_obj=None, 115 x_init=None, 116 y_init=None, 117 n_init=10, 118 n_choices=100000, 119 n_iter=190, 120 alpha=1e-6, 121 n_restarts_optimizer=25, 122 seed=123, 123 save=None, 124 n_jobs=1, 125 acquisition="ei", 126 method="bayesian", 127 min_value=None, 128 per_second=False, # /!\ very experimental 129 log_scale=False, # /!\ experimental 130 ): 131 n_dims = len(lower_bound) 132 133 assert n_dims == len( 134 upper_bound 135 ), "'upper_bound' and 'lower_bound' must have the same dimensions" 136 137 self.objective_func = objective_func 138 self.params_names = params_names 139 self.lower_bound = lower_bound 140 self.upper_bound = upper_bound 141 self.y_init = y_init 142 self.log_scale = log_scale 143 self.n_dims = n_dims 144 self.n_init = n_init 145 self.n_choices = n_choices 146 self.n_iter = n_iter 147 self.alpha = alpha 148 self.n_restarts_optimizer = n_restarts_optimizer 149 self.seed = seed 150 self.save = save 151 self.n_jobs = n_jobs # for parallel case on initial design 152 self.per_second = per_second 153 self.x_min = None 154 self.y_min = None 155 self.y_mean = None 156 self.y_std = None 157 self.y_lower = None 158 self.y_upper = None 159 self.best_surrogate = None 160 self.acquisition = acquisition 161 self.min_value = min_value 162 self.acq = np.array([]) 163 self.max_acq = [] 164 if surrogate_obj is None: 165 self.surrogate_obj = GaussianProcessRegressor( 166 kernel=Matern(nu=2.5), 167 alpha=self.alpha, 168 normalize_y=True, 169 n_restarts_optimizer=self.n_restarts_optimizer, 170 random_state=self.seed, 171 ) 172 else: 173 self.surrogate_obj = surrogate_obj 174 assert method in ( 175 "bayesian", 176 "mc", 177 "splitconformal", 178 ), "method must be in ('bayesian', 'mc', 'splitconformal')" 179 self.method = method 180 self.posterior_ = None 181 182 # Sobol seqs for initial design and choices 183 sobol_seq_init = np.transpose( 184 generate_sobol2( 185 n_dims=self.n_dims, 186 n_points=self.n_init, 187 skip=2, 188 ) 189 ) 190 sobol_seq_choices = np.transpose( 191 generate_sobol2( 192 n_dims=self.n_dims, 193 n_points=self.n_choices, 194 skip=self.n_init + 2, 195 ) 196 ) 197 198 # Sobol seqs for initial design and choices with bounds 199 if self.log_scale == False: 200 bounds_range = upper_bound - lower_bound 201 self.x_init = ( 202 bounds_range * sobol_seq_init + lower_bound 203 if x_init is None 204 else x_init 205 ) 206 self.x_choices = bounds_range * sobol_seq_choices + lower_bound 207 208 else: # (!) experimental 209 assert ( 210 lower_bound > 0 211 ).all(), "all elements of `lower_bound` must be > 0" 212 assert ( 213 upper_bound > 0 214 ).all(), "all elements of `upper_bound` must be > 0" 215 216 log_lower_bound = np.log(lower_bound) 217 log_upper_bound = np.log(upper_bound) 218 log_bounds_range = log_upper_bound - log_lower_bound 219 self.x_init = ( 220 np.minimum( 221 np.exp(log_bounds_range * sobol_seq_init + log_lower_bound), 222 1.7976931348623157e308, 223 ) 224 if x_init is None 225 else x_init 226 ) 227 self.x_choices = np.minimum( 228 np.exp(log_bounds_range * sobol_seq_choices + log_lower_bound), 229 1.7976931348623157e308, 230 ) 231 232 # shelve for saving (not for loading) 233 if self.save is not None: 234 self.sh = shelve.open(filename=save, flag="c", writeback=True) 235 236 if self.per_second: 237 self.timings = [] 238 self.rf_obj = RandomForestRegressor( 239 n_estimators=250, random_state=self.seed 240 ) 241 242 self.n_jobs = n_jobs 243 244 # from sklearn.base 245 def get_params(self): 246 """Get object attributes. 247 248 Returns 249 ------- 250 params : mapping of string to any 251 Parameter names mapped to their values. 252 """ 253 out = dict() 254 param_names = dir(self) 255 for key in param_names: 256 if key.startswith("_") is False: 257 out[key] = getattr(self, key, None) 258 259 return out 260 261 # for parallel case on initial design 262 def eval_objective(self, arg): 263 try: 264 return self.objective_func(self.x_init[arg, :]) 265 except: 266 return 1e06 267 268 # load data from stored shelve 269 def load(self, path): 270 """load data from stored shelve. 271 272 # Arguments 273 274 path : a string; path to stored shelve. 275 276 See also: [Bayesian Optimization with GPopt Part 2 (save and resume)](https://thierrymoudiki.github.io/blog/2021/04/30/python/misc/gpopt) 277 """ 278 279 self.sh = shelve.open(filename=path) 280 for key, value in self.sh.items(): 281 setattr(self, key, value) 282 283 # update shelve in optimization loop 284 def update_shelve(self): 285 for key, value in self.get_params().items(): 286 if (callable(value) is False) & (key != "sh"): 287 self.sh[key] = value 288 self.sh.sync() 289 290 # closing shelve (can't be modified after) 291 def close_shelve(self): 292 """Close shelve. 293 294 # Arguments 295 296 No argument. 297 298 See also: [Bayesian Optimization with GPopt Part 2 (save and resume)](https://thierrymoudiki.github.io/blog/2021/04/30/python/misc/gpopt) 299 """ 300 301 self.sh.close() 302 303 # fit predict 304 def surrogate_fit_predict( 305 self, 306 X_train, 307 y_train, 308 X_test, 309 return_std=False, 310 return_pi=False, 311 param_search_init_design=False, 312 param_distributions=None, 313 **kwargs, 314 ): 315 if len(X_train.shape) == 1: 316 X_train = X_train.reshape((-1, 1)) 317 X_test = X_test.reshape((-1, 1)) 318 319 if ( 320 X_train.shape[0] <= self.n_init and param_search_init_design == True 321 ): # on initial design 322 try: 323 rs_obj = RandomizedSearchCV( 324 self.surrogate_obj, 325 param_distributions=param_distributions, 326 random_state=42, 327 cv=3, 328 **kwargs, 329 ) 330 rs_obj.fit(X_train, y_train) 331 self.surrogate_obj = rs_obj.best_estimator_ 332 except Exception as e: 333 print(str(e)) 334 335 # Get mean and standard deviation (+ lower and upper for not GPs) 336 assert ( 337 return_std == True and return_pi == True 338 ) == False, "must have either return_std == True or return_pi == True" 339 340 if return_std == True: 341 self.posterior_ = "gaussian" 342 self.surrogate_obj.fit(X_train, y_train) 343 return self.surrogate_obj.predict(X_test, return_std=True) 344 345 elif ( 346 return_pi == True 347 ): # here, self.surrogate_obj must have `replications` not None 348 if self.surrogate_obj.replications is not None: 349 self.posterior_ = "mc" 350 self.surrogate_obj.fit(X_train, y_train) 351 try: # it's a nnetsauce.CustomRegressor 352 res = self.surrogate_obj.predict( 353 X_test, return_pi=True, method="splitconformal" 354 ) 355 except Exception: # it's a nnetsauce.PredictionInterval 356 try: 357 res = self.surrogate_obj.predict(X_test, return_pi=True) 358 except Exception as e: 359 print( 360 str(e) 361 + "Sureproof way is to encapsulate your surrogate in nnetsauce.PredictionInterval model" 362 ) 363 364 self.y_sims = res.sims 365 self.y_mean, self.y_std = ( 366 np.mean(self.y_sims, axis=1), 367 np.std(self.y_sims, axis=1), 368 ) 369 return self.y_mean, self.y_std, self.y_sims 370 371 else: # self.surrogate_obj is conformalized (uses nnetsauce.PredictionInterval) 372 assert ( 373 self.acquisition == "ucb" 374 ), "'acquisition' must be 'ucb' for conformalized surrogates" 375 self.posterior_ = None 376 self.surrogate_obj.fit(X_train, y_train) 377 try: 378 res = self.surrogate_obj.predict( 379 X_test, return_pi=True, method="splitconformal" 380 ) 381 except Exception: 382 res = self.surrogate_obj.predict(X_test, return_pi=True) 383 self.y_mean = res.mean 384 self.y_lower = res.lower 385 self.y_upper = res.upper 386 return self.y_mean, self.y_lower, self.y_upper 387 388 else: 389 raise NotImplementedError 390 391 # fit predict timings 392 def timings_fit_predict(self, X_train, y_train, X_test): 393 if len(X_train.shape) == 1: 394 X_train = X_train.reshape((-1, 1)) 395 X_test = X_test.reshape((-1, 1)) 396 397 # Get mean preds for timings 398 return self.rf_obj.fit(X_train, y_train).predict(X_test) 399 400 # find next parameter by using expected improvement (ei) 401 def next_parameter_by_acq(self, i, acq="ei"): 402 if acq == "ei": 403 if self.posterior_ == "gaussian": 404 gamma_hat = (self.y_min - self.y_mean) / self.y_std 405 self.acq = -self.y_std * ( 406 gamma_hat * st.norm.cdf(gamma_hat) + st.norm.pdf(gamma_hat) 407 ) 408 elif self.posterior_ == "mc": 409 self.acq = -np.mean( 410 np.maximum(self.y_min - self.y_sims, 0), axis=1 411 ) 412 413 if acq == "ucb": 414 if self.posterior_ == "gaussian": 415 self.acq = self.y_mean - 1.96 * self.y_std 416 self.ucb = self.y_mean + 1.96 * self.y_std 417 418 elif self.posterior_ is None: # split conformal(ized) estimator 419 self.acq = self.y_lower 420 self.ucb = self.y_upper 421 422 # find max index ----- 423 424 if self.per_second is False: 425 # find index for max. ei 426 max_index = self.acq.argmin() 427 428 else: # self.per_second is True 429 # predict timings on self.x_choices 430 # train on X = self.parameters and y = self.timings 431 # (must have same shape[0]) 432 timing_preds = self.timings_fit_predict( 433 X_train=np.asarray(self.parameters), 434 y_train=np.asarray(self.timings), 435 X_test=self.x_choices, 436 ) 437 438 # find index for max. ei (and min. timings) 439 max_index = (-self.acq / timing_preds).argmax() 440 441 self.max_acq.append(np.abs(self.acq[max_index])) 442 443 # Select next choice 444 next_param = self.x_choices[max_index, :] 445 446 if next_param in np.asarray(self.parameters): 447 if self.log_scale == False: 448 np.random.seed(self.seed * i + 1000) 449 next_param = ( 450 self.upper_bound - self.lower_bound 451 ) * np.random.rand(self.n_dims) + self.lower_bound 452 453 else: # /!\ very... experimental 454 np.random.seed(self.seed) 455 log_upper_bound = np.log(self.upper_bound) 456 log_lower_bound = np.log(self.lower_bound) 457 log_bounds_range = log_upper_bound - log_lower_bound 458 459 next_param = np.minimum( 460 np.exp( 461 log_bounds_range * np.random.rand(self.n_dims) 462 + log_lower_bound 463 ), 464 1.7976931348623157e308, 465 ) 466 467 return next_param 468 469 # optimize the objective 470 def optimize( 471 self, 472 verbose=1, 473 n_more_iter=None, 474 abs_tol=None, # suggested 1e-4, for n_iter = 200 475 ucb_tol=None, 476 min_budget=50, # minimum budget for early stopping 477 func_args=None, 478 param_search_init_design=False, 479 param_distributions=None, 480 ): 481 """Launch optimization loop. 482 483 # Arguments: 484 485 verbose: an integer; 486 verbose = 0: nothing is printed, 487 verbose = 1: a progress bar is printed (longer than 0), 488 verbose = 2: information about each iteration is printed (longer than 1) 489 490 n_more_iter: an integer; 491 additional number of iterations for the optimizer (which has been run once) 492 493 abs_tol: a float; 494 tolerance for convergence of the optimizer (early stopping based on acquisition function) 495 496 ucb_tol: a float; 497 tolerance for convergence of the optimizer (early stopping based on length of prediction intervals) 498 for UCB criterion 499 500 min_budget: an integer (default is 50); 501 minimum number of iterations before early stopping controlled by `abs_tol` 502 503 func_args: a list; 504 additional parameters for the objective function (if necessary) 505 506 param_search_init_design: a boolean; 507 whether random search tuning must occur on the initial design or not 508 509 param_distributions: dict or list of dicts; 510 Dictionary with parameters names (str) as keys and distributions or lists of 511 parameters to try. Distributions must provide a rvs method for sampling 512 (such as those from scipy.stats.distributions). If a list is given, it 513 is sampled uniformly. If a list of dicts is given, first a dict is sampled 514 uniformly, and then a parameter is sampled using that dict as above. 515 516 see also [Bayesian Optimization with GPopt](https://thierrymoudiki.github.io/blog/2021/04/16/python/misc/gpopt) 517 and [Hyperparameters tuning with GPopt](https://thierrymoudiki.github.io/blog/2021/06/11/python/misc/hyperparam-tuning-gpopt) 518 519 """ 520 521 # verbose = 0: nothing is printed 522 # verbose = 1: a progress bar is printed (longer than 0) 523 # verbose = 2: information about each iteration is printed (longer than 1) 524 if func_args is None: 525 func_args = [] 526 527 if ( 528 n_more_iter is None 529 ): # initial optimization, before more iters are requested 530 n_iter = self.n_iter 531 # stopping iter for early stopping (default is total budget) 532 iter_stop = n_iter # potentially # got to check this 533 534 # initial design ---------- 535 536 if (verbose == 1) | (verbose == 2): 537 print(f"\n Creating initial design... \n") 538 539 if verbose == 1: 540 progbar = Progbar(target=self.n_init) 541 542 self.parameters = self.x_init.tolist() 543 self.scores = [] 544 545 if self.save is not None: 546 self.update_shelve() 547 548 if self.y_init is None: # calculate scores on initial design 549 assert ( 550 self.objective_func is not None 551 ), "self.y_init is None: must have 'objective_func' not None" 552 553 if self.n_jobs == 1: 554 for i in range(self.n_init): 555 x_next = self.x_init[i, :] 556 557 try: 558 if self.per_second is True: 559 start = time() 560 score = self.objective_func(x_next, *func_args) 561 if (np.isfinite(score) == False) or ( 562 np.isnan(score) == True 563 ): 564 continue 565 self.timings.append(np.log(time() - start)) 566 567 else: # self.per_second is False 568 score = self.objective_func(x_next, *func_args) 569 if (np.isfinite(score) == False) or ( 570 np.isnan(score) == True 571 ): 572 continue 573 574 self.scores.append(score) 575 576 if self.save is not None: 577 self.update_shelve() 578 579 except: 580 continue 581 582 if verbose == 1: 583 progbar.update(i) # update progress bar 584 585 if verbose == 2: 586 print(f"point: {x_next}; score: {score}") 587 # end loop # calculate scores on initial design 588 589 if verbose == 1: 590 progbar.update(self.n_init) 591 592 else: # self.n_jobs != 1 593 assert ( 594 self.per_second is False 595 ), "timings not calculated here" 596 597 scores = Parallel(n_jobs=self.n_jobs, prefer="threads")( 598 delayed(self.objective_func)(self.x_init[i, :]) 599 for i in range(self.n_init) 600 ) 601 602 self.scores = scores 603 604 if self.save is not None: 605 self.update_shelve() 606 607 else: # if self.y_init is not None: 608 assert self.x_init.shape[0] == len( 609 self.y_init 610 ), "must have: self.x_init.shape[0] == len(self.y_init)" 611 612 self.scores = pickle.loads( 613 pickle.dumps(self.y_init.tolist(), -1) 614 ) 615 616 # current best score on initial design 617 min_index = (np.asarray(self.scores)).argmin() 618 self.y_min = self.scores[min_index] 619 self.x_min = self.x_init[min_index, :] 620 621 # current gp mean and std on initial design 622 # /!\ if GP 623 if param_search_init_design == False: 624 if self.method == "bayesian": 625 self.posterior_ = "gaussian" 626 try: 627 y_mean, y_std = self.surrogate_fit_predict( 628 np.asarray(self.parameters), 629 np.asarray(self.scores), 630 self.x_choices, 631 return_std=True, 632 return_pi=False, 633 ) 634 except ValueError: # do not remove this 635 preds_with_std = self.surrogate_fit_predict( 636 np.asarray(self.parameters), 637 np.asarray(self.scores), 638 self.x_choices, 639 return_std=True, 640 return_pi=False, 641 ) 642 y_mean, y_std = preds_with_std[0], preds_with_std[1] 643 self.y_mean = y_mean 644 self.y_std = np.maximum(2.220446049250313e-16, y_std) 645 646 elif self.method == "mc": 647 self.posterior_ = "mc" 648 assert self.surrogate_obj.__class__.__name__.startswith( 649 "CustomRegressor" 650 ) or self.surrogate_obj.__class__.__name__.startswith( 651 "PredictionInterval" 652 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() or nnetsauce.PredictionInterval()" 653 assert ( 654 self.surrogate_obj.replications is not None 655 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() with a number of 'replications' provided" 656 preds_with_std = self.surrogate_fit_predict( 657 np.asarray(self.parameters), 658 np.asarray(self.scores), 659 self.x_choices, 660 return_std=False, 661 return_pi=True, 662 ) 663 y_mean, y_std = preds_with_std[0], preds_with_std[1] 664 self.y_mean = y_mean 665 self.y_std = np.maximum(2.220446049250313e-16, y_std) 666 667 elif self.method == "splitconformal": 668 self.posterior_ = None 669 # assert self.surrogate_obj.__class__.__name__.startswith( 670 # "PredictionInterval" 671 # ), "for `method = 'splitconformal'`, the surrogate must be a nnetsauce.PredictionInterval()" 672 preds_with_pi = self.surrogate_fit_predict( 673 np.asarray(self.parameters), 674 np.asarray(self.scores), 675 self.x_choices, 676 return_std=False, 677 return_pi=True, 678 ) 679 y_lower = preds_with_pi[1] 680 self.lower = y_lower 681 682 else: 683 assert ( 684 param_distributions is not None 685 ), "When 'param_search_init_design == False', 'param_distributions' must be provided" 686 687 if self.method == "bayesian": 688 self.posterior_ = "gaussian" 689 try: 690 y_mean, y_std = self.surrogate_fit_predict( 691 np.asarray(self.parameters), 692 np.asarray(self.scores), 693 self.x_choices, 694 return_std=True, 695 return_pi=False, 696 param_search_init_design=True, 697 param_distributions=param_distributions, 698 ) 699 except ValueError: # do not remove this 700 preds_with_std = self.surrogate_fit_predict( 701 np.asarray(self.parameters), 702 np.asarray(self.scores), 703 self.x_choices, 704 return_std=True, 705 return_pi=False, 706 param_search_init_design=True, 707 param_distributions=param_distributions, 708 ) 709 y_mean, y_std = preds_with_std[0], preds_with_std[1] 710 self.y_mean = y_mean 711 self.y_std = np.maximum(2.220446049250313e-16, y_std) 712 713 elif self.method == "mc": 714 self.posterior_ = "mc" 715 assert self.surrogate_obj.__class__.__name__.startswith( 716 "CustomRegressor" 717 ) or self.surrogate_obj.__class__.__name__.startswith( 718 "PredictionInterval" 719 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() or nnetsauce.PredictionInterval()" 720 assert ( 721 self.surrogate_obj.replications is not None 722 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() with a number of 'replications' provided" 723 preds_with_std = self.surrogate_fit_predict( 724 np.asarray(self.parameters), 725 np.asarray(self.scores), 726 self.x_choices, 727 return_std=False, 728 return_pi=True, 729 param_search_init_design=True, 730 param_distributions=param_distributions, 731 ) 732 y_mean, y_std = preds_with_std[0], preds_with_std[1] 733 self.y_mean = y_mean 734 self.y_std = np.maximum(2.220446049250313e-16, y_std) 735 736 elif self.method == "splitconformal": 737 self.posterior_ = None 738 # assert self.surrogate_obj.__class__.__name__.startswith( 739 # "PredictionInterval" 740 # ), "for `method = 'splitconformal'`, the surrogate must be a nnetsauce.PredictionInterval()" 741 preds_with_pi = self.surrogate_fit_predict( 742 np.asarray(self.parameters), 743 np.asarray(self.scores), 744 self.x_choices, 745 return_std=False, 746 return_pi=True, 747 param_search_init_design=True, 748 param_distributions=param_distributions, 749 ) 750 y_lower = preds_with_pi[1] 751 self.lower = y_lower 752 753 # saving after initial design computation 754 if self.save is not None: 755 self.update_shelve() 756 757 else: # if n_more_iter is not None 758 assert self.n_iter > 5, "you must have n_iter > 5" 759 n_iter = n_more_iter 760 iter_stop = len(self.max_acq) + n_more_iter # potentially 761 762 if (verbose == 1) | (verbose == 2): 763 print(f"\n ...Done. \n") 764 try: 765 print(np.hstack((self.x_init, self.y_init.reshape(-1, 1)))) 766 except: 767 pass 768 769 # end init design ---------- 770 771 # if n_more_iter is None: # initial optimization, before more iters are requested 772 773 if (verbose == 1) | (verbose == 2): 774 print(f"\n Optimization loop... \n") 775 776 # early stopping? 777 if abs_tol is not None: 778 assert ( 779 min_budget > 20 780 ), "With 'abs_tol' provided, you must have 'min_budget' > 20" 781 self.abs_tol = abs_tol 782 783 if ucb_tol is not None: 784 assert ( 785 min_budget > 20 786 ), "With 'ucb_tol' provided, you must have 'min_budget' > 20" 787 assert ( 788 self.acquisition == "ucb" 789 ), "With 'ucb_tol' provided, you must have 'acquisition' == 'ucb'" 790 self.ucb_tol = ucb_tol 791 792 if verbose == 1: 793 progbar = Progbar(target=n_iter) 794 795 # main loop ---------- 796 797 for i in range(n_iter): 798 # find next set of parameters (vector), maximizing acquisition function 799 next_param = self.next_parameter_by_acq(i=i, acq=self.acquisition) 800 801 try: 802 if self.per_second is True: 803 start = time() 804 805 if self.objective_func is not None: 806 score_next_param = self.objective_func( 807 next_param, *func_args 808 ) 809 810 if (np.isfinite(score_next_param) == False) or ( 811 np.isnan(score_next_param) == True 812 ): 813 continue 814 815 else: 816 assert (self.x_init is not None) and ( 817 self.y_init is not None 818 ), "self.objective_func is not None: must have (self.x_init is not None) and (self.y_init is not None)" 819 820 print(f"\n next param: {next_param} \n") 821 score_next_param = float( 822 input("get new score: \n") 823 ) # or an API response 824 825 if (np.isfinite(score_next_param) == False) or ( 826 np.isnan(score_next_param) == True 827 ): 828 continue 829 830 self.timings.append(np.log(time() - start)) 831 832 else: # self.per_second is False: 833 if self.objective_func is not None: 834 score_next_param = self.objective_func( 835 next_param, *func_args 836 ) 837 838 if (np.isfinite(score_next_param) == False) or ( 839 np.isnan(score_next_param) == True 840 ): 841 continue 842 843 else: 844 assert (self.x_init is not None) and ( 845 self.y_init is not None 846 ), "self.objective_func is not None: must have (self.x_init is not None) and (self.y_init is not None)" 847 848 print(f"\n next param: {next_param} \n") 849 score_next_param = float( 850 input("get new score: \n") 851 ) # or an API response 852 853 if (np.isfinite(score_next_param) == False) or ( 854 np.isnan(score_next_param) == True 855 ): 856 continue 857 858 except: 859 continue 860 861 self.parameters.append(next_param.tolist()) 862 863 self.scores.append(score_next_param) 864 865 if self.save is not None: 866 self.update_shelve() 867 868 if verbose == 2: 869 print(f"iteration {i + 1} -----") 870 print(f"current minimum: {self.x_min}") 871 print(f"current minimum score: {self.y_min}") 872 print(f"next parameter: {next_param}") 873 print(f"score for next parameter: {score_next_param} \n") 874 875 if score_next_param < self.y_min: 876 self.x_min = next_param 877 self.y_min = score_next_param 878 if self.save is not None: 879 self.update_shelve() 880 if self.y_min == self.min_value: 881 break 882 883 if self.posterior_ == "gaussian" and self.method == "bayesian": 884 try: 885 self.y_mean, self.y_std = self.surrogate_fit_predict( 886 np.asarray(self.parameters), 887 np.asarray(self.scores), 888 self.x_choices, 889 return_std=True, 890 return_pi=False, 891 ) 892 except: 893 ( 894 self.y_mean, 895 self.y_std, 896 lower, 897 upper, 898 ) = self.surrogate_fit_predict( 899 np.asarray(self.parameters), 900 np.asarray(self.scores), 901 self.x_choices, 902 return_std=True, 903 return_pi=False, 904 ) 905 906 elif self.posterior_ in (None, "mc") and self.method in ( 907 "mc", 908 "splitconformal", 909 ): 910 ( 911 self.y_mean, 912 self.y_lower, 913 self.y_upper, 914 ) = self.surrogate_fit_predict( 915 np.asarray(self.parameters), 916 np.asarray(self.scores), 917 self.x_choices, 918 return_std=False, 919 return_pi=True, 920 ) 921 922 else: 923 return NotImplementedError 924 925 if self.save is not None: 926 self.update_shelve() 927 928 if verbose == 1: 929 progbar.update(i + 1) # update progress bar 930 931 # early stopping 932 933 if abs_tol is not None: 934 # if self.max_acq.size > (self.n_init + self.n_iter * min_budget_pct): 935 if len(self.max_acq) > min_budget: 936 diff_max_acq = np.abs(np.diff(np.asarray(self.max_acq))) 937 938 if diff_max_acq[-1] <= abs_tol: 939 iter_stop = len(self.max_acq) # index i starts at 0 940 941 break 942 943 if ucb_tol is not None: 944 if len(self.max_acq) > min_budget: 945 # print(f"self.ucb: {self.ucb}") 946 # print(f"self.acq: {self.acq}") 947 # print(f"mean(self.ucb/self.acq): {np.mean(self.ucb/self.acq)/100}") 948 949 if ( 950 np.abs(np.mean(self.ucb / self.acq) / 100) <= ucb_tol 951 ): # self.ucb is the upper confidence bound for UCB criterion 952 iter_stop = len(self.max_acq) 953 954 break 955 956 # end main loop ---------- 957 958 if (verbose == 1) & (i < (n_iter - 1)): 959 progbar.update(n_iter) 960 961 self.n_iter = iter_stop 962 if self.save is not None: 963 self.update_shelve() 964 965 DescribeResult = namedtuple( 966 "DescribeResult", ("best_params", "best_score") 967 ) 968 969 if self.params_names is None: 970 return DescribeResult(self.x_min, self.y_min) 971 972 else: 973 return DescribeResult( 974 dict(zip(self.params_names, self.x_min)), self.y_min 975 ) 976 977 # optimize the objective 978 def lazyoptimize( 979 self, 980 verbose=1, 981 abs_tol=None, # suggested 1e-4, for n_iter = 200 982 ucb_tol=None, 983 min_budget=50, # minimum budget for early stopping 984 func_args=None, 985 estimators="all", 986 type_pi="kde", # for now, 'kde', 'bootstrap', 'splitconformal' 987 type_exec="independent", # "queue" or "independent" (default) 988 ): 989 """Launch optimization loop. 990 991 # Arguments: 992 993 verbose: an integer; 994 verbose = 0: nothing is printed, 995 verbose = 1: a progress bar is printed (longer than 0), 996 verbose = 2: information about each iteration is printed (longer than 1) 997 998 n_more_iter: an integer; 999 additional number of iterations for the optimizer (which has been run once) 1000 1001 abs_tol: a float; 1002 tolerance for convergence of the optimizer (early stopping based on expected improvement) 1003 1004 ucb_tol: a float; 1005 tolerance for convergence of the optimizer (early stopping based on length of prediction intervals) 1006 1007 min_budget: an integer (default is 50); 1008 minimum number of iterations before early stopping controlled by `abs_tol` 1009 1010 func_args: a list; 1011 additional parameters for the objective function (if necessary) 1012 1013 estimators: an str or a list of strs (estimators names) 1014 if "all", then 30 models are fitted. Otherwise, only those provided in the list 1015 are adjusted; for example ["RandomForestRegressor", "Ridge"] 1016 1017 type_pi: an str; 1018 "kde" (default) or, "splitconformal"; type of prediction intervals for the surrogate 1019 model 1020 1021 type_exec: an str; 1022 "independent" (default) is when surrogate models are adjusted independently on 1023 the same design set and the best model is chosen eventually; "queue" is when 1024 surrogate models are adjusted one after the other, on a design set with 1025 increasing size; 1026 1027 """ 1028 1029 # Base case: Gaussian Process 1030 gp_opt_obj = GPOpt( 1031 objective_func=self.objective_func, 1032 lower_bound=self.lower_bound, 1033 upper_bound=self.upper_bound, 1034 n_init=self.n_init, 1035 n_iter=self.n_iter, 1036 alpha=self.alpha, 1037 n_restarts_optimizer=self.n_restarts_optimizer, 1038 seed=self.seed, 1039 n_jobs=self.n_jobs, 1040 acquisition="ei", 1041 surrogate_obj=GaussianProcessRegressor( 1042 kernel=Matern(nu=2.5), 1043 alpha=self.alpha, 1044 normalize_y=True, 1045 n_restarts_optimizer=self.n_restarts_optimizer, 1046 random_state=self.seed, 1047 ), 1048 ) 1049 if verbose == 2: 1050 print( 1051 f"\n adjusting surrogate model # {0} (GaussianProcessRegressor(Matern(5/2)))... \n" 1052 ) 1053 res_base = gp_opt_obj.optimize( 1054 verbose=verbose, 1055 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1056 min_budget=min_budget, # minimum budget for early stopping 1057 func_args=func_args, 1058 ) 1059 1060 if estimators == "all": 1061 if type_pi == "kde": 1062 self.regressors = REGRESSORS 1063 1064 else: 1065 self.regressors = [ 1066 ( 1067 est[0], 1068 ns.PredictionInterval( 1069 est[1](), type_pi="splitconformal" 1070 ), 1071 ) 1072 for est in all_estimators() 1073 if ( 1074 issubclass(est[1], RegressorMixin) 1075 and (est[0] not in REMOVED_REGRESSORS) 1076 ) 1077 ] 1078 1079 else: 1080 if type_pi == "kde": 1081 self.regressors = [ 1082 ( 1083 "CustomRegressor(" + est[0] + ")", 1084 ns.CustomRegressor( 1085 est[1](), replications=150, type_pi=type_pi 1086 ), 1087 ) 1088 for est in all_estimators() 1089 if ( 1090 issubclass(est[1], RegressorMixin) 1091 and (est[0] not in REMOVED_REGRESSORS) 1092 and (est[0] in estimators) 1093 ) 1094 ] 1095 1096 elif type_pi == "splitconformal": 1097 self.regressors = [ 1098 ( 1099 est[0], 1100 ns.PredictionInterval( 1101 est[1](), type_pi="splitconformal" 1102 ), 1103 ) 1104 for est in all_estimators() 1105 if ( 1106 issubclass(est[1], RegressorMixin) 1107 and (est[0] not in REMOVED_REGRESSORS) 1108 and (est[0] in estimators) 1109 ) 1110 ] 1111 1112 df_res = pd.DataFrame( 1113 np.empty((len(self.regressors) + 1, 2)), columns=["Model", "Score"] 1114 ) 1115 df_res.iloc[0, 0] = "GaussianProcessRegressor" 1116 df_res.iloc[0, 1] = res_base.best_score 1117 1118 self.surrogate_fit_predict = partial( 1119 self.surrogate_fit_predict, return_pi=True 1120 ) 1121 1122 if ( 1123 type_exec == "queue" 1124 ): # when models are adjusted one after the other on a design set with increasing size 1125 self.x_min = None 1126 1127 self.y_min = np.inf 1128 1129 score_next_param = np.inf 1130 1131 DescribeResult = namedtuple( 1132 "DescribeResult", ("best_params", "best_score", "scores") 1133 ) 1134 1135 if verbose == 2: 1136 print( 1137 f"\n adjusting surrogate model # {1} ({self.regressors[0][0]})... \n" 1138 ) 1139 1140 df_res.iloc[0, 0] = self.regressors[0][0] 1141 1142 gp_opt_obj_prev = GPOpt( 1143 objective_func=self.objective_func, 1144 lower_bound=self.lower_bound, 1145 upper_bound=self.upper_bound, 1146 n_init=self.n_init, 1147 n_iter=self.n_iter, 1148 alpha=self.alpha, 1149 n_restarts_optimizer=self.n_restarts_optimizer, 1150 seed=self.seed, 1151 n_jobs=self.n_jobs, 1152 acquisition=self.acquisition, 1153 method=self.method, 1154 min_value=self.min_value, 1155 surrogate_obj=copy.deepcopy(self.regressors[0][1]), 1156 ) 1157 1158 gp_opt_obj_prev.optimize( 1159 verbose=verbose, 1160 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1161 ucb_tol=ucb_tol, 1162 min_budget=min_budget, # minimum budget for early stopping 1163 func_args=func_args, 1164 ) 1165 1166 score_next_param = gp_opt_obj_prev.y_min 1167 1168 df_res.iloc[0, 1] = score_next_param 1169 1170 if self.n_jobs is None: # sequential optimization 1171 for i in range(len(self.regressors)): 1172 try: 1173 if verbose == 2: 1174 print( 1175 f"\n adjusting surrogate model # {i + 2} ({self.regressors[i][0]})... \n" 1176 ) 1177 1178 df_res["Model"][i + 1] = self.regressors[i][0] 1179 1180 gp_opt_obj = GPOpt( 1181 objective_func=self.objective_func, 1182 lower_bound=self.lower_bound, 1183 upper_bound=self.upper_bound, 1184 n_init=self.n_init, 1185 n_iter=self.n_iter, 1186 alpha=self.alpha, 1187 n_restarts_optimizer=self.n_restarts_optimizer, 1188 seed=self.seed, 1189 n_jobs=self.n_jobs, 1190 acquisition=self.acquisition, 1191 method=self.method, 1192 min_value=self.min_value, 1193 surrogate_obj=copy.deepcopy(self.regressors[i][1]), 1194 x_init=np.asarray(gp_opt_obj_prev.parameters), 1195 y_init=np.asarray(gp_opt_obj_prev.scores), 1196 ) 1197 1198 gp_opt_obj.optimize( 1199 verbose=verbose, 1200 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1201 ucb_tol=ucb_tol, 1202 min_budget=min_budget, # minimum budget for early stopping 1203 func_args=func_args, 1204 ) 1205 1206 score_next_param = gp_opt_obj.y_min 1207 1208 df_res.iloc[i, 1] = score_next_param 1209 1210 if score_next_param < self.y_min: 1211 self.x_min = gp_opt_obj.x_min 1212 self.y_min = score_next_param 1213 if self.y_min == self.min_value: 1214 break 1215 1216 if verbose == 2: 1217 print(f"Global iteration #{i + 1} -----") 1218 print(f"current minimum: {self.x_min}") 1219 print(f"current minimum score: {self.y_min}") 1220 print( 1221 f"score for next parameter: {score_next_param} \n" 1222 ) 1223 1224 gp_opt_obj_prev = copy.deepcopy(gp_opt_obj) 1225 1226 except ValueError: 1227 continue 1228 1229 elif self.n_jobs >= 2 or self.n_jobs == -1: # parallel optimization 1230 pass 1231 else: 1232 raise ValueError( 1233 "n_jobs must be either None or >= 2 or equal to -1" 1234 ) 1235 return DescribeResult( 1236 self.x_min, self.y_min, df_res.sort_values(by="Score") 1237 ) 1238 1239 elif ( 1240 type_exec == "independent" 1241 ): # when models are adjusted independently on the same design set and the best model is chosen eventually 1242 self.x_min = None 1243 1244 self.y_min = np.inf 1245 1246 score_next_param = np.inf 1247 1248 DescribeResult = namedtuple( 1249 "DescribeResult", 1250 ("best_params", "best_score", "best_surrogate", "scores"), 1251 ) 1252 1253 if verbose == 2: 1254 print( 1255 f"\n adjusting surrogate model # {1} ({self.regressors[0][0]})... \n" 1256 ) 1257 1258 if self.n_jobs is None: # sequential optimization 1259 for i in range(len(self.regressors)): 1260 # try: 1261 1262 if verbose == 2: 1263 print( 1264 f"\n adjusting surrogate model # {i + 1} ({self.regressors[i][0]})... \n" 1265 ) 1266 1267 df_res["Model"][i + 1] = self.regressors[i][0] 1268 1269 gp_opt_obj = GPOpt( 1270 objective_func=self.objective_func, 1271 lower_bound=self.lower_bound, 1272 upper_bound=self.upper_bound, 1273 n_init=self.n_init, 1274 n_iter=self.n_iter, 1275 alpha=self.alpha, 1276 n_restarts_optimizer=self.n_restarts_optimizer, 1277 seed=self.seed, 1278 n_jobs=self.n_jobs, 1279 acquisition=self.acquisition, 1280 method=self.method, 1281 min_value=self.min_value, 1282 surrogate_obj=copy.deepcopy(self.regressors[i][1]), 1283 ) 1284 1285 gp_opt_obj.optimize( 1286 verbose=verbose, 1287 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1288 ucb_tol=ucb_tol, 1289 min_budget=min_budget, # minimum budget for early stopping 1290 func_args=func_args, 1291 ) 1292 1293 score_next_param = gp_opt_obj.y_min 1294 1295 df_res.iloc[i, 1] = score_next_param 1296 1297 if score_next_param < self.y_min: 1298 self.x_min = gp_opt_obj.x_min 1299 self.y_min = score_next_param 1300 self.best_surrogate = copy.deepcopy( 1301 gp_opt_obj.surrogate_obj 1302 ) 1303 if self.y_min == self.min_value: 1304 break 1305 1306 if verbose == 2: 1307 print(f"Global iteration #{i + 1} -----") 1308 print(f"current minimum: {self.x_min}") 1309 print(f"current minimum score: {self.y_min}") 1310 print( 1311 f"score for next parameter: {score_next_param} \n" 1312 ) 1313 1314 # except ValueError: 1315 1316 # continue 1317 1318 elif self.n_jobs >= 2 or self.n_jobs == -1: # parallel optimization 1319 1320 def foo(i): 1321 df_res["Model"][i + 1] = self.regressors[i][0] 1322 1323 gp_opt_obj = GPOpt( 1324 objective_func=self.objective_func, 1325 lower_bound=self.lower_bound, 1326 upper_bound=self.upper_bound, 1327 n_init=self.n_init, 1328 n_iter=self.n_iter, 1329 alpha=self.alpha, 1330 n_restarts_optimizer=self.n_restarts_optimizer, 1331 seed=self.seed, 1332 n_jobs=self.n_jobs, 1333 acquisition=self.acquisition, 1334 method=self.method, 1335 min_value=self.min_value, 1336 surrogate_obj=copy.deepcopy(self.regressors[i][1]), 1337 ) 1338 1339 try: 1340 gp_opt_obj.optimize( 1341 verbose=0, # important 1342 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1343 ucb_tol=ucb_tol, 1344 min_budget=min_budget, # minimum budget for early stopping 1345 func_args=func_args, 1346 ) 1347 1348 return gp_opt_obj 1349 except ValueError: 1350 return None 1351 1352 tmp_results = Parallel(n_jobs=self.n_jobs)( 1353 delayed(foo)(i) for i in tqdm(range(len(self.regressors))) 1354 ) 1355 1356 for i in tqdm(range(len(tmp_results))): 1357 if tmp_results[i] is not None: 1358 gp_opt_obj = copy.deepcopy(tmp_results[i]) 1359 1360 score_next_param = gp_opt_obj.y_min 1361 1362 df_res.iloc[i, 1] = score_next_param 1363 1364 if score_next_param < self.y_min: 1365 self.x_min = gp_opt_obj.x_min 1366 self.y_min = score_next_param 1367 self.best_surrogate = copy.deepcopy( 1368 gp_opt_obj.surrogate_obj 1369 ) 1370 if self.y_min == self.min_value: 1371 break 1372 1373 if verbose == 2: 1374 print(f"Global iteration #{i + 1} -----") 1375 print(f"current minimum: {self.x_min}") 1376 print(f"current minimum score: {self.y_min}") 1377 print( 1378 f"score for next parameter: {score_next_param} \n" 1379 ) 1380 1381 else: 1382 continue 1383 1384 else: 1385 raise ValueError( 1386 "n_jobs must be either None or >= 2 or equal to -1" 1387 ) 1388 return DescribeResult( 1389 self.x_min, 1390 self.y_min, 1391 self.best_surrogate, 1392 df_res.sort_values(by="Score"), 1393 ) 1394 1395 else: 1396 NotImplementedError( 1397 "type_exec must be either 'queue' or 'independent'" 1398 )
class
GPOpt:
36class GPOpt: 37 """Class GPOpt. 38 39 # Arguments: 40 41 lower_bound: a numpy array; 42 lower bound for researched minimum 43 44 upper_bound: a numpy array; 45 upper bound for researched minimum 46 47 objective_func: a function; 48 the objective function to be minimized 49 50 params_names: a list; 51 names of the parameters of the objective function (optional) 52 53 surrogate_obj: an object; 54 An ML model for estimating the uncertainty around the objective function 55 Must be nnetsauce.CustomRegressor or nnetsauce.PredictionInterval 56 57 x_init: 58 initial setting of points where `objective_func` is evaluated (optional) 59 60 y_init: 61 initial setting values at points where `objective_func` is evaluated (optional) 62 63 n_init: an integer; 64 number of points in the initial setting, when `x_init` and `y_init` are not provided 65 66 n_choices: an integer; 67 number of points for the calculation of expected improvement 68 69 n_iter: an integer; 70 number of iterations of the minimization algorithm 71 72 alpha: a float; 73 Value added to the diagonal of the kernel matrix during fitting (for Matern 5/2 kernel) 74 75 n_restarts_optimizer: an integer; 76 The number of restarts of the optimizer for finding the kernel’s parameters which maximize the log-marginal likelihood. 77 78 seed: an integer; 79 reproducibility seed 80 81 save: a string; 82 Specifies where to save the optimizer in its current state 83 84 n_jobs: an integer; 85 number of jobs for parallel computing on initial setting or method `lazyoptimize` (can be -1) 86 87 acquisition: a string; 88 acquisition function: "ei" (expected improvement) or "ucb" (upper confidence bound) 89 90 method: an str; 91 "bayesian" (default) for Gaussian posteriors, "mc" for Monte Carlo posteriors, 92 "splitconformal" (only for acquisition = "ucb") for conformalized surrogates 93 94 min_value: a float; 95 minimum value of the objective function (default is None). For example, 96 if objective function is accuracy, will be 1, and the algorithm will stop 97 98 per_second: a boolean; 99 __experimental__, default is False (leave to default for now) 100 101 log_scale: a boolean; 102 __experimental__, default is False (leave to default for now) 103 104 see also [Bayesian Optimization with GPopt](https://thierrymoudiki.github.io/blog/2021/04/16/python/misc/gpopt) 105 or [Hyperparameters tuning with GPopt](https://thierrymoudiki.github.io/blog/2021/06/11/python/misc/hyperparam-tuning-gpopt) [Agnostic BayesOpt](https://thierrymoudiki.github.io/blog/2024/12/09/python/bayesconfoptim) 106 107 """ 108 109 def __init__( 110 self, 111 lower_bound, 112 upper_bound, 113 objective_func=None, 114 params_names=None, 115 surrogate_obj=None, 116 x_init=None, 117 y_init=None, 118 n_init=10, 119 n_choices=100000, 120 n_iter=190, 121 alpha=1e-6, 122 n_restarts_optimizer=25, 123 seed=123, 124 save=None, 125 n_jobs=1, 126 acquisition="ei", 127 method="bayesian", 128 min_value=None, 129 per_second=False, # /!\ very experimental 130 log_scale=False, # /!\ experimental 131 ): 132 n_dims = len(lower_bound) 133 134 assert n_dims == len( 135 upper_bound 136 ), "'upper_bound' and 'lower_bound' must have the same dimensions" 137 138 self.objective_func = objective_func 139 self.params_names = params_names 140 self.lower_bound = lower_bound 141 self.upper_bound = upper_bound 142 self.y_init = y_init 143 self.log_scale = log_scale 144 self.n_dims = n_dims 145 self.n_init = n_init 146 self.n_choices = n_choices 147 self.n_iter = n_iter 148 self.alpha = alpha 149 self.n_restarts_optimizer = n_restarts_optimizer 150 self.seed = seed 151 self.save = save 152 self.n_jobs = n_jobs # for parallel case on initial design 153 self.per_second = per_second 154 self.x_min = None 155 self.y_min = None 156 self.y_mean = None 157 self.y_std = None 158 self.y_lower = None 159 self.y_upper = None 160 self.best_surrogate = None 161 self.acquisition = acquisition 162 self.min_value = min_value 163 self.acq = np.array([]) 164 self.max_acq = [] 165 if surrogate_obj is None: 166 self.surrogate_obj = GaussianProcessRegressor( 167 kernel=Matern(nu=2.5), 168 alpha=self.alpha, 169 normalize_y=True, 170 n_restarts_optimizer=self.n_restarts_optimizer, 171 random_state=self.seed, 172 ) 173 else: 174 self.surrogate_obj = surrogate_obj 175 assert method in ( 176 "bayesian", 177 "mc", 178 "splitconformal", 179 ), "method must be in ('bayesian', 'mc', 'splitconformal')" 180 self.method = method 181 self.posterior_ = None 182 183 # Sobol seqs for initial design and choices 184 sobol_seq_init = np.transpose( 185 generate_sobol2( 186 n_dims=self.n_dims, 187 n_points=self.n_init, 188 skip=2, 189 ) 190 ) 191 sobol_seq_choices = np.transpose( 192 generate_sobol2( 193 n_dims=self.n_dims, 194 n_points=self.n_choices, 195 skip=self.n_init + 2, 196 ) 197 ) 198 199 # Sobol seqs for initial design and choices with bounds 200 if self.log_scale == False: 201 bounds_range = upper_bound - lower_bound 202 self.x_init = ( 203 bounds_range * sobol_seq_init + lower_bound 204 if x_init is None 205 else x_init 206 ) 207 self.x_choices = bounds_range * sobol_seq_choices + lower_bound 208 209 else: # (!) experimental 210 assert ( 211 lower_bound > 0 212 ).all(), "all elements of `lower_bound` must be > 0" 213 assert ( 214 upper_bound > 0 215 ).all(), "all elements of `upper_bound` must be > 0" 216 217 log_lower_bound = np.log(lower_bound) 218 log_upper_bound = np.log(upper_bound) 219 log_bounds_range = log_upper_bound - log_lower_bound 220 self.x_init = ( 221 np.minimum( 222 np.exp(log_bounds_range * sobol_seq_init + log_lower_bound), 223 1.7976931348623157e308, 224 ) 225 if x_init is None 226 else x_init 227 ) 228 self.x_choices = np.minimum( 229 np.exp(log_bounds_range * sobol_seq_choices + log_lower_bound), 230 1.7976931348623157e308, 231 ) 232 233 # shelve for saving (not for loading) 234 if self.save is not None: 235 self.sh = shelve.open(filename=save, flag="c", writeback=True) 236 237 if self.per_second: 238 self.timings = [] 239 self.rf_obj = RandomForestRegressor( 240 n_estimators=250, random_state=self.seed 241 ) 242 243 self.n_jobs = n_jobs 244 245 # from sklearn.base 246 def get_params(self): 247 """Get object attributes. 248 249 Returns 250 ------- 251 params : mapping of string to any 252 Parameter names mapped to their values. 253 """ 254 out = dict() 255 param_names = dir(self) 256 for key in param_names: 257 if key.startswith("_") is False: 258 out[key] = getattr(self, key, None) 259 260 return out 261 262 # for parallel case on initial design 263 def eval_objective(self, arg): 264 try: 265 return self.objective_func(self.x_init[arg, :]) 266 except: 267 return 1e06 268 269 # load data from stored shelve 270 def load(self, path): 271 """load data from stored shelve. 272 273 # Arguments 274 275 path : a string; path to stored shelve. 276 277 See also: [Bayesian Optimization with GPopt Part 2 (save and resume)](https://thierrymoudiki.github.io/blog/2021/04/30/python/misc/gpopt) 278 """ 279 280 self.sh = shelve.open(filename=path) 281 for key, value in self.sh.items(): 282 setattr(self, key, value) 283 284 # update shelve in optimization loop 285 def update_shelve(self): 286 for key, value in self.get_params().items(): 287 if (callable(value) is False) & (key != "sh"): 288 self.sh[key] = value 289 self.sh.sync() 290 291 # closing shelve (can't be modified after) 292 def close_shelve(self): 293 """Close shelve. 294 295 # Arguments 296 297 No argument. 298 299 See also: [Bayesian Optimization with GPopt Part 2 (save and resume)](https://thierrymoudiki.github.io/blog/2021/04/30/python/misc/gpopt) 300 """ 301 302 self.sh.close() 303 304 # fit predict 305 def surrogate_fit_predict( 306 self, 307 X_train, 308 y_train, 309 X_test, 310 return_std=False, 311 return_pi=False, 312 param_search_init_design=False, 313 param_distributions=None, 314 **kwargs, 315 ): 316 if len(X_train.shape) == 1: 317 X_train = X_train.reshape((-1, 1)) 318 X_test = X_test.reshape((-1, 1)) 319 320 if ( 321 X_train.shape[0] <= self.n_init and param_search_init_design == True 322 ): # on initial design 323 try: 324 rs_obj = RandomizedSearchCV( 325 self.surrogate_obj, 326 param_distributions=param_distributions, 327 random_state=42, 328 cv=3, 329 **kwargs, 330 ) 331 rs_obj.fit(X_train, y_train) 332 self.surrogate_obj = rs_obj.best_estimator_ 333 except Exception as e: 334 print(str(e)) 335 336 # Get mean and standard deviation (+ lower and upper for not GPs) 337 assert ( 338 return_std == True and return_pi == True 339 ) == False, "must have either return_std == True or return_pi == True" 340 341 if return_std == True: 342 self.posterior_ = "gaussian" 343 self.surrogate_obj.fit(X_train, y_train) 344 return self.surrogate_obj.predict(X_test, return_std=True) 345 346 elif ( 347 return_pi == True 348 ): # here, self.surrogate_obj must have `replications` not None 349 if self.surrogate_obj.replications is not None: 350 self.posterior_ = "mc" 351 self.surrogate_obj.fit(X_train, y_train) 352 try: # it's a nnetsauce.CustomRegressor 353 res = self.surrogate_obj.predict( 354 X_test, return_pi=True, method="splitconformal" 355 ) 356 except Exception: # it's a nnetsauce.PredictionInterval 357 try: 358 res = self.surrogate_obj.predict(X_test, return_pi=True) 359 except Exception as e: 360 print( 361 str(e) 362 + "Sureproof way is to encapsulate your surrogate in nnetsauce.PredictionInterval model" 363 ) 364 365 self.y_sims = res.sims 366 self.y_mean, self.y_std = ( 367 np.mean(self.y_sims, axis=1), 368 np.std(self.y_sims, axis=1), 369 ) 370 return self.y_mean, self.y_std, self.y_sims 371 372 else: # self.surrogate_obj is conformalized (uses nnetsauce.PredictionInterval) 373 assert ( 374 self.acquisition == "ucb" 375 ), "'acquisition' must be 'ucb' for conformalized surrogates" 376 self.posterior_ = None 377 self.surrogate_obj.fit(X_train, y_train) 378 try: 379 res = self.surrogate_obj.predict( 380 X_test, return_pi=True, method="splitconformal" 381 ) 382 except Exception: 383 res = self.surrogate_obj.predict(X_test, return_pi=True) 384 self.y_mean = res.mean 385 self.y_lower = res.lower 386 self.y_upper = res.upper 387 return self.y_mean, self.y_lower, self.y_upper 388 389 else: 390 raise NotImplementedError 391 392 # fit predict timings 393 def timings_fit_predict(self, X_train, y_train, X_test): 394 if len(X_train.shape) == 1: 395 X_train = X_train.reshape((-1, 1)) 396 X_test = X_test.reshape((-1, 1)) 397 398 # Get mean preds for timings 399 return self.rf_obj.fit(X_train, y_train).predict(X_test) 400 401 # find next parameter by using expected improvement (ei) 402 def next_parameter_by_acq(self, i, acq="ei"): 403 if acq == "ei": 404 if self.posterior_ == "gaussian": 405 gamma_hat = (self.y_min - self.y_mean) / self.y_std 406 self.acq = -self.y_std * ( 407 gamma_hat * st.norm.cdf(gamma_hat) + st.norm.pdf(gamma_hat) 408 ) 409 elif self.posterior_ == "mc": 410 self.acq = -np.mean( 411 np.maximum(self.y_min - self.y_sims, 0), axis=1 412 ) 413 414 if acq == "ucb": 415 if self.posterior_ == "gaussian": 416 self.acq = self.y_mean - 1.96 * self.y_std 417 self.ucb = self.y_mean + 1.96 * self.y_std 418 419 elif self.posterior_ is None: # split conformal(ized) estimator 420 self.acq = self.y_lower 421 self.ucb = self.y_upper 422 423 # find max index ----- 424 425 if self.per_second is False: 426 # find index for max. ei 427 max_index = self.acq.argmin() 428 429 else: # self.per_second is True 430 # predict timings on self.x_choices 431 # train on X = self.parameters and y = self.timings 432 # (must have same shape[0]) 433 timing_preds = self.timings_fit_predict( 434 X_train=np.asarray(self.parameters), 435 y_train=np.asarray(self.timings), 436 X_test=self.x_choices, 437 ) 438 439 # find index for max. ei (and min. timings) 440 max_index = (-self.acq / timing_preds).argmax() 441 442 self.max_acq.append(np.abs(self.acq[max_index])) 443 444 # Select next choice 445 next_param = self.x_choices[max_index, :] 446 447 if next_param in np.asarray(self.parameters): 448 if self.log_scale == False: 449 np.random.seed(self.seed * i + 1000) 450 next_param = ( 451 self.upper_bound - self.lower_bound 452 ) * np.random.rand(self.n_dims) + self.lower_bound 453 454 else: # /!\ very... experimental 455 np.random.seed(self.seed) 456 log_upper_bound = np.log(self.upper_bound) 457 log_lower_bound = np.log(self.lower_bound) 458 log_bounds_range = log_upper_bound - log_lower_bound 459 460 next_param = np.minimum( 461 np.exp( 462 log_bounds_range * np.random.rand(self.n_dims) 463 + log_lower_bound 464 ), 465 1.7976931348623157e308, 466 ) 467 468 return next_param 469 470 # optimize the objective 471 def optimize( 472 self, 473 verbose=1, 474 n_more_iter=None, 475 abs_tol=None, # suggested 1e-4, for n_iter = 200 476 ucb_tol=None, 477 min_budget=50, # minimum budget for early stopping 478 func_args=None, 479 param_search_init_design=False, 480 param_distributions=None, 481 ): 482 """Launch optimization loop. 483 484 # Arguments: 485 486 verbose: an integer; 487 verbose = 0: nothing is printed, 488 verbose = 1: a progress bar is printed (longer than 0), 489 verbose = 2: information about each iteration is printed (longer than 1) 490 491 n_more_iter: an integer; 492 additional number of iterations for the optimizer (which has been run once) 493 494 abs_tol: a float; 495 tolerance for convergence of the optimizer (early stopping based on acquisition function) 496 497 ucb_tol: a float; 498 tolerance for convergence of the optimizer (early stopping based on length of prediction intervals) 499 for UCB criterion 500 501 min_budget: an integer (default is 50); 502 minimum number of iterations before early stopping controlled by `abs_tol` 503 504 func_args: a list; 505 additional parameters for the objective function (if necessary) 506 507 param_search_init_design: a boolean; 508 whether random search tuning must occur on the initial design or not 509 510 param_distributions: dict or list of dicts; 511 Dictionary with parameters names (str) as keys and distributions or lists of 512 parameters to try. Distributions must provide a rvs method for sampling 513 (such as those from scipy.stats.distributions). If a list is given, it 514 is sampled uniformly. If a list of dicts is given, first a dict is sampled 515 uniformly, and then a parameter is sampled using that dict as above. 516 517 see also [Bayesian Optimization with GPopt](https://thierrymoudiki.github.io/blog/2021/04/16/python/misc/gpopt) 518 and [Hyperparameters tuning with GPopt](https://thierrymoudiki.github.io/blog/2021/06/11/python/misc/hyperparam-tuning-gpopt) 519 520 """ 521 522 # verbose = 0: nothing is printed 523 # verbose = 1: a progress bar is printed (longer than 0) 524 # verbose = 2: information about each iteration is printed (longer than 1) 525 if func_args is None: 526 func_args = [] 527 528 if ( 529 n_more_iter is None 530 ): # initial optimization, before more iters are requested 531 n_iter = self.n_iter 532 # stopping iter for early stopping (default is total budget) 533 iter_stop = n_iter # potentially # got to check this 534 535 # initial design ---------- 536 537 if (verbose == 1) | (verbose == 2): 538 print(f"\n Creating initial design... \n") 539 540 if verbose == 1: 541 progbar = Progbar(target=self.n_init) 542 543 self.parameters = self.x_init.tolist() 544 self.scores = [] 545 546 if self.save is not None: 547 self.update_shelve() 548 549 if self.y_init is None: # calculate scores on initial design 550 assert ( 551 self.objective_func is not None 552 ), "self.y_init is None: must have 'objective_func' not None" 553 554 if self.n_jobs == 1: 555 for i in range(self.n_init): 556 x_next = self.x_init[i, :] 557 558 try: 559 if self.per_second is True: 560 start = time() 561 score = self.objective_func(x_next, *func_args) 562 if (np.isfinite(score) == False) or ( 563 np.isnan(score) == True 564 ): 565 continue 566 self.timings.append(np.log(time() - start)) 567 568 else: # self.per_second is False 569 score = self.objective_func(x_next, *func_args) 570 if (np.isfinite(score) == False) or ( 571 np.isnan(score) == True 572 ): 573 continue 574 575 self.scores.append(score) 576 577 if self.save is not None: 578 self.update_shelve() 579 580 except: 581 continue 582 583 if verbose == 1: 584 progbar.update(i) # update progress bar 585 586 if verbose == 2: 587 print(f"point: {x_next}; score: {score}") 588 # end loop # calculate scores on initial design 589 590 if verbose == 1: 591 progbar.update(self.n_init) 592 593 else: # self.n_jobs != 1 594 assert ( 595 self.per_second is False 596 ), "timings not calculated here" 597 598 scores = Parallel(n_jobs=self.n_jobs, prefer="threads")( 599 delayed(self.objective_func)(self.x_init[i, :]) 600 for i in range(self.n_init) 601 ) 602 603 self.scores = scores 604 605 if self.save is not None: 606 self.update_shelve() 607 608 else: # if self.y_init is not None: 609 assert self.x_init.shape[0] == len( 610 self.y_init 611 ), "must have: self.x_init.shape[0] == len(self.y_init)" 612 613 self.scores = pickle.loads( 614 pickle.dumps(self.y_init.tolist(), -1) 615 ) 616 617 # current best score on initial design 618 min_index = (np.asarray(self.scores)).argmin() 619 self.y_min = self.scores[min_index] 620 self.x_min = self.x_init[min_index, :] 621 622 # current gp mean and std on initial design 623 # /!\ if GP 624 if param_search_init_design == False: 625 if self.method == "bayesian": 626 self.posterior_ = "gaussian" 627 try: 628 y_mean, y_std = self.surrogate_fit_predict( 629 np.asarray(self.parameters), 630 np.asarray(self.scores), 631 self.x_choices, 632 return_std=True, 633 return_pi=False, 634 ) 635 except ValueError: # do not remove this 636 preds_with_std = self.surrogate_fit_predict( 637 np.asarray(self.parameters), 638 np.asarray(self.scores), 639 self.x_choices, 640 return_std=True, 641 return_pi=False, 642 ) 643 y_mean, y_std = preds_with_std[0], preds_with_std[1] 644 self.y_mean = y_mean 645 self.y_std = np.maximum(2.220446049250313e-16, y_std) 646 647 elif self.method == "mc": 648 self.posterior_ = "mc" 649 assert self.surrogate_obj.__class__.__name__.startswith( 650 "CustomRegressor" 651 ) or self.surrogate_obj.__class__.__name__.startswith( 652 "PredictionInterval" 653 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() or nnetsauce.PredictionInterval()" 654 assert ( 655 self.surrogate_obj.replications is not None 656 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() with a number of 'replications' provided" 657 preds_with_std = self.surrogate_fit_predict( 658 np.asarray(self.parameters), 659 np.asarray(self.scores), 660 self.x_choices, 661 return_std=False, 662 return_pi=True, 663 ) 664 y_mean, y_std = preds_with_std[0], preds_with_std[1] 665 self.y_mean = y_mean 666 self.y_std = np.maximum(2.220446049250313e-16, y_std) 667 668 elif self.method == "splitconformal": 669 self.posterior_ = None 670 # assert self.surrogate_obj.__class__.__name__.startswith( 671 # "PredictionInterval" 672 # ), "for `method = 'splitconformal'`, the surrogate must be a nnetsauce.PredictionInterval()" 673 preds_with_pi = self.surrogate_fit_predict( 674 np.asarray(self.parameters), 675 np.asarray(self.scores), 676 self.x_choices, 677 return_std=False, 678 return_pi=True, 679 ) 680 y_lower = preds_with_pi[1] 681 self.lower = y_lower 682 683 else: 684 assert ( 685 param_distributions is not None 686 ), "When 'param_search_init_design == False', 'param_distributions' must be provided" 687 688 if self.method == "bayesian": 689 self.posterior_ = "gaussian" 690 try: 691 y_mean, y_std = self.surrogate_fit_predict( 692 np.asarray(self.parameters), 693 np.asarray(self.scores), 694 self.x_choices, 695 return_std=True, 696 return_pi=False, 697 param_search_init_design=True, 698 param_distributions=param_distributions, 699 ) 700 except ValueError: # do not remove this 701 preds_with_std = self.surrogate_fit_predict( 702 np.asarray(self.parameters), 703 np.asarray(self.scores), 704 self.x_choices, 705 return_std=True, 706 return_pi=False, 707 param_search_init_design=True, 708 param_distributions=param_distributions, 709 ) 710 y_mean, y_std = preds_with_std[0], preds_with_std[1] 711 self.y_mean = y_mean 712 self.y_std = np.maximum(2.220446049250313e-16, y_std) 713 714 elif self.method == "mc": 715 self.posterior_ = "mc" 716 assert self.surrogate_obj.__class__.__name__.startswith( 717 "CustomRegressor" 718 ) or self.surrogate_obj.__class__.__name__.startswith( 719 "PredictionInterval" 720 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() or nnetsauce.PredictionInterval()" 721 assert ( 722 self.surrogate_obj.replications is not None 723 ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() with a number of 'replications' provided" 724 preds_with_std = self.surrogate_fit_predict( 725 np.asarray(self.parameters), 726 np.asarray(self.scores), 727 self.x_choices, 728 return_std=False, 729 return_pi=True, 730 param_search_init_design=True, 731 param_distributions=param_distributions, 732 ) 733 y_mean, y_std = preds_with_std[0], preds_with_std[1] 734 self.y_mean = y_mean 735 self.y_std = np.maximum(2.220446049250313e-16, y_std) 736 737 elif self.method == "splitconformal": 738 self.posterior_ = None 739 # assert self.surrogate_obj.__class__.__name__.startswith( 740 # "PredictionInterval" 741 # ), "for `method = 'splitconformal'`, the surrogate must be a nnetsauce.PredictionInterval()" 742 preds_with_pi = self.surrogate_fit_predict( 743 np.asarray(self.parameters), 744 np.asarray(self.scores), 745 self.x_choices, 746 return_std=False, 747 return_pi=True, 748 param_search_init_design=True, 749 param_distributions=param_distributions, 750 ) 751 y_lower = preds_with_pi[1] 752 self.lower = y_lower 753 754 # saving after initial design computation 755 if self.save is not None: 756 self.update_shelve() 757 758 else: # if n_more_iter is not None 759 assert self.n_iter > 5, "you must have n_iter > 5" 760 n_iter = n_more_iter 761 iter_stop = len(self.max_acq) + n_more_iter # potentially 762 763 if (verbose == 1) | (verbose == 2): 764 print(f"\n ...Done. \n") 765 try: 766 print(np.hstack((self.x_init, self.y_init.reshape(-1, 1)))) 767 except: 768 pass 769 770 # end init design ---------- 771 772 # if n_more_iter is None: # initial optimization, before more iters are requested 773 774 if (verbose == 1) | (verbose == 2): 775 print(f"\n Optimization loop... \n") 776 777 # early stopping? 778 if abs_tol is not None: 779 assert ( 780 min_budget > 20 781 ), "With 'abs_tol' provided, you must have 'min_budget' > 20" 782 self.abs_tol = abs_tol 783 784 if ucb_tol is not None: 785 assert ( 786 min_budget > 20 787 ), "With 'ucb_tol' provided, you must have 'min_budget' > 20" 788 assert ( 789 self.acquisition == "ucb" 790 ), "With 'ucb_tol' provided, you must have 'acquisition' == 'ucb'" 791 self.ucb_tol = ucb_tol 792 793 if verbose == 1: 794 progbar = Progbar(target=n_iter) 795 796 # main loop ---------- 797 798 for i in range(n_iter): 799 # find next set of parameters (vector), maximizing acquisition function 800 next_param = self.next_parameter_by_acq(i=i, acq=self.acquisition) 801 802 try: 803 if self.per_second is True: 804 start = time() 805 806 if self.objective_func is not None: 807 score_next_param = self.objective_func( 808 next_param, *func_args 809 ) 810 811 if (np.isfinite(score_next_param) == False) or ( 812 np.isnan(score_next_param) == True 813 ): 814 continue 815 816 else: 817 assert (self.x_init is not None) and ( 818 self.y_init is not None 819 ), "self.objective_func is not None: must have (self.x_init is not None) and (self.y_init is not None)" 820 821 print(f"\n next param: {next_param} \n") 822 score_next_param = float( 823 input("get new score: \n") 824 ) # or an API response 825 826 if (np.isfinite(score_next_param) == False) or ( 827 np.isnan(score_next_param) == True 828 ): 829 continue 830 831 self.timings.append(np.log(time() - start)) 832 833 else: # self.per_second is False: 834 if self.objective_func is not None: 835 score_next_param = self.objective_func( 836 next_param, *func_args 837 ) 838 839 if (np.isfinite(score_next_param) == False) or ( 840 np.isnan(score_next_param) == True 841 ): 842 continue 843 844 else: 845 assert (self.x_init is not None) and ( 846 self.y_init is not None 847 ), "self.objective_func is not None: must have (self.x_init is not None) and (self.y_init is not None)" 848 849 print(f"\n next param: {next_param} \n") 850 score_next_param = float( 851 input("get new score: \n") 852 ) # or an API response 853 854 if (np.isfinite(score_next_param) == False) or ( 855 np.isnan(score_next_param) == True 856 ): 857 continue 858 859 except: 860 continue 861 862 self.parameters.append(next_param.tolist()) 863 864 self.scores.append(score_next_param) 865 866 if self.save is not None: 867 self.update_shelve() 868 869 if verbose == 2: 870 print(f"iteration {i + 1} -----") 871 print(f"current minimum: {self.x_min}") 872 print(f"current minimum score: {self.y_min}") 873 print(f"next parameter: {next_param}") 874 print(f"score for next parameter: {score_next_param} \n") 875 876 if score_next_param < self.y_min: 877 self.x_min = next_param 878 self.y_min = score_next_param 879 if self.save is not None: 880 self.update_shelve() 881 if self.y_min == self.min_value: 882 break 883 884 if self.posterior_ == "gaussian" and self.method == "bayesian": 885 try: 886 self.y_mean, self.y_std = self.surrogate_fit_predict( 887 np.asarray(self.parameters), 888 np.asarray(self.scores), 889 self.x_choices, 890 return_std=True, 891 return_pi=False, 892 ) 893 except: 894 ( 895 self.y_mean, 896 self.y_std, 897 lower, 898 upper, 899 ) = self.surrogate_fit_predict( 900 np.asarray(self.parameters), 901 np.asarray(self.scores), 902 self.x_choices, 903 return_std=True, 904 return_pi=False, 905 ) 906 907 elif self.posterior_ in (None, "mc") and self.method in ( 908 "mc", 909 "splitconformal", 910 ): 911 ( 912 self.y_mean, 913 self.y_lower, 914 self.y_upper, 915 ) = self.surrogate_fit_predict( 916 np.asarray(self.parameters), 917 np.asarray(self.scores), 918 self.x_choices, 919 return_std=False, 920 return_pi=True, 921 ) 922 923 else: 924 return NotImplementedError 925 926 if self.save is not None: 927 self.update_shelve() 928 929 if verbose == 1: 930 progbar.update(i + 1) # update progress bar 931 932 # early stopping 933 934 if abs_tol is not None: 935 # if self.max_acq.size > (self.n_init + self.n_iter * min_budget_pct): 936 if len(self.max_acq) > min_budget: 937 diff_max_acq = np.abs(np.diff(np.asarray(self.max_acq))) 938 939 if diff_max_acq[-1] <= abs_tol: 940 iter_stop = len(self.max_acq) # index i starts at 0 941 942 break 943 944 if ucb_tol is not None: 945 if len(self.max_acq) > min_budget: 946 # print(f"self.ucb: {self.ucb}") 947 # print(f"self.acq: {self.acq}") 948 # print(f"mean(self.ucb/self.acq): {np.mean(self.ucb/self.acq)/100}") 949 950 if ( 951 np.abs(np.mean(self.ucb / self.acq) / 100) <= ucb_tol 952 ): # self.ucb is the upper confidence bound for UCB criterion 953 iter_stop = len(self.max_acq) 954 955 break 956 957 # end main loop ---------- 958 959 if (verbose == 1) & (i < (n_iter - 1)): 960 progbar.update(n_iter) 961 962 self.n_iter = iter_stop 963 if self.save is not None: 964 self.update_shelve() 965 966 DescribeResult = namedtuple( 967 "DescribeResult", ("best_params", "best_score") 968 ) 969 970 if self.params_names is None: 971 return DescribeResult(self.x_min, self.y_min) 972 973 else: 974 return DescribeResult( 975 dict(zip(self.params_names, self.x_min)), self.y_min 976 ) 977 978 # optimize the objective 979 def lazyoptimize( 980 self, 981 verbose=1, 982 abs_tol=None, # suggested 1e-4, for n_iter = 200 983 ucb_tol=None, 984 min_budget=50, # minimum budget for early stopping 985 func_args=None, 986 estimators="all", 987 type_pi="kde", # for now, 'kde', 'bootstrap', 'splitconformal' 988 type_exec="independent", # "queue" or "independent" (default) 989 ): 990 """Launch optimization loop. 991 992 # Arguments: 993 994 verbose: an integer; 995 verbose = 0: nothing is printed, 996 verbose = 1: a progress bar is printed (longer than 0), 997 verbose = 2: information about each iteration is printed (longer than 1) 998 999 n_more_iter: an integer; 1000 additional number of iterations for the optimizer (which has been run once) 1001 1002 abs_tol: a float; 1003 tolerance for convergence of the optimizer (early stopping based on expected improvement) 1004 1005 ucb_tol: a float; 1006 tolerance for convergence of the optimizer (early stopping based on length of prediction intervals) 1007 1008 min_budget: an integer (default is 50); 1009 minimum number of iterations before early stopping controlled by `abs_tol` 1010 1011 func_args: a list; 1012 additional parameters for the objective function (if necessary) 1013 1014 estimators: an str or a list of strs (estimators names) 1015 if "all", then 30 models are fitted. Otherwise, only those provided in the list 1016 are adjusted; for example ["RandomForestRegressor", "Ridge"] 1017 1018 type_pi: an str; 1019 "kde" (default) or, "splitconformal"; type of prediction intervals for the surrogate 1020 model 1021 1022 type_exec: an str; 1023 "independent" (default) is when surrogate models are adjusted independently on 1024 the same design set and the best model is chosen eventually; "queue" is when 1025 surrogate models are adjusted one after the other, on a design set with 1026 increasing size; 1027 1028 """ 1029 1030 # Base case: Gaussian Process 1031 gp_opt_obj = GPOpt( 1032 objective_func=self.objective_func, 1033 lower_bound=self.lower_bound, 1034 upper_bound=self.upper_bound, 1035 n_init=self.n_init, 1036 n_iter=self.n_iter, 1037 alpha=self.alpha, 1038 n_restarts_optimizer=self.n_restarts_optimizer, 1039 seed=self.seed, 1040 n_jobs=self.n_jobs, 1041 acquisition="ei", 1042 surrogate_obj=GaussianProcessRegressor( 1043 kernel=Matern(nu=2.5), 1044 alpha=self.alpha, 1045 normalize_y=True, 1046 n_restarts_optimizer=self.n_restarts_optimizer, 1047 random_state=self.seed, 1048 ), 1049 ) 1050 if verbose == 2: 1051 print( 1052 f"\n adjusting surrogate model # {0} (GaussianProcessRegressor(Matern(5/2)))... \n" 1053 ) 1054 res_base = gp_opt_obj.optimize( 1055 verbose=verbose, 1056 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1057 min_budget=min_budget, # minimum budget for early stopping 1058 func_args=func_args, 1059 ) 1060 1061 if estimators == "all": 1062 if type_pi == "kde": 1063 self.regressors = REGRESSORS 1064 1065 else: 1066 self.regressors = [ 1067 ( 1068 est[0], 1069 ns.PredictionInterval( 1070 est[1](), type_pi="splitconformal" 1071 ), 1072 ) 1073 for est in all_estimators() 1074 if ( 1075 issubclass(est[1], RegressorMixin) 1076 and (est[0] not in REMOVED_REGRESSORS) 1077 ) 1078 ] 1079 1080 else: 1081 if type_pi == "kde": 1082 self.regressors = [ 1083 ( 1084 "CustomRegressor(" + est[0] + ")", 1085 ns.CustomRegressor( 1086 est[1](), replications=150, type_pi=type_pi 1087 ), 1088 ) 1089 for est in all_estimators() 1090 if ( 1091 issubclass(est[1], RegressorMixin) 1092 and (est[0] not in REMOVED_REGRESSORS) 1093 and (est[0] in estimators) 1094 ) 1095 ] 1096 1097 elif type_pi == "splitconformal": 1098 self.regressors = [ 1099 ( 1100 est[0], 1101 ns.PredictionInterval( 1102 est[1](), type_pi="splitconformal" 1103 ), 1104 ) 1105 for est in all_estimators() 1106 if ( 1107 issubclass(est[1], RegressorMixin) 1108 and (est[0] not in REMOVED_REGRESSORS) 1109 and (est[0] in estimators) 1110 ) 1111 ] 1112 1113 df_res = pd.DataFrame( 1114 np.empty((len(self.regressors) + 1, 2)), columns=["Model", "Score"] 1115 ) 1116 df_res.iloc[0, 0] = "GaussianProcessRegressor" 1117 df_res.iloc[0, 1] = res_base.best_score 1118 1119 self.surrogate_fit_predict = partial( 1120 self.surrogate_fit_predict, return_pi=True 1121 ) 1122 1123 if ( 1124 type_exec == "queue" 1125 ): # when models are adjusted one after the other on a design set with increasing size 1126 self.x_min = None 1127 1128 self.y_min = np.inf 1129 1130 score_next_param = np.inf 1131 1132 DescribeResult = namedtuple( 1133 "DescribeResult", ("best_params", "best_score", "scores") 1134 ) 1135 1136 if verbose == 2: 1137 print( 1138 f"\n adjusting surrogate model # {1} ({self.regressors[0][0]})... \n" 1139 ) 1140 1141 df_res.iloc[0, 0] = self.regressors[0][0] 1142 1143 gp_opt_obj_prev = GPOpt( 1144 objective_func=self.objective_func, 1145 lower_bound=self.lower_bound, 1146 upper_bound=self.upper_bound, 1147 n_init=self.n_init, 1148 n_iter=self.n_iter, 1149 alpha=self.alpha, 1150 n_restarts_optimizer=self.n_restarts_optimizer, 1151 seed=self.seed, 1152 n_jobs=self.n_jobs, 1153 acquisition=self.acquisition, 1154 method=self.method, 1155 min_value=self.min_value, 1156 surrogate_obj=copy.deepcopy(self.regressors[0][1]), 1157 ) 1158 1159 gp_opt_obj_prev.optimize( 1160 verbose=verbose, 1161 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1162 ucb_tol=ucb_tol, 1163 min_budget=min_budget, # minimum budget for early stopping 1164 func_args=func_args, 1165 ) 1166 1167 score_next_param = gp_opt_obj_prev.y_min 1168 1169 df_res.iloc[0, 1] = score_next_param 1170 1171 if self.n_jobs is None: # sequential optimization 1172 for i in range(len(self.regressors)): 1173 try: 1174 if verbose == 2: 1175 print( 1176 f"\n adjusting surrogate model # {i + 2} ({self.regressors[i][0]})... \n" 1177 ) 1178 1179 df_res["Model"][i + 1] = self.regressors[i][0] 1180 1181 gp_opt_obj = GPOpt( 1182 objective_func=self.objective_func, 1183 lower_bound=self.lower_bound, 1184 upper_bound=self.upper_bound, 1185 n_init=self.n_init, 1186 n_iter=self.n_iter, 1187 alpha=self.alpha, 1188 n_restarts_optimizer=self.n_restarts_optimizer, 1189 seed=self.seed, 1190 n_jobs=self.n_jobs, 1191 acquisition=self.acquisition, 1192 method=self.method, 1193 min_value=self.min_value, 1194 surrogate_obj=copy.deepcopy(self.regressors[i][1]), 1195 x_init=np.asarray(gp_opt_obj_prev.parameters), 1196 y_init=np.asarray(gp_opt_obj_prev.scores), 1197 ) 1198 1199 gp_opt_obj.optimize( 1200 verbose=verbose, 1201 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1202 ucb_tol=ucb_tol, 1203 min_budget=min_budget, # minimum budget for early stopping 1204 func_args=func_args, 1205 ) 1206 1207 score_next_param = gp_opt_obj.y_min 1208 1209 df_res.iloc[i, 1] = score_next_param 1210 1211 if score_next_param < self.y_min: 1212 self.x_min = gp_opt_obj.x_min 1213 self.y_min = score_next_param 1214 if self.y_min == self.min_value: 1215 break 1216 1217 if verbose == 2: 1218 print(f"Global iteration #{i + 1} -----") 1219 print(f"current minimum: {self.x_min}") 1220 print(f"current minimum score: {self.y_min}") 1221 print( 1222 f"score for next parameter: {score_next_param} \n" 1223 ) 1224 1225 gp_opt_obj_prev = copy.deepcopy(gp_opt_obj) 1226 1227 except ValueError: 1228 continue 1229 1230 elif self.n_jobs >= 2 or self.n_jobs == -1: # parallel optimization 1231 pass 1232 else: 1233 raise ValueError( 1234 "n_jobs must be either None or >= 2 or equal to -1" 1235 ) 1236 return DescribeResult( 1237 self.x_min, self.y_min, df_res.sort_values(by="Score") 1238 ) 1239 1240 elif ( 1241 type_exec == "independent" 1242 ): # when models are adjusted independently on the same design set and the best model is chosen eventually 1243 self.x_min = None 1244 1245 self.y_min = np.inf 1246 1247 score_next_param = np.inf 1248 1249 DescribeResult = namedtuple( 1250 "DescribeResult", 1251 ("best_params", "best_score", "best_surrogate", "scores"), 1252 ) 1253 1254 if verbose == 2: 1255 print( 1256 f"\n adjusting surrogate model # {1} ({self.regressors[0][0]})... \n" 1257 ) 1258 1259 if self.n_jobs is None: # sequential optimization 1260 for i in range(len(self.regressors)): 1261 # try: 1262 1263 if verbose == 2: 1264 print( 1265 f"\n adjusting surrogate model # {i + 1} ({self.regressors[i][0]})... \n" 1266 ) 1267 1268 df_res["Model"][i + 1] = self.regressors[i][0] 1269 1270 gp_opt_obj = GPOpt( 1271 objective_func=self.objective_func, 1272 lower_bound=self.lower_bound, 1273 upper_bound=self.upper_bound, 1274 n_init=self.n_init, 1275 n_iter=self.n_iter, 1276 alpha=self.alpha, 1277 n_restarts_optimizer=self.n_restarts_optimizer, 1278 seed=self.seed, 1279 n_jobs=self.n_jobs, 1280 acquisition=self.acquisition, 1281 method=self.method, 1282 min_value=self.min_value, 1283 surrogate_obj=copy.deepcopy(self.regressors[i][1]), 1284 ) 1285 1286 gp_opt_obj.optimize( 1287 verbose=verbose, 1288 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1289 ucb_tol=ucb_tol, 1290 min_budget=min_budget, # minimum budget for early stopping 1291 func_args=func_args, 1292 ) 1293 1294 score_next_param = gp_opt_obj.y_min 1295 1296 df_res.iloc[i, 1] = score_next_param 1297 1298 if score_next_param < self.y_min: 1299 self.x_min = gp_opt_obj.x_min 1300 self.y_min = score_next_param 1301 self.best_surrogate = copy.deepcopy( 1302 gp_opt_obj.surrogate_obj 1303 ) 1304 if self.y_min == self.min_value: 1305 break 1306 1307 if verbose == 2: 1308 print(f"Global iteration #{i + 1} -----") 1309 print(f"current minimum: {self.x_min}") 1310 print(f"current minimum score: {self.y_min}") 1311 print( 1312 f"score for next parameter: {score_next_param} \n" 1313 ) 1314 1315 # except ValueError: 1316 1317 # continue 1318 1319 elif self.n_jobs >= 2 or self.n_jobs == -1: # parallel optimization 1320 1321 def foo(i): 1322 df_res["Model"][i + 1] = self.regressors[i][0] 1323 1324 gp_opt_obj = GPOpt( 1325 objective_func=self.objective_func, 1326 lower_bound=self.lower_bound, 1327 upper_bound=self.upper_bound, 1328 n_init=self.n_init, 1329 n_iter=self.n_iter, 1330 alpha=self.alpha, 1331 n_restarts_optimizer=self.n_restarts_optimizer, 1332 seed=self.seed, 1333 n_jobs=self.n_jobs, 1334 acquisition=self.acquisition, 1335 method=self.method, 1336 min_value=self.min_value, 1337 surrogate_obj=copy.deepcopy(self.regressors[i][1]), 1338 ) 1339 1340 try: 1341 gp_opt_obj.optimize( 1342 verbose=0, # important 1343 abs_tol=abs_tol, # suggested 1e-4, for n_iter = 200 1344 ucb_tol=ucb_tol, 1345 min_budget=min_budget, # minimum budget for early stopping 1346 func_args=func_args, 1347 ) 1348 1349 return gp_opt_obj 1350 except ValueError: 1351 return None 1352 1353 tmp_results = Parallel(n_jobs=self.n_jobs)( 1354 delayed(foo)(i) for i in tqdm(range(len(self.regressors))) 1355 ) 1356 1357 for i in tqdm(range(len(tmp_results))): 1358 if tmp_results[i] is not None: 1359 gp_opt_obj = copy.deepcopy(tmp_results[i]) 1360 1361 score_next_param = gp_opt_obj.y_min 1362 1363 df_res.iloc[i, 1] = score_next_param 1364 1365 if score_next_param < self.y_min: 1366 self.x_min = gp_opt_obj.x_min 1367 self.y_min = score_next_param 1368 self.best_surrogate = copy.deepcopy( 1369 gp_opt_obj.surrogate_obj 1370 ) 1371 if self.y_min == self.min_value: 1372 break 1373 1374 if verbose == 2: 1375 print(f"Global iteration #{i + 1} -----") 1376 print(f"current minimum: {self.x_min}") 1377 print(f"current minimum score: {self.y_min}") 1378 print( 1379 f"score for next parameter: {score_next_param} \n" 1380 ) 1381 1382 else: 1383 continue 1384 1385 else: 1386 raise ValueError( 1387 "n_jobs must be either None or >= 2 or equal to -1" 1388 ) 1389 return DescribeResult( 1390 self.x_min, 1391 self.y_min, 1392 self.best_surrogate, 1393 df_res.sort_values(by="Score"), 1394 ) 1395 1396 else: 1397 NotImplementedError( 1398 "type_exec must be either 'queue' or 'independent'" 1399 )
Class GPOpt.
Arguments:
lower_bound: a numpy array;
lower bound for researched minimum
upper_bound: a numpy array;
upper bound for researched minimum
objective_func: a function;
the objective function to be minimized
params_names: a list;
names of the parameters of the objective function (optional)
surrogate_obj: an object;
An ML model for estimating the uncertainty around the objective function
Must be nnetsauce.CustomRegressor or nnetsauce.PredictionInterval
x_init:
initial setting of points where `objective_func` is evaluated (optional)
y_init:
initial setting values at points where `objective_func` is evaluated (optional)
n_init: an integer;
number of points in the initial setting, when `x_init` and `y_init` are not provided
n_choices: an integer;
number of points for the calculation of expected improvement
n_iter: an integer;
number of iterations of the minimization algorithm
alpha: a float;
Value added to the diagonal of the kernel matrix during fitting (for Matern 5/2 kernel)
n_restarts_optimizer: an integer;
The number of restarts of the optimizer for finding the kernel’s parameters which maximize the log-marginal likelihood.
seed: an integer;
reproducibility seed
save: a string;
Specifies where to save the optimizer in its current state
n_jobs: an integer;
number of jobs for parallel computing on initial setting or method `lazyoptimize` (can be -1)
acquisition: a string;
acquisition function: "ei" (expected improvement) or "ucb" (upper confidence bound)
method: an str;
"bayesian" (default) for Gaussian posteriors, "mc" for Monte Carlo posteriors,
"splitconformal" (only for acquisition = "ucb") for conformalized surrogates
min_value: a float;
minimum value of the objective function (default is None). For example,
if objective function is accuracy, will be 1, and the algorithm will stop
per_second: a boolean;
__experimental__, default is False (leave to default for now)
log_scale: a boolean;
__experimental__, default is False (leave to default for now)
see also Bayesian Optimization with GPopt or Hyperparameters tuning with GPopt Agnostic BayesOpt