Source code for smac.facade.blackbox_facade

from __future__ import annotations

import numpy as np
import sklearn.gaussian_process.kernels as kernels
from ConfigSpace import Configuration

from smac.acquisition.function.expected_improvement import EI
from smac.acquisition.maximizer.local_and_random_search import (
    LocalAndSortedRandomSearch,
)
from smac.facade.abstract_facade import AbstractFacade
from smac.initial_design.sobol_design import SobolInitialDesign
from smac.intensifier.intensifier import Intensifier
from smac.model.gaussian_process.abstract_gaussian_process import (
    AbstractGaussianProcess,
)
from smac.model.gaussian_process.gaussian_process import GaussianProcess
from smac.model.gaussian_process.kernels import (
    ConstantKernel,
    HammingKernel,
    MaternKernel,
    WhiteKernel,
)
from smac.model.gaussian_process.mcmc_gaussian_process import MCMCGaussianProcess
from smac.model.gaussian_process.priors import HorseshoePrior, LogNormalPrior
from smac.multi_objective.aggregation_strategy import MeanAggregationStrategy
from smac.random_design.probability_design import ProbabilityRandomDesign
from smac.runhistory.encoder.encoder import RunHistoryEncoder
from smac.scenario import Scenario
from smac.utils.configspace import get_types

__copyright__ = "Copyright 2022, automl.org"
__license__ = "3-clause BSD"


