[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 <rliaw@berkeley.edu>
This commit is contained in:
Anthony Yu
2020-03-10 08:40:36 -07:00
committed by GitHub
co-authored by Richard Liaw
parent fa785a2ad2
commit 89ec4adb72
7 changed files with 274 additions and 12 deletions
+4
View File
@@ -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 \
+28 -1
View File
@@ -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 <https://scikit-optimize.github.io/notebooks/ask-and-tell.html>`__. This interface requires using the `Optimizer <https://scikit-optimize.github.io/#skopt.Optimizer>`__ provided by Scikit-Optimize. You can use SkOptSearch like follows:
This algorithm requires using the `Scikit-Optimize ask and tell interface <https://scikit-optimize.github.io/notebooks/ask-and-tell.html>`__. This interface requires using the `Optimizer <https://scikit-optimize.github.io/#skopt.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 <https://github.com/ray-pro
:show-inheritance:
:noindex:
Dragonfly Search
----------------
The ``DragonflySearch`` is a SearchAlgorithm that is backed by `Dragonfly <https://github.com/dragonfly/dragonfly>`__ 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 <https://dragonfly-opt.readthedocs.io/en/master/getting_started_ask_tell/>`__. 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 <https://github.com/ray-project/ray/blob/master/python/ray/tune/examples/dragonfly_example.py>`__.
.. autoclass:: ray.tune.suggest.dragonfly.DragonflySearch
:show-inheritance:
:noindex:
Ax Search
---------
+1 -1
View File
@@ -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
@@ -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)
+150
View File
@@ -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]
+5 -7
View File
@@ -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
+3 -3
View File
@@ -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()