diff --git a/HEBO/hebo/acq_optimizers/evolution_optimizer.py b/HEBO/hebo/acq_optimizers/evolution_optimizer.py index 94a67e44..65faf15f 100644 --- a/HEBO/hebo/acq_optimizers/evolution_optimizer.py +++ b/HEBO/hebo/acq_optimizers/evolution_optimizer.py @@ -18,9 +18,11 @@ from pymoo.optimize import minimize from pymoo.core.problem import Problem from pymoo.config import Config +from pymoo.core.repair import Repair + Config.show_compile_hint = False -from ..design_space.design_space import DesignSpace +from ..design_space.design_space import DesignSpace, SparseGridPara from ..acquisitions.acq import Acquisition def space_to_pymoo_vars(space): @@ -37,7 +39,7 @@ def space_to_pymoo_vars(space): elif p.is_categorical: vars[p_name] = Choice(options = list(range(int(lb), int(ub + 1)))) else: - raise ValueError('Un recognoized parameter type {p_name}') + raise ValueError('Unrecognized parameter type {p_name}') return vars def get_init_pop(space, pop, initial_suggest : pd.DataFrame = None, sobol_init = False) -> Population: @@ -46,12 +48,7 @@ def get_init_pop(space, pop, initial_suggest : pd.DataFrame = None, sobol_init = else: eng = SobolEngine(space.num_paras, scramble = True) sobol_samp = eng.draw(pop) - sobol_samp = sobol_samp * (space.opt_ub - space.opt_lb) + space.opt_lb - x = sobol_samp[:, :space.num_numeric] - xe = sobol_samp[:, space.num_numeric:].round().long() - for i, n in enumerate(space.numeric_names): - if space.paras[n].is_discrete_after_transform: - x[:, i] = x[:, i].round() + x, xe = self.space.transform_random_uniform(sobol_samp) init_pop = space.inverse_transform(x, xe) if initial_suggest is not None: init_pop = pd.concat([initial_suggest, init_pop], axis = 0).head(pop) @@ -69,6 +66,33 @@ def get_init_pop(space, pop, initial_suggest : pd.DataFrame = None, sobol_init = pop = Population.new(X = pop_lst) return pop +# Add a repair operator for sparse grid +class SparseGridRepair(Repair): + def __init__(self, space): + self.space = space + # Pre-compute valid values for each sparse parameter + self.sparse_params = {} + for i, p_name in enumerate(space.para_names): + p = space.paras[p_name] + if isinstance(p, SparseGridPara): + self.sparse_params[p_name] = np.array(sorted(p.values)) + + def repair(self, para): + """Repair function that works directly on parameter arrays""" + if not self.sparse_params: + return para + + para_repaired = para.copy() + for i in range(para.shape[0]): + sample = para[i] + for p_name, values in self.sparse_params.items(): + if p_name in sample: + current_value = sample[p_name] + # Find nearest valid value + idx = np.argmin(np.abs(values - current_value)) + para_repaired[i][p_name] = values[idx] + return para_repaired + class BOProblem(Problem): def __init__(self, acq : Acquisition, @@ -78,10 +102,12 @@ def __init__(self, self.acq = acq self.space = space self.fix = fix # NOTE: use self.fix to enable contextual BO + self.repair_helper = SparseGridRepair(self.space) vars = space_to_pymoo_vars(self.space) super().__init__(vars = vars, n_obj = acq.num_obj, n_constr = acq.num_constr) def _evaluate(self, para : np.ndarray, out : dict, *args, **kwargs): + para = self.repair_helper.repair(para) ## TODO: Use the repair operator to handle fix_input num_x = para.shape[0] x = [] @@ -125,8 +151,6 @@ def __init__(self, def optimize(self, initial_suggest : pd.DataFrame = None, fix_input : dict = None, return_pop = False) -> pd.DataFrame: - lb = self.space.opt_lb.numpy() - ub = self.space.opt_ub.numpy() prob = BOProblem(self.acq, self.space, fix_input) init_pop = get_init_pop(self.space, self.pop, initial_suggest, self.sobol_init) if self.acq.num_obj == 1: diff --git a/HEBO/hebo/design_space/__init__.py b/HEBO/hebo/design_space/__init__.py index 4f12a1f2..489e0e7e 100644 --- a/HEBO/hebo/design_space/__init__.py +++ b/HEBO/hebo/design_space/__init__.py @@ -16,4 +16,5 @@ from . import pow_param from . import pow_integer_param from . import step_int +from . import sparse_grid_param from .design_space import DesignSpace diff --git a/HEBO/hebo/design_space/design_space.py b/HEBO/hebo/design_space/design_space.py index 29d03567..ccdef9db 100644 --- a/HEBO/hebo/design_space/design_space.py +++ b/HEBO/hebo/design_space/design_space.py @@ -19,6 +19,7 @@ from .pow_integer_param import PowIntegerPara from .int_exponent_param import IntExponentPara from .step_int import StepIntPara +from .sparse_grid_param import SparseGridPara class DesignSpace: def __init__(self): @@ -31,6 +32,7 @@ def __init__(self): self.register_para_type('step_int', StepIntPara) self.register_para_type('cat', CategoricalPara) self.register_para_type('bool', BoolPara) + self.register_para_type('sparse_grid', SparseGridPara) self.paras = {} self.para_names = [] self.numeric_names = [] @@ -94,6 +96,11 @@ def transform(self, data : pd.DataFrame) -> (Tensor, Tensor): xe[:, i] = self.paras[name].transform(xe[:, i]) return torch.FloatTensor(xc), torch.LongTensor(xe.astype(int)) + def transform_random_uniform(self, samp : Tensor) -> (Tensor, Tensor): + xc = [[self.paras[name].transform_random_uniform(s) for name, s in zip(self.numeric_names, samp[0, :len(self.numeric_names)])]] + xe = [[int(self.paras[name].transform_random_uniform(s)) for name, s in zip(self.enum_names, samp[0, len(self.numeric_names):])]] + return torch.FloatTensor(xc), torch.LongTensor(xe) + def inverse_transform(self, x : Tensor, xe : Tensor) -> pd.DataFrame: """ input: x and xe diff --git a/HEBO/hebo/design_space/param.py b/HEBO/hebo/design_space/param.py index 93d388eb..63d2dd4e 100644 --- a/HEBO/hebo/design_space/param.py +++ b/HEBO/hebo/design_space/param.py @@ -10,6 +10,7 @@ import pandas as pd import numpy as np from abc import ABC, abstractmethod +from torch import Tensor class Parameter(ABC): def __init__(self, param_dict): @@ -61,3 +62,13 @@ def opt_lb(self) -> float: @abstractmethod def opt_ub(self) -> float: pass + + def transform_random_uniform(self, s : Tensor) -> float: + """Take a random uniform s in [0, 1] and return a random value from the space. + + Can be overridden by speciality spaces. + """ + if self.is_discrete_after_transform: + return (s * (self.opt_ub + 1 - self.opt_lb) + self.opt_lb - 0.5).round() + else: + return s * (self.opt_ub - self.opt_lb) + self.opt_lb diff --git a/HEBO/hebo/design_space/sparse_grid_param.py b/HEBO/hebo/design_space/sparse_grid_param.py new file mode 100644 index 00000000..dc233541 --- /dev/null +++ b/HEBO/hebo/design_space/sparse_grid_param.py @@ -0,0 +1,56 @@ +# Copyright (C) 2020. Huawei Technologies Co., Ltd. All rights reserved. + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the MIT license. + +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the MIT License for more details. + +import sys +import numpy as np +from .param import Parameter +from torch import Tensor + +class SparseGridPara(Parameter): + def __init__(self, param_dict): + super().__init__(param_dict) + self.values = param_dict['values'] + self.lb = min(self.values) + self.ub = max(self.values) + + def sample(self, num = 1): + assert(num > 0) + return np.random.choice(self.values, num) + + def transform(self, x): + return x.astype(float) + + def inverse_transform(self, x): + return x.round().astype(int) + + @property + def is_numeric(self): + return True + + @property + def opt_lb(self): + return float(self.lb) + + @property + def opt_ub(self): + return float(self.ub) + + @property + def is_discrete(self): + return True + + @property + def is_discrete_after_transform(self): + return True + + def transform_random_uniform(self, s : Tensor) -> float: + """Take a random uniform s in [0, 1] and return a random value from the space. + """ + return self.values[int(s * len(self.values))] + diff --git a/HEBO/hebo/models/gp/gp_util.py b/HEBO/hebo/models/gp/gp_util.py index 96ecdbff..682a6c8c 100644 --- a/HEBO/hebo/models/gp/gp_util.py +++ b/HEBO/hebo/models/gp/gp_util.py @@ -14,6 +14,7 @@ ScaleKernel) from gpytorch.priors import GammaPrior from torch import FloatTensor, LongTensor +from gpytorch.constraints.constraints import Interval from ..layers import EmbTransform from ..util import get_random_graph @@ -43,23 +44,26 @@ def default_kern(x, xe, y, total_dim = None, ard_kernel = True, fe = None, max_x kerns = [] if has_num: ard_num_dims = x.shape[1] if ard_kernel else None - kernel = MaternKernel(nu = 1.5, ard_num_dims = ard_num_dims, active_dims = torch.arange(x.shape[1])) + kernel = MaternKernel(nu = 1.5, ard_num_dims = ard_num_dims, active_dims = torch.arange(x.shape[1]), + lengthscale_constraint=Interval(1e-1, 5)) if ard_kernel: lscales = kernel.lengthscale.detach().clone().view(1, -1) for i in range(x.shape[1]): idx = np.random.choice(x.shape[0], min(x.shape[0], max_x), replace = False) - lscales[0, i] = torch.pdist(x[idx, i].view(-1, 1)).median().clamp(min = 0.02) + lscales[0, i] = torch.pdist(x[idx, i].view(-1, 1)).median().clamp(min = 0.1) kernel.lengthscale = lscales kerns.append(kernel) if has_enum: - kernel = MaternKernel(nu = 1.5, active_dims = torch.arange(x.shape[1], total_dim)) + kernel = MaternKernel(nu = 1.5, active_dims = torch.arange(x.shape[1], total_dim), + lengthscale_constraint=Interval(1e-1, 5)) kerns.append(kernel) final_kern = ScaleKernel(ProductKernel(*kerns), outputscale_prior = GammaPrior(0.5, 0.5)) final_kern.outputscale = y[torch.isfinite(y)].var() return final_kern else: if ard_kernel: - kernel = ScaleKernel(MaternKernel(nu = 1.5, ard_num_dims = total_dim)) + kernel = ScaleKernel(MaternKernel(nu = 1.5, ard_num_dims = total_dim), + lengthscale_constraint=Interval(1e-1, 5)) else: kernel = ScaleKernel(MaternKernel(nu = 1.5)) kernel.outputscale = y[torch.isfinite(y)].var() diff --git a/HEBO/hebo/optimizers/hebo.py b/HEBO/hebo/optimizers/hebo.py index 860fda19..152b21cb 100644 --- a/HEBO/hebo/optimizers/hebo.py +++ b/HEBO/hebo/optimizers/hebo.py @@ -62,12 +62,7 @@ def __init__( def quasi_sample(self, n, fix_input=None): samp = self.sobol.draw(n) - samp = samp * (self.space.opt_ub - self.space.opt_lb) + self.space.opt_lb - x = samp[:, : self.space.num_numeric] - xe = samp[:, self.space.num_numeric :] - for i, n in enumerate(self.space.numeric_names): - if self.space.paras[n].is_discrete_after_transform: - x[:, i] = x[:, i].round() + x, xe = self.space.transform_random_uniform(samp) df_samp = self.space.inverse_transform(x, xe) if fix_input is not None: for k, v in fix_input.items():