diff --git a/doc/source/tune/api_docs/overview.rst b/doc/source/tune/api_docs/overview.rst index cc68b3bbb..66b5b955e 100644 --- a/doc/source/tune/api_docs/overview.rst +++ b/doc/source/tune/api_docs/overview.rst @@ -17,7 +17,7 @@ on `Github`_. trainable.rst reporters.rst analysis.rst - grid_random.rst + search_space.rst suggestion.rst schedulers.rst sklearn.rst diff --git a/doc/source/tune/api_docs/grid_random.rst b/doc/source/tune/api_docs/search_space.rst similarity index 73% rename from doc/source/tune/api_docs/grid_random.rst rename to doc/source/tune/api_docs/search_space.rst index 7269507cc..005942fe9 100644 --- a/doc/source/tune/api_docs/grid_random.rst +++ b/doc/source/tune/api_docs/search_space.rst @@ -1,11 +1,11 @@ -.. _tune-grid-random: +.. _tune-search-space: -Grid/Random Search -================== +Search Space API +================ Overview -------- -Tune has a native interface for specifying a grid search or random search. You can specify the search space via ``tune.run(config=...)``. +Tune has a native interface for specifying search spaces. You can specify the search space via ``tune.run(config=...)``. Thereby, you can either use the ``tune.grid_search`` primitive to specify an axis of a grid search... @@ -32,7 +32,7 @@ Thereby, you can either use the ``tune.grid_search`` primitive to specify an axi .. caution:: If you use a Search Algorithm, you may not be able to specify lambdas or grid search with this - interface, as the search algorithm may require a different search space declaration. + interface, as some search algorithms may not be compatible. To sample multiple times/run multiple trials, specify ``tune.run(num_samples=N``. If ``grid_search`` is provided as an argument, the *same* grid will be repeated ``N`` times. @@ -159,15 +159,64 @@ Here's an example showing a grid search over two nested parameters combined with Random Distributions API ------------------------ -tune.randn -~~~~~~~~~~ +This section covers the functions you can use to define your search spaces. -.. autofunction:: ray.tune.randn +For a high-level overview, see this example: -tune.qrandn -~~~~~~~~~~~ +.. code-block :: python -.. autofunction:: ray.tune.qrandn + config = { + # Sample a float uniformly between -5.0 and -1.0 + "uniform": tune.uniform(-5, -1), + + # Sample a float uniformly between 3.2 and 5.4, + # rounding to increments of 0.2 + "quniform": tune.quniform(3.2, 5.4, 0.2), + + # Sample a float uniformly between 0.0001 and 0.01, while + # sampling in log space + "loguniform": tune.loguniform(1e-4, 1e-2), + + # Sample a float uniformly between 0.0001 and 0.1, while + # sampling in log space and rounding to increments of 0.0005 + "qloguniform": tune.qloguniform(1e-4, 1e-1, 5e-4), + + # Sample a random float from a normal distribution with + # mean=10 and sd=2 + "randn": tune.randn(10, 2), + + # Sample a random float from a normal distribution with + # mean=10 and sd=2, rounding to increments of 0.2 + "qrandn": tune.qrandn(10, 2, 0.2), + + # Sample a integer uniformly between -9 (inclusive) and 15 (exclusive) + "randint": tune.randint(-9, 15), + + # Sample a random uniformly between -21 (inclusive) and 12 (inclusive (!)) + # rounding to increments of 3 (includes 12) + "qrandint": tune.qrandint(-21, 12, 3), + + # Sample an option uniformly from the specified choices + "choice": tune.choice(["a", "b", "c"]), + + # Sample from a random function, in this case one that + # depends on another value from the search space + "func": tune.sample_from(lambda spec: spec.config.uniform * 0.01), + + # Do a grid search over these values. Every value will be sampled + # `num_samples` times (`num_samples` is the parameter you pass to `tune.run()`) + "grid": tune.grid_search([32, 64, 128]) + } + +tune.uniform +~~~~~~~~~~~~ + +.. autofunction:: ray.tune.uniform + +tune.quniform +~~~~~~~~~~~~~ + +.. autofunction:: ray.tune.quniform tune.loguniform ~~~~~~~~~~~~~~~ @@ -179,15 +228,15 @@ tune.qloguniform .. autofunction:: ray.tune.qloguniform -tune.uniform -~~~~~~~~~~~~ +tune.randn +~~~~~~~~~~ -.. autofunction:: ray.tune.uniform +.. autofunction:: ray.tune.randn -tune.quniform -~~~~~~~~~~~~~ +tune.qrandn +~~~~~~~~~~~ -.. autofunction:: ray.tune.quniform +.. autofunction:: ray.tune.qrandn tune.randint ~~~~~~~~~~~~ diff --git a/doc/source/tune/api_docs/suggestion.rst b/doc/source/tune/api_docs/suggestion.rst index ea2bbfa8c..9df6d4223 100644 --- a/doc/source/tune/api_docs/suggestion.rst +++ b/doc/source/tune/api_docs/suggestion.rst @@ -64,7 +64,11 @@ Summary - :doc:`/tune/examples/sigopt_example` -.. note::Search algorithms will require a different search space declaration than the default Tune format - meaning that you will not be able to combine ``tune.grid_search`` with the below integrations. +.. note:: We are currently in the process of implementing automatic search space + conversions for all search algorithms. Currently this works for + AxSearch, BayesOpt, Hyperopt and Optuna. The other search algorithms + will follow shortly, but have to be instantiated with their respective + search spaces at the moment. See the code examples and docstrings for more details. .. note:: Unlike :ref:`Tune's Trial Schedulers `, Tune SearchAlgorithms cannot affect or stop training processes. However, you can use them together to **early stop the evaluation of bad trials**. @@ -191,7 +195,7 @@ Nevergrad (tune.suggest.nevergrad.NevergradSearch) .. _tune-optuna: Optuna (tune.suggest.optuna.OptunaSearch) --------------------------------------------------- +----------------------------------------- .. autoclass:: ray.tune.suggest.optuna.OptunaSearch diff --git a/doc/source/tune/key-concepts.rst b/doc/source/tune/key-concepts.rst index 0d7ae38b5..11247895b 100644 --- a/doc/source/tune/key-concepts.rst +++ b/doc/source/tune/key-concepts.rst @@ -101,6 +101,40 @@ Finally, you can randomly sample or grid search hyperparameters via Tune's :ref: See more documentation: :ref:`tune-run-ref`. +Search spaces +------------- + +To optimize your hyperparameters, you have to define a *search space*. +A search space defines valid values for your hyperparameters and can specify +how these values are sampled (e.g. from a uniform distribution or a normal +distribution). + +Tune offers various functions to define search spaces and sampling methods. +:ref:`You can find the documentation of these search space definitions here `. + +Usually you pass your search space definition in the `config` parameter of +``tune.run()``. + +Here's an example covering all search space functions. Again, +:ref:`here is the full explanation of all these functions `. + + +.. code-block :: python + + config = { + "uniform": tune.uniform(-5, -1), # Uniform float between -5 and -1 + "quniform": tune.quniform(3.2, 5.4, 0.2), # Round to increments of 0.2 + "loguniform": tune.loguniform(1e-4, 1e-2), # Uniform float in log space + "qloguniform": tune.qloguniform(1e-4, 1e-1, 5e-4), # Round to increments of 0.0005 + "randn": tune.randn(10, 2), # Normal distribution with mean 10 and sd 2 + "qrandn": tune.qrandn(10, 2, 0.2), # Round to increments of 0.2 + "randint": tune.randint(-9, 15), # Random integer between -9 and 15 + "qrandint": tune.qrandint(-21, 12, 3), # Round to increments of 3 (includes 12) + "choice": tune.choice(["a", "b", "c"]), # Choose one of these options uniformly + "func": tune.sample_from(lambda spec: spec.config.uniform * 0.01), # Depends on other value + "grid": tune.grid_search([32, 64, 128]) # Search over all these values + } + Search Algorithms ----------------- @@ -114,26 +148,35 @@ To optimize the hyperparameters of your training process, you will want to use a from ray.tune.suggest.hyperopt import HyperOptSearch # Create a HyperOpt search space - space = { - "a": hp.uniform("a", 0, 1), - "b": hp.uniform("b", 0, 20) + config = { + "a": tune.uniform(0, 1), + "b": tune.uniform(0, 20) # Note: Arbitrary HyperOpt search spaces should be supported! - # "foo": hp.lognormal("foo", 0, 1)) + # "foo": tune.randn(0, 1)) } # Specify the search space and maximize score - hyperopt = HyperOptSearch(space, metric="score", mode="max") + hyperopt = HyperOptSearch(metric="score", mode="max") # Execute 20 trials using HyperOpt and stop after 20 iterations tune.run( trainable, + config=config, search_alg=hyperopt, num_samples=20, stop={"training_iteration": 20} ) -Tune has SearchAlgorithms that integrate with many popular **optimization** libraries, such as :ref:`Nevergrad ` and :ref:`Hyperopt `. +Tune has SearchAlgorithms that integrate with many popular **optimization** libraries, such as :ref:`Nevergrad ` and :ref:`Hyperopt `. Tune automatically converts the provided search space into the search +spaces the search algorithms/underlying library expect. + +.. note:: + We are currently in the process of implementing automatic search space + conversions for all search algorithms. Currently this works for + AxSearch, BayesOpt, Hyperopt and Optuna. The other search algorithms + will follow shortly, but have to be instantiated with their respective + search spaces at the moment. See the documentation: :ref:`tune-search-alg`. diff --git a/doc/source/tune/user-guide.rst b/doc/source/tune/user-guide.rst index ddbf58256..efe91a7a4 100644 --- a/doc/source/tune/user-guide.rst +++ b/doc/source/tune/user-guide.rst @@ -69,9 +69,7 @@ To attach to a Ray cluster, simply run ``ray.init`` before ``tune.run``: Search Space (Grid/Random) -------------------------- -.. warning:: If you use a Search Algorithm, you will need to use a different search space API. - -You can specify a grid search or random search via the dict passed into ``tune.run(config=)``. +You can specify a grid search or sampling distribution via the dict passed into ``tune.run(config=)``. .. code-block:: python @@ -84,7 +82,7 @@ You can specify a grid search or random search via the dict passed into ``tune.r tune.run(trainable, config=parameters) -By default, each random variable and grid search point is sampled once. To take multiple random samples, add ``num_samples: N`` to the experiment config. If `grid_search` is provided as an argument, the grid will be repeated `num_samples` of times. +By default, each random variable and grid search point is sampled once. To take multiple random samples, add ``num_samples: N`` to the experiment config. If `grid_search` is provided as an argument, the grid will be repeated ``num_samples`` of times. .. code-block:: python :emphasize-lines: 13 @@ -104,7 +102,7 @@ By default, each random variable and grid search point is sampled once. To take num_samples=10 ) -Read about this in the :ref:`Grid/Random Search API ` page. +Note that search spaces may not be interoperable across different search algorithms. For example, for many search algorithms, you will not be able to use a ``grid_search`` parameter. Read about this in the :ref:`Search Space API ` page. Reporting Metrics ----------------- diff --git a/python/ray/tune/examples/ax_example.py b/python/ray/tune/examples/ax_example.py index 71335a094..1bcb401a9 100644 --- a/python/ray/tune/examples/ax_example.py +++ b/python/ray/tune/examples/ax_example.py @@ -46,7 +46,6 @@ def easy_objective(config): if __name__ == "__main__": import argparse - from ax.service.ax_client import AxClient parser = argparse.ArgumentParser() parser.add_argument( @@ -55,62 +54,32 @@ if __name__ == "__main__": ray.init() - config = { + tune_kwargs = { "num_samples": 10 if args.smoke_test else 50, "config": { "iterations": 100, + "x1": tune.uniform(0.0, 1.0), + "x2": tune.uniform(0.0, 1.0), + "x3": tune.uniform(0.0, 1.0), + "x4": tune.uniform(0.0, 1.0), + "x5": tune.uniform(0.0, 1.0), + "x6": tune.uniform(0.0, 1.0), }, "stop": { "timesteps_total": 100 } } - parameters = [ - { - "name": "x1", - "type": "range", - "bounds": [0.0, 1.0], - "value_type": "float", # Optional, defaults to "bounds". - "log_scale": False, # Optional, defaults to False. - }, - { - "name": "x2", - "type": "range", - "bounds": [0.0, 1.0], - }, - { - "name": "x3", - "type": "range", - "bounds": [0.0, 1.0], - }, - { - "name": "x4", - "type": "range", - "bounds": [0.0, 1.0], - }, - { - "name": "x5", - "type": "range", - "bounds": [0.0, 1.0], - }, - { - "name": "x6", - "type": "range", - "bounds": [0.0, 1.0], - }, - ] - client = AxClient(enforce_sequential_optimization=False) - client.create_experiment( - parameters=parameters, - objective_name="hartmann6", - minimize=True, # Optional, defaults to False. + algo = AxSearch( + max_concurrent=4, + metric="hartmann6", + mode="min", parameter_constraints=["x1 + x2 <= 2.0"], # Optional. outcome_constraints=["l2norm <= 1.25"], # Optional. ) - algo = AxSearch(ax_client=client, max_concurrent=4) scheduler = AsyncHyperBandScheduler(metric="hartmann6", mode="min") tune.run( easy_objective, name="ax", search_alg=algo, scheduler=scheduler, - **config) + **tune_kwargs) diff --git a/python/ray/tune/examples/bayesopt_example.py b/python/ray/tune/examples/bayesopt_example.py index afa6e81c2..d9f552658 100644 --- a/python/ray/tune/examples/bayesopt_example.py +++ b/python/ray/tune/examples/bayesopt_example.py @@ -35,16 +35,15 @@ if __name__ == "__main__": args, _ = parser.parse_known_args() ray.init() - space = {"width": (0, 20), "height": (-100, 100)} - - config = { + tune_kwargs = { "num_samples": 10 if args.smoke_test else 1000, "config": { "steps": 100, + "width": tune.uniform(0, 20), + "height": tune.uniform(-100, 100) } } algo = BayesOptSearch( - space, metric="mean_loss", mode="min", utility_kwargs={ @@ -58,4 +57,4 @@ if __name__ == "__main__": name="my_exp", search_alg=algo, scheduler=scheduler, - **config) + **tune_kwargs) diff --git a/python/ray/tune/examples/hyperopt_example.py b/python/ray/tune/examples/hyperopt_example.py index 3ba120e81..3385376b6 100644 --- a/python/ray/tune/examples/hyperopt_example.py +++ b/python/ray/tune/examples/hyperopt_example.py @@ -28,7 +28,6 @@ def easy_objective(config): if __name__ == "__main__": import argparse - from hyperopt import hp parser = argparse.ArgumentParser() parser.add_argument( @@ -36,13 +35,6 @@ if __name__ == "__main__": args, _ = parser.parse_known_args() ray.init(configure_logging=False) - space = { - "width": hp.uniform("width", 0, 20), - "height": hp.uniform("height", -100, 100), - # This is an ignored parameter. - "activation": hp.choice("activation", ["relu", "tanh"]) - } - current_best_params = [ { "width": 1, @@ -56,16 +48,18 @@ if __name__ == "__main__": } ] - config = { + tune_kwargs = { "num_samples": 10 if args.smoke_test else 1000, "config": { "steps": 100, + "width": tune.uniform(0, 20), + "height": tune.uniform(-100, 100), + # This is an ignored parameter. + "activation": tune.choice(["relu", "tanh"]) } } algo = HyperOptSearch( - space, - metric="mean_loss", - mode="min", - points_to_evaluate=current_best_params) + metric="mean_loss", mode="min", points_to_evaluate=current_best_params) scheduler = AsyncHyperBandScheduler(metric="mean_loss", mode="min") - tune.run(easy_objective, search_alg=algo, scheduler=scheduler, **config) + tune.run( + easy_objective, search_alg=algo, scheduler=scheduler, **tune_kwargs) diff --git a/python/ray/tune/examples/optuna_example.py b/python/ray/tune/examples/optuna_example.py index 6bafbfeb9..ded76a425 100644 --- a/python/ray/tune/examples/optuna_example.py +++ b/python/ray/tune/examples/optuna_example.py @@ -7,7 +7,7 @@ import time import ray from ray import tune from ray.tune.schedulers import AsyncHyperBandScheduler -from ray.tune.suggest.optuna import OptunaSearch, param +from ray.tune.suggest.optuna import OptunaSearch def evaluation_fn(step, width, height): @@ -35,19 +35,17 @@ if __name__ == "__main__": args, _ = parser.parse_known_args() ray.init(configure_logging=False) - space = [ - param.suggest_uniform("width", 0, 20), - param.suggest_uniform("height", -100, 100), - # This is an ignored parameter. - param.suggest_categorical("activation", ["relu", "tanh"]) - ] - - config = { + tune_kwargs = { "num_samples": 10 if args.smoke_test else 100, "config": { "steps": 100, + "width": tune.uniform(0, 20), + "height": tune.uniform(-100, 100), + # This is an ignored parameter. + "activation": tune.choice(["relu", "tanh"]) } } - algo = OptunaSearch(space, metric="mean_loss", mode="min") + algo = OptunaSearch(metric="mean_loss", mode="min") scheduler = AsyncHyperBandScheduler(metric="mean_loss", mode="min") - tune.run(easy_objective, search_alg=algo, scheduler=scheduler, **config) + tune.run( + easy_objective, search_alg=algo, scheduler=scheduler, **tune_kwargs) diff --git a/python/ray/tune/suggest/ax.py b/python/ray/tune/suggest/ax.py index e25556f86..9574f80ce 100644 --- a/python/ray/tune/suggest/ax.py +++ b/python/ray/tune/suggest/ax.py @@ -56,9 +56,34 @@ class AxSearch(Searcher): use_early_stopped_trials: Deprecated. max_concurrent (int): Deprecated. + Tune automatically converts search spaces to Ax's format: + + .. code-block:: python + + from ray import tune + from ray.tune.suggest.ax import AxSearch + + config = { + "x1": tune.uniform(0.0, 1.0), + "x2": tune.uniform(0.0, 1.0) + } + + def easy_objective(config): + for i in range(100): + intermediate_result = config["x1"] + config["x2"] * i + tune.report(score=intermediate_result) + + ax_search = AxSearch(objective_name="score") + tune.run( + config=config, + easy_objective, + search_alg=ax_search) + + If you would like to pass the search space manually, the code would + look like this: + .. code-block:: python - from ax.service.ax_client import AxClient from ray import tune from ray.tune.suggest.ax import AxSearch @@ -72,9 +97,8 @@ class AxSearch(Searcher): intermediate_result = config["x1"] + config["x2"] * i tune.report(score=intermediate_result) - client = AxClient() - algo = AxSearch(space=parameters, objective_name="score") - tune.run(easy_objective, search_alg=algo) + ax_search = AxSearch(space=parameters, objective_name="score") + tune.run(easy_objective, search_alg=ax_search) """ diff --git a/python/ray/tune/suggest/bayesopt.py b/python/ray/tune/suggest/bayesopt.py index 7aa4e2bbb..340f200a5 100644 --- a/python/ray/tune/suggest/bayesopt.py +++ b/python/ray/tune/suggest/bayesopt.py @@ -65,6 +65,24 @@ class BayesOptSearch(Searcher): max_concurrent: Deprecated. use_early_stopped_trials: Deprecated. + Tune automatically converts search spaces to BayesOptSearch's format: + + .. code-block:: python + + from ray import tune + from ray.tune.suggest.bayesopt import BayesOptSearch + + config = { + "width": tune.uniform(0, 20), + "height": tune.uniform(-100, 100) + } + + bayesopt = BayesOptSearch(metric="mean_loss", mode="min") + tune.run(my_func, config=config, search_alg=bayesopt) + + If you would like to pass the search space manually, the code would + look like this: + .. code-block:: python from ray import tune @@ -74,8 +92,9 @@ class BayesOptSearch(Searcher): 'width': (0, 20), 'height': (-100, 100), } - algo = BayesOptSearch(space, metric="mean_loss", mode="min") - tune.run(my_func, search_alg=algo) + bayesopt = BayesOptSearch(space, metric="mean_loss", mode="min") + tune.run(my_func, search_alg=bayesopt) + """ # bayes_opt.BayesianOptimization: Optimization object optimizer = None @@ -336,11 +355,6 @@ class BayesOptSearch(Searcher): "Grid search parameters cannot be automatically converted " "to a BayesOpt search space.") - if resolved_vars: - raise ValueError( - "BayesOpt does not support fixed parameters. Please find a " - "different way to pass constants to your training function.") - def resolve_value(domain): sampler = domain.get_sampler() if isinstance(sampler, Quantized): diff --git a/python/ray/tune/suggest/hyperopt.py b/python/ray/tune/suggest/hyperopt.py index 6cb6d1dfe..b05cc3cc2 100644 --- a/python/ray/tune/suggest/hyperopt.py +++ b/python/ray/tune/suggest/hyperopt.py @@ -42,27 +42,6 @@ class HyperOptSearch(Searcher): pip install -U hyperopt - You will not be able to leverage Tune's default ``grid_search`` - and random search primitives when using HyperOptSearch. You need to - use the `HyperOpt search space specification - `_. - - .. code-block:: python - - space = { - 'width': hp.uniform('width', 0, 20), - 'height': hp.uniform('height', -100, 100), - 'activation': hp.choice("activation", ["relu", "tanh"]) - } - current_best_params = [{ - 'width': 10, - 'height': 0, - 'activation': 0, # The index of "relu" - }] - algo = HyperOptSearch( - space, metric="mean_loss", mode="min", - points_to_evaluate=current_best_params) - Parameters: space (dict): HyperOpt configuration. Parameters will be sampled @@ -88,6 +67,52 @@ class HyperOptSearch(Searcher): max_concurrent: Deprecated. use_early_stopped_trials: Deprecated. + Tune automatically converts search spaces to HyperOpt's format: + + .. code-block:: python + + config = { + 'width': tune.uniform(0, 20), + 'height': tune.uniform(-100, 100), + 'activation': tune.choice(["relu", "tanh"]) + } + + current_best_params = [{ + 'width': 10, + 'height': 0, + 'activation': 0, # The index of "relu" + }] + + hyperopt_search = HyperOptSearch( + metric="mean_loss", mode="min", + points_to_evaluate=current_best_params) + + tune.run(trainable, config=config, search_alg=hyperopt_search) + + If you would like to pass the search space manually, the code would + look like this: + + .. code-block:: python + + space = { + 'width': hp.uniform('width', 0, 20), + 'height': hp.uniform('height', -100, 100), + 'activation': hp.choice("activation", ["relu", "tanh"]) + } + + current_best_params = [{ + 'width': 10, + 'height': 0, + 'activation': 0, # The index of "relu" + }] + + hyperopt_search = HyperOptSearch( + space, metric="mean_loss", mode="min", + points_to_evaluate=current_best_params) + + tune.run(trainable, search_alg=hyperopt_search) + + """ def __init__( diff --git a/python/ray/tune/suggest/optuna.py b/python/ray/tune/suggest/optuna.py index 2d184f78d..792df0fc3 100644 --- a/python/ray/tune/suggest/optuna.py +++ b/python/ray/tune/suggest/optuna.py @@ -60,7 +60,25 @@ class OptunaSearch(Searcher): sampler (optuna.samplers.BaseSampler): Optuna sampler used to draw hyperparameter configurations. Defaults to ``TPESampler``. - Example: + Tune automatically converts search spaces to Optuna's format: + + .. code-block:: python + + from ray.tune.suggest.optuna import OptunaSearch + + config = { + "a": tune.uniform(6, 8) + "b": tune.uniform(10, 20) + } + + optuna_search = OptunaSearch( + metric="loss", + mode="min") + + tune.run(trainable, config=config, search_alg=optuna_search) + + If you would like to pass the search space manually, the code would + look like this: .. code-block:: python @@ -76,6 +94,8 @@ class OptunaSearch(Searcher): metric="loss", mode="min") + tune.run(trainable, search_alg=optuna_search) + .. versionadded:: 0.8.8 """