From 9797028a91ed98a3251be95d2b33ae5f68c7e128 Mon Sep 17 00:00:00 2001 From: Adi Zimmerman Date: Mon, 11 Feb 2019 17:06:02 -0800 Subject: [PATCH] [tune] Add scikit-optimize to Tune (#3924) --- doc/source/tune-searchalg.rst | 24 ++++++ docker/examples/Dockerfile | 1 + python/ray/tune/examples/skopt_example.py | 56 ++++++++++++++ python/ray/tune/suggest/__init__.py | 2 + python/ray/tune/suggest/skopt.py | 88 ++++++++++++++++++++++ test/jenkins_tests/run_multi_node_tests.sh | 4 + 6 files changed, 175 insertions(+) create mode 100644 python/ray/tune/examples/skopt_example.py create mode 100644 python/ray/tune/suggest/skopt.py diff --git a/doc/source/tune-searchalg.rst b/doc/source/tune-searchalg.rst index 7cac438c2..9c3ab49e7 100644 --- a/doc/source/tune-searchalg.rst +++ b/doc/source/tune-searchalg.rst @@ -103,6 +103,30 @@ An example of this can be found in `sigopt_example.py `__ to perform sequential model-based hyperparameter optimization. Note that this class does not extend ``ray.tune.suggest.BasicVariantGenerator``, so you will not be able to use Tune's default variant generation/search space declaration when using SkOptSearch. + +In order to use this search algorithm, you will need to install Scikit-Optimize via the following command: + +.. code-block:: bash + + $ pip install scikit-optimize + +This algorithm requires using the `Scikit-Optimize ask and tell interface `__. This interface requires using the `Optimizer `__ provided by Scikit-Optimize. You can use SkOptSearch like follows: + +.. code-block:: python + + optimizer = Optimizer(dimension, ...) + run_experiments(experiment_config, search_alg=SkOptSearch(optimizer, parameter_names, ... )) + +An example of this can be found in `skopt_example.py `__. + +.. autoclass:: ray.tune.suggest.SkOptSearch + :show-inheritance: + :noindex: + Contributing a New Algorithm ---------------------------- diff --git a/docker/examples/Dockerfile b/docker/examples/Dockerfile index 9244ec398..15a744034 100644 --- a/docker/examples/Dockerfile +++ b/docker/examples/Dockerfile @@ -10,4 +10,5 @@ RUN pip install -U h5py # Mutes FutureWarnings RUN pip install --upgrade bayesian-optimization RUN pip install --upgrade git+git://github.com/hyperopt/hyperopt.git RUN pip install --upgrade sigopt +RUN pip install --upgrade scikit-optimize RUN conda install pytorch-cpu torchvision-cpu -c pytorch diff --git a/python/ray/tune/examples/skopt_example.py b/python/ray/tune/examples/skopt_example.py new file mode 100644 index 000000000..d2ce69221 --- /dev/null +++ b/python/ray/tune/examples/skopt_example.py @@ -0,0 +1,56 @@ +"""This test checks that Skopt is functional. + +It also checks that it is usable with a separate scheduler. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import ray +from ray.tune import run_experiments, register_trainable +from ray.tune.schedulers import AsyncHyperBandScheduler +from ray.tune.suggest import SkOptSearch + + +def easy_objective(config, reporter): + import time + time.sleep(0.2) + for i in range(config["iterations"]): + reporter( + timesteps_total=i, + neg_mean_loss=-(config["height"] - 14)**2 + + abs(config["width"] - 3)) + time.sleep(0.02) + + +if __name__ == '__main__': + import argparse + from skopt import Optimizer + + parser = argparse.ArgumentParser() + parser.add_argument( + "--smoke-test", action="store_true", help="Finish quickly for testing") + args, _ = parser.parse_known_args() + ray.init(redirect_output=True) + + register_trainable("exp", easy_objective) + + config = { + "skopt_exp": { + "run": "exp", + "num_samples": 10 if args.smoke_test else 50, + "config": { + "iterations": 100, + }, + "stop": { + "timesteps_total": 100 + }, + } + } + optimizer = Optimizer([(0, 20), (-100, 100)]) + algo = SkOptSearch( + optimizer, ["width", "height"], + max_concurrent=4, + reward_attr="neg_mean_loss") + scheduler = AsyncHyperBandScheduler(reward_attr="neg_mean_loss") + run_experiments(config, search_alg=algo, scheduler=scheduler) diff --git a/python/ray/tune/suggest/__init__.py b/python/ray/tune/suggest/__init__.py index 576420c53..c9e25a343 100644 --- a/python/ray/tune/suggest/__init__.py +++ b/python/ray/tune/suggest/__init__.py @@ -3,6 +3,7 @@ from ray.tune.suggest.basic_variant import BasicVariantGenerator from ray.tune.suggest.suggestion import SuggestionAlgorithm from ray.tune.suggest.bayesopt import BayesOptSearch from ray.tune.suggest.hyperopt import HyperOptSearch +from ray.tune.suggest.skopt import SkOptSearch from ray.tune.suggest.sigopt import SigOptSearch from ray.tune.suggest.variant_generator import grid_search, function, \ sample_from @@ -12,6 +13,7 @@ __all__ = [ "BasicVariantGenerator", "BayesOptSearch", "HyperOptSearch", + "SkOptSearch", "SigOptSearch", "SuggestionAlgorithm", "grid_search", diff --git a/python/ray/tune/suggest/skopt.py b/python/ray/tune/suggest/skopt.py new file mode 100644 index 000000000..039c9d015 --- /dev/null +++ b/python/ray/tune/suggest/skopt.py @@ -0,0 +1,88 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +try: + import skopt +except Exception: + skopt = None + +from ray.tune.suggest.suggestion import SuggestionAlgorithm + + +class SkOptSearch(SuggestionAlgorithm): + """A wrapper around skopt to provide trial suggestions. + + Requires skopt to be installed. + + Parameters: + optimizer (skopt.optimizer.Optimizer): Optimizer provided + 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. + reward_attr (str): The training result objective value attribute. + This refers to an increasing value. + + Example: + >>> from skopt import Optimizer + >>> optimizer = Optimizer([(0,20),(-100,100)]) + >>> config = { + >>> "my_exp": { + >>> "run": "exp", + >>> "num_samples": 10, + >>> "stop": { + >>> "training_iteration": 100 + >>> }, + >>> } + >>> } + >>> algo = SkOptSearch(optimizer, + >>> ["width", "height"], max_concurrent=4, + >>> reward_attr="neg_mean_loss") + """ + + def __init__(self, + optimizer, + parameter_names, + max_concurrent=10, + reward_attr="episode_reward_mean", + **kwargs): + assert skopt 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 + self._max_concurrent = max_concurrent + self._parameters = parameter_names + self._reward_attr = reward_attr + self._skopt_opt = optimizer + self._live_trial_mapping = {} + super(SkOptSearch, self).__init__(**kwargs) + + def _suggest(self, trial_id): + if self._num_live_trials() >= self._max_concurrent: + return None + suggested_config = self._skopt_opt.ask() + 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): + """Passes the result to skopt unless early terminated or errored. + + The result is internally negated when interacting with Skopt + so that Skopt Optimizers can "maximize" this value, + as it minimizes on default. + """ + skopt_trial_info = self._live_trial_mapping.pop(trial_id) + if result: + self._skopt_opt.tell(skopt_trial_info, -result[self._reward_attr]) + + def _num_live_trials(self): + return len(self._live_trial_mapping) diff --git a/test/jenkins_tests/run_multi_node_tests.sh b/test/jenkins_tests/run_multi_node_tests.sh index 573052fa1..bdb70a3e8 100755 --- a/test/jenkins_tests/run_multi_node_tests.sh +++ b/test/jenkins_tests/run_multi_node_tests.sh @@ -377,6 +377,10 @@ docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ python /ray/python/ray/tune/examples/genetic_example.py \ --smoke-test +docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ + python /ray/python/ray/tune/examples/skopt_example.py \ + --smoke-test + docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ python /ray/python/ray/rllib/examples/multiagent_cartpole.py --num-iters=2