From 8323419a6da14e81dd0e0551690fcc6180fe46e9 Mon Sep 17 00:00:00 2001 From: Andrew Tan Date: Sun, 3 Feb 2019 18:23:57 -0800 Subject: [PATCH] [tune] Add SigOpt Integration (#3844) --- doc/source/tune-searchalg.rst | 30 +++++ docker/examples/Dockerfile | 1 + python/ray/tune/examples/sigopt_example.py | 78 ++++++++++++ python/ray/tune/suggest/__init__.py | 2 + python/ray/tune/suggest/sigopt.py | 132 +++++++++++++++++++++ test/jenkins_tests/run_multi_node_tests.sh | 8 +- 6 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 python/ray/tune/examples/sigopt_example.py create mode 100644 python/ray/tune/suggest/sigopt.py diff --git a/doc/source/tune-searchalg.rst b/doc/source/tune-searchalg.rst index aab1a6321..7cac438c2 100644 --- a/doc/source/tune-searchalg.rst +++ b/doc/source/tune-searchalg.rst @@ -13,6 +13,7 @@ Currently, Tune offers the following search algorithms: - `Grid Search and Random Search `__ - `HyperOpt `__ +- `SigOpt `__ Variant Generation (Grid Search/Random Search) @@ -73,6 +74,35 @@ An example of this can be found in `hyperopt_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 SigOptSearch. + +In order to use this search algorithm, you will need to install SigOpt via the following command: + +.. code-block:: bash + + $ pip install sigopt + +This algorithm requires the user to have a `SigOpt API key `__ to make requests to the API. Store the API token as an environment variable named ``SIGOPT_KEY`` like follows: + +.. code-block:: bash + + $ export SIGOPT_KEY= ... + +This algorithm requires using the `SigOpt experiment and space specification `__. You can use SigOptSearch like follows: + +.. code-block:: python + + run_experiments(experiment_config, search_alg=SigOptSearch(sigopt_space, ... )) + +An example of this can be found in `sigopt_example.py `__. + +.. autoclass:: ray.tune.suggest.SigOptSearch + :show-inheritance: + :noindex: + Contributing a New Algorithm ---------------------------- diff --git a/docker/examples/Dockerfile b/docker/examples/Dockerfile index 8258f390d..286de726f 100644 --- a/docker/examples/Dockerfile +++ b/docker/examples/Dockerfile @@ -9,4 +9,5 @@ RUN pip install gym[atari] opencv-python==3.2.0.8 tensorflow lz4 keras pytest-ti 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 conda install pytorch-cpu torchvision-cpu -c pytorch diff --git a/python/ray/tune/examples/sigopt_example.py b/python/ray/tune/examples/sigopt_example.py new file mode 100644 index 000000000..e242a7ae5 --- /dev/null +++ b/python/ray/tune/examples/sigopt_example.py @@ -0,0 +1,78 @@ +"""This test checks that SigOpt 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 SigOptSearch + + +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 + import os + + assert "SIGOPT_KEY" in os.environ, \ + "SigOpt API key must be stored as environment variable at SIGOPT_KEY" + + 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) + + space = [ + { + 'name': 'width', + 'type': 'int', + 'bounds': { + 'min': 0, + 'max': 20 + }, + }, + { + 'name': 'height', + 'type': 'int', + 'bounds': { + 'min': -100, + 'max': 100 + }, + }, + ] + + config = { + "my_exp": { + "run": "exp", + "num_samples": 10 if args.smoke_test else 1000, + "config": { + "iterations": 100, + }, + "stop": { + "timesteps_total": 100 + }, + } + } + algo = SigOptSearch( + space, + name="SigOpt Example Experiment", + max_concurrent=1, + 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 042982d4b..576420c53 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.sigopt import SigOptSearch from ray.tune.suggest.variant_generator import grid_search, function, \ sample_from @@ -11,6 +12,7 @@ __all__ = [ "BasicVariantGenerator", "BayesOptSearch", "HyperOptSearch", + "SigOptSearch", "SuggestionAlgorithm", "grid_search", "function", diff --git a/python/ray/tune/suggest/sigopt.py b/python/ray/tune/suggest/sigopt.py new file mode 100644 index 000000000..12b338f88 --- /dev/null +++ b/python/ray/tune/suggest/sigopt.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import copy +import os + +try: + import sigopt as sgo +except Exception: + sgo = None + +from ray.tune.suggest.suggestion import SuggestionAlgorithm + + +class SigOptSearch(SuggestionAlgorithm): + """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`. + + Parameters: + space (list of dict): SigOpt configuration. Parameters will be sampled + from this configuration and will be used to override + parameters generated in the variant generation process. + name (str): Name of experiment. Required by SigOpt. + max_concurrent (int): Number of maximum concurrent trials supported + based on the user's SigOpt plan. Defaults to 1. + reward_attr (str): The training result objective value attribute. + This refers to an increasing value. + + Example: + >>> space = [ + >>> { + >>> 'name': 'width', + >>> 'type': 'int', + >>> 'bounds': { + >>> 'min': 0, + >>> 'max': 20 + >>> }, + >>> }, + >>> { + >>> 'name': 'height', + >>> 'type': 'int', + >>> 'bounds': { + >>> 'min': -100, + >>> 'max': 100 + >>> }, + >>> }, + >>> ] + >>> config = { + >>> "my_exp": { + >>> "run": "exp", + >>> "num_samples": 10 if args.smoke_test else 1000, + >>> "stop": { + >>> "training_iteration": 100 + >>> }, + >>> } + >>> } + >>> algo = SigOptSearch( + >>> parameters, name="SigOpt Example Experiment", + >>> max_concurrent=1, reward_attr="neg_mean_loss") + """ + + def __init__(self, + space, + name="Default Tune Experiment", + max_concurrent=1, + reward_attr="episode_reward_mean", + **kwargs): + assert sgo is not None, "SigOpt must be installed!" + assert type(max_concurrent) is int and max_concurrent > 0 + assert "SIGOPT_KEY" in os.environ, \ + "SigOpt API key must be stored as environ variable at SIGOPT_KEY" + self._max_concurrent = max_concurrent + self._reward_attr = reward_attr + self._live_trial_mapping = {} + + # Create a connection with SigOpt API, requires API key + self.conn = sgo.Connection(client_token=os.environ['SIGOPT_KEY']) + + self.experiment = self.conn.experiments().create( + name=name, + parameters=space, + parallel_bandwidth=self._max_concurrent, + ) + + super(SigOptSearch, self).__init__(**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() + + self._live_trial_mapping[trial_id] = suggestion + + 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): + """Passes the result to SigOpt unless early terminated or errored. + + If a trial fails, it will be reported as a failed Observation, telling + the optimizer that the Suggestion led to a metric failure, which + updates the feasible region and improves parameter recommendation. + + Creates SigOpt Observation object for trial. + """ + if result: + self.conn.experiments(self.experiment.id).observations().create( + suggestion=self._live_trial_mapping[trial_id].id, + value=result[self._reward_attr], + ) + # Update the experiment object + self.experiment = self.conn.experiments(self.experiment.id).fetch() + elif error or early_terminated: + # 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) diff --git a/test/jenkins_tests/run_multi_node_tests.sh b/test/jenkins_tests/run_multi_node_tests.sh index ed7e1c8bc..492f49e73 100755 --- a/test/jenkins_tests/run_multi_node_tests.sh +++ b/test/jenkins_tests/run_multi_node_tests.sh @@ -351,11 +351,15 @@ docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ --smoke-test docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ - python /ray/python/ray/tune/examples/hyperopt_example.py \ + python /ray/python/ray/tune/examples/bayesopt_example.py \ --smoke-test docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ - python /ray/python/ray/tune/examples/bayesopt_example.py \ + python /ray/python/ray/tune/examples/hyperopt_example.py \ + --smoke-test + +docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} -e SIGOPT_KEY $DOCKER_SHA \ + python /ray/python/ray/tune/examples/sigopt_example.py \ --smoke-test docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \