From 89ec4adb726aafd70026ac3ffdb21cadcde36278 Mon Sep 17 00:00:00 2001 From: Anthony Yu <13611707+anthonyhsyu@users.noreply.github.com> Date: Tue, 10 Mar 2020 08:40:36 -0700 Subject: [PATCH] [tune] Dragonfly Optimizer (#5955) * Add sample example * Copy relevant lines of ask from inherited Optimizer * Ignore strategy * Additional changes * Add DragonflySearch for tune connector for Dragonfly * Add example and fix small errors * lint * Remove skopt references * Update example based off of Dragonfly changes * Edit example for final Dragonfly edits * Formatting and documentation edits * Add documentation and add to test pipeline * Address PR comments * Fix Jenkins test * Adjust Dragonfly to PR#7366 * Lint * fix_tests Co-authored-by: Richard Liaw --- ci/jenkins_tests/run_tune_tests.sh | 4 + doc/source/tune-searchalg.rst | 29 +++- docker/examples/Dockerfile | 2 +- python/ray/tune/examples/dragonfly_example.py | 83 ++++++++++ python/ray/tune/suggest/dragonfly.py | 150 ++++++++++++++++++ python/ray/tune/suggest/nevergrad.py | 12 +- python/ray/tune/tests/test_tune_restore.py | 6 +- 7 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 python/ray/tune/examples/dragonfly_example.py create mode 100644 python/ray/tune/suggest/dragonfly.py diff --git a/ci/jenkins_tests/run_tune_tests.sh b/ci/jenkins_tests/run_tune_tests.sh index b421eddd0..638317fd9 100755 --- a/ci/jenkins_tests/run_tune_tests.sh +++ b/ci/jenkins_tests/run_tune_tests.sh @@ -174,6 +174,10 @@ $SUPPRESS_OUTPUT docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} python /ray/python/ray/tune/examples/skopt_example.py \ --smoke-test +$SUPPRESS_OUTPUT docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} --memory-swap=-1 $DOCKER_SHA \ + python /ray/python/ray/tune/examples/dragonfly_example.py \ + --smoke-test + # Commenting out because flaky # $SUPPRESS_OUTPUT docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} --memory-swap=-1 $DOCKER_SHA \ # python /ray/python/ray/tune/examples/pbt_memnn_example.py \ diff --git a/doc/source/tune-searchalg.rst b/doc/source/tune-searchalg.rst index 8f2bf90a1..6318e2c99 100644 --- a/doc/source/tune-searchalg.rst +++ b/doc/source/tune-searchalg.rst @@ -172,7 +172,7 @@ In order to use this search algorithm, you will need to install Scikit-Optimize $ 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: +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 @@ -185,6 +185,33 @@ An example of this can be found in `skopt_example.py `__ to perform sequential Bayesian 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 DragonflySearch. + +.. code-block:: bash + + $ pip install dragonfly + +This algorithm requires using the `Dragonfly ask and tell interface `__. This interface requires using FunctionCallers and optimizers provided by Dragonfly. You can use `DragonflySearch` like follows: + +.. code-block:: python + + from dragonfly.opt.gp_bandit import EuclideanGPBandit + from dragonfly.exd.experiment_caller import EuclideanFunctionCaller + from dragonfly import load_config + domain_config = load_config({'domain': ...}) + func_caller = EuclideanFunctionCaller(None, domain_config.domain.list_of_domains[0]) + optimizer = EuclideanGPBandit(func_caller, ask_tell_mode=True) + algo = DragonflySearch(optimizer, ...) + +An example of this can be found in `dragonfly_example.py `__. + +.. autoclass:: ray.tune.suggest.dragonfly.DragonflySearch + :show-inheritance: + :noindex: + Ax Search --------- diff --git a/docker/examples/Dockerfile b/docker/examples/Dockerfile index 64fd9afce..c57c3fa0b 100644 --- a/docker/examples/Dockerfile +++ b/docker/examples/Dockerfile @@ -14,6 +14,6 @@ RUN pip install -U h5py # Mutes FutureWarnings RUN pip install --upgrade bayesian-optimization RUN pip install --upgrade hyperopt==0.1.2 RUN pip install ConfigSpace==0.4.10 -RUN pip install --upgrade sigopt nevergrad scikit-optimize hpbandster lightgbm xgboost torch torchvision tensorboardX +RUN pip install --upgrade sigopt nevergrad scikit-optimize hpbandster lightgbm xgboost torch torchvision tensorboardX dragonfly-opt RUN pip install -U tabulate mlflow RUN pip install -U pytest-remotedata>=0.3.1 diff --git a/python/ray/tune/examples/dragonfly_example.py b/python/ray/tune/examples/dragonfly_example.py new file mode 100644 index 000000000..6e433f6fa --- /dev/null +++ b/python/ray/tune/examples/dragonfly_example.py @@ -0,0 +1,83 @@ +"""This test checks that Dragonfly 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 +from ray.tune.schedulers import AsyncHyperBandScheduler +from ray.tune.suggest.dragonfly import DragonflySearch + + +def objective(config, reporter): + import numpy as np + import time + time.sleep(0.2) + for i in range(config["iterations"]): + vol1 = config["point"][0] # LiNO3 + vol2 = config["point"][1] # Li2SO4 + vol3 = config["point"][2] # NaClO4 + vol4 = 10 - (vol1 + vol2 + vol3) # Water + # Synthetic functions + conductivity = vol1 + 0.1 * (vol2 + vol3)**2 + 2.3 * vol4 * (vol1**1.5) + # Add Gaussian noise to simulate experimental noise + conductivity += np.random.normal() * 0.01 + reporter(timesteps_total=i, objective=conductivity) + time.sleep(0.02) + + +if __name__ == "__main__": + import argparse + from dragonfly.opt.gp_bandit import EuclideanGPBandit + from dragonfly.exd.experiment_caller import EuclideanFunctionCaller + from dragonfly import load_config + + parser = argparse.ArgumentParser() + parser.add_argument( + "--smoke-test", action="store_true", help="Finish quickly for testing") + args, _ = parser.parse_known_args() + ray.init() + + config = { + "num_samples": 10 if args.smoke_test else 50, + "config": { + "iterations": 100, + }, + "stop": { + "timesteps_total": 100 + }, + } + + domain_vars = [{ + "name": "LiNO3_vol", + "type": "float", + "min": 0, + "max": 7 + }, { + "name": "Li2SO4_vol", + "type": "float", + "min": 0, + "max": 7 + }, { + "name": "NaClO4_vol", + "type": "float", + "min": 0, + "max": 7 + }] + + domain_config = load_config({"domain": domain_vars}) + + func_caller = EuclideanFunctionCaller( + None, 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") + scheduler = AsyncHyperBandScheduler(metric="objective", mode="max") + run(objective, + name="dragonfly_search", + search_alg=algo, + scheduler=scheduler, + **config) diff --git a/python/ray/tune/suggest/dragonfly.py b/python/ray/tune/suggest/dragonfly.py new file mode 100644 index 000000000..b915fd6ec --- /dev/null +++ b/python/ray/tune/suggest/dragonfly.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging +import pickle + +try: # Python 3 only -- needed for lint test. + import dragonfly +except ImportError: + dragonfly = None + +from ray.tune.suggest.suggestion import SuggestionAlgorithm + +logger = logging.getLogger(__name__) + + +class DragonflySearch(SuggestionAlgorithm): + """A wrapper around Dragonfly to provide trial suggestions. + + Requires Dragonfly to be installed. + + 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. + points_to_evaluate (list of lists): A list of points you'd like to run + first before sampling from the optimiser, e.g. these could be + parameter configurations you already know work well to help + the optimiser select good values. Each point is a list of the + parameters using the order definition given by parameter_names. + evaluated_rewards (list): If you have previously evaluated the + parameters passed in as points_to_evaluate you can avoid + re-running those trials by passing in the reward attributes + 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. + + Example: + >>> from dragonfly.opt.gp_bandit import EuclideanGPBandit + >>> from dragonfly.exd.experiment_caller import EuclideanFunctionCaller + >>> from dragonfly import load_config + >>> domain_vars = [{ + "name": "LiNO3_vol", + "type": "float", + "min": 0, + "max": 7 + }, { + "name": "Li2SO4_vol", + "type": "float", + "min": 0, + "max": 7 + }, { + "name": "NaClO4_vol", + "type": "float", + "min": 0, + "max": 7 + }] + + >>> domain_config = load_config({"domain": domain_vars}) + >>> func_caller = EuclideanFunctionCaller(None, + 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") + """ + + def __init__(self, + optimizer, + max_concurrent=10, + reward_attr=None, + metric="episode_reward_mean", + mode="max", + points_to_evaluate=None, + evaluated_rewards=None, + **kwargs): + 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 = [] + 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._metric = metric + # Dragonfly internally maximizes, so "min" => -1 + if mode == "min": + self._metric_op = -1. + elif mode == "max": + self._metric_op = 1. + self._opt = optimizer + self._opt.initialise() + self._live_trial_mapping = {} + super(DragonflySearch, 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] + else: + suggested_config = self._opt.ask() + 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): + """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: + pickle.dump(trials_object, outputFile) + + def restore(self, checkpoint_dir): + with open(checkpoint_dir, "rb") as inputFile: + trials_object = pickle.load(inputFile) + self._initial_points = trials_object[0] + self._opt = trials_object[1] diff --git a/python/ray/tune/suggest/nevergrad.py b/python/ray/tune/suggest/nevergrad.py index f83ec552c..221d0517a 100644 --- a/python/ray/tune/suggest/nevergrad.py +++ b/python/ray/tune/suggest/nevergrad.py @@ -116,13 +116,11 @@ class NevergradSearch(SuggestionAlgorithm): self._live_trial_mapping[trial_id] = suggested_config # in v0.2.0+, output of ask() is a Candidate, # with fields args and kwargs - if hasattr(self._nevergrad_opt, "instrumentation"): - if not suggested_config.kwargs: - return dict(zip(self._parameters, suggested_config.args[0])) - else: - return suggested_config.kwargs - # legacy: output of ask() is a np.ndarray - return dict(zip(self._parameters, suggested_config)) + if not suggested_config.kwargs: + print(suggested_config.args, suggested_config.kwargs) + return dict(zip(self._parameters, suggested_config.args[0])) + else: + return suggested_config.kwargs def on_trial_result(self, trial_id, result): pass diff --git a/python/ray/tune/tests/test_tune_restore.py b/python/ray/tune/tests/test_tune_restore.py index 257a20897..5a75d29fd 100644 --- a/python/ray/tune/tests/test_tune_restore.py +++ b/python/ray/tune/tests/test_tune_restore.py @@ -144,7 +144,7 @@ class AbstractWarmStartTest: def run_exp_1(self): np.random.seed(162) search_alg, cost = self.set_basic_conf() - results_exp_1 = tune.run(cost, num_samples=15, search_alg=search_alg) + results_exp_1 = tune.run(cost, num_samples=5, search_alg=search_alg) self.log_dir = os.path.join(self.tmpdir, "warmStartTest.pkl") search_alg.save(self.log_dir) return results_exp_1 @@ -152,12 +152,12 @@ class AbstractWarmStartTest: def run_exp_2(self): search_alg2, cost = self.set_basic_conf() search_alg2.restore(self.log_dir) - return tune.run(cost, num_samples=15, search_alg=search_alg2) + return tune.run(cost, num_samples=5, search_alg=search_alg2) def run_exp_3(self): np.random.seed(162) search_alg3, cost = self.set_basic_conf() - return tune.run(cost, num_samples=30, search_alg=search_alg3) + return tune.run(cost, num_samples=10, search_alg=search_alg3) def testWarmStart(self): results_exp_1 = self.run_exp_1()