Source code for orion.algo.space

# pylint:disable=too-many-lines
"""
Search space of optimization problems
=====================================

Classes for representing the search space of an optimization problem.

There are 3 classes representing possible parameter types. All of them subclass
the base class `Dimension`:

    * `Real`
    * `Integer`
    * `Categorical`

These are instantiated to declare a problem's parameter space. Oríon registers
them in a ordered dictionary, `Space`, which describes how the parameters should
be in order for `orion.algo.base.AbstractAlgorithm` implementations to
communicate with `orion.core`.

Parameter values recorded in `orion.core.worker.trial.Trial` objects must be
and are in concordance with `orion.algo.space` objects. These objects will be
defined by `orion.core` using the user script's configuration file.

Prior distributions, contained in `Dimension` classes, are based on
:scipy.stats:`distributions` and should be configured as noted in the
scipy documentation for each specific implementation of a random variable type,
unless noted otherwise!

"""
from __future__ import annotations

import copy
import logging
import numbers
from dataclasses import dataclass, field
from distutils.log import error
from functools import singledispatch
from typing import Any, Generic, TypeVar

import numpy
from scipy.stats import distributions

from orion.core.utils import float_to_digits_list, format_trials
from orion.core.utils.flatten import flatten

logger = logging.getLogger(__name__)


