From 4b26f4bb584078fd97311243081515e83e4f17d2 Mon Sep 17 00:00:00 2001 From: Diederick Vermetten Date: Mon, 21 Dec 2020 14:25:16 +0100 Subject: [PATCH 1/3] Initial bugfixes + basic test --- bayes_optim/BayesOpt.py | 70 +++++++++++++++++++++++++++++++---------- bayes_optim/__init__.py | 2 +- bayes_optim/base.py | 19 ++++++----- unittest/test_BO.py | 53 ++++++++++++++++++++++++++++++- 4 files changed, 119 insertions(+), 25 deletions(-) diff --git a/bayes_optim/BayesOpt.py b/bayes_optim/BayesOpt.py index 85ab401..05f00db 100644 --- a/bayes_optim/BayesOpt.py +++ b/bayes_optim/BayesOpt.py @@ -170,7 +170,7 @@ def _batch_arg_max_acquisition( self._acquisition_par['t'] = np.mean([_t_list[i] for i in idx]) return tuple(zip(*__)) -class NoisyBO(ParallelBO): +class IntensificationBO(ParallelBO): def __init__( self, max_r: int = 200, @@ -219,25 +219,62 @@ def evaluate(self, X: Solution) -> List: List The objective value for each solution in `X` """ - incumbent = self.xopt - for i in enumerate(X): - x = X[i] - + #Convert to internal representation to allow storing n_eval + X = self._to_geno(X) + #Need to explicitly check remaining budget to not exceed in intensification + remaining_budget = self.max_FEs - self.eval_count + print(f"rem: {remaining_budget}; max: {self.max_FEs}") + if self.xopt is None: + #First iteration, no incumbant yet + incumbent = X[0] + else: + incumbent = self.xopt + + + for x in X: # add one more sampling point to the incumbent if incumbent.n_eval < self._max_r: - incumbent.fitness = ( - incumbent.fitness * incumbent.n_eval + self.obj_fun(incumbent) - ) / (incumbent.n_eval + 1) - incumbent.n_eval += 1 + if remaining_budget > 0: + inc_pheno = self._to_pheno(incumbent) + if self._eval_type == 'dict': + inc_pheno = inc_pheno[0] + if incumbent.n_eval == 0: + #check for 0 since otherwise fitness keeps being nan + incumbent.fitness = self.obj_fun(inc_pheno) + incumbent.n_eval = 1 + remaining_budget -= 1 + self.eval_count += 1 + else: + incumbent.fitness = ( + incumbent.fitness * incumbent.n_eval + self.obj_fun(inc_pheno) + ) / (incumbent.n_eval + 1) + incumbent.n_eval += 1 + remaining_budget -= 1 + self.eval_count += 1 + else: + break N = 1 while True: - _N = min(N, incumbent.n_eval - x.n_eval) - if _N != 0: - _val = np.sum([self.obj_fun(x) for _ in range(_N)]) - x.fitness = (x.fitness * x.n_eval + _val) / (x.n_eval + _N) - x.n_eval += _N - + _N = min(N, incumbent.n_eval[0] - x.n_eval[0]) + _N = min(_N, remaining_budget) + if _N > 0: + if self._eval_type == 'dict': + vals = [self.obj_fun(self._to_pheno(x)[0]) for _ in range(_N)] + else: + vals = [self.obj_fun(self._to_pheno(x)) for _ in range(_N)] + remaining_budget -= _N + self.eval_count += _N + print(f"new rem: {remaining_budget} ({self.max_FEs} - {self.eval_count})") + _val = np.sum(vals) + if x.n_eval == 0: + #check for 0 since otherwise fitness keeps being nan + x.fitness = _val + x.n_eval = _N + else: + x.fitness = (x.fitness * x.n_eval + _val) / (x.n_eval + _N) + x.n_eval += _N + if self._compare(x.fitness, incumbent.fitness): break elif _N == 0: @@ -245,7 +282,8 @@ def evaluate(self, X: Solution) -> List: break else: N *= 2 - + x.n_eval -= 1 #TODO: Fix this. Currently here because tell always adds 1 + self.eval_count -= 1 #TODO: Fix this. Currently here because tell always adds 1 return X.fitness.tolist() def _create_acquisition( diff --git a/bayes_optim/__init__.py b/bayes_optim/__init__.py index f02ef38..58380ea 100755 --- a/bayes_optim/__init__.py +++ b/bayes_optim/__init__.py @@ -3,7 +3,7 @@ from typing import Callable, Any, Tuple, List, Union, Optional from . import AcquisitionFunction, Surrogate -from .BayesOpt import BO, ParallelBO, NoisyBO, AnnealingBO +from .BayesOpt import BO, ParallelBO, IntensificationBO, AnnealingBO from .Solution import Solution from .Surrogate import RandomForest, GaussianProcess, trend from .SearchSpace import SearchSpace, OrdinalSpace, ContinuousSpace, NominalSpace diff --git a/bayes_optim/base.py b/bayes_optim/base.py index b0ed787..23bcfab 100644 --- a/bayes_optim/base.py +++ b/bayes_optim/base.py @@ -283,7 +283,9 @@ def __init__( self._set_aux_vars() self._set_internal_optimization(**acquisition_optimization) self.warm_data = warm_data - + self.xopt = None + self.fopt = None + @property def acquisition_fun(self): return self._acquisition_fun @@ -463,7 +465,10 @@ def _compare(self, f1, f2): def run(self): while not self.check_stop(): self.step() - return self.xopt, self.fopt, self.stop_dict + _xopt_write = self._to_pheno(self.xopt) + if self._eval_type == 'dict': + _xopt_write = _xopt_write[0] + return _xopt_write, self.fopt, self.stop_dict def step(self): X = self.ask() @@ -552,13 +557,13 @@ def tell(self, X, func_vals, warm_start=False): X.to_csv(self.data_file, header=False, append=True) self.fopt = self._get_best(self.data.fitness) - _xopt = self.data[np.where(self.data.fitness == self.fopt)[0][0]] - self.xopt = self._to_pheno(_xopt) + self.xopt = self.data[np.where(self.data.fitness == self.fopt)[0][0]] + _xopt_write = self._to_pheno(self.xopt) if self._eval_type == 'dict': - self.xopt = self.xopt[0] + _xopt_write = _xopt_write[0] self._logger.info('fopt: {}'.format(self.fopt)) - self._logger.info('xopt: {}'.format(self.xopt)) + self._logger.info('xopt: {}'.format(_xopt_write)) if not self.model.is_fitted: self._fBest_DoE = copy(self.fopt) # the best f-value from DoE @@ -586,7 +591,7 @@ def pre_eval_check(self, X): def post_eval_check(self, X): _ = np.isnan(X.fitness) | np.isinf(X.fitness) if np.any(_): - self._logger.warn( + self._logger.warning( '{} candidate solutions are removed ' 'due to falied fitness evaluation: \n{}'.format(sum(_), str(X[_, :])) ) diff --git a/unittest/test_BO.py b/unittest/test_BO.py index c5b12b5..8797dcc 100644 --- a/unittest/test_BO.py +++ b/unittest/test_BO.py @@ -3,7 +3,7 @@ import pytest sys.path.insert(0, '../') -from bayes_optim import ParallelBO, BO, ContinuousSpace, OrdinalSpace, NominalSpace +from bayes_optim import ParallelBO, BO, ContinuousSpace, OrdinalSpace, NominalSpace, IntensificationBO from bayes_optim.Surrogate import trend, GaussianProcess, RandomForest np.random.seed(123) @@ -162,6 +162,57 @@ def obj_fun(x): ) xopt, fopt, stop_dict = opt.run() + print('xopt: {}'.format(xopt)) + print('fopt: {}'.format(fopt)) + print('stop criteria: {}'.format(stop_dict)) + + +# Test for intensification BO is the same as the mixed_param test, can probably merge them in future +@pytest.mark.parametrize("eval_type", ['list', 'dict', 'dataframe']) # type: ignore +def test_intensification(eval_type): + dim_r = 2 # dimension of the real values + if eval_type == 'dict' or eval_type == 'dataframe': + def obj_fun(x): + #Do explicit type-casting since dataframe rows might be strings otherwise + x_r = np.array([float(x['continuous_%d'%i]) for i in range(dim_r)]) + x_i = int(x['ordinal']) + x_d = x['nominal'] + if type(x_d) != str: #TODO: Check why this is needed here + x_d = x_d[0] + _ = 0 if x_d == 'OK' else 1 + temp = np.random.normal(1,0.1)*np.sum(x_r ** 2) + abs(x_i - 10) / 123. + _ * 2 + return temp + elif eval_type == 'list': + def obj_fun(x): + x_r = np.array([x[i] for i in range(dim_r)]) + x_i = x[-2] + x_d = x[-1] + _ = 0 if x_d == 'OK' else 1 + temp = np.random.normal(1,0.1)*np.sum(x_r ** 2) + abs(x_i - 10) / 123. + _ * 2 + return temp + else: + raise NotImplemented + search_space = ContinuousSpace([-5, 5], var_name='continuous') * dim_r + \ + OrdinalSpace([5, 15], var_name='ordinal') + \ + NominalSpace(['OK', 'A', 'B', 'C', 'D', 'E', 'F', 'G'], var_name='nominal') + + model = RandomForest(levels=search_space.levels) + + opt = IntensificationBO( + search_space=search_space, + obj_fun=obj_fun, + model=model, + max_FEs=60, + DoE_size=5, # the initial DoE size + eval_type=eval_type, + acquisition_fun='MGFI', + acquisition_par={'t' : 2}, + n_job=3, # number of processes + n_point=3, # number of the candidate solution proposed in each iteration + verbose=False # turn this off, if you prefer no output + ) + xopt, fopt, stop_dict = opt.run() + print('xopt: {}'.format(xopt)) print('fopt: {}'.format(fopt)) print('stop criteria: {}'.format(stop_dict)) \ No newline at end of file From de58720633a71d6402107391ac2ecb7248a395de Mon Sep 17 00:00:00 2001 From: Diederick Vermetten Date: Wed, 23 Dec 2020 11:12:52 +0100 Subject: [PATCH 2/3] Re-add original noisyBO --- bayes_optim/BayesOpt.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bayes_optim/BayesOpt.py b/bayes_optim/BayesOpt.py index 05f00db..e23105f 100644 --- a/bayes_optim/BayesOpt.py +++ b/bayes_optim/BayesOpt.py @@ -170,6 +170,24 @@ def _batch_arg_max_acquisition( self._acquisition_par['t'] = np.mean([_t_list[i] for i in idx]) return tuple(zip(*__)) + +class NoisyBO(ParallelBO): + def pre_eval_check(self, X): + if not isinstance(X, Solution): + X = Solution(X, var_name=self.var_names) + return X + + def _create_acquisition(self, fun=None, par={}, return_dx=False): + if hasattr(getattr(AcquisitionFunction, self._acquisition_fun), 'plugin'): + # use the model prediction to determine the plugin under noisy scenarios + # TODO: add more options for determining the plugin value + y_ = self.model.predict(self.data) + plugin = np.min(y_) if self.minimize else np.max(y_) + par.update({'plugin' : plugin}) + + return super()._create_acquisition(par=par, return_dx=return_dx) + + class IntensificationBO(ParallelBO): def __init__( self, From fb218c03cd455ac94070182043fc7368b8e5885d Mon Sep 17 00:00:00 2001 From: Diederick Vermetten Date: Wed, 23 Dec 2020 13:33:20 +0100 Subject: [PATCH 3/3] Remove needless prints --- bayes_optim/BayesOpt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bayes_optim/BayesOpt.py b/bayes_optim/BayesOpt.py index e23105f..54f2c62 100644 --- a/bayes_optim/BayesOpt.py +++ b/bayes_optim/BayesOpt.py @@ -59,7 +59,7 @@ def pre_eval_check(self, X: Solution) -> Solution: class ParallelBO(BO): def __init__(self, **kwargs): super().__init__(**kwargs) - assert self.n_point > 1 +# assert self.n_point > 1 if self._acquisition_fun == 'MGFI': self._par_name = 't' @@ -241,7 +241,6 @@ def evaluate(self, X: Solution) -> List: X = self._to_geno(X) #Need to explicitly check remaining budget to not exceed in intensification remaining_budget = self.max_FEs - self.eval_count - print(f"rem: {remaining_budget}; max: {self.max_FEs}") if self.xopt is None: #First iteration, no incumbant yet incumbent = X[0] @@ -283,7 +282,6 @@ def evaluate(self, X: Solution) -> List: vals = [self.obj_fun(self._to_pheno(x)) for _ in range(_N)] remaining_budget -= _N self.eval_count += _N - print(f"new rem: {remaining_budget} ({self.max_FEs} - {self.eval_count})") _val = np.sum(vals) if x.n_eval == 0: #check for 0 since otherwise fitness keeps being nan