mirror of
https://github.com/wassname/ray.git
synced 2026-08-03 13:10:57 +08:00
[tune] Refactor search algorithms (#7037)
* start refactoring of search algorithms * format * needs tests * fix * suggestions * Fix PBT * lint * refactoring * hyperopt_working * dragonfly * hyperopt * change_half_of_algs * save * code-removed * remove_lots_of_unneccessary * changes * formatting * suggest * reset * rm * tests * search-change * exception * refactor-doc * search * py * moredocs * Update doc/source/tune-searchalg.rst * concurrency * max * tune * betterwarning * bohb * tests * test-change Co-authored-by: ujvl <misraujval@gmail.com>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
from ray.tune.suggest.search import SearchAlgorithm
|
||||
from ray.tune.suggest.basic_variant import BasicVariantGenerator
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest.suggestion import (SearchGenerator, Searcher,
|
||||
ConcurrencyLimiter)
|
||||
from ray.tune.suggest.variant_generator import grid_search
|
||||
from ray.tune.suggest.repeater import Repeater
|
||||
from ray.tune.suggest.bohb import TuneBOHB
|
||||
|
||||
__all__ = [
|
||||
"SearchAlgorithm", "BasicVariantGenerator", "SuggestionAlgorithm",
|
||||
"grid_search", "TuneBOHB", "Repeater"
|
||||
"SearchAlgorithm", "Searcher", "BasicVariantGenerator", "SearchGenerator",
|
||||
"grid_search", "Repeater", "ConcurrencyLimiter"
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,18 +4,20 @@ except ImportError:
|
||||
ax = None
|
||||
import logging
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AxSearch(SuggestionAlgorithm):
|
||||
class AxSearch(Searcher):
|
||||
"""A wrapper around Ax to provide trial suggestions.
|
||||
|
||||
Requires Ax to be installed. Ax is an open source tool from
|
||||
Facebook for configuring and optimizing experiments. More information
|
||||
can be found in https://ax.dev/.
|
||||
|
||||
This module manages its own concurrency.
|
||||
|
||||
Parameters:
|
||||
parameters (list[dict]): Parameters in the experiment search space.
|
||||
Required elements in the dictionaries are: "name" (name of
|
||||
@@ -36,9 +38,7 @@ class AxSearch(SuggestionAlgorithm):
|
||||
"x3 >= x4" or "x3 + x4 >= 2".
|
||||
outcome_constraints (list[str]): Outcome constraints of form
|
||||
"metric_name >= bound", like "m1 <= 3."
|
||||
use_early_stopped_trials (bool): Whether to use early terminated
|
||||
trial results in the optimization process.
|
||||
|
||||
use_early_stopped_trials: Deprecated.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -56,7 +56,11 @@ class AxSearch(SuggestionAlgorithm):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, ax_client, max_concurrent=10, mode="max", **kwargs):
|
||||
def __init__(self,
|
||||
ax_client,
|
||||
max_concurrent=10,
|
||||
mode="max",
|
||||
use_early_stopped_trials=None):
|
||||
assert ax is not None, "Ax must be installed!"
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
self._ax = ax_client
|
||||
@@ -66,38 +70,29 @@ class AxSearch(SuggestionAlgorithm):
|
||||
logger.warning("Detected sequential enforcement. Setting max "
|
||||
"concurrency to 1.")
|
||||
max_concurrent = 1
|
||||
self._max_concurrent = max_concurrent
|
||||
self._parameters = list(exp.parameters)
|
||||
self._live_index_mapping = {}
|
||||
super(AxSearch, self).__init__(
|
||||
metric=self._objective_name, mode=mode, **kwargs)
|
||||
metric=self._objective_name,
|
||||
mode=mode,
|
||||
max_concurrent=max_concurrent,
|
||||
use_early_stopped_trials=use_early_stopped_trials)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
parameters, trial_index = self._ax.get_next_trial()
|
||||
self._live_index_mapping[trial_id] = trial_index
|
||||
return parameters
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
Data of form key value dictionary of metric names and values.
|
||||
"""
|
||||
if result:
|
||||
self._process_result(trial_id, result, early_terminated)
|
||||
self._process_result(trial_id, result)
|
||||
self._live_index_mapping.pop(trial_id)
|
||||
|
||||
def _process_result(self, trial_id, result, early_terminated=False):
|
||||
if early_terminated and self._use_early_stopped is False:
|
||||
return
|
||||
def _process_result(self, trial_id, result):
|
||||
ax_trial_index = self._live_index_mapping[trial_id]
|
||||
metric_dict = {
|
||||
self._objective_name: (result[self._objective_name], 0.0)
|
||||
@@ -109,6 +104,3 @@ class AxSearch(SuggestionAlgorithm):
|
||||
metric_dict.update({on: (result[on], 0.0) for on in outcome_names})
|
||||
self._ax.complete_trial(
|
||||
trial_index=ax_trial_index, raw_data=metric_dict)
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_index_mapping)
|
||||
|
||||
@@ -6,12 +6,12 @@ try: # Python 3 only -- needed for lint test.
|
||||
except ImportError:
|
||||
byo = None
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BayesOptSearch(SuggestionAlgorithm):
|
||||
class BayesOptSearch(Searcher):
|
||||
"""A wrapper around BayesOpt to provide trial suggestions.
|
||||
|
||||
Requires BayesOpt to be installed. You can install BayesOpt with the
|
||||
@@ -20,8 +20,6 @@ class BayesOptSearch(SuggestionAlgorithm):
|
||||
Parameters:
|
||||
space (dict): Continuous search space. Parameters will be sampled from
|
||||
this space which will be used to run trials.
|
||||
max_concurrent (int): Number of maximum concurrent trials. Defaults
|
||||
to 10.
|
||||
metric (str): The training result objective value attribute.
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
minimizing or maximizing the metric attribute.
|
||||
@@ -29,8 +27,8 @@ class BayesOptSearch(SuggestionAlgorithm):
|
||||
provide values for the keys `kind`, `kappa`, and `xi`.
|
||||
random_state (int): Used to initialize BayesOpt.
|
||||
verbose (int): Sets verbosity level for BayesOpt packages.
|
||||
use_early_stopped_trials (bool): Whether to use early terminated
|
||||
trial results in the optimization process.
|
||||
max_concurrent: Deprecated.
|
||||
use_early_stopped_trials: Deprecated.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -41,9 +39,7 @@ class BayesOptSearch(SuggestionAlgorithm):
|
||||
'width': (0, 20),
|
||||
'height': (-100, 100),
|
||||
}
|
||||
algo = BayesOptSearch(
|
||||
space, max_concurrent=4, metric="mean_loss", mode="min")
|
||||
|
||||
algo = BayesOptSearch(space, metric="mean_loss", mode="min")
|
||||
tune.run(my_func, algo=algo)
|
||||
"""
|
||||
# bayes_opt.BayesianOptimization: Optimization object
|
||||
@@ -51,32 +47,26 @@ class BayesOptSearch(SuggestionAlgorithm):
|
||||
|
||||
def __init__(self,
|
||||
space,
|
||||
max_concurrent=10,
|
||||
reward_attr=None,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
utility_kwargs=None,
|
||||
random_state=1,
|
||||
verbose=0,
|
||||
**kwargs):
|
||||
max_concurrent=None,
|
||||
use_early_stopped_trials=None):
|
||||
assert byo is not None, (
|
||||
"BayesOpt must be installed!. You can install BayesOpt with"
|
||||
" the command: `pip install bayesian-optimization`.")
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
assert utility_kwargs is not None, (
|
||||
"Must define arguments for the utiliy function!")
|
||||
"Must define arguments for the utility function!")
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
|
||||
if reward_attr is not None:
|
||||
mode = "max"
|
||||
metric = reward_attr
|
||||
logger.warning(
|
||||
"`reward_attr` is deprecated and will be removed in a future "
|
||||
"version of Tune. "
|
||||
"Setting `metric={}` and `mode=max`.".format(reward_attr))
|
||||
super(BayesOptSearch, self).__init__(
|
||||
metric=metric,
|
||||
mode=mode,
|
||||
max_concurrent=max_concurrent,
|
||||
use_early_stopped_trials=use_early_stopped_trials)
|
||||
|
||||
self._max_concurrent = max_concurrent
|
||||
self._metric = metric
|
||||
if mode == "max":
|
||||
self._metric_op = 1.
|
||||
elif mode == "min":
|
||||
@@ -88,41 +78,23 @@ class BayesOptSearch(SuggestionAlgorithm):
|
||||
|
||||
self.utility = byo.UtilityFunction(**utility_kwargs)
|
||||
|
||||
super(BayesOptSearch, self).__init__(
|
||||
metric=self._metric, mode=mode, **kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
|
||||
new_trial = self.optimizer.suggest(self.utility)
|
||||
|
||||
self._live_trial_mapping[trial_id] = new_trial
|
||||
|
||||
return copy.deepcopy(new_trial)
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial."""
|
||||
if result:
|
||||
self._process_result(trial_id, result, early_terminated)
|
||||
self._process_result(trial_id, result)
|
||||
del self._live_trial_mapping[trial_id]
|
||||
|
||||
def _process_result(self, trial_id, result, early_terminated=False):
|
||||
if early_terminated and self._use_early_stopped is False:
|
||||
return
|
||||
def _process_result(self, trial_id, result):
|
||||
self.optimizer.register(
|
||||
params=self._live_trial_mapping[trial_id],
|
||||
target=self._metric_op * result[self._metric])
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
target=self._metric_op * result[self.metric])
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = self.optimizer
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from ray.tune.suggest import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,7 +17,7 @@ class _BOHBJobWrapper():
|
||||
self.exception = None
|
||||
|
||||
|
||||
class TuneBOHB(SuggestionAlgorithm):
|
||||
class TuneBOHB(Searcher):
|
||||
"""BOHB suggestion component.
|
||||
|
||||
|
||||
@@ -104,11 +104,7 @@ class TuneBOHB(SuggestionAlgorithm):
|
||||
hbs_wrapper = self.to_wrapper(trial_id, result)
|
||||
self.bohber.new_result(hbs_wrapper)
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
del self.trial_to_params[trial_id]
|
||||
if trial_id in self.paused:
|
||||
self.paused.remove(trial_id)
|
||||
|
||||
@@ -10,12 +10,12 @@ try: # Python 3 only -- needed for lint test.
|
||||
except ImportError:
|
||||
dragonfly = None
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest.suggestion import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DragonflySearch(SuggestionAlgorithm):
|
||||
class DragonflySearch(Searcher):
|
||||
"""A wrapper around Dragonfly to provide trial suggestions.
|
||||
|
||||
Requires Dragonfly to be installed via ``pip install dragonfly-opt``.
|
||||
@@ -23,8 +23,6 @@ class DragonflySearch(SuggestionAlgorithm):
|
||||
Parameters:
|
||||
optimizer (dragonfly.opt.BlackboxOptimiser): Optimizer provided
|
||||
from dragonfly. Choose an optimiser that extends BlackboxOptimiser.
|
||||
max_concurrent (int): Number of maximum concurrent trials. Defaults
|
||||
to 10.
|
||||
metric (str): The training result objective value attribute.
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
minimizing or maximizing the metric attribute.
|
||||
@@ -69,16 +67,13 @@ class DragonflySearch(SuggestionAlgorithm):
|
||||
domain_config.domain.list_of_domains[0])
|
||||
optimizer = EuclideanGPBandit(func_caller, ask_tell_mode=True)
|
||||
|
||||
algo = DragonflySearch(optimizer, max_concurrent=4,
|
||||
metric="objective", mode="max")
|
||||
algo = DragonflySearch(optimizer, metric="objective", mode="max")
|
||||
|
||||
tune.run(my_func, algo=algo)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
max_concurrent=10,
|
||||
reward_attr=None,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
points_to_evaluate=None,
|
||||
@@ -87,17 +82,8 @@ class DragonflySearch(SuggestionAlgorithm):
|
||||
assert dragonfly is not None, """dragonfly must be installed!
|
||||
You can install Dragonfly with the command:
|
||||
`pip install dragonfly`."""
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
|
||||
if reward_attr is not None:
|
||||
mode = "max"
|
||||
metric = reward_attr
|
||||
logger.warning(
|
||||
"`reward_attr` is deprecated and will be removed in a future "
|
||||
"version of Tune. "
|
||||
"Setting `metric={}` and `mode=max`.".format(reward_attr))
|
||||
|
||||
self._initial_points = []
|
||||
self._opt = optimizer
|
||||
self._opt.initialise()
|
||||
@@ -105,8 +91,6 @@ class DragonflySearch(SuggestionAlgorithm):
|
||||
self._opt.tell([(points_to_evaluate, evaluated_rewards)])
|
||||
elif points_to_evaluate:
|
||||
self._initial_points = points_to_evaluate
|
||||
self._max_concurrent = max_concurrent
|
||||
self._metric = metric
|
||||
# Dragonfly internally maximizes, so "min" => -1
|
||||
if mode == "min":
|
||||
self._metric_op = -1.
|
||||
@@ -114,36 +98,31 @@ class DragonflySearch(SuggestionAlgorithm):
|
||||
self._metric_op = 1.
|
||||
self._live_trial_mapping = {}
|
||||
super(DragonflySearch, self).__init__(
|
||||
metric=self._metric, mode=mode, **kwargs)
|
||||
metric=metric, mode=mode, **kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
if self._initial_points:
|
||||
suggested_config = self._initial_points[0]
|
||||
del self._initial_points[0]
|
||||
else:
|
||||
suggested_config = self._opt.ask()
|
||||
try:
|
||||
suggested_config = self._opt.ask()
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Dragonfly errored when querying. This may be due to a "
|
||||
"higher level of parallelism than supported. Try reducing "
|
||||
"parallelism in the experiment: %s", str(exc))
|
||||
return None
|
||||
self._live_trial_mapping[trial_id] = suggested_config
|
||||
return {"point": suggested_config}
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Passes result to Dragonfly unless early terminated or errored."""
|
||||
trial_info = self._live_trial_mapping.pop(trial_id)
|
||||
if result:
|
||||
self._opt.tell([(trial_info,
|
||||
self._metric_op * result[self._metric])])
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = (self._initial_points, self._opt)
|
||||
with open(checkpoint_dir, "wb") as outputFile:
|
||||
|
||||
@@ -11,12 +11,12 @@ except ImportError:
|
||||
hpo = None
|
||||
|
||||
from ray.tune.error import TuneError
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HyperOptSearch(SuggestionAlgorithm):
|
||||
class HyperOptSearch(Searcher):
|
||||
"""A wrapper around HyperOpt to provide trial suggestions.
|
||||
|
||||
Requires HyperOpt to be installed from source.
|
||||
@@ -30,8 +30,6 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
space (dict): HyperOpt configuration. Parameters will be sampled
|
||||
from this configuration and will be used to override
|
||||
parameters generated in the variant generation process.
|
||||
max_concurrent (int): Number of maximum concurrent trials. Defaults
|
||||
to 10.
|
||||
metric (str): The training result objective value attribute.
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
minimizing or maximizing the metric attribute.
|
||||
@@ -49,8 +47,8 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
results. Defaults to None.
|
||||
gamma (float in range (0,1)): parameter governing the tree parzen
|
||||
estimators suggestion algorithm. Defaults to 0.25.
|
||||
use_early_stopped_trials (bool): Whether to use early terminated
|
||||
trial results in the optimization process.
|
||||
max_concurrent: Deprecated.
|
||||
use_early_stopped_trials: Deprecated.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -65,42 +63,37 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
'activation': 0, # The index of "relu"
|
||||
}]
|
||||
algo = HyperOptSearch(
|
||||
space, max_concurrent=4, metric="mean_loss", mode="min",
|
||||
space, metric="mean_loss", mode="min",
|
||||
points_to_evaluate=current_best_params)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
space,
|
||||
max_concurrent=10,
|
||||
reward_attr=None,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
points_to_evaluate=None,
|
||||
n_initial_points=20,
|
||||
random_state_seed=None,
|
||||
gamma=0.25,
|
||||
**kwargs):
|
||||
assert hpo is not None, "HyperOpt must be installed!"
|
||||
def __init__(
|
||||
self,
|
||||
space,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
points_to_evaluate=None,
|
||||
n_initial_points=20,
|
||||
random_state_seed=None,
|
||||
gamma=0.25,
|
||||
max_concurrent=None,
|
||||
use_early_stopped_trials=None,
|
||||
):
|
||||
assert hpo is not None, (
|
||||
"HyperOpt must be installed! Run `pip install hyperopt`.")
|
||||
from hyperopt.fmin import generate_trials_to_calculate
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
|
||||
if reward_attr is not None:
|
||||
mode = "max"
|
||||
metric = reward_attr
|
||||
logger.warning(
|
||||
"`reward_attr` is deprecated and will be removed in a future "
|
||||
"version of Tune. "
|
||||
"Setting `metric={}` and `mode=max`.".format(reward_attr))
|
||||
|
||||
self._max_concurrent = max_concurrent
|
||||
self._metric = metric
|
||||
super(HyperOptSearch, self).__init__(
|
||||
metric=metric,
|
||||
mode=mode,
|
||||
max_concurrent=max_concurrent,
|
||||
use_early_stopped_trials=use_early_stopped_trials)
|
||||
# hyperopt internally minimizes, so "max" => -1
|
||||
if mode == "max":
|
||||
self._metric_op = -1.
|
||||
self.metric_op = -1.
|
||||
elif mode == "min":
|
||||
self._metric_op = 1.
|
||||
self.metric_op = 1.
|
||||
|
||||
if n_initial_points is None:
|
||||
self.algo = hpo.tpe.suggest
|
||||
else:
|
||||
@@ -124,13 +117,7 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
else:
|
||||
self.rstate = np.random.RandomState(random_state_seed)
|
||||
|
||||
super(HyperOptSearch, self).__init__(
|
||||
metric=self._metric, mode=mode, **kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
|
||||
if self._points_to_evaluate > 0:
|
||||
new_trial = self._hpopt_trials.trials[self._points_to_evaluate - 1]
|
||||
self._points_to_evaluate -= 1
|
||||
@@ -167,11 +154,7 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
ho_trial["book_time"] = now
|
||||
ho_trial["refresh_time"] = now
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
The result is internally negated when interacting with HyperOpt
|
||||
@@ -185,18 +168,15 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
ho_trial["state"] = hpo.base.JOB_STATE_ERROR
|
||||
ho_trial["misc"]["error"] = (str(TuneError), "Tune Error")
|
||||
self._hpopt_trials.refresh()
|
||||
else:
|
||||
self._process_result(trial_id, result, early_terminated)
|
||||
elif result:
|
||||
self._process_result(trial_id, result)
|
||||
del self._live_trial_mapping[trial_id]
|
||||
|
||||
def _process_result(self, trial_id, result, early_terminated=False):
|
||||
def _process_result(self, trial_id, result):
|
||||
ho_trial = self._get_hyperopt_trial(trial_id)
|
||||
ho_trial["refresh_time"] = hpo.utils.coarse_utcnow()
|
||||
|
||||
if early_terminated and self._use_early_stopped is False:
|
||||
ho_trial["state"] = hpo.base.JOB_STATE_ERROR
|
||||
ho_trial["misc"]["error"] = (str(TuneError), "Tune Removed")
|
||||
if not ho_trial:
|
||||
return
|
||||
ho_trial["refresh_time"] = hpo.utils.coarse_utcnow()
|
||||
|
||||
ho_trial["state"] = hpo.base.JOB_STATE_DONE
|
||||
hp_result = self._to_hyperopt_result(result)
|
||||
@@ -204,7 +184,7 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
self._hpopt_trials.refresh()
|
||||
|
||||
def _to_hyperopt_result(self, result):
|
||||
return {"loss": self._metric_op * result[self._metric], "status": "ok"}
|
||||
return {"loss": self.metric_op * result[self.metric], "status": "ok"}
|
||||
|
||||
def _get_hyperopt_trial(self, trial_id):
|
||||
if trial_id not in self._live_trial_mapping:
|
||||
@@ -214,9 +194,6 @@ class HyperOptSearch(SuggestionAlgorithm):
|
||||
t for t in self._hpopt_trials.trials if t["tid"] == hyperopt_tid
|
||||
][0]
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = (self._hpopt_trials, self.rstate.get_state())
|
||||
with open(checkpoint_dir, "wb") as outputFile:
|
||||
|
||||
@@ -5,15 +5,16 @@ try:
|
||||
except ImportError:
|
||||
ng = None
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NevergradSearch(SuggestionAlgorithm):
|
||||
class NevergradSearch(Searcher):
|
||||
"""A wrapper around Nevergrad to provide trial suggestions.
|
||||
|
||||
Requires Nevergrad to be installed.
|
||||
|
||||
Nevergrad is an open source tool from Facebook for derivative free
|
||||
optimization of parameters and/or hyperparameters. It features a wide
|
||||
range of optimizers in a standard ask and tell interface. More information
|
||||
@@ -26,20 +27,20 @@ class NevergradSearch(SuggestionAlgorithm):
|
||||
the dimension of the optimizer output. Alternatively, set to None
|
||||
if the optimizer is already instrumented with kwargs
|
||||
(see nevergrad v0.2.0+).
|
||||
max_concurrent (int): Number of maximum concurrent trials. Defaults
|
||||
to 10.
|
||||
metric (str): The training result objective value attribute.
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
minimizing or maximizing the metric attribute.
|
||||
use_early_stopped_trials (bool): Whether to use early terminated
|
||||
trial results in the optimization process.
|
||||
use_early_stopped_trials: Deprecated.
|
||||
max_concurrent: Deprecated.
|
||||
|
||||
Example:
|
||||
>>> from nevergrad.optimization import optimizerlib
|
||||
>>> instrumentation = 1
|
||||
>>> optimizer = optimizerlib.OnePlusOne(instrumentation, budget=100)
|
||||
>>> algo = NevergradSearch(optimizer, ["lr"], max_concurrent=4,
|
||||
>>> metric="mean_loss", mode="min")
|
||||
.. code-block:: python
|
||||
|
||||
from nevergrad.optimization import optimizerlib
|
||||
|
||||
instrumentation = 1
|
||||
optimizer = optimizerlib.OnePlusOne(instrumentation, budget=100)
|
||||
algo = NevergradSearch(
|
||||
optimizer, ["lr"], metric="mean_loss", mode="min")
|
||||
|
||||
Note:
|
||||
In nevergrad v0.2.0+, optimizers can be instrumented.
|
||||
@@ -51,34 +52,21 @@ class NevergradSearch(SuggestionAlgorithm):
|
||||
>>> lr = inst.var.Array(1).bounded(1, 2).asfloat()
|
||||
>>> instrumentation = inst.Instrumentation(lr=lr)
|
||||
>>> optimizer = optimizerlib.OnePlusOne(instrumentation, budget=100)
|
||||
>>> algo = NevergradSearch(optimizer, None, max_concurrent=4,
|
||||
>>> metric="mean_loss", mode="min")
|
||||
>>> algo = NevergradSearch(
|
||||
optimizer, None, metric="mean_loss", mode="min")
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
parameter_names,
|
||||
max_concurrent=10,
|
||||
reward_attr=None,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
**kwargs):
|
||||
assert ng is not None, "Nevergrad must be installed!"
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
|
||||
if reward_attr is not None:
|
||||
mode = "max"
|
||||
metric = reward_attr
|
||||
logger.warning(
|
||||
"`reward_attr` is deprecated and will be removed in a future "
|
||||
"version of Tune. "
|
||||
"Setting `metric={}` and `mode=max`.".format(reward_attr))
|
||||
|
||||
self._max_concurrent = max_concurrent
|
||||
self._parameters = parameter_names
|
||||
self._metric = metric
|
||||
# nevergrad.tell internally minimizes, so "max" => -1
|
||||
if mode == "max":
|
||||
self._metric_op = -1.
|
||||
@@ -110,8 +98,6 @@ class NevergradSearch(SuggestionAlgorithm):
|
||||
"dimension for non-instrumented optimizers")
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
suggested_config = self._nevergrad_opt.ask()
|
||||
self._live_trial_mapping[trial_id] = suggested_config
|
||||
# in v0.2.0+, output of ask() is a Candidate,
|
||||
@@ -122,14 +108,7 @@ class NevergradSearch(SuggestionAlgorithm):
|
||||
else:
|
||||
return suggested_config.kwargs
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
The result is internally negated when interacting with Nevergrad
|
||||
@@ -137,20 +116,15 @@ class NevergradSearch(SuggestionAlgorithm):
|
||||
as it minimizes on default.
|
||||
"""
|
||||
if result:
|
||||
self._process_result(trial_id, result, early_terminated)
|
||||
self._process_result(trial_id, result)
|
||||
|
||||
self._live_trial_mapping.pop(trial_id)
|
||||
|
||||
def _process_result(self, trial_id, result, early_terminated=False):
|
||||
if early_terminated and self._use_early_stopped is False:
|
||||
return
|
||||
def _process_result(self, trial_id, result):
|
||||
ng_trial_info = self._live_trial_mapping[trial_id]
|
||||
self._nevergrad_opt.tell(ng_trial_info,
|
||||
self._metric_op * result[self._metric])
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = (self._nevergrad_opt, self._parameters)
|
||||
with open(checkpoint_dir, "wb") as outputFile:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import copy
|
||||
import itertools
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.experiment import convert_to_experiment_list
|
||||
from ray.tune.suggest.suggestion import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,6 +10,16 @@ TRIAL_INDEX = "__trial_index__"
|
||||
"""str: A constant value representing the repeat index of the trial."""
|
||||
|
||||
|
||||
def _warn_num_samples(searcher, num_samples):
|
||||
if isinstance(searcher, Repeater) and num_samples % searcher.repeat:
|
||||
logger.warning(
|
||||
"`num_samples` is now expected to be the total number of trials, "
|
||||
"including the repeat trials. For example, set num_samples=15 if "
|
||||
"you intend to obtain 3 search algorithm suggestions and repeat "
|
||||
"each suggestion 5 times. Any leftover trials "
|
||||
"(num_samples mod repeat) will be ignored.")
|
||||
|
||||
|
||||
class _TrialGroup:
|
||||
"""Internal class for grouping trials of same parameters.
|
||||
|
||||
@@ -57,17 +65,22 @@ class _TrialGroup:
|
||||
return len(self._trials)
|
||||
|
||||
|
||||
class Repeater(SuggestionAlgorithm):
|
||||
class Repeater(Searcher):
|
||||
"""A wrapper algorithm for repeating trials of same parameters.
|
||||
|
||||
Set tune.run(num_samples=...) to be a multiple of `repeat`. For example,
|
||||
set num_samples=15 if you intend to obtain 3 search algorithm suggestions
|
||||
and repeat each suggestion 5 times. Any leftover trials
|
||||
(num_samples mod repeat) will be ignored.
|
||||
|
||||
It is recommended that you do not run an early-stopping TrialScheduler
|
||||
simultaneously.
|
||||
|
||||
Args:
|
||||
search_alg (SearchAlgorithm): SearchAlgorithm object that the
|
||||
Repeater will optimize. Note that the SearchAlgorithm
|
||||
searcher (Searcher): Searcher object that the
|
||||
Repeater will optimize. Note that the Searcher
|
||||
will only see 1 trial among multiple repeated trials.
|
||||
The result/metric passed to the SearchAlgorithm upon
|
||||
The result/metric passed to the Searcher upon
|
||||
trial completion will be averaged among all repeats.
|
||||
repeat (int): Number of times to generate a trial with a repeated
|
||||
configuration. Defaults to 1.
|
||||
@@ -77,41 +90,23 @@ class Repeater(SuggestionAlgorithm):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, search_alg, repeat=1, set_index=True):
|
||||
self.search_alg = search_alg
|
||||
self._repeat = repeat
|
||||
def __init__(self, searcher, repeat=1, set_index=True):
|
||||
self.searcher = searcher
|
||||
self.repeat = repeat
|
||||
self._set_index = set_index
|
||||
self._groups = []
|
||||
self._trial_id_to_group = {}
|
||||
self._current_group = None
|
||||
super(Repeater, self).__init__(
|
||||
metric=self.search_alg.metric,
|
||||
mode=self.search_alg.mode,
|
||||
use_early_stopped_trials=self.search_alg._use_early_stopped)
|
||||
|
||||
def add_configurations(self, experiments):
|
||||
"""Chains generator given experiment specifications.
|
||||
|
||||
Multiplies the number of trials by the repeat factor.
|
||||
|
||||
Arguments:
|
||||
experiments (Experiment | list | dict): Experiments to run.
|
||||
"""
|
||||
experiment_list = convert_to_experiment_list(experiments)
|
||||
for experiment in experiment_list:
|
||||
self._trial_generator = itertools.chain(
|
||||
self._trial_generator,
|
||||
self._generate_trials(
|
||||
experiment.spec.get("num_samples", 1) * self._repeat,
|
||||
experiment.spec, experiment.name))
|
||||
metric=self.searcher.metric, mode=self.searcher.mode)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._current_group is None or self._current_group.full():
|
||||
config = self.search_alg.suggest(trial_id)
|
||||
config = self.searcher.suggest(trial_id)
|
||||
if config is None:
|
||||
return config
|
||||
self._current_group = _TrialGroup(
|
||||
trial_id, copy.deepcopy(config), max_trials=self._repeat)
|
||||
trial_id, copy.deepcopy(config), max_trials=self.repeat)
|
||||
self._groups.append(self._current_group)
|
||||
index_in_group = 0
|
||||
else:
|
||||
@@ -139,15 +134,21 @@ class Repeater(SuggestionAlgorithm):
|
||||
"Seen trials: {}".format(
|
||||
trial_id, list(self._trial_id_to_group)))
|
||||
trial_group = self._trial_id_to_group[trial_id]
|
||||
if not result or self.search_alg.metric not in result:
|
||||
if not result or self.searcher.metric not in result:
|
||||
score = np.nan
|
||||
else:
|
||||
score = result[self.search_alg.metric]
|
||||
score = result[self.searcher.metric]
|
||||
trial_group.report(trial_id, score)
|
||||
|
||||
if trial_group.finished_reporting():
|
||||
scores = trial_group.scores()
|
||||
self.search_alg.on_trial_complete(
|
||||
self.searcher.on_trial_complete(
|
||||
trial_group.primary_trial_id,
|
||||
result={self.search_alg.metric: np.nanmean(scores)},
|
||||
result={self.searcher.metric: np.nanmean(scores)},
|
||||
**kwargs)
|
||||
|
||||
def save(self, path):
|
||||
self.searcher.save(path)
|
||||
|
||||
def restore(self, path):
|
||||
self.searcher.restore(path)
|
||||
|
||||
@@ -38,11 +38,7 @@ class SearchAlgorithm:
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
Arguments:
|
||||
@@ -53,8 +49,6 @@ class SearchAlgorithm:
|
||||
by manual termination.
|
||||
error (bool): Defaults to False. True if the trial is in
|
||||
the RUNNING state and errors.
|
||||
early_terminated (bool): Defaults to False. True if the trial
|
||||
is stopped while in PAUSED or PENDING state.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -7,17 +7,19 @@ try:
|
||||
except ImportError:
|
||||
sgo = None
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SigOptSearch(SuggestionAlgorithm):
|
||||
class SigOptSearch(Searcher):
|
||||
"""A wrapper around SigOpt to provide trial suggestions.
|
||||
|
||||
Requires SigOpt to be installed. Requires user to store their SigOpt
|
||||
API key locally as an environment variable at `SIGOPT_KEY`.
|
||||
|
||||
This module manages its own concurrency.
|
||||
|
||||
Parameters:
|
||||
space (list of dict): SigOpt configuration. Parameters will be sampled
|
||||
from this configuration and will be used to override
|
||||
@@ -70,17 +72,6 @@ class SigOptSearch(SuggestionAlgorithm):
|
||||
"SigOpt API key must be stored as environ variable at SIGOPT_KEY"
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
|
||||
if reward_attr is not None:
|
||||
mode = "max"
|
||||
metric = reward_attr
|
||||
logger.warning(
|
||||
"`reward_attr` is deprecated and will be removed in a future "
|
||||
"version of Tune. "
|
||||
"Setting `metric={}` and `mode=max`.".format(reward_attr))
|
||||
if "use_early_stopped_trials" in kwargs:
|
||||
logger.warning(
|
||||
"`use_early_stopped_trials` is not used in SigOptSearch.")
|
||||
|
||||
self._max_concurrent = max_concurrent
|
||||
self._metric = metric
|
||||
if mode == "max":
|
||||
@@ -101,9 +92,6 @@ class SigOptSearch(SuggestionAlgorithm):
|
||||
super(SigOptSearch, self).__init__(metric=metric, mode=mode, **kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
|
||||
# Get new suggestion from SigOpt
|
||||
suggestion = self.conn.experiments(
|
||||
self.experiment.id).suggestions().create()
|
||||
@@ -112,14 +100,7 @@ class SigOptSearch(SuggestionAlgorithm):
|
||||
|
||||
return copy.deepcopy(suggestion.assignments)
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
If a trial fails, it will be reported as a failed Observation, telling
|
||||
@@ -135,15 +116,12 @@ class SigOptSearch(SuggestionAlgorithm):
|
||||
)
|
||||
# Update the experiment object
|
||||
self.experiment = self.conn.experiments(self.experiment.id).fetch()
|
||||
elif error or early_terminated:
|
||||
elif error:
|
||||
# Reports a failed Observation
|
||||
self.conn.experiments(self.experiment.id).observations().create(
|
||||
failed=True, suggestion=self._live_trial_mapping[trial_id].id)
|
||||
del self._live_trial_mapping[trial_id]
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = (self.conn, self.experiment)
|
||||
with open(checkpoint_dir, "wb") as outputFile:
|
||||
|
||||
@@ -5,7 +5,7 @@ try:
|
||||
except ImportError:
|
||||
sko = None
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,7 +40,7 @@ def _validate_warmstart(parameter_names, points_to_evaluate,
|
||||
" do not match.")
|
||||
|
||||
|
||||
class SkOptSearch(SuggestionAlgorithm):
|
||||
class SkOptSearch(Searcher):
|
||||
"""A wrapper around skopt to provide trial suggestions.
|
||||
|
||||
Requires skopt to be installed.
|
||||
@@ -50,8 +50,6 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
from skopt.
|
||||
parameter_names (list): List of parameter names. Should match
|
||||
the dimension of the optimizer output.
|
||||
max_concurrent (int): Number of maximum concurrent trials. Defaults
|
||||
to 10.
|
||||
metric (str): The training result objective value attribute.
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
minimizing or maximizing the metric attribute.
|
||||
@@ -66,8 +64,8 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
as a list so the optimiser can be told the results without
|
||||
needing to re-compute the trial. Must be the same length as
|
||||
points_to_evaluate. (See tune/examples/skopt_example.py)
|
||||
use_early_stopped_trials (bool): Whether to use early terminated
|
||||
trial results in the optimization process.
|
||||
max_concurrent: Deprecated.
|
||||
use_early_stopped_trials: Deprecated.
|
||||
|
||||
Example:
|
||||
>>> from skopt import Optimizer
|
||||
@@ -75,7 +73,6 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
>>> current_best_params = [[10, 0], [15, -20]]
|
||||
>>> algo = SkOptSearch(optimizer,
|
||||
>>> ["width", "height"],
|
||||
>>> max_concurrent=4,
|
||||
>>> metric="mean_loss",
|
||||
>>> mode="min",
|
||||
>>> points_to_evaluate=current_best_params)
|
||||
@@ -84,37 +81,30 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
parameter_names,
|
||||
max_concurrent=10,
|
||||
reward_attr=None,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
points_to_evaluate=None,
|
||||
evaluated_rewards=None,
|
||||
**kwargs):
|
||||
max_concurrent=None,
|
||||
use_early_stopped_trials=None):
|
||||
assert sko is not None, """skopt must be installed!
|
||||
You can install Skopt with the command:
|
||||
`pip install scikit-optimize`."""
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
_validate_warmstart(parameter_names, points_to_evaluate,
|
||||
evaluated_rewards)
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
|
||||
if reward_attr is not None:
|
||||
mode = "max"
|
||||
metric = reward_attr
|
||||
logger.warning(
|
||||
"`reward_attr` is deprecated and will be removed in a future "
|
||||
"version of Tune. "
|
||||
"Setting `metric={}` and `mode=max`.".format(reward_attr))
|
||||
super(SkOptSearch, self).__init__(
|
||||
metric=metric,
|
||||
mode=mode,
|
||||
max_concurrent=max_concurrent,
|
||||
use_early_stopped_trials=use_early_stopped_trials)
|
||||
|
||||
self._initial_points = []
|
||||
if points_to_evaluate and evaluated_rewards:
|
||||
optimizer.tell(points_to_evaluate, evaluated_rewards)
|
||||
elif points_to_evaluate:
|
||||
self._initial_points = points_to_evaluate
|
||||
self._max_concurrent = max_concurrent
|
||||
self._parameters = parameter_names
|
||||
self._metric = metric
|
||||
# Skopt internally minimizes, so "max" => -1
|
||||
if mode == "max":
|
||||
self._metric_op = -1.
|
||||
@@ -122,12 +112,8 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
self._metric_op = 1.
|
||||
self._skopt_opt = optimizer
|
||||
self._live_trial_mapping = {}
|
||||
super(SkOptSearch, self).__init__(
|
||||
metric=self._metric, mode=mode, **kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
if self._initial_points:
|
||||
suggested_config = self._initial_points[0]
|
||||
del self._initial_points[0]
|
||||
@@ -136,14 +122,7 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
self._live_trial_mapping[trial_id] = suggested_config
|
||||
return dict(zip(self._parameters, suggested_config))
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
The result is internally negated when interacting with Skopt
|
||||
@@ -152,19 +131,14 @@ class SkOptSearch(SuggestionAlgorithm):
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._process_result(trial_id, result, early_terminated)
|
||||
self._process_result(trial_id, result)
|
||||
self._live_trial_mapping.pop(trial_id)
|
||||
|
||||
def _process_result(self, trial_id, result, early_terminated=False):
|
||||
if early_terminated and self._use_early_stopped is False:
|
||||
return
|
||||
def _process_result(self, trial_id, result):
|
||||
skopt_trial_info = self._live_trial_mapping[trial_id]
|
||||
self._skopt_opt.tell(skopt_trial_info,
|
||||
self._metric_op * result[self._metric])
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = (self._initial_points, self._skopt_opt)
|
||||
with open(checkpoint_dir, "wb") as outputFile:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import itertools
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from ray.tune.error import TuneError
|
||||
from ray.tune.experiment import convert_to_experiment_list
|
||||
@@ -9,129 +9,127 @@ from ray.tune.suggest.variant_generator import format_vars, resolve_nested_dict
|
||||
from ray.tune.trial import Trial
|
||||
from ray.tune.utils import merge_dicts, flatten_dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SuggestionAlgorithm(SearchAlgorithm):
|
||||
"""Abstract class for suggestion-based algorithms.
|
||||
|
||||
Custom search algorithms can extend this class easily by overriding the
|
||||
def _warn_on_repeater(searcher, total_samples):
|
||||
from ray.tune.suggest.repeater import _warn_num_samples
|
||||
_warn_num_samples(searcher, total_samples)
|
||||
|
||||
|
||||
class Searcher:
|
||||
"""Abstract class for wrapping suggesting algorithms.
|
||||
|
||||
Custom algorithms can extend this class easily by overriding the
|
||||
`suggest` method provide generated parameters for the trials.
|
||||
|
||||
Any subclass that implements ``__init__`` must also call the
|
||||
constructor of this class: ``super(Subclass, self).__init__(...)``.
|
||||
|
||||
To track suggestions and their corresponding evaluations, the method
|
||||
`suggest` will be passed a trial_id, which will be used in
|
||||
subsequent notifications.
|
||||
|
||||
Args:
|
||||
metric (str): The training result objective value attribute.
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
minimizing or maximizing the metric attribute.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
suggester = SuggestionAlgorithm()
|
||||
suggester.add_configurations({ ... })
|
||||
new_parameters = suggester.suggest()
|
||||
suggester.on_trial_complete(trial_id, result)
|
||||
better_parameters = suggester.suggest()
|
||||
class ExampleSearch(Searcher):
|
||||
def __init__(self, metric="mean_loss", mode="min", **kwargs):
|
||||
super(ExampleSearch, self).__init__(
|
||||
metric=metric, mode=mode, **kwargs)
|
||||
self.optimizer = Optimizer()
|
||||
self.configurations = {}
|
||||
|
||||
def suggest(self, trial_id):
|
||||
configuration = self.optimizer.query()
|
||||
self.configurations[trial_id] = configuration
|
||||
|
||||
def on_trial_complete(self, trial_id, result, **kwargs):
|
||||
configuration = self.configurations[trial_id]
|
||||
if result and self.metric in result:
|
||||
self.optimizer.update(configuration, result[self.metric])
|
||||
|
||||
tune.run(trainable_function, search_alg=ExampleSearch())
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, metric=None, mode="max", use_early_stopped_trials=True):
|
||||
"""Constructs a generator given experiment specifications."""
|
||||
self._parser = make_parser()
|
||||
self._trial_generator = []
|
||||
self._counter = 0
|
||||
def __init__(self,
|
||||
metric="episode_reward_mean",
|
||||
mode="max",
|
||||
max_concurrent=None,
|
||||
use_early_stopped_trials=None):
|
||||
if use_early_stopped_trials is False:
|
||||
raise DeprecationWarning(
|
||||
"Early stopped trials are now always used. If this is a "
|
||||
"problem, file an issue: https://github.com/ray-project/ray.")
|
||||
if max_concurrent is not None:
|
||||
raise DeprecationWarning(
|
||||
"max_concurrent is now deprecated for this search algorithm. "
|
||||
"Please use tune.suggest.ConcurrencyLimiter instead.")
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'!"
|
||||
self._metric = metric
|
||||
assert mode in ["min", "max"]
|
||||
self._mode = mode
|
||||
self._use_early_stopped = use_early_stopped_trials
|
||||
self._finished = False
|
||||
|
||||
def add_configurations(self, experiments):
|
||||
"""Chains generator given experiment specifications.
|
||||
def on_trial_result(self, trial_id, result):
|
||||
"""Optional notification for result during training.
|
||||
|
||||
Arguments:
|
||||
experiments (Experiment | list | dict): Experiments to run.
|
||||
Note that by default, the result dict may include NaNs or
|
||||
may not include the optimization metric. It is up to the
|
||||
subclass implementation to preprocess the result to
|
||||
avoid breaking the optimization process.
|
||||
|
||||
Args:
|
||||
trial_id (str): A unique string ID for the trial.
|
||||
result (dict): Dictionary of metrics for current training progress.
|
||||
Note that the result dict may include NaNs or
|
||||
may not include the optimization metric. It is up to the
|
||||
subclass implementation to preprocess the result to
|
||||
avoid breaking the optimization process.
|
||||
"""
|
||||
experiment_list = convert_to_experiment_list(experiments)
|
||||
for experiment in experiment_list:
|
||||
self._trial_generator = itertools.chain(
|
||||
self._trial_generator,
|
||||
self._generate_trials(
|
||||
experiment.spec.get("num_samples", 1), experiment.spec,
|
||||
experiment.name))
|
||||
pass
|
||||
|
||||
def next_trials(self):
|
||||
"""Provides a batch of Trial objects to be queued into the TrialRunner.
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial.
|
||||
|
||||
A batch ends when self._trial_generator returns None.
|
||||
Typically, this method is used for notifying the underlying
|
||||
optimizer of the result.
|
||||
|
||||
Args:
|
||||
trial_id (str): A unique string ID for the trial.
|
||||
result (dict): Dictionary of metrics for current training progress.
|
||||
Note that the result dict may include NaNs or
|
||||
may not include the optimization metric. It is up to the
|
||||
subclass implementation to preprocess the result to
|
||||
avoid breaking the optimization process. Upon errors, this
|
||||
may also be None.
|
||||
error (bool): True if the training process raised an error.
|
||||
|
||||
Returns:
|
||||
trials (list): Returns a list of trials.
|
||||
"""
|
||||
trials = []
|
||||
|
||||
for trial in self._trial_generator:
|
||||
if trial is None:
|
||||
return trials
|
||||
trials += [trial]
|
||||
|
||||
self.set_finished()
|
||||
return trials
|
||||
|
||||
def _generate_trials(self, num_samples, experiment_spec, output_path=""):
|
||||
"""Generates trials with configurations from `suggest`.
|
||||
|
||||
Creates a trial_id that is passed into `suggest`.
|
||||
|
||||
Yields:
|
||||
Trial objects constructed according to `spec`
|
||||
"""
|
||||
if "run" not in experiment_spec:
|
||||
raise TuneError("Must specify `run` in {}".format(experiment_spec))
|
||||
for _ in range(num_samples):
|
||||
trial_id = Trial.generate_id()
|
||||
while True:
|
||||
suggested_config = self.suggest(trial_id)
|
||||
if suggested_config is None:
|
||||
yield None
|
||||
else:
|
||||
break
|
||||
spec = copy.deepcopy(experiment_spec)
|
||||
spec["config"] = merge_dicts(spec["config"],
|
||||
copy.deepcopy(suggested_config))
|
||||
flattened_config = resolve_nested_dict(spec["config"])
|
||||
self._counter += 1
|
||||
tag = "{0}_{1}".format(
|
||||
str(self._counter), format_vars(flattened_config))
|
||||
yield create_trial_from_spec(
|
||||
spec,
|
||||
output_path,
|
||||
self._parser,
|
||||
evaluated_params=flatten_dict(suggested_config),
|
||||
experiment_tag=tag,
|
||||
trial_id=trial_id)
|
||||
raise NotImplementedError
|
||||
|
||||
def suggest(self, trial_id):
|
||||
"""Queries the algorithm to retrieve the next set of parameters.
|
||||
|
||||
Arguments:
|
||||
trial_id: Trial ID used for subsequent notifications.
|
||||
trial_id (str): Trial ID used for subsequent notifications.
|
||||
|
||||
Returns:
|
||||
dict|None: Configuration for a trial, if possible.
|
||||
Else, returns None, which will temporarily stop the
|
||||
TrialRunner from querying.
|
||||
|
||||
Example:
|
||||
>>> suggester = SuggestionAlgorithm(max_concurrent=1)
|
||||
>>> suggester.add_configurations({ ... })
|
||||
>>> parameters_1 = suggester.suggest()
|
||||
>>> parameters_2 = suggester.suggest()
|
||||
>>> parameters_2 is None
|
||||
>>> suggester.on_trial_complete(trial_id, result)
|
||||
>>> parameters_2 = suggester.suggest()
|
||||
>>> parameters_2 is not None
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
"""Save function for this object."""
|
||||
raise NotImplementedError
|
||||
|
||||
def restore(self, checkpoint_dir):
|
||||
"""Restore function for this object."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@@ -145,18 +143,156 @@ class SuggestionAlgorithm(SearchAlgorithm):
|
||||
return self._mode
|
||||
|
||||
|
||||
class _MockSuggestionAlgorithm(SuggestionAlgorithm):
|
||||
def __init__(self, max_concurrent=2, **kwargs):
|
||||
self._max_concurrent = max_concurrent
|
||||
class ConcurrencyLimiter(Searcher):
|
||||
"""A wrapper algorithm for limiting the number of concurrent trials.
|
||||
|
||||
Args:
|
||||
searcher (Searcher): Searcher object that the
|
||||
ConcurrencyLimiter will manage.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ray.tune.suggest import ConcurrencyLimiter
|
||||
search_alg = HyperOptSearch(metric="accuracy")
|
||||
search_alg = ConcurrencyLimiter(search_alg, max_concurrent=2)
|
||||
tune.run(trainable, search_alg=search_alg)
|
||||
"""
|
||||
|
||||
def __init__(self, searcher, max_concurrent):
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
self.searcher = searcher
|
||||
self.max_concurrent = max_concurrent
|
||||
self.live_trials = set()
|
||||
super(ConcurrencyLimiter, self).__init__(
|
||||
metric=self.searcher.metric, mode=self.searcher.mode)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if len(self.live_trials) >= self.max_concurrent:
|
||||
return
|
||||
self.live_trials.add(trial_id)
|
||||
return self.searcher.suggest(trial_id)
|
||||
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
if trial_id not in self.live_trials:
|
||||
return
|
||||
else:
|
||||
self.searcher.on_trial_complete(
|
||||
trial_id, result=result, error=error)
|
||||
self.live_trials.remove(trial_id)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
self.searcher.save(checkpoint_dir)
|
||||
|
||||
def restore(self, checkpoint_dir):
|
||||
self.searcher.restore(checkpoint_dir)
|
||||
|
||||
|
||||
class SearchGenerator(SearchAlgorithm):
|
||||
"""Generates trials to be passed to the TrialRunner.
|
||||
|
||||
Uses the provided ``searcher`` object to generate trials. This class
|
||||
transparently handles repeating trials with score aggregation
|
||||
without embedding logic into the Searcher.
|
||||
|
||||
Args:
|
||||
searcher: Search object that subclasses the Searcher base class. This
|
||||
is then used for generating new hyperparameter samples.
|
||||
"""
|
||||
|
||||
def __init__(self, searcher):
|
||||
assert issubclass(
|
||||
type(searcher),
|
||||
Searcher), ("Searcher should be subclassing Searcher.")
|
||||
self.searcher = searcher
|
||||
self._parser = make_parser()
|
||||
self._experiment = None
|
||||
self._counter = 0 # Keeps track of number of trials created.
|
||||
self._total_samples = 0 # int: total samples to evaluate.
|
||||
self._finished = False
|
||||
|
||||
def add_configurations(self, experiments):
|
||||
"""Registers experiment specifications.
|
||||
|
||||
Arguments:
|
||||
experiments (Experiment | list | dict): Experiments to run.
|
||||
"""
|
||||
logger.debug("added configurations")
|
||||
experiment_list = convert_to_experiment_list(experiments)
|
||||
assert len(experiment_list) == 1, (
|
||||
"SearchAlgorithms can only support 1 experiment at a time.")
|
||||
self._experiment = experiment_list[0]
|
||||
experiment_spec = self._experiment.spec
|
||||
self._total_samples = experiment_spec.get("num_samples", 1)
|
||||
|
||||
_warn_on_repeater(self.searcher, self._total_samples)
|
||||
|
||||
if "run" not in experiment_spec:
|
||||
raise TuneError("Must specify `run` in {}".format(experiment_spec))
|
||||
|
||||
def next_trials(self):
|
||||
"""Provides a batch of Trial objects to be queued into the TrialRunner.
|
||||
|
||||
Returns:
|
||||
List[Trial]: A list of trials for the Runner to consume.
|
||||
"""
|
||||
trials = []
|
||||
while not self.is_finished():
|
||||
trial = self.create_trial_if_possible(self._experiment.spec,
|
||||
self._experiment.name)
|
||||
if trial is None:
|
||||
break
|
||||
trials.append(trial)
|
||||
return trials
|
||||
|
||||
def create_trial_if_possible(self, experiment_spec, output_path):
|
||||
logger.debug("creating trial")
|
||||
trial_id = Trial.generate_id()
|
||||
suggested_config = self.searcher.suggest(trial_id)
|
||||
if suggested_config is None:
|
||||
return
|
||||
spec = copy.deepcopy(experiment_spec)
|
||||
spec["config"] = merge_dicts(spec["config"],
|
||||
copy.deepcopy(suggested_config))
|
||||
|
||||
# Create a new trial_id if duplicate trial is created
|
||||
flattened_config = resolve_nested_dict(spec["config"])
|
||||
self._counter += 1
|
||||
tag = "{0}_{1}".format(
|
||||
str(self._counter), format_vars(flattened_config))
|
||||
trial = create_trial_from_spec(
|
||||
spec,
|
||||
output_path,
|
||||
self._parser,
|
||||
evaluated_params=flatten_dict(suggested_config),
|
||||
experiment_tag=tag,
|
||||
trial_id=trial_id)
|
||||
return trial
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
"""Notifies the underlying searcher."""
|
||||
self.searcher.on_trial_result(trial_id, result)
|
||||
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
self.searcher.on_trial_complete(
|
||||
trial_id=trial_id, result=result, error=error)
|
||||
|
||||
def is_finished(self):
|
||||
return self._counter >= self._total_samples or self._finished
|
||||
|
||||
|
||||
class _MockSearcher(Searcher):
|
||||
def __init__(self, **kwargs):
|
||||
self.live_trials = {}
|
||||
self.counter = {"result": 0, "complete": 0}
|
||||
self.final_results = []
|
||||
self.stall = False
|
||||
self.results = []
|
||||
super(_MockSuggestionAlgorithm, self).__init__(**kwargs)
|
||||
super(_MockSearcher, self).__init__(**kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if len(self.live_trials) < self._max_concurrent and not self.stall:
|
||||
if not self.stall:
|
||||
self.live_trials[trial_id] = 1
|
||||
return {"test_variable": 2}
|
||||
return None
|
||||
@@ -165,16 +301,29 @@ class _MockSuggestionAlgorithm(SuggestionAlgorithm):
|
||||
self.counter["result"] += 1
|
||||
self.results += [result]
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
self.counter["complete"] += 1
|
||||
if result:
|
||||
self._process_result(result, early_terminated)
|
||||
del self.live_trials[trial_id]
|
||||
self._process_result(result)
|
||||
if trial_id in self.live_trials:
|
||||
del self.live_trials[trial_id]
|
||||
|
||||
def _process_result(self, result, early_terminated):
|
||||
if early_terminated and self._use_early_stopped:
|
||||
self.final_results += [result]
|
||||
def _process_result(self, result):
|
||||
self.final_results += [result]
|
||||
|
||||
|
||||
class _MockSuggestionAlgorithm(SearchGenerator):
|
||||
def __init__(self, max_concurrent=None, **kwargs):
|
||||
self.searcher = _MockSearcher(**kwargs)
|
||||
if max_concurrent:
|
||||
self.searcher = ConcurrencyLimiter(
|
||||
self.searcher, max_concurrent=max_concurrent)
|
||||
super(_MockSuggestionAlgorithm, self).__init__(self.searcher)
|
||||
|
||||
@property
|
||||
def live_trials(self):
|
||||
return self.searcher.live_trials
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
return self.searcher.results
|
||||
|
||||
@@ -7,12 +7,12 @@ try:
|
||||
except ImportError:
|
||||
zoopt = None
|
||||
|
||||
from ray.tune.suggest.suggestion import SuggestionAlgorithm
|
||||
from ray.tune.suggest import Searcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZOOptSearch(SuggestionAlgorithm):
|
||||
class ZOOptSearch(Searcher):
|
||||
"""A wrapper around ZOOpt to provide trial suggestions.
|
||||
|
||||
Requires zoopt package (>=0.4.0) to be installed. You can install it
|
||||
@@ -26,8 +26,6 @@ class ZOOptSearch(SuggestionAlgorithm):
|
||||
For continuous dimensions: (continuous, search_range, precision);
|
||||
For discrete dimensions: (discrete, search_range, has_order).
|
||||
More details can be found in zoopt package.
|
||||
max_concurrent (int): Number of maximum concurrent trials.
|
||||
Defaults to 10.
|
||||
metric (str): The training result objective value attribute.
|
||||
Defaults to "episode_reward_mean".
|
||||
mode (str): One of {min, max}. Determines whether objective is
|
||||
@@ -59,7 +57,6 @@ class ZOOptSearch(SuggestionAlgorithm):
|
||||
algo="Asracos", # only support Asracos currently
|
||||
budget=config["num_samples"],
|
||||
dim_dict=dim_dict,
|
||||
max_concurrent=4,
|
||||
metric="mean_loss",
|
||||
mode="min")
|
||||
|
||||
@@ -76,20 +73,17 @@ class ZOOptSearch(SuggestionAlgorithm):
|
||||
algo="asracos",
|
||||
budget=None,
|
||||
dim_dict=None,
|
||||
max_concurrent=10,
|
||||
metric="episode_reward_mean",
|
||||
mode="min",
|
||||
**kwargs):
|
||||
assert zoopt is not None, "Zoopt not found - please install zoopt."
|
||||
assert budget is not None, "`budget` should not be None!"
|
||||
assert dim_dict is not None, "`dim_list` should not be None!"
|
||||
assert type(max_concurrent) is int and max_concurrent > 0
|
||||
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._max_concurrent = max_concurrent
|
||||
self._metric = metric
|
||||
if mode == "max":
|
||||
self._metric_op = -1.
|
||||
@@ -116,9 +110,6 @@ class ZOOptSearch(SuggestionAlgorithm):
|
||||
metric=self._metric, mode=mode, **kwargs)
|
||||
|
||||
def suggest(self, trial_id):
|
||||
if self._num_live_trials() >= self._max_concurrent:
|
||||
return None
|
||||
|
||||
_solution = self.optimizer.suggest()
|
||||
if _solution:
|
||||
self.solution_dict[str(trial_id)] = _solution
|
||||
@@ -127,14 +118,7 @@ class ZOOptSearch(SuggestionAlgorithm):
|
||||
self._live_trial_mapping[trial_id] = new_trial
|
||||
return copy.deepcopy(new_trial)
|
||||
|
||||
def on_trial_result(self, trial_id, result):
|
||||
pass
|
||||
|
||||
def on_trial_complete(self,
|
||||
trial_id,
|
||||
result=None,
|
||||
error=False,
|
||||
early_terminated=False):
|
||||
def on_trial_complete(self, trial_id, result=None, error=False):
|
||||
"""Notification for the completion of trial."""
|
||||
if result:
|
||||
_solution = self.solution_dict[str(trial_id)]
|
||||
@@ -142,17 +126,9 @@ class ZOOptSearch(SuggestionAlgorithm):
|
||||
_solution, self._metric_op * result[self._metric])
|
||||
if _best_solution_so_far:
|
||||
self.best_solution_list.append(_best_solution_so_far)
|
||||
self._process_result(trial_id, result, early_terminated)
|
||||
|
||||
del self._live_trial_mapping[trial_id]
|
||||
|
||||
def _process_result(self, trial_id, result, early_terminated=False):
|
||||
if early_terminated and self._use_early_stopped is False:
|
||||
return
|
||||
|
||||
def _num_live_trials(self):
|
||||
return len(self._live_trial_mapping)
|
||||
|
||||
def save(self, checkpoint_dir):
|
||||
trials_object = self.optimizer
|
||||
with open(checkpoint_dir, "wb") as output:
|
||||
|
||||
Reference in New Issue
Block a user