[docs]def check_random_state(seed): """Return numpy global rng or RandomState if seed is specified""" if seed is None or seed is numpy.random: # pylint:disable=protected-access,c-extension-no-member rng = numpy.random.mtrand._rand elif isinstance(seed, numpy.random.RandomState): rng = seed else: try: rng = numpy.random.RandomState(seed) except Exception as e: raise ValueError( f"'{seed}' cannot be used to seed a numpy.random.RandomState" " instance" ) from e return rng
# helper class to be able to print [1, ..., 4] instead of [1, '...', 4] class _Ellipsis: # pylint:disable=too-few-public-methods def __repr__(self): return "..." def _to_snake_case(name: str) -> str: """Transform a class name ``MyClassName`` to snakecase ``my_class_name``""" frags = [] frag = [] for char in name: if char.isupper() and frag: frags.append("".join(frag).lower()) frag = [] frag.append(char) if frag: frags.append("".join(frag).lower()) return "_".join(frags) T = TypeVar("T")
[docs]class SpaceConverter(Generic[T]): """SpaceConverter iterates over an Orion search space. This can be used to implement new features for ``orion.algo.space.Space`` outside of Orion's code base. """
[docs] def convert_dimension(self, dimension: Dimension) -> T: """Call the dimension conversion handler""" return getattr(self, _to_snake_case(type(dimension).__name__))(dimension)
[docs] def dimension(self, dim: Dimension) -> T: """Called when the dimension does not have a decicated handler"""
[docs] def real(self, dim: Real) -> T: """Called by real dimension"""
[docs] def integer(self, dim: Integer) -> T: """Called by integer dimension"""
[docs] def categorical(self, dim: Categorical) -> T: """Called by categorical dimension"""
[docs] def fidelity(self, dim: Fidelity) -> T: """Called by fidelity dimension"""
[docs] def space(self, space: Space) -> None: """Iterate through a research space and visit each dimensions""" for _, dim in space.items(): self.visit(dim)
[docs]class Dimension: """Base class for search space dimensions. Attributes ---------- name : str Unique identifier for this `Dimension`. type : str Identifier for the type of parameters this `Dimension` is representing. it can be 'real', 'integer', or 'categorical' (name of a subclass). prior : `scipy.stats.distributions.rv_generic` A distribution over the original dimension. shape : tuple Defines how many dimensions are packed in this `Dimension`. Describes the shape of the corresponding tensor. """ NO_DEFAULT_VALUE = None def __init__(self, name, prior, *args, **kwargs): """Init code which is common for `Dimension` subclasses. Parameters ---------- name : str Unique identifier associated with this `Dimension`, e.g. 'learning_rate'. prior : str | `scipy.stats.distributions.rv_generic` Corresponds to a name of an instance or an instance itself of `scipy.stats.distributions.rv_generic`. Basically, the name of the distribution one wants to use as a :attr:`prior`. args : list kwargs : dict Shape parameter(s) for the `prior` distribution. Should include all the non-optional arguments. It may include ``loc``, ``scale``, ``shape``. .. seealso:: `scipy.stats.distributions` for possible values of `prior` and their arguments. """ self._name = None self.name = name if isinstance(prior, str): self._prior_name = prior self.prior = getattr(distributions, prior) elif prior is None: self._prior_name = "None" self.prior = prior else: self._prior_name = prior.name self.prior = prior self._args = args self._kwargs = kwargs self._default_value = kwargs.pop("default_value", self.NO_DEFAULT_VALUE) self._shape = kwargs.pop("shape", None) self.validate()
[docs] def validate(self): """Validate dimension arguments""" if "random_state" in self._kwargs or "seed" in self._kwargs: raise ValueError( "random_state/seed cannot be set in a " "parameter's definition! Set seed globally!" ) if "discrete" in self._kwargs: raise ValueError( "Do not use kwarg 'discrete' on `Dimension`, " "use pure `_Discrete` class instead!" ) if "size" in self._kwargs: raise ValueError("Use 'shape' keyword only instead of 'size'.") if ( self.default_value is not self.NO_DEFAULT_VALUE and self.default_value not in self ): raise ValueError( f"{self.default_value} is not a valid value for dimension: {self.name}, " "Can't set default value." )
def _get_hashable_members(self): return ( self.name, self.shape, self.type, tuple(self._args), tuple(self._kwargs.items()), self.default_value, self._prior_name, ) # pylint:disable=protected-access def __eq__(self, other): """Return True if other is the same dimension as self""" if not isinstance(other, Dimension): return False return self._get_hashable_members() == other._get_hashable_members() def __hash__(self): """Return the hash of the hashable members""" return hash(self._get_hashable_members())
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. Parameters ---------- n_samples : int, optional The number of samples to be drawn. Default is 1 sample. seed : None | int | ``numpy.random.RandomState`` instance, optional This parameter defines the RandomState object to use for drawing random variates. If None (or np.random), the **global** np.random state is used. If integer, it is used to seed a RandomState instance **just for the call of this function**. Default is None. Set random state to something other than None for reproducible results. .. warning:: Setting `seed` with an integer will cause the same ndarray to be sampled if ``n_samples > 0``. Set `seed` with a ``numpy.random.RandomState`` to carry on the changes in random state across many samples. """ samples = [ self.prior.rvs( *self._args, size=self.shape, random_state=seed, **self._kwargs ) for _ in range(n_samples) ] return samples
[docs] def cast(self, point): """Cast a point to dimension's type If casted point will stay a list or a numpy array depending on the given point's type. """ raise NotImplementedError
[docs] def interval(self, alpha=1.0): """Return a tuple containing lower and upper bound for parameters. If parameters are drawn from an 'open' supported random variable, then it will be attempted to calculate the interval from which a variable is `alpha`-likely to be drawn from. """ return self.prior.interval(alpha, *self._args, **self._kwargs)
def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like .. note:: Default `Dimension` does not have any extra constraints. It just checks whether point lies inside the support and the shape. """ raise NotImplementedError def __repr__(self): """Represent the object as a string.""" # pylint:disable=consider-using-f-string return "{0}(name={1}, prior={{{2}: {3}, {4}}}, shape={5}, default value={6})".format( self.__class__.__name__, self.name, self._prior_name, self._args, self._kwargs, self.shape, self._default_value, )
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" args = copy.deepcopy(list(self._args[:])) if self._prior_name == "uniform" and len(args) == 2: args[1] = args[0] + args[1] args[0] = args[0] args = list(map(str, args)) for k, v in self._kwargs.items(): if isinstance(v, str): args += [f"{k}='{v}'"] else: args += [f"{k}={v}"] if self._shape is not None: args += [f"shape={self._shape}"] if self.default_value is not self.NO_DEFAULT_VALUE: args += [f"default_value={repr(self.default_value)}"] prior_name = self._prior_name if prior_name == "reciprocal": prior_name = "loguniform" if prior_name == "norm": prior_name = "normal" return f"{prior_name}({', '.join(args)})"
[docs] def get_string(self): """Build the string corresponding to current dimension""" return f"{self.name}~{self.get_prior_string()}"
@property def name(self): """See `Dimension` attributes.""" return self._name @name.setter def name(self, value): if isinstance(value, str) or value is None: self._name = value else: raise TypeError( "Dimension's name must be either string or None. " f"Provided: {value}, of type: {type(value)}" ) @property def default_value(self): """Return the default value for this dimensions""" return self._default_value @property def type(self): """See `Dimension` attributes.""" return self.__class__.__name__.lower() @property def prior_name(self): """Return the name of the prior""" return self._prior_name @property def shape(self): """Return the shape of dimension.""" # Default shape `None` corresponds to 0-dim (scalar) or shape == (). # Read about ``size`` argument in # `scipy.stats._distn_infrastructure.rv_generic._argcheck_rvs` if self.prior is None: return None _, _, _, size = self.prior._parse_args_rvs( *self._args, # pylint:disable=protected-access size=self._shape, **self._kwargs, ) return size @property def cardinality(self): """Return the number of all the possible points from `Dimension`. The default value is ``numpy.inf``. """ return numpy.inf
def _is_numeric_array(point): """Test whether a point is numerical object or an array containing only numerical objects""" def _is_numeric(item): return isinstance(item, (numbers.Number, numpy.ndarray)) try: return numpy.all(numpy.vectorize(_is_numeric)(point)) except TypeError: return _is_numeric(point) return False
[docs]class Real(Dimension): """Search space dimension that can take on any real value. Parameters ---------- name: str prior: str See Parameters of `Dimension.__init__()`. args: list kwargs: dict See Parameters of `Dimension.__init__()` for general. Notes ----- Real kwargs (extra) low: float Lower bound (inclusive), optional; default ``-numpy.inf``. high: float: Upper bound (inclusive), optional; default ``numpy.inf``. The upper bound must be inclusive because of rounding errors during optimization which may cause values to round exactly to the upper bound. precision: int Precision, optional; default ``4``. shape: tuple Defines how many dimensions are packed in this `Dimension`. Describes the shape of the corresponding tensor. """ def __init__(self, name, prior, *args, **kwargs): self._low = kwargs.pop("low", -numpy.inf) self._high = kwargs.pop("high", numpy.inf) if self._high <= self._low: raise ValueError( "Lower bound {self._low} has to be less than upper bound {self._high}" ) precision = kwargs.pop("precision", 4) if (isinstance(precision, int) and precision > 0) or precision is None: self.precision = precision else: raise TypeError( "Precision should be a non-negative int or None, " "instead was {precision} of type {type(precision)}." ) super().__init__(name, prior, *args, **kwargs) def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like .. note:: Default `Dimension` does not have any extra constraints. It just checks whether point lies inside the support and the shape. """ if not _is_numeric_array(point): return False low, high = self.interval() point_ = numpy.asarray(point) if point_.shape != self.shape: return False return numpy.all(point_ >= low) and numpy.all(point_ <= high)
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" prior_string = super().get_prior_string() if self.precision != 4: return prior_string[:-1] + f", precision={self.precision})" return prior_string
[docs] def interval(self, alpha=1.0): """Return a tuple containing lower and upper bound for parameters. If parameters are drawn from an 'open' supported random variable, then it will be attempted to calculate the interval from which a variable is `alpha`-likely to be drawn from. .. note:: Both lower and upper bounds are inclusive. """ prior_low, prior_high = super().interval(alpha) return (max(prior_low, self._low), min(prior_high, self._high))
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. .. seealso:: `Dimension.sample` """ samples = [] for _ in range(n_samples): for _ in range(4): sample = super().sample(1, seed) if sample[0] not in self: nice = False continue nice = True samples.extend(sample) break if not nice: raise ValueError( f"Improbable bounds: (low={self._low}, high={self._high}). " "Please make interval larger." ) return samples
[docs] def cast(self, point): """Cast a point to float If casted point will stay a list or a numpy array depending on the given point's type. """ casted_point = numpy.asarray(point).astype(float) if not isinstance(point, numpy.ndarray): return casted_point.tolist() return casted_point
[docs] @staticmethod def get_cardinality(shape, interval, precision, prior_name): """Return the number of all the possible points based and shape and interval""" if precision is None or prior_name not in ["loguniform", "reciprocal"]: return numpy.inf # If loguniform, compute every possible combinations based on precision # for each orders of magnitude. def format_number(number): """Turn number into an array of digits, the size of the precision""" formated_number = numpy.zeros(precision) digits_list = float_to_digits_list(number) length = min(len(digits_list), precision) formated_number[:length] = digits_list[:length] return formated_number min_number = format_number(interval[0]) max_number = format_number(interval[1]) # Compute the number of orders of magnitude spanned by lower and upper bounds # (if lower and upper bounds on same order of magnitude, span is equal to 1) lower_order = numpy.floor(numpy.log10(numpy.abs(interval[0]))) upper_order = numpy.floor(numpy.log10(numpy.abs(interval[1]))) order_span = upper_order - lower_order + 1 # Total number of possibilities for an order of magnitude full_cardinality = 9 * 10 ** (precision - 1) def num_below(number): return ( numpy.clip(number, a_min=0, a_max=9) * 10 ** numpy.arange(precision - 1, -1, -1) ).sum() # Number of values out of lower bound on lowest order of magnitude cardinality_below = num_below(min_number) # Number of values out of upper bound on highest order of magnitude. # Remove 1 to be inclusive. cardinality_above = full_cardinality - num_below(max_number) - 1 # Full cardinality on all orders of magnitude, minus those out of bounds. cardinality = ( full_cardinality * order_span - cardinality_below - cardinality_above ) return int(cardinality) ** int(numpy.prod(shape) if shape else 1)
@property def cardinality(self): """Return the number of all the possible points from Integer `Dimension`""" return Real.get_cardinality( self.shape, self.interval(), self.precision, self._prior_name )
class _Discrete(Dimension): def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. Discretizes with `numpy.floor` the results from `Dimension.sample`. .. seealso:: `Dimension.sample` .. seealso:: Discussion in https://github.com/epistimio/orion/issues/56 if you want to understand better how this `Integer` diamond inheritance works. """ samples = super().sample(n_samples, seed) # Making discrete by ourselves because scipy does not use **floor** return list(map(self.cast, samples)) def interval(self, alpha=1.0): """Return a tuple containing lower and upper bound for parameters. If parameters are drawn from an 'open' supported random variable, then it will be attempted to calculate the interval from which a variable is `alpha`-likely to be drawn from. Bounds are integers. .. note:: Both lower and upper bounds are inclusive. """ low, high = super().interval(alpha) try: int_low = int(numpy.floor(low)) except OverflowError: # infinity cannot be converted to Python int type int_low = -numpy.inf try: int_high = int(numpy.ceil(high)) except OverflowError: # infinity cannot be converted to Python int type int_high = numpy.inf return (int_low, int_high) def __contains__(self, point): raise NotImplementedError
[docs]class Integer(Real, _Discrete): """Search space dimension representing integer values. Parameters ---------- name: str prior: str See Parameters of `Dimension.__init__()`. args: list kwargs: dict See Parameters of `Dimension.__init__()` for general. Notes ----- Real kwargs (extra) low: float Lower bound (inclusive), optional; default ``-numpy.inf``. high: float: Upper bound (inclusive), optional; default ``numpy.inf``. precision: int Precision, optional; default ``4``. shape: tuple Defines how many dimensions are packed in this `Dimension`. Describes the shape of the corresponding tensor. """ def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like `Integer` will check whether `point` contains only integers. """ if not _is_numeric_array(point): return False point_ = numpy.asarray(point) if not numpy.all(numpy.equal(numpy.mod(point_, 1), 0)): return False return super().__contains__(point)
[docs] def cast(self, point): """Cast a point to int If casted point will stay a list or a numpy array depending on the given point's type. """ casted_point = numpy.asarray(point).astype(float) # Rescale point to make high bound inclusive. low, high = self.interval() if not numpy.any(numpy.isinf([low, high])): high = high - low casted_point -= low casted_point = casted_point / high casted_point = casted_point * (high + (1 - 1e-10)) casted_point += low casted_point = numpy.floor(casted_point).astype(int) else: casted_point = numpy.floor(casted_point).astype(int) if not isinstance(point, numpy.ndarray): return casted_point.tolist() return casted_point
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" prior_string = super().get_prior_string() return prior_string[:-1] + ", discrete=True)"
@property def prior_name(self): """Return the name of the prior""" return f"int_{super().prior_name}" # pylint: disable=unused-argument
[docs] @staticmethod def get_cardinality(shape, interval, *args): """Return the number of all the possible points based and shape and interval""" return int(interval[1] - interval[0] + 1) ** _get_shape_cardinality(shape)
@property def cardinality(self): """Return the number of all the possible points from Integer `Dimension`""" return Integer.get_cardinality(self.shape, self.interval())
def _get_shape_cardinality(shape): """Get the cardinality in a shape which can be int or tuple""" shape_cardinality = 1 if shape is None: return shape_cardinality if isinstance(shape, int): shape = (shape,) for cardinality in shape: shape_cardinality *= cardinality return shape_cardinality
[docs]class Categorical(Dimension): """Search space dimension that can take on categorical values. Parameters ---------- name : str See Parameters of `Dimension.__init__()`. categories : dict or other iterable A dictionary would associate categories to probabilities, else it assumes to be drawn uniformly from the iterable. kwargs : dict See Parameters of `Dimension.__init__()` for general. """ def __init__(self, name, categories, **kwargs): if isinstance(categories, dict): self.categories = tuple(categories.keys()) self._probs = tuple(categories.values()) else: self.categories = tuple(categories) self._probs = tuple(numpy.tile(1.0 / len(categories), len(categories))) # Just for compatibility; everything should be `Dimension` to let the # `Transformer` decorators be able to wrap smoothly anything. prior = distributions.rv_discrete( values=(list(range(len(self.categories))), self._probs) ) super().__init__(name, prior, **kwargs)
[docs] @staticmethod def get_cardinality(shape, categories): """Return the number of all the possible points based and shape and categories""" return len(categories) ** _get_shape_cardinality(shape)
@property def cardinality(self): """Return the number of all the possible values from Categorical `Dimension`""" return Categorical.get_cardinality(self.shape, self.interval())
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. .. seealso:: `Dimension.sample` """ rng = check_random_state(seed) cat_ndarray = numpy.array(self.categories, dtype=object) samples = [ rng.choice(cat_ndarray, p=self._probs, size=self._shape) for _ in range(n_samples) ] return samples
[docs] def interval(self, alpha=1.0): """Return a tuple of possible values that this categorical dimension can take.""" return self.categories
def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like """ point_ = numpy.asarray(point, dtype=object) if point_.shape != self.shape: return False _check = numpy.vectorize(lambda x: x in self.categories) return numpy.all(_check(point_)) def __repr__(self): """Represent the object as a string.""" if len(self.categories) > 5: cats = self.categories[:2] + self.categories[-2:] probs = self._probs[:2] + self._probs[-2:] prior = list(zip(cats, probs)) prior.insert(2, _Ellipsis()) else: cats = self.categories probs = self._probs prior = list(zip(cats, probs)) prior = map( lambda x: f"{x[0]}: {x[1]:.2f}" if not isinstance(x, _Ellipsis) else str(x), prior, ) prior = "{" + ", ".join(prior) + "}" return ( f"Categorical(name={self.name}, prior={prior}, shape={self.shape}, " f"default value={self.default_value})" )
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" args = list(map(str, self._args[:])) args += [f"{k}={v}" for k, v in self._kwargs.items()] if self.default_value is not self.NO_DEFAULT_VALUE: args += [f"default_value={self.default_value}"] cats = [repr(c) for c in self.categories] if all(p == self._probs[0] for p in self._probs): prior = f"[{', '.join(cats)}]" else: probs = list(zip(cats, self._probs)) prior = "{" + ", ".join(f"{c}: {p:.2f}" for c, p in probs) + "}" args = [prior] if self._shape is not None: args += [f"shape={self._shape}"] if self.default_value is not self.NO_DEFAULT_VALUE: args += [f"default_value={repr(self.default_value)}"] return f"choices({', '.join(args)})"
@property def get_prior(self): """Return the priors""" return self._probs @property def prior_name(self): """Return the name of the prior""" return "choices"
[docs] def cast(self, point): """Cast a point to some category Casted point will stay a list or a numpy array depending on the given point's type. Raises ------ ValueError If one of the category in `point` is not present in current Categorical Dimension. """ categorical_strings = {str(c): c for c in self.categories} def get_category(value): """Return category corresponding to a string else return singleton object""" if str(value) not in categorical_strings: raise ValueError(f"Invalid category: {value}") return categorical_strings[str(value)] point_ = numpy.asarray(point, dtype=object) cast = numpy.vectorize(get_category, otypes=[object]) casted_point = cast(point_) if not isinstance(point, numpy.ndarray): return casted_point.tolist() return casted_point
[docs]class Fidelity(Dimension): """Fidelity `Dimension` for representing multi-fidelity. Fidelity dimensions are not optimized by the algorithms. If it supports multi-fidelity, the algorithm will select a fidelity level for which it will sample hyper-parameter values to explore a low fidelity space. This class is used as a place-holder so that algorithms can discern fidelity dimensions from hyper-parameter dimensions. Parameters ---------- name : str Name of the dimension low: int Minimum of the fidelity interval. high: int Maximum of the fidelity interval. base: int Base logarithm of the fidelity dimension. Attributes ---------- name : str Name of the dimension default_value: int Maximum of the fidelity interval. """ # pylint:disable=super-init-not-called def __init__(self, name, low, high, base=2): if low <= 0: raise AttributeError("Minimum resources must be a positive number.") elif low > high: raise AttributeError( "Minimum resources must be smaller than maximum resources." ) if base < 1: raise AttributeError("Base should be greater than or equal to 1") self.name = name self.low = int(low) self.high = int(high) self.base = int(base) self.prior = None self._prior_name = "None" @property def default_value(self): """Return `high`""" return self.high # pylint: disable=unused-argument
[docs] @staticmethod def get_cardinality(shape, interval): """Return cardinality of Fidelity dimension, leave it to 1 as Fidelity dimension does not contribute to cardinality in a fixed way now. """ return 1
@property def cardinality(self): """Return cardinality of Fidelity dimension, leave it to 1 as Fidelity dimension does not contribute to cardinality in a fixed way now. """ return Fidelity.get_cardinality(self.shape, self.interval())
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" args = [str(self.low), str(self.high)] if self.base != 2: args += [f"base={self.base}"] return f"fidelity({', '.join(args)})"
[docs] def validate(self): """Do not do anything.""" raise NotImplementedError
[docs] def sample(self, n_samples=1, seed=None): """Do not do anything.""" return [self.high for i in range(n_samples)]
[docs] def interval(self, alpha=1.0): """Do not do anything.""" return (self.low, self.high)
[docs] def cast(self, point=0): """Do not do anything.""" raise NotImplementedError
def __repr__(self): """Represent the object as a string.""" return ( f"{self.__class__.__name__}(name={self.name}, low={self.low}, " f"high={self.high}, base={self.base})" ) def __contains__(self, value): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like """ return self.low <= value <= self.high
[docs]class Space(dict): """Represents the search space. It is a sorted dictionary which contains `Dimension` objects. The dimensions are sorted based on their names. """ contains = Dimension
[docs] def register(self, dimension): """Register a new dimension to `Space`.""" self[dimension.name] = dimension
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from this space. Parameters ---------- n_samples : int, optional The number of samples to be drawn. Default is 1 sample. seed : None | int | ``numpy.random.RandomState`` instance, optional This parameter defines the RandomState object to use for drawing random variates. If None (or np.random), the **global** np.random state is used. If integer, it is used to seed a RandomState instance **just for the call of this function**. Default is None. Set random state to something other than None for reproducible results. Returns ------- trials: list of `orion.core.worker.trial.Trial` Each element is a separate sample of this space, a trial containing values associated with the corresponding dimension. """ rng = check_random_state(seed) samples = [dim.sample(n_samples, rng) for dim in self.values()] return [format_trials.tuple_to_trial(point, self) for point in zip(*samples)]
[docs] def interval(self, alpha=1.0): """Return a list with the intervals for each contained dimension.""" res = [] for dim in self.values(): if dim.type == "categorical": res.append(dim.categories) else: res.append(dim.interval(alpha)) return res
def __getitem__(self, key): """Wrap __getitem__ to allow searching with position.""" if isinstance(key, str): return super().__getitem__(key) values = list(self.values()) return values[key] def __setitem__(self, key, value): """Wrap __setitem__ to allow only ``Space.contains`` class, e.g. `Dimension`, values and string keys. """ if not isinstance(key, str): raise TypeError( f"Keys registered to {self.__class__.__name__} must be string types. " f"Provided: {key}" ) if not isinstance(value, self.contains): raise TypeError( f"Values registered to {self.__class__.__name__} " f"must be {self.contains.__name__} types. " f"Provided: {value}" ) if key in self: raise ValueError( "There is already a Dimension registered with this name. " f"Register it with another name. Provided: {key}" ) super().__setitem__(key, value)
[docs] def assert_contains(self, trial): """Same as __contains__ but instead of return true or false it will raise an exception with the exact causes of the mismatch. Raises ------ ValueError if the trial has parameters that are not contained by the space. """ if isinstance(trial, str): if not super().__contains__(trial): raise ValueError("{trial} does not belong to the dimension") return True flattened_params = flatten(trial.params) keys = set(flattened_params.keys()) errors = [] for dim_name, dim in self.items(): if dim_name not in keys: errors.append(f"{dim_name} is missing") continue value = flattened_params[dim_name] if value not in dim: errors.append(f"{value} does not belong to the dimension {dim}") keys.remove(dim_name) if len(errors) > 0: raise ValueError(f"Trial {trial.id} is not contained in space:\n{errors}") if len(keys) != 0: errors = "\n - ".join(keys) raise ValueError(f"Trial {trial.id} has additional parameters:\n{errors}") return True
def __contains__(self, key_or_trial): """Check whether `trial` is within the bounds of the space. Or check if a name for a dimension is registered in this space. Parameters ---------- key_or_trial: str or `orion.core.worker.trial.Trial` If str, test if the string is a dimension part of the search space. If a Trial, test if trial's hyperparameters fit the current search space. """ try: self.assert_contains(key_or_trial) return True except ValueError: return False def __repr__(self): """Represent as a string the space and the dimensions it contains.""" dims = list(self.values()) return "Space([{}])".format(",\n ".join(map(str, dims)))
[docs] def items(self): """Return items sorted according to keys""" # pylint: disable=consider-using-dict-items return [(k, self[k]) for k in self.keys()]
[docs] def values(self): """Return values sorted according to keys""" # pylint: disable=consider-using-dict-items return [self[k] for k in self.keys()]
[docs] def keys(self): """Return sorted keys""" return list(iter(self))
def __iter__(self): """Return sorted keys""" return iter(sorted(super().keys())) @property def configuration(self): """Return a dictionary of priors.""" return {name: dim.get_prior_string() for name, dim in self.items()} @property def cardinality(self): """Return the number of all all possible sets of samples in the space""" capacities = 1 for dim in self.values(): capacities *= dim.cardinality return capacities
[docs]@singledispatch def to_orionspace(space: Any) -> Space: """Convert a third party search space into an Orion compatible space Raises ------ NotImplementedError if no conversion was registered """ raise NotImplementedError()