from typing import List, Optional
import logging
from collections import OrderedDict
import numpy as np
from ConfigSpace.configuration_space import Configuration, ConfigurationSpace
from ConfigSpace.hyperparameters import (
CategoricalHyperparameter,
Constant,
NumericalHyperparameter,
OrdinalHyperparameter,
)
from ConfigSpace.util import ForbiddenValueError, deactivate_inactive_hyperparameters
from smac.utils.io.traj_logging import TrajLogger
__author__ = "Marius Lindauer"
__copyright__ = "Copyright 2019, AutoML"
__license__ = "3-clause BSD"
[docs]class InitialDesign:
"""Base class for initial design strategies that evaluates multiple configurations.
Parameters
----------
cs: ConfigurationSpace
configuration space object
rng: np.random.RandomState
Random state
traj_logger: TrajLogger
Trajectory logging to add new incumbents found by the initial
design.
ta_run_limit: int
Number of iterations allowed for the target algorithm
configs: Optional[List[Configuration]]
List of initial configurations. Disables the arguments ``n_configs_x_params`` if given.
Either this, or ``n_configs_x_params`` or ``init_budget`` must be provided.
n_configs_x_params: int
how many configurations will be used at most in the initial design (X*D). Either
this, or ``init_budget`` or ``configs`` must be provided. Disables the argument
``n_configs_x_params`` if given.
max_config_fracs: float
use at most X*budget in the initial design. Not active if a time limit is given.
init_budget : int, optional
Maximal initial budget (disables the arguments ``n_configs_x_params`` and ``configs``
if both are given). Either this, or ``n_configs_x_params`` or ``configs`` must be
provided.
Attributes
----------
cs : ConfigurationSpace
configs : List[Configuration]
List of configurations to be evaluated
"""
def __init__(
self,
cs: ConfigurationSpace,
rng: np.random.RandomState,
traj_logger: TrajLogger,
ta_run_limit: int,
configs: Optional[List[Configuration]] = None,
n_configs_x_params: Optional[int] = 10,
max_config_fracs: float = 0.25,
init_budget: Optional[int] = None,
):
self.cs = cs
self.rng = rng
self.traj_logger = traj_logger
self.configs = configs
self.logger = logging.getLogger(self.__module__ + "." + self.__class__.__name__)
n_params = len(self.cs.get_hyperparameters())
if init_budget is not None:
self.init_budget = init_budget
if n_configs_x_params is not None:
self.logger.debug(
"Ignoring argument `n_configs_x_params` (value %d).",
n_configs_x_params,
)
elif configs is not None:
self.init_budget = len(configs)
elif n_configs_x_params is not None:
self.init_budget = int(max(1, min(n_configs_x_params * n_params, (max_config_fracs * ta_run_limit))))
else:
raise ValueError(
"Need to provide either argument `init_budget`, `configs` or "
"`n_configs_x_params`, but provided none of them."
)
if self.init_budget > ta_run_limit:
raise ValueError(
"Initial budget %d cannot be higher than the run limit %d." % (self.init_budget, ta_run_limit)
)
self.logger.info("Running initial design for %d configurations" % self.init_budget)
[docs] def select_configurations(self) -> List[Configuration]:
"""Selects the initial configurations."""
if self.init_budget == 0:
return []
if self.configs is None:
self.configs = self._select_configurations()
for config in self.configs:
if config.origin is None:
config.origin = "Initial design"
# add this incumbent right away to have an entry to time point 0
self.traj_logger.add_entry(train_perf=2**31, incumbent_id=1, incumbent=self.configs[0])
# removing duplicates
# (Reference: https://stackoverflow.com/questions/7961363/removing-duplicates-in-lists)
self.configs = list(OrderedDict.fromkeys(self.configs))
return self.configs
def _select_configurations(self) -> List[Configuration]:
raise NotImplementedError
def _transform_continuous_designs(
self, design: np.ndarray, origin: str, cs: ConfigurationSpace
) -> List[Configuration]:
params = cs.get_hyperparameters()
for idx, param in enumerate(params):
if isinstance(param, NumericalHyperparameter):
continue
elif isinstance(param, Constant):
# add a vector with zeros
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 in LHD")
self.logger.debug("Initial Design")
configs = []
for vector in design:
try:
conf = deactivate_inactive_hyperparameters(configuration=None, configuration_space=cs, vector=vector)
except ForbiddenValueError:
continue
conf.origin = origin
configs.append(conf)
self.logger.debug(conf)
self.logger.debug("Size of initial design: %d" % (len(configs)))
return configs