Source code for orion.algo.base

# -*- coding: utf-8 -*-
"""
:mod:`orion.algo.base` -- What is a search algorithm, optimizer of a process
==============================================================================

.. module:: base
   :platform: Unix
   :synopsis: Formulation of a general search algorithm with respect to some
      objective.

"""
from abc import (ABCMeta, abstractmethod)
import logging

from orion.core.utils import Factory

log = logging.getLogger(__name__)


# pylint: disable=too-many-public-methods
[docs]class BaseAlgorithm(object, metaclass=ABCMeta): """Base class describing what an algorithm can do. Notes ----- We are using the No Free Lunch theorem's [1]_[3]_ formulation of an `BaseAlgorithm`. We treat it as a part of a procedure which in each iteration suggests a sample of the parameter space of the problem as a candidate solution and observes the results of its evaluation. **Developer Note**: Each algorithm's complete specification, i.e. implementation of its methods and parameters of its own, lies in a separate concrete algorithm class, which must be an **immediate** subclass of `BaseAlgorithm`. [The reason for this is current implementation of `Factory` metaclass which uses `BaseAlgorithm.__subclasses__()`.] Second, one must declare an algorithm's own parameters (tunable elements which could be set by configuration). This is done by passing them to `BaseAlgorithm.__init__` by calling Python's super with a `Space` object as a positional argument plus algorithm's own parameters as keyword arguments. The keys of the keyword arguments passed to `BaseAlgorithm.__init__` are interpreted as the algorithm's parameter names. So for example, a subclass could be as simple as this (regarding the logistics, not an actual algorithm's implementation): Examples -------- .. code-block:: python :linenos: :emphasize-lines: 7 from orion.algo.base import BaseAlgorithm from orion.algo.space import (Integer, Space) class MySimpleAlgo(BaseAlgorithm): def __init__(self, space, multiplier=1, another_param="a string param"): super().__init__(space, multiplier=multiplier, another_param=another_param) def suggest(self, num=1): print(self.another_param) return list(map(lambda x: tuple(map(lambda y: self.multiplier * y, x)), self.space.sample(num))) def observe(self, points, results): pass dim = Integer('named_param', 'norm', 3, 2, shape=(2, 3)) s = Space() s.register(dim) algo = MySimpleAlgo(s, 2, "I am just sampling!") algo.suggest() References ---------- .. [1] D. H. Wolpert and W. G. Macready, “No Free Lunch Theorems for Optimization,” IEEE Transactions on Evolutionary Computation, vol. 1, no. 1, pp. 67–82, Apr. 1997. .. [2] W. G. Macready and D. H. Wolpert, “What Makes An Optimization Problem Hard?,” Complexity, vol. 1, no. 5, pp. 40–46, 1996. .. [3] D. H. Wolpert and W. G. Macready, “No Free Lunch Theorems for Search,” Technical Report SFI-TR-95-02-010, Santa Fe Institute, 1995. """ requires = [] def __init__(self, space, **kwargs): """Declare problem's parameter space and set up algo's hyperparameters. Parameters ---------- space : `orion.algo.space.Space` Definition of a problem's parameter space. kwargs : dict Tunable elements of a particular algorithm, a dictionary from hyperparameter names to values. """ log.debug("Creating Algorithm object of %s type with parameters:\n%s", type(self).__name__, kwargs) self._space = space self._param_names = list(kwargs.keys()) # Instantiate tunable parameters of an algorithm for varname, param in kwargs.items(): # Check if tunable element is another algorithm if isinstance(param, dict) and len(param) == 1: subalgo_type = list(param)[0] subalgo_kwargs = param[subalgo_type] if isinstance(subalgo_kwargs, dict): param = OptimizationAlgorithm(subalgo_type, space, **subalgo_kwargs) elif isinstance(param, str) and \ param.lower() in OptimizationAlgorithm.typenames: # pylint: disable=too-many-function-args param = OptimizationAlgorithm(param, space) elif varname == 'seed': self.seed_rng(param) setattr(self, varname, param)
[docs] def seed_rng(self, seed): """Seed the state of the random number generator. :param seed: Integer seed for the random number generator. .. note:: This methods does nothing if the algorithm is deterministic. """ pass
@property def state_dict(self): """Return a state dict that can be used to reset the state of the algorithm.""" return {}
[docs] def set_state(self, state_dict): """Reset the state of the algorithm based on the given state_dict :param state_dict: Dictionary representing state of an algorithm """ pass
[docs] @abstractmethod def suggest(self, num=1): """Suggest a `num` of new sets of parameters. Parameters ---------- num: int, optional Number of points to suggest. Defaults to 1. Returns ------- list of points or None A list of lists representing points suggested by the algorithm. The algorithm may opt out if it cannot make a good suggestion at the moment (it may be waiting for other trials to complete), in which case it will return None. Notes ----- New parameters must be compliant with the problem's domain `orion.algo.space.Space`. """ pass
[docs] @abstractmethod def observe(self, points, results): """Observe the `results` of the evaluation of the `points` in the process defined in user's script. Parameters ---------- points : list of tuples of array-likes Points from a `orion.algo.space.Space`. Evaluated problem parameters by a consumer. results : list of dicts Contains the result of an evaluation; partial information about the black-box function at each point in `params`. Result ------ objective : numeric Evaluation of this problem's objective function. gradient : 1D array-like, optional Contains values of the derivatives of the `objective` function with respect to `params`. constraint : list of numeric, optional List of constraints expression evaluation which must be greater or equal to zero by the problem's definition. """ pass
@property def is_done(self): """Return True, if an algorithm holds that there can be no further improvement.""" return False
[docs] def score(self, point): # pylint:disable=no-self-use,unused-argument """Allow algorithm to evaluate `point` based on a prediction about this parameter set's performance. By default, return the same score any parameter (no preference). :returns: A subjective measure of expected perfomance. :rtype: float """ return 0
[docs] def judge(self, point, measurements): # pylint:disable=no-self-use,unused-argument """Inform an algorithm about online `measurements` of a running trial. :param point: A tuple which specifies the values of the (hyper)parameters used to execute user's script with. This method is to be used as a callback in a client-server communication between user's script and a orion's worker using a `BaseAlgorithm`. Data returned from this method must be serializable and will be used as a response to the running environment. Default response is None. .. note:: Calling algorithm to `judge` a `point` based on its online `measurements` will effectively change a state in the algorithm (like a reinforcement learning agent's hidden state or an automatic early stopping mechanism's regression), which it may change the value of the property `should_suspend`. :returns: None or a serializable dictionary containing named data """ return None
@property def should_suspend(self): """Allow algorithm to decide whether a particular running trial is still worth to complete its evaluation, based on information provided by the `judge` method. """ return False @property def configuration(self): """Return tunable elements of this algorithm in a dictionary form appropriate for saving. """ dict_form = dict() for attrname in self._param_names: if attrname.startswith('_'): # Do not log _space or others in conf continue attr = getattr(self, attrname) if isinstance(attr, BaseAlgorithm): attr = attr.configuration dict_form[attrname] = attr return {self.__class__.__name__.lower(): dict_form} @property def space(self): """Domain of problem associated with this algorithm's instance.""" return self._space @space.setter def space(self, space_): """Propagate changes in defined space to possibly nested algorithms.""" self._space = space_ for attr in self.__dict__.values(): if isinstance(attr, BaseAlgorithm): attr.space = space_
# pylint: disable=too-few-public-methods,abstract-method
[docs]class OptimizationAlgorithm(BaseAlgorithm, metaclass=Factory): """Class used to inject dependency on an algorithm implementation. .. seealso:: `orion.core.utils.Factory` metaclass and `BaseAlgorithm` interface. """ pass