[docs]class BlackBoxFacade(AbstractFacade): def _validate(self) -> None: """Ensure that the SMBO configuration with all its (updated) dependencies is valid.""" super()._validate() # Activate predict incumbent # self.solver.epm_chooser.predict_x_best = True if self._scenario.instance_features is not None and len(self._scenario.instance_features) > 0: raise NotImplementedError("The Black-Box GP cannot handle instances.") if not isinstance(self._model, AbstractGaussianProcess): raise ValueError("The Black-Box facade only works with Gaussian Processes")
[docs] @staticmethod def get_model( scenario: Scenario, *, model_type: str | None = None, kernel: kernels.Kernel | None = None, ) -> AbstractGaussianProcess: """Returns a Gaussian Process surrogate model. Parameters ---------- scenario : Scenario model_type : str | None, defaults to None Which gaussian process model should be chosen. Choose between `vanilla` and `mcmc`. kernel : kernels.Kernel | None, defaults to None The kernel used in the surrogate model. Returns ------- model : GaussianProcess | MCMCGaussianProcess The instantiated gaussian process. """ available_model_types = [None, "vanilla", "mcmc"] if model_type not in available_model_types: types = [str(t) for t in available_model_types] raise ValueError(f"The model_type `{model_type}` is not supported. Choose one of {', '.join(types)}") if kernel is None: kernel = BlackBoxFacade.get_kernel(scenario=scenario) if model_type is None or model_type == "vanilla": return GaussianProcess( configspace=scenario.configspace, kernel=kernel, normalize_y=True, seed=scenario.seed, ) elif model_type == "mcmc": n_mcmc_walkers = 3 * len(kernel.theta) if n_mcmc_walkers % 2 == 1: n_mcmc_walkers += 1 return MCMCGaussianProcess( configspace=scenario.configspace, kernel=kernel, n_mcmc_walkers=n_mcmc_walkers, chain_length=250, burning_steps=250, normalize_y=True, seed=scenario.seed, ) else: raise ValueError("Unknown model type %s" % model_type)
[docs] @staticmethod def get_kernel(scenario: Scenario) -> kernels.Kernel: """Returns a kernel for the Gaussian Process surrogate model. The kernel is a composite of kernels depending on the type of hyperparameters: categorical (HammingKernel), continuous (Matern), and noise kernels (White). """ types, _ = get_types(scenario.configspace, instance_features=None) cont_dims = np.where(np.array(types) == 0)[0] cat_dims = np.where(np.array(types) != 0)[0] if (len(cont_dims) + len(cat_dims)) != len(scenario.configspace.get_hyperparameters()): raise ValueError( "The inferred number of continuous and categorical hyperparameters " "must equal the total number of hyperparameters. Got " f"{(len(cont_dims) + len(cat_dims))} != {len(scenario.configspace.get_hyperparameters())}." ) # Constant Kernel cov_amp = ConstantKernel( 2.0, constant_value_bounds=(np.exp(-10), np.exp(2)), prior=LogNormalPrior( mean=0.0, sigma=1.0, seed=scenario.seed, ), ) # Continuous / Categorical Kernels exp_kernel, ham_kernel = 0.0, 0.0 if len(cont_dims) > 0: exp_kernel = MaternKernel( np.ones([len(cont_dims)]), [(np.exp(-6.754111155189306), np.exp(0.0858637988771976)) for _ in range(len(cont_dims))], nu=2.5, operate_on=cont_dims, ) if len(cat_dims) > 0: ham_kernel = HammingKernel( np.ones([len(cat_dims)]), [(np.exp(-6.754111155189306), np.exp(0.0858637988771976)) for _ in range(len(cat_dims))], operate_on=cat_dims, ) # Noise Kernel noise_kernel = WhiteKernel( noise_level=1e-8, noise_level_bounds=(np.exp(-25), np.exp(2)), prior=HorseshoePrior(scale=0.1, seed=scenario.seed), ) # Continuous and categecorical HPs if len(cont_dims) > 0 and len(cat_dims) > 0: kernel = cov_amp * (exp_kernel * ham_kernel) + noise_kernel # Only continuous HPs elif len(cont_dims) > 0 and len(cat_dims) == 0: kernel = cov_amp * exp_kernel + noise_kernel # Only categorical HPs elif len(cont_dims) == 0 and len(cat_dims) > 0: kernel = cov_amp * ham_kernel + noise_kernel else: raise ValueError("The number of continuous and categorical hyperparameters must be greater than zero.") return kernel
[docs] @staticmethod def get_acquisition_function( # type: ignore scenario: Scenario, *, xi: float = 0.0, ) -> EI: """Returns an Expected Improvement acquisition function. Parameters ---------- scenario : Scenario xi : float, defaults to 0.0 Controls the balance between exploration and exploitation of the acquisition function. """ return EI(xi=xi)
[docs] @staticmethod def get_acquisition_maximizer( # type: ignore scenario: Scenario, *, challengers: int = 1000, local_search_iterations: int = 10, ) -> LocalAndSortedRandomSearch: """Returns local and sorted random search as acquisition maximizer. Parameters ---------- challengers : int, defaults to 1000 Number of challengers. local_search_iterations: int, defauts to 10 Number of local search iterations. """ return LocalAndSortedRandomSearch( configspace=scenario.configspace, challengers=challengers, local_search_iterations=local_search_iterations, seed=scenario.seed, )
[docs] @staticmethod def get_intensifier( # type: ignore scenario: Scenario, *, min_challenger: int = 1, min_config_calls: int = 1, max_config_calls: int = 3, intensify_percentage: float = 0.5, ) -> Intensifier: """Returns ``Intensifier`` as intensifier. Uses the default configuration for ``race_against``. Parameters ---------- scenario : Scenario min_config_calls : int, defaults to 1 Minimum number of trials per config (summed over all calls to intensify). max_config_calls : int, defaults to 1 Maximum number of trials per config (summed over all calls to intensify). min_challenger : int, defaults to 3 Minimal number of challengers to be considered (even if time_bound is exhausted earlier). intensify_percentage : float, defaults to 0.5 How much percentage of the time should configurations be intensified (evaluated on higher budgets or more instances). This parameter is accessed in the SMBO class. """ return Intensifier( scenario=scenario, min_challenger=min_challenger, race_against=scenario.configspace.get_default_configuration(), min_config_calls=min_config_calls, max_config_calls=max_config_calls, intensify_percentage=intensify_percentage, )
[docs] @staticmethod def get_initial_design( # type: ignore scenario: Scenario, *, n_configs: int | None = None, n_configs_per_hyperparamter: int = 10, max_ratio: float = 0.1, additional_configs: list[Configuration] = [], ) -> SobolInitialDesign: """Returns a Sobol design instance. Parameters ---------- scenario : Scenario n_configs : int | None, defaults to None Number of initial configurations (disables the arguments ``n_configs_per_hyperparameter``). n_configs_per_hyperparameter: int, defaults to 10 Number of initial configurations per hyperparameter. For example, if my configuration space covers five hyperparameters and ``n_configs_per_hyperparameter`` is set to 10, then 50 initial configurations will be samples. max_ratio: float, defaults to 0.1 Use at most ``scenario.n_trials`` * ``max_ratio`` number of configurations in the initial design. Additional configurations are not affected by this parameter. additional_configs: list[Configuration], defaults to [] Adds additional configurations to the initial design. """ return SobolInitialDesign( scenario=scenario, n_configs=n_configs, n_configs_per_hyperparameter=n_configs_per_hyperparamter, max_ratio=max_ratio, additional_configs=additional_configs, seed=scenario.seed, )
[docs] @staticmethod def get_random_design( # type: ignore scenario: Scenario, *, probability: float = 0.08447232371720552, ) -> ProbabilityRandomDesign: """Returns ``ProbabilityRandomDesign`` for interleaving configurations. Parameters ---------- probability : float, defaults to 0.08447232371720552 Probability that a configuration will be drawn at random. """ return ProbabilityRandomDesign(seed=scenario.seed, probability=probability)
[docs] @staticmethod def get_multi_objective_algorithm( # type: ignore scenario: Scenario, *, objective_weights: list[float] | None = None, ) -> MeanAggregationStrategy: """Returns the mean aggregation strategy for the multi objective algorithm. Parameters ---------- scenario : Scenario objective_weights : list[float] | None, defaults to None Weights for an weighted average. Must be of the same length as the number of objectives. """ return MeanAggregationStrategy( scenario=scenario, objective_weights=objective_weights, )
[docs] @staticmethod def get_runhistory_encoder( scenario: Scenario, ) -> RunHistoryEncoder: """Returns the default runhistory encoder.""" return RunHistoryEncoder(scenario)