Skip to content

Commit

Permalink
Fix differential evolution (#1150)
Browse files Browse the repository at this point in the history
* Fix differential evolution

* Update CHANGELOG.md

* Vectorize diff evo

* Add test for categorical search space

* Formatting

* Remove unused imports

* Another formatting fix
  • Loading branch information
hadarshavit authored Dec 5, 2024
1 parent 14a6b5c commit 7042a94
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 80 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
## Dependencies
- Allow numpy >= 2.x (#1146)

## Bugfixes
- Fix bug in differential evolution acquisition maximizer in case the search space contains categorical hyperparameters (#1150)

# Examples
- Add warmstarting example (#1120)

Expand Down
78 changes: 68 additions & 10 deletions smac/acquisition/maximizer/differential_evolution.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from __future__ import annotations

import numpy as np
from ConfigSpace import Configuration
from ConfigSpace import Configuration, ConfigurationSpace
from scipy.optimize._differentialevolution import DifferentialEvolutionSolver

from smac.acquisition.function.abstract_acquisition_function import (
AbstractAcquisitionFunction,
)
from smac.acquisition.maximizer import AbstractAcquisitionMaximizer
from smac.utils.configspace import transform_continuous_designs

__copyright__ = "Copyright 2022, automl.org"
__license__ = "3-clause BSD"
Expand All @@ -23,8 +27,46 @@ class DifferentialEvolution(AbstractAcquisitionMaximizer):
[1] Storn, R and Price, K, Differential Evolution - a Simple and Efficient Heuristic for Global
Optimization over Continuous Spaces, Journal of Global Optimization, 1997, 11, 341 - 359.
Parameters
----------
configspace : ConfigurationSpace
acquisition_function : AbstractAcquisitionFunction
challengers : int, defaults to 50000
Number of challengers.
max_iter: int | None, defaults to None
Maximum number of iterations that the DE will perform.
strategy: str, defaults to "best1bin"
The strategy to use for the DE.
polish: bool, defaults to True
Whether to polish the final solution using L-BFGS-B.
mutation: tuple[float, float], defaults to (0.5, 1.0)
The mutation constant.
recombination: float, defaults to 0.7
The recombination constant.
seed : int, defaults to 0
"""

def __init__(
self,
configspace: ConfigurationSpace,
acquisition_function: AbstractAcquisitionFunction | None = None,
max_iter: int = 1000,
challengers: int = 50000,
strategy: str = "best1bin",
polish: bool = True,
mutation: tuple[float, float] = (0.5, 1.0),
recombination: float = 0.7,
seed: int = 0,
):
super().__init__(configspace, acquisition_function, challengers, seed)
# raise NotImplementedError("DifferentialEvolution is not yet implemented.")
self.max_iter = max_iter
self.strategy = strategy
self.polish = polish
self.mutation = mutation
self.recombination = recombination

def _maximize(
self,
previous_configs: list[Configuration],
Expand All @@ -36,30 +78,46 @@ def _maximize(

def func(x: np.ndarray) -> np.ndarray:
assert self._acquisition_function is not None
return -self._acquisition_function([Configuration(self._configspace, vector=x)])
if len(x.shape) == 1:
return -self._acquisition_function(
[
transform_continuous_designs(
design=np.expand_dims(x, axis=0),
origin="Diffrential Evolution",
configspace=self._configspace,
)[0]
]
)
return -self._acquisition_function(
transform_continuous_designs(design=x.T, origin="Diffrential Evolution", configspace=self._configspace)
)

ds = DifferentialEvolutionSolver(
func,
bounds=[[0, 1] for _ in range(len(self._configspace))],
args=(),
strategy="best1bin",
maxiter=1000,
popsize=50,
strategy=self.strategy,
maxiter=self.max_iter,
popsize=self._challengers // self.max_iter,
tol=0.01,
mutation=(0.5, 1),
recombination=0.7,
mutation=self.mutation,
recombination=self.recombination,
seed=self._rng.randint(1000),
polish=True,
polish=self.polish,
callback=None,
disp=False,
init="latinhypercube",
atol=0,
vectorized=True,
)

_ = ds.solve()
for pop, val in zip(ds.population, ds.population_energies):
rc = Configuration(self._configspace, vector=pop)
rc.origin = "Acquisition Function Maximizer: Differential Evolution"
rc = transform_continuous_designs(
design=np.expand_dims(pop, axis=0),
origin="Acquisition Function Maximizer: Differential Evolution",
configspace=self._configspace,
)[0]
configs.append((-val, rc))

configs.sort(key=lambda t: t[0])
Expand Down
64 changes: 1 addition & 63 deletions smac/initial_design/abstract_initial_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@
from collections import OrderedDict

import numpy as np
from ConfigSpace.configuration_space import Configuration, ConfigurationSpace
from ConfigSpace.hyperparameters import (
CategoricalHyperparameter,
Constant,
IntegerHyperparameter,
NumericalHyperparameter,
OrdinalHyperparameter,
)
from ConfigSpace.util import ForbiddenValueError, deactivate_inactive_hyperparameters
from ConfigSpace.configuration_space import Configuration

from smac.scenario import Scenario
from smac.utils.logging import get_logger
Expand Down Expand Up @@ -155,57 +147,3 @@ def select_configurations(self) -> list[Configuration]:
def _select_configurations(self) -> list[Configuration]:
"""Selects the initial configurations, depending on the implementation of the initial design."""
raise NotImplementedError

def _transform_continuous_designs(
self, design: np.ndarray, origin: str, configspace: ConfigurationSpace
) -> list[Configuration]:
"""Transforms the continuous designs into a discrete list of configurations.
Parameters
----------
design : np.ndarray
Array of hyperparameters originating from the initial design strategy.
origin : str | None, defaults to None
Label for a configuration where it originated from.
configspace : ConfigurationSpace
Returns
-------
configs : list[Configuration]
Continuous transformed configs.
"""
params = list(configspace.values())
for idx, param in enumerate(params):
if isinstance(param, IntegerHyperparameter):
design[:, idx] = param.to_vector(param.to_value(design[:, idx]))
elif isinstance(param, NumericalHyperparameter):
continue
elif isinstance(param, Constant):
design_ = np.zeros(np.array(design.shape) + np.array((0, 1)))
design_[:, :idx] = design[:, :idx]
design_[:, idx + 1 :] = design[:, idx:]
design = design_
elif isinstance(param, CategoricalHyperparameter):
v_design = design[:, idx]
v_design[v_design == 1] = 1 - 10**-10
design[:, idx] = np.array(v_design * len(param.choices), dtype=int)
elif isinstance(param, OrdinalHyperparameter):
v_design = design[:, idx]
v_design[v_design == 1] = 1 - 10**-10
design[:, idx] = np.array(v_design * len(param.sequence), dtype=int)
else:
raise ValueError("Hyperparameter not supported when transforming a continuous design.")

configs = []
for vector in design:
try:
conf = deactivate_inactive_hyperparameters(
configuration=None, configuration_space=configspace, vector=vector
)
except ForbiddenValueError:
continue

conf.origin = origin
configs.append(conf)

return configs
3 changes: 2 additions & 1 deletion smac/initial_design/latin_hypercube_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from scipy.stats.qmc import LatinHypercube

from smac.initial_design.abstract_initial_design import AbstractInitialDesign
from smac.utils.configspace import transform_continuous_designs

__copyright__ = "Copyright 2022, automl.org"
__license__ = "3-clause BSD"
Expand All @@ -25,6 +26,6 @@ def _select_configurations(self) -> list[Configuration]:

lhd = LatinHypercube(d=len(params) - constants, seed=self._rng.randint(0, 1000000)).random(n=self._n_configs)

return self._transform_continuous_designs(
return transform_continuous_designs(
design=lhd, origin="Initial Design: Latin Hypercube", configspace=self._configspace
)
5 changes: 2 additions & 3 deletions smac/initial_design/sobol_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from scipy.stats.qmc import Sobol

from smac.initial_design.abstract_initial_design import AbstractInitialDesign
from smac.utils.configspace import transform_continuous_designs

__copyright__ = "Copyright 2022, automl.org"
__license__ = "3-clause BSD"
Expand Down Expand Up @@ -43,6 +44,4 @@ def _select_configurations(self) -> list[Configuration]:
warnings.simplefilter("ignore")
sobol = sobol_gen.random(self._n_configs)

return self._transform_continuous_designs(
design=sobol, origin="Initial Design: Sobol", configspace=self._configspace
)
return transform_continuous_designs(design=sobol, origin="Initial Design: Sobol", configspace=self._configspace)
63 changes: 62 additions & 1 deletion smac/utils/configspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@
BetaIntegerHyperparameter,
CategoricalHyperparameter,
Constant,
IntegerHyperparameter,
NormalFloatHyperparameter,
NormalIntegerHyperparameter,
NumericalHyperparameter,
OrdinalHyperparameter,
UniformFloatHyperparameter,
UniformIntegerHyperparameter,
)
from ConfigSpace.util import get_one_exchange_neighbourhood
from ConfigSpace.util import (
ForbiddenValueError,
deactivate_inactive_hyperparameters,
get_one_exchange_neighbourhood,
)

__copyright__ = "Copyright 2022, automl.org"
__license__ = "3-clause BSD"
Expand Down Expand Up @@ -182,6 +188,61 @@ def print_config_changes(
logger.debug(msg)


def transform_continuous_designs(
design: np.ndarray, origin: str, configspace: ConfigurationSpace
) -> list[Configuration]:
"""Transforms the continuous designs into a discrete list of configurations.
Parameters
----------
design : np.ndarray
Array of hyperparameters originating from the initial design strategy.
origin : str | None, defaults to None
Label for a configuration where it originated from.
configspace : ConfigurationSpace
Returns
-------
configs : list[Configuration]
Continuous transformed configs.
"""
params = configspace.get_hyperparameters()
for idx, param in enumerate(params):
if isinstance(param, IntegerHyperparameter):
design[:, idx] = param._inverse_transform(param._transform(design[:, idx]))
elif isinstance(param, NumericalHyperparameter):
continue
elif isinstance(param, Constant):
design_ = np.zeros(np.array(design.shape) + np.array((0, 1)))
design_[:, :idx] = design[:, :idx]
design_[:, idx + 1 :] = design[:, idx:]
design = design_
elif isinstance(param, CategoricalHyperparameter):
v_design = design[:, idx]
v_design[v_design == 1] = 1 - 10**-10
design[:, idx] = np.array(v_design * len(param.choices), dtype=int)
elif isinstance(param, OrdinalHyperparameter):
v_design = design[:, idx]
v_design[v_design == 1] = 1 - 10**-10
design[:, idx] = np.array(v_design * len(param.sequence), dtype=int)
else:
raise ValueError("Hyperparameter not supported when transforming a continuous design.")

configs = []
for vector in design:
try:
conf = deactivate_inactive_hyperparameters(
configuration=None, configuration_space=configspace, vector=vector
)
except ForbiddenValueError:
continue

conf.origin = origin
configs.append(conf)

return configs


# def check_subspace_points(
# X: np.ndarray,
# cont_dims: np.ndarray | list = [],
Expand Down
Loading

0 comments on commit 7042a94

Please sign in to comment.