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