Source code for orion.algo.pbt.explore

"""
Explore classes for Population Based Training
---------------------------------------------

Formulation of a general explore function for population based training.
Implementations must inherit from ``orion.algo.pbt.BaseExplore``.

Explore objects can be created using `explore_factory.create()`.

Examples
--------
>>> explore_factory.create('PerturbExplore')
>>> explore_factory.create('PerturbExplore', factor=1.5)

"""

import numpy

from orion.core.utils import GenericFactory
from orion.core.utils.flatten import flatten, unflatten


[docs]class BaseExplore: """Abstract class for Explore in :py:class:`orion.algo.pbt.pbt.PBT` The explore class is responsible for proposing new parameters for a given trial and space. This class is expected to be stateless and serve as a configurable callable object. """ def __init__(self): pass
[docs] def __call__(self, rng, space, params): """Execute explore The method receives the space and the parameters of the current trial under examination. It must then select new parameters for the trial. Parameters ---------- rng: numpy.random.Generator A random number generator. It is not contained in ``BaseExplore`` because the explore class must be stateless. space: Space The search space optimized by the algorithm. params: dict Dictionary representing the parameters of the current trial under examination (`trial.params`). Returns ------- ``dict`` The new set of parameters for the trial to be branched. """
@property def configuration(self): """Configuration of the exploit object""" return dict(of_type=self.__class__.__name__.lower())
[docs]class PipelineExplore(BaseExplore): """ Pipeline of BaseExploit objects The pipeline executes the BaseExplore objects sequentially. If one object returns the parameters that are different than the ones passed (``params``), then the pipeline returns these parameter values. Otherwise, if all BaseExplore objects return the same parameters as the one passed to the pipeline, then the pipeline returns it. Parameters ---------- explore_configs: list of dict List of dictionary representing the configurations of BaseExplore children. Examples -------- This pipeline is useful if for instance you want to sample from the space with a small probability, but otherwise use a local perturbation. >>> PipelineExplore( explore_configs=[ {'of_type': 'ResampleExplore', probability=0.05}, {'of_type': 'PerturbExplore'} ]) """ # pylint: disable=super-init-not-called def __init__(self, explore_configs): self.pipeline = [] for explore_config in explore_configs: self.pipeline.append(explore_factory.create(**explore_config))
[docs] def __call__(self, rng, space, params): """Execute explore objects sequentially If one explore object returns the parameters that are different than the ones passed (``params``), then the pipeline returns these parameter values. Otherwise, if all BaseExplore objects return the same parameters as the one passed to the pipeline, then the pipeline returns it. Parameters ---------- rng: numpy.random.Generator A random number generator. It is not contained in ``BaseExplore`` because the explore class must be stateless. space: Space The search space optimized by the algorithm. params: dict Dictionary representing the parameters of the current trial under examination (`trial.params`). Returns ------- ``dict`` The new set of parameters for the trial to be branched. """ for explore in self.pipeline: new_params = explore(rng, space, params) if new_params is not params: return new_params return params
@property def configuration(self): """Configuration of the exploit object""" configuration = super().configuration configuration["explore_configs"] = [ explore.configuration for explore in self.pipeline ] return configuration
[docs]class PerturbExplore(BaseExplore): """ Perturb parameters for exploration Given a set of parameter values, this exploration object randomly perturb them with a given ``factor``. It will multiply the value of a dimension with probability 0.5, otherwise divide it. Values are clamped to limits of the search space when exceeding it. For categorical dimensions, a new value is sampled from categories with equal probability for each categories. Parameters ---------- factor: float, optional Factor used to multiply or divide with probability 0.5 the values of the dimensions. Only applies to real or int dimensions. Integer dimensions are pushed to next integer if ``new_value > value`` otherwise reduced to previous integer, where new_value is the result of either ``value * factor`` or ``value / factor``. Categorial dimensions are sampled from categories randomly. Default: 1.2 volatility: float, optional If the results of ``value * factor`` or ``value / factor`` exceeds the limit of the search space, the new value is set to limit and then added or subtracted ``abs(normal(0, volatility))`` (if at lower limit or upper limit). Default: 0.0001 Notes ----- Categorical dimensions with special probabilities are not supported for now. A category with be sampled with equal probability for each categories. """ # pylint: disable=super-init-not-called def __init__(self, factor=1.2, volatility=0.0001): self.factor = factor self.volatility = volatility
[docs] def perturb_real(self, rng, dim_value, interval): """Perturb real value dimension Parameters ---------- rng: numpy.random.Generator Random number generator dim_value: float Value of the dimension interval: tuple of float Limit of the dimension (lower, upper) """ if rng.random() > 0.5: dim_value *= self.factor else: dim_value *= 1.0 / self.factor if dim_value > interval[1]: dim_value = max( interval[1] - numpy.abs(rng.normal(0, self.volatility)), interval[0] ) elif dim_value < interval[0]: dim_value = min( interval[0] + numpy.abs(rng.normal(0, self.volatility)), interval[1] ) return dim_value
[docs] def perturb_int(self, rng, dim_value, interval): """Perturb integer value dimension Parameters ---------- rng: numpy.random.Generator Random number generator dim_value: int Value of the dimension interval: tuple of int Limit of the dimension (lower, upper) """ new_dim_value = self.perturb_real(rng, dim_value, interval) rounded_new_dim_value = int(numpy.round(new_dim_value)) if rounded_new_dim_value == dim_value and new_dim_value > dim_value: new_dim_value = dim_value + 1 elif rounded_new_dim_value == dim_value and new_dim_value < dim_value: new_dim_value = dim_value - 1 else: new_dim_value = rounded_new_dim_value # Avoid out of dimension. new_dim_value = min(max(new_dim_value, interval[0]), interval[1]) return new_dim_value
# pylint: disable=unused-argument
[docs] def perturb_cat(self, rng, dim_value, dim): """Perturb categorical dimension Parameters ---------- rng: numpy.random.Generator Random number generator dim_value: object Value of the dimension, can be any type. dim: orion.algo.space.CategoricalDimension CategoricalDimension object defining the search space for this dimension. """ # NOTE: Can't use rng.choice otherwise the interval is an array of strings # and the value is casted. choices = dim.interval() choice = choices[rng.randint(len(choices))] return choice
[docs] def __call__(self, rng, space, params): """Execute perturbation Given a set of parameter values, this exploration object randomly perturb them with a given ``factor``. It will multiply the value of a dimension with probability 0.5, otherwise divide it. Values are clamped to limits of the search space when exceeding it. For categorical dimensions, a new value is sampled from categories with equal probability for each categories. Parameters ---------- rng: numpy.random.Generator A random number generator. It is not contained in ``BaseExplore`` because the explore class must be stateless. space: Space The search space optimized by the algorithm. params: dict Dictionary representing the parameters of the current trial under examination (`trial.params`). Returns ------- ``dict`` The new set of parameters for the trial to be branched. """ new_params = {} params = flatten(params) for dim in space.values(): dim_value = params[dim.name] if dim.type == "real": dim_value = self.perturb_real(rng, dim_value, dim.interval()) elif dim.type == "integer": dim_value = self.perturb_int(rng, dim_value, dim.interval()) elif dim.type == "categorical": dim_value = self.perturb_cat(rng, dim_value, dim) elif dim.type == "fidelity": # do nothing pass else: raise ValueError(f"Unsupported dimension type {dim.type}") new_params[dim.name] = dim_value return unflatten(new_params)
@property def configuration(self): """Configuration of the exploit object""" configuration = super().configuration configuration["factor"] = self.factor configuration["volatility"] = self.volatility return configuration
[docs]class ResampleExplore(BaseExplore): """ Sample parameters search space With given probability ``probability``, it will sample a new set of parameters from the search space totally independently of the ``parameters`` passed to ``__call__``. Otherwise, it will return the passed ``parameters``. Parameters ---------- probability: float, optional Probability of sampling a new set of parameters. Default: 0.2 """ # pylint: disable=super-init-not-called def __init__(self, probability=0.2): self.probability = probability
[docs] def __call__(self, rng, space, params): """Execute resampling With given probability ``self.probability``, it will sample a new set of parameters from the search space totally independently of the ``parameters`` passed to ``__call__``. Otherwise, it will return the passed ``parameters``. Parameters ---------- rng: numpy.random.Generator A random number generator. It is not contained in ``BaseExplore`` because the explore class must be stateless. space: Space The search space optimized by the algorithm. params: dict Dictionary representing the parameters of the current trial under examination (`trial.params`). Returns ------- ``dict`` The new set of parameters for the trial to be branched. """ if rng.random() < self.probability: trial = space.sample(1, seed=tuple(rng.randint(0, 1000000, size=3)))[0] params = trial.params return params
@property def configuration(self): """Configuration of the exploit object""" configuration = super().configuration configuration["probability"] = self.probability return configuration
explore_factory = GenericFactory(BaseExplore)