Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions HEBO/hebo/acq_optimizers/evolution_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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 = []
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions HEBO/hebo/design_space/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions HEBO/hebo/design_space/design_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 = []
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions HEBO/hebo/design_space/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
56 changes: 56 additions & 0 deletions HEBO/hebo/design_space/sparse_grid_param.py
Original file line number Diff line number Diff line change
@@ -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))]

12 changes: 8 additions & 4 deletions HEBO/hebo/models/gp/gp_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 1 addition & 6 deletions HEBO/hebo/optimizers/hebo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down