| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- import copy
- import logging
- from typing import Dict, List, Optional, Tuple
- import ray
- import ray.cloudpickle as pickle
- 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,
- Quantized,
- Uniform,
- )
- from ray.tune.search.variant_generator import parse_spec_vars
- from ray.tune.utils.util import unflatten_dict
- try:
- import zoopt
- from zoopt import Solution, ValueType
- except ImportError:
- zoopt = None
- Solution = ValueType = None
- logger = logging.getLogger(__name__)
- class ZOOptSearch(Searcher):
- """A wrapper around ZOOpt to provide trial suggestions.
- ZOOptSearch is a library for derivative-free optimization. It is backed by
- the `ZOOpt <https://github.com/polixir/ZOOpt>`__ package. Currently,
- Asynchronous Sequential RAndomized COordinate Shrinking (ASRacos)
- is implemented in Tune.
- To use ZOOptSearch, install zoopt (>=0.4.1): ``pip install -U zoopt``.
- Tune automatically converts search spaces to ZOOpt"s format:
- .. code-block:: python
- from ray import tune
- from ray.tune.search.zoopt import ZOOptSearch
- "config": {
- "iterations": 10, # evaluation times
- "width": tune.uniform(-10, 10),
- "height": tune.uniform(-10, 10)
- }
- zoopt_search_config = {
- "parallel_num": 8, # how many workers to parallel
- }
- zoopt_search = ZOOptSearch(
- algo="Asracos", # only support Asracos currently
- budget=20, # must match `num_samples` in `tune.TuneConfig()`.
- dim_dict=dim_dict,
- metric="mean_loss",
- mode="min",
- **zoopt_search_config
- )
- tuner = tune.Tuner(
- my_objective,
- tune_config=tune.TuneConfig(
- search_alg=zoopt_search,
- num_samples=20
- ),
- run_config=tune.RunConfig(
- name="zoopt_search",
- stop={"timesteps_total": 10}
- ),
- param_space=config
- )
- tuner.fit()
- If you would like to pass the search space manually, the code would
- look like this:
- .. code-block:: python
- from ray import tune
- from ray.tune.search.zoopt import ZOOptSearch
- from zoopt import ValueType
- dim_dict = {
- "height": (ValueType.CONTINUOUS, [-10, 10], 1e-2),
- "width": (ValueType.DISCRETE, [-10, 10], False),
- "layers": (ValueType.GRID, [4, 8, 16])
- }
- "config": {
- "iterations": 10, # evaluation times
- }
- zoopt_search_config = {
- "parallel_num": 8, # how many workers to parallel
- }
- zoopt_search = ZOOptSearch(
- algo="Asracos", # only support Asracos currently
- budget=20, # must match `num_samples` in `tune.TuneConfig()`.
- dim_dict=dim_dict,
- metric="mean_loss",
- mode="min",
- **zoopt_search_config
- )
- tuner = tune.Tuner(
- my_objective,
- tune_config=tune.TuneConfig(
- search_alg=zoopt_search,
- num_samples=20
- ),
- run_config=tune.RunConfig(
- name="zoopt_search",
- stop={"timesteps_total": 10}
- ),
- )
- tuner.fit()
- Parameters:
- algo: To specify an algorithm in zoopt you want to use.
- Only support ASRacos currently.
- budget: Number of samples.
- dim_dict: Dimension dictionary.
- For continuous dimensions: (continuous, search_range, precision);
- For discrete dimensions: (discrete, search_range, has_order);
- For grid dimensions: (grid, grid_list).
- More details can be found in zoopt package.
- 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.
- parallel_num: How many workers to parallel. Note that initial
- phase may start less workers than this number. More details can
- be found in zoopt package.
- """
- optimizer = None
- def __init__(
- self,
- algo: str = "asracos",
- budget: Optional[int] = None,
- dim_dict: Optional[Dict] = None,
- metric: Optional[str] = None,
- mode: Optional[str] = None,
- points_to_evaluate: Optional[List[Dict]] = None,
- parallel_num: int = 1,
- **kwargs
- ):
- assert (
- zoopt is not None
- ), "ZOOpt not found - please install zoopt by `pip install -U zoopt`."
- assert budget is not None, "`budget` should not be None!"
- if mode:
- assert mode in ["min", "max"], "`mode` must be 'min' or 'max'."
- _algo = algo.lower()
- assert _algo in [
- "asracos",
- "sracos",
- ], "`algo` must be in ['asracos', 'sracos'] currently"
- self._algo = _algo
- if isinstance(dim_dict, dict) and dim_dict:
- resolved_vars, domain_vars, grid_vars = parse_spec_vars(dim_dict)
- if domain_vars or grid_vars:
- logger.warning(
- UNRESOLVED_SEARCH_SPACE.format(par="dim_dict", cls=type(self))
- )
- dim_dict = self.convert_search_space(dim_dict, join=True)
- self._dim_dict = dim_dict
- self._budget = budget
- self._metric = metric
- if mode == "max":
- self._metric_op = -1.0
- elif mode == "min":
- self._metric_op = 1.0
- self._points_to_evaluate = copy.deepcopy(points_to_evaluate)
- self._live_trial_mapping = {}
- self._dim_keys = []
- self.solution_dict = {}
- self.best_solution_list = []
- self.optimizer = None
- self.kwargs = kwargs
- self.parallel_num = parallel_num
- super(ZOOptSearch, self).__init__(metric=self._metric, mode=mode)
- if self._dim_dict:
- self._setup_zoopt()
- def _setup_zoopt(self):
- if self._metric is None and self._mode:
- # If only a mode was passed, use anonymous metric
- self._metric = DEFAULT_METRIC
- _dim_list = []
- for k in self._dim_dict:
- self._dim_keys.append(k)
- _dim_list.append(self._dim_dict[k])
- init_samples = None
- if self._points_to_evaluate:
- logger.warning(
- "`points_to_evaluate` is ignored by ZOOpt in versions <= 0.4.1."
- )
- init_samples = [
- Solution(x=tuple(point[dim] for dim in self._dim_keys))
- for point in self._points_to_evaluate
- ]
- dim = zoopt.Dimension2(_dim_list)
- par = zoopt.Parameter(budget=self._budget, init_samples=init_samples)
- if self._algo == "sracos" or self._algo == "asracos":
- from zoopt.algos.opt_algorithms.racos.sracos import SRacosTune
- self.optimizer = SRacosTune(
- dimension=dim,
- parameter=par,
- parallel_num=self.parallel_num,
- **self.kwargs
- )
- def set_search_properties(
- self, metric: Optional[str], mode: Optional[str], config: Dict, **spec
- ) -> bool:
- if self._dim_dict:
- return False
- space = self.convert_search_space(config)
- self._dim_dict = space
- if metric:
- self._metric = metric
- if mode:
- self._mode = mode
- if self._mode == "max":
- self._metric_op = -1.0
- elif self._mode == "min":
- self._metric_op = 1.0
- self._setup_zoopt()
- return True
- def suggest(self, trial_id: str) -> Optional[Dict]:
- if not self._dim_dict or not self.optimizer:
- raise RuntimeError(
- UNDEFINED_SEARCH_SPACE.format(
- cls=self.__class__.__name__, space="dim_dict"
- )
- )
- if not self._metric or not self._mode:
- raise RuntimeError(
- UNDEFINED_METRIC_MODE.format(
- cls=self.__class__.__name__, metric=self._metric, mode=self._mode
- )
- )
- _solution = self.optimizer.suggest()
- if _solution == "FINISHED":
- if ray.__version__ >= "0.8.7":
- return Searcher.FINISHED
- else:
- return None
- if _solution:
- self.solution_dict[str(trial_id)] = _solution
- _x = _solution.get_x()
- new_trial = dict(zip(self._dim_keys, _x))
- self._live_trial_mapping[trial_id] = new_trial
- return unflatten_dict(new_trial)
- def on_trial_complete(
- self, trial_id: str, result: Optional[Dict] = None, error: bool = False
- ):
- """Notification for the completion of trial."""
- if result:
- _solution = self.solution_dict[str(trial_id)]
- _best_solution_so_far = self.optimizer.complete(
- _solution, self._metric_op * result[self._metric]
- )
- if _best_solution_so_far:
- self.best_solution_list.append(_best_solution_so_far)
- del self._live_trial_mapping[trial_id]
- 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, join: bool = False) -> Dict[str, Tuple]:
- spec = copy.deepcopy(spec)
- resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
- if not domain_vars and not grid_vars:
- return {}
- if grid_vars:
- raise ValueError(
- "Grid search parameters cannot be automatically converted "
- "to a ZOOpt search space."
- )
- def resolve_value(domain: Domain) -> Tuple:
- quantize = None
- sampler = domain.get_sampler()
- if isinstance(sampler, Quantized):
- quantize = sampler.q
- sampler = sampler.sampler
- if isinstance(domain, Float):
- precision = quantize or 1e-12
- if isinstance(sampler, Uniform):
- return (
- ValueType.CONTINUOUS,
- [domain.lower, domain.upper],
- precision,
- )
- elif isinstance(domain, Integer):
- if isinstance(sampler, Uniform):
- return (ValueType.DISCRETE, [domain.lower, domain.upper - 1], True)
- elif isinstance(domain, Categorical):
- # Categorical variables would use ValueType.DISCRETE with
- # has_partial_order=False, however, currently we do not
- # keep track of category values and cannot automatically
- # translate back and forth between them.
- if isinstance(sampler, Uniform):
- return (ValueType.GRID, domain.categories)
- raise ValueError(
- "ZOOpt does not support parameters of type "
- "`{}` with samplers of type `{}`".format(
- type(domain).__name__, type(domain.sampler).__name__
- )
- )
- conv_spec = {
- "/".join(path): resolve_value(domain) for path, domain in domain_vars
- }
- if join:
- spec.update(conv_spec)
- conv_spec = spec
- return conv_spec
|