| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- import inspect
- import logging
- import math
- import pickle
- from typing import Dict, List, Optional, Sequence, Type, Union
- from ray.tune.result import DEFAULT_METRIC
- from ray.tune.search import (
- UNDEFINED_METRIC_MODE,
- UNDEFINED_SEARCH_SPACE,
- UNRESOLVED_SEARCH_SPACE,
- Searcher,
- )
- from ray.tune.search.sample import (
- Categorical,
- Domain,
- Float,
- Integer,
- LogUniform,
- Quantized,
- )
- from ray.tune.search.variant_generator import parse_spec_vars
- from ray.tune.utils.util import flatten_dict, unflatten_dict
- try:
- import nevergrad as ng
- from nevergrad.optimization import Optimizer
- from nevergrad.optimization.base import ConfiguredOptimizer
- Parameter = ng.p.Parameter
- except ImportError:
- ng = None
- Optimizer = None
- ConfiguredOptimizer = None
- Parameter = None
- logger = logging.getLogger(__name__)
- class NevergradSearch(Searcher):
- """Uses Nevergrad to optimize hyperparameters.
- Nevergrad is an open source tool from Facebook for derivative free
- optimization. More info can be found at:
- https://github.com/facebookresearch/nevergrad.
- You will need to install Nevergrad via the following command:
- .. code-block:: bash
- $ pip install nevergrad
- Parameters:
- optimizer: Optimizer class provided from Nevergrad.
- See here for available optimizers:
- https://facebookresearch.github.io/nevergrad/optimizers_ref.html#optimizers
- This can also be an instance of a `ConfiguredOptimizer`. See the
- section on configured optimizers in the above link.
- optimizer_kwargs: Kwargs passed in when instantiating the `optimizer`
- space: Nevergrad parametrization
- to be passed to optimizer on instantiation, or list of parameter
- names if you passed an optimizer object.
- metric: The training result objective value attribute. If None
- but a mode was passed, the anonymous metric `_metric` will be used
- per default.
- mode: One of {min, max}. Determines whether objective is
- minimizing or maximizing the metric attribute.
- points_to_evaluate: Initial parameter suggestions to be run
- first. This is for when you already have some good parameters
- you want to run first to help the algorithm make better suggestions
- for future parameters. Needs to be a list of dicts containing the
- configurations.
- Tune automatically converts search spaces to Nevergrad's format:
- .. code-block:: python
- import nevergrad as ng
- config = {
- "width": tune.uniform(0, 20),
- "height": tune.uniform(-100, 100),
- "activation": tune.choice(["relu", "tanh"])
- }
- current_best_params = [{
- "width": 10,
- "height": 0,
- "activation": relu",
- }]
- ng_search = NevergradSearch(
- optimizer=ng.optimizers.OnePlusOne,
- metric="mean_loss",
- mode="min",
- points_to_evaluate=current_best_params)
- run(my_trainable, config=config, search_alg=ng_search)
- If you would like to pass the search space manually, the code would
- look like this:
- .. code-block:: python
- import nevergrad as ng
- space = ng.p.Dict(
- width=ng.p.Scalar(lower=0, upper=20),
- height=ng.p.Scalar(lower=-100, upper=100),
- activation=ng.p.Choice(choices=["relu", "tanh"])
- )
- ng_search = NevergradSearch(
- optimizer=ng.optimizers.OnePlusOne,
- space=space,
- metric="mean_loss",
- mode="min")
- run(my_trainable, search_alg=ng_search)
- """
- def __init__(
- self,
- optimizer: Optional[
- Union[Optimizer, Type[Optimizer], ConfiguredOptimizer]
- ] = None,
- optimizer_kwargs: Optional[Dict] = None,
- space: Optional[Union[Dict, Parameter]] = None,
- metric: Optional[str] = None,
- mode: Optional[str] = None,
- points_to_evaluate: Optional[List[Dict]] = None,
- ):
- assert (
- ng is not None
- ), """Nevergrad must be installed!
- You can install Nevergrad with the command:
- `pip install nevergrad`."""
- if mode:
- assert mode in ["min", "max"], "`mode` must be 'min' or 'max'."
- super(NevergradSearch, self).__init__(metric=metric, mode=mode)
- self._space = None
- self._opt_factory = None
- self._nevergrad_opt = None
- self._optimizer_kwargs = optimizer_kwargs or {}
- if points_to_evaluate is None:
- self._points_to_evaluate = None
- elif not isinstance(points_to_evaluate, Sequence):
- raise ValueError(
- "Invalid object type passed for `points_to_evaluate`: "
- f"{type(points_to_evaluate)}. "
- "Please pass a list of points (dictionaries) instead."
- )
- else:
- self._points_to_evaluate = list(points_to_evaluate)
- if isinstance(space, dict) and space:
- resolved_vars, domain_vars, grid_vars = parse_spec_vars(space)
- if domain_vars or grid_vars:
- logger.warning(
- UNRESOLVED_SEARCH_SPACE.format(par="space", cls=type(self))
- )
- space = self.convert_search_space(space)
- if isinstance(optimizer, Optimizer):
- if space is not None and not isinstance(space, list):
- raise ValueError(
- "If you pass a configured optimizer to Nevergrad, either "
- "pass a list of parameter names or None as the `space` "
- "parameter."
- )
- if self._optimizer_kwargs:
- raise ValueError(
- "If you pass in optimizer kwargs, either pass "
- "an `Optimizer` subclass or an instance of "
- "`ConfiguredOptimizer`."
- )
- self._parameters = space
- self._nevergrad_opt = optimizer
- elif (
- inspect.isclass(optimizer) and issubclass(optimizer, Optimizer)
- ) or isinstance(optimizer, ConfiguredOptimizer):
- self._opt_factory = optimizer
- self._parameters = None
- self._space = space
- else:
- raise ValueError(
- "The `optimizer` argument passed to NevergradSearch must be "
- "either an `Optimizer` or a `ConfiguredOptimizer`."
- )
- self._live_trial_mapping = {}
- if self._nevergrad_opt or self._space:
- self._setup_nevergrad()
- def _setup_nevergrad(self):
- if self._opt_factory:
- self._nevergrad_opt = self._opt_factory(
- self._space, **self._optimizer_kwargs
- )
- # nevergrad.tell internally minimizes, so "max" => -1
- if self._mode == "max":
- self._metric_op = -1.0
- elif self._mode == "min":
- self._metric_op = 1.0
- if self._metric is None and self._mode:
- # If only a mode was passed, use anonymous metric
- self._metric = DEFAULT_METRIC
- if hasattr(self._nevergrad_opt, "instrumentation"): # added in v0.2.0
- if self._nevergrad_opt.instrumentation.kwargs:
- if self._nevergrad_opt.instrumentation.args:
- raise ValueError("Instrumented optimizers should use kwargs only")
- if self._parameters is not None:
- raise ValueError(
- "Instrumented optimizers should provide "
- "None as parameter_names"
- )
- else:
- if self._parameters is None:
- raise ValueError(
- "Non-instrumented optimizers should have "
- "a list of parameter_names"
- )
- if len(self._nevergrad_opt.instrumentation.args) != 1:
- raise ValueError("Instrumented optimizers should use kwargs only")
- if self._parameters is not None and self._nevergrad_opt.dimension != len(
- self._parameters
- ):
- raise ValueError(
- "len(parameters_names) must match optimizer "
- "dimension for non-instrumented optimizers"
- )
- if self._points_to_evaluate:
- # Nevergrad is LIFO, so we add the points to evaluate in reverse
- # order.
- for i in range(len(self._points_to_evaluate) - 1, -1, -1):
- self._nevergrad_opt.suggest(self._points_to_evaluate[i])
- def set_search_properties(
- self, metric: Optional[str], mode: Optional[str], config: Dict, **spec
- ) -> bool:
- if self._nevergrad_opt or self._space:
- return False
- space = self.convert_search_space(config)
- self._space = space
- if metric:
- self._metric = metric
- if mode:
- self._mode = mode
- self._setup_nevergrad()
- return True
- def suggest(self, trial_id: str) -> Optional[Dict]:
- if not self._nevergrad_opt:
- raise RuntimeError(
- UNDEFINED_SEARCH_SPACE.format(
- cls=self.__class__.__name__, space="space"
- )
- )
- if not self._metric or not self._mode:
- raise RuntimeError(
- UNDEFINED_METRIC_MODE.format(
- cls=self.__class__.__name__, metric=self._metric, mode=self._mode
- )
- )
- suggested_config = self._nevergrad_opt.ask()
- self._live_trial_mapping[trial_id] = suggested_config
- # in v0.2.0+, output of ask() is a Candidate,
- # with fields args and kwargs
- if not suggested_config.kwargs:
- if self._parameters:
- return unflatten_dict(
- dict(zip(self._parameters, suggested_config.args[0]))
- )
- return unflatten_dict(suggested_config.value)
- else:
- return unflatten_dict(suggested_config.kwargs)
- def on_trial_complete(
- self, trial_id: str, result: Optional[Dict] = None, error: bool = False
- ):
- """Notification for the completion of trial.
- The result is internally negated when interacting with Nevergrad
- so that Nevergrad Optimizers can "maximize" this value,
- as it minimizes on default.
- """
- if result:
- self._process_result(trial_id, result)
- self._live_trial_mapping.pop(trial_id)
- def _process_result(self, trial_id: str, result: Dict):
- ng_trial_info = self._live_trial_mapping[trial_id]
- self._nevergrad_opt.tell(ng_trial_info, self._metric_op * result[self._metric])
- def save(self, checkpoint_path: str):
- save_object = self.__dict__
- with open(checkpoint_path, "wb") as outputFile:
- pickle.dump(save_object, outputFile)
- def restore(self, checkpoint_path: str):
- with open(checkpoint_path, "rb") as inputFile:
- save_object = pickle.load(inputFile)
- self.__dict__.update(save_object)
- @staticmethod
- def convert_search_space(spec: Dict) -> Parameter:
- resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
- if grid_vars:
- raise ValueError(
- "Grid search parameters cannot be automatically converted "
- "to a Nevergrad search space."
- )
- # Flatten and resolve again after checking for grid search.
- spec = flatten_dict(spec, prevent_delimiter=True)
- resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
- def resolve_value(domain: Domain) -> Parameter:
- sampler = domain.get_sampler()
- if isinstance(sampler, Quantized):
- logger.warning(
- "Nevergrad does not support quantization. Dropped quantization."
- )
- sampler = sampler.get_sampler()
- if isinstance(domain, Float):
- if isinstance(sampler, LogUniform):
- return ng.p.Log(
- lower=domain.lower, upper=domain.upper, exponent=math.e
- )
- return ng.p.Scalar(lower=domain.lower, upper=domain.upper)
- elif isinstance(domain, Integer):
- if isinstance(sampler, LogUniform):
- return ng.p.Log(
- lower=domain.lower,
- upper=domain.upper - 1, # Upper bound exclusive
- exponent=math.e,
- ).set_integer_casting()
- return ng.p.Scalar(
- lower=domain.lower,
- upper=domain.upper - 1, # Upper bound exclusive
- ).set_integer_casting()
- elif isinstance(domain, Categorical):
- return ng.p.Choice(choices=domain.categories)
- raise ValueError(
- "Nevergrad does not support parameters of type "
- "`{}` with samplers of type `{}`".format(
- type(domain).__name__, type(domain.sampler).__name__
- )
- )
- # Parameter name is e.g. "a/b/c" for nested dicts
- space = {"/".join(path): resolve_value(domain) for path, domain in domain_vars}
- return ng.p.Dict(**space)
|