BBopt aims to provide the easiest hyperparameter optimization you'll ever do. Think of BBopt like Keras (back when Theano was still a thing) for black box optimization: one universal interface for working with any black box optimization backend.
BBopt's features include:
random
module (so you don't even have to learn anything new!),scikit-optimize
or Tree Structured Parzen Estimation from hyperopt
for tuning parameters,skopt.plots
,2.7
or 3.6+
), andOnce you've defined your parameters, training a black box optimization model on those parameters is as simple as
bbopt your_file.py
and serving your file with optimized parameters as easy as
import your_file
Questions? Head over to BBopt's Gitter if you have any questions/comments/etc. regarding BBopt.
To get going with BBopt, simply install it with
pip install bbopt
or, to also install the extra dependencies necessary for running BBopt's examples, run pip install bbopt[examples]
.
To use bbopt, just add
# BBopt setup:
from bbopt import BlackBoxOptimizer
bb = BlackBoxOptimizer(file=__file__)
if __name__ == "__main__":
bb.run()
to the top of your file, then call a random
method like
x = bb.uniform("x", 0, 1)
for each of the tunable parameters in your model, and finally add
bb.maximize(y) or bb.minimize(y)
to set the value being optimized. Then, run
bbopt <your file here> -n <number of trials> -j <number of processes>
to train your model, and just
import <your module here>
to serve it!
Note: Neither __file__
nor __name__
are available in Jupyter notebooks. In that case, just setup BBopt with:
import os
# BBopt setup:
from bbopt import BlackBoxOptimizer
bb = BlackBoxOptimizer(data_dir=os.getcwd(), data_name="my_project_name")
Some examples of BBopt in action:
random_example.py
: Extremely basic example using the random
backend.skopt_example.py
: Slightly more complex example making use of the gaussian_process
algorithm from the scikit-optimize
backend.hyperopt_example.py
: Example showcasing the tree_structured_parzen_estimator
algorithm from the hyperopt
backend.meta_example.py
: Example of using run_meta to dynamically choose an algorithm.numpy_example.py
: Example which showcases how to have numpy array parameters.conditional_skopt_example.py
: Example of having black box parameters that are dependent on other black box parameters using the gaussian_process
algorithm from the scikit-optimize
backend.conditional_hyperopt_example.py
: Example of doing conditional parameters with the tree_structured_parzen_estimator
algorithm from the hyperopt
backend.bask_example.py
: Example of using conditional parameters with a semi-random target using the bask_gp
algorithm from the bayes-skopt
backend.pysot_example.py
: Example of using the full API to implement an optimization loop and avoid the overhead of running the entire file multiple times while making use of the pySOT
backend.keras_example.py
: Complete example of using BBopt to optimize a neural network built with Keras. Uses the full API to implement its own optimization loop and thus avoid the overhead of running the entire file multiple times.any_fast_example.py
: Example of using the default algorithm "any_fast"
to dynamically select a good backend.mixture_example.py
: Example of using the mixture
backend to randomly switch between different algorithms.json_example.py
: Example of using json
instead of pickle
to save parameters.The bbopt
command is extremely simple in terms of what it actually does. For the command bbopt <file> -n <trials> -j <processes>
, BBopt simply runs python <file>
a number of times equal to <trials>
, split across <processes>
different processes.
Why does this work? If you're using the basic boilerplate, then running python <file>
will trigger the if __name__ == "__main__":
clause, which will run a training episode. But when you go to import
your file, the if __name__ == "__main__":
clause won't get triggered, and you'll just get served the best parameters found so far. Since the command-line interface is so simple, advanced users who want to use the full API instead of the boilerplate need not use the bbopt
command at all. If you want more information on the bbopt
command, just run bbopt -h
.
BlackBoxOptimizer(file, *, tag=None
, protocol=None
)
BlackBoxOptimizer(data_dir, data_name, *, tag=None
, protocol=None
)
Create a new bb
object; this should be done at the beginning of your program as all the other functions are methods of this object.
file is used by BBopt to figure out where to load and save data to, and should usually just be set to __file__
. tag allows additional customization of the BBopt data file for when multiple BBopt instances might be desired for the same file. Specifically, BBopt will save data to os.path.splitext(file)[0] + "_" + tag + extension
.
Alternatively, data_dir and data_name can be used to specify where to save and load data to. In that case, BBopt will save data to os.path.join(data_dir, data_name + extension)
if no tag is passed, or os.path.join(data_dir, data_name + "_" + tag + extension)
if a tag is given.
protocol determines how BBopt serializes data. If None
(the default), BBopt will use pickle protocol 2, which is the highest version that works on both Python 2 and Python 3 (unless a json
file is present, in which case BBopt will use json
). To use the newest protocol instead, pass protocol=-1
. If protocol="json"
, BBopt will use json
instead of pickle
, which is occasionally useful if you want to access your data outside of Python.
run
BlackBoxOptimizer.run(alg="any_fast"
)
Start optimizing using the given black box optimization algorithm. Use algs to get the valid values for alg.
If this method is never called, or called with alg="serving"
, BBopt will just serve the best parameters found so far, which is how the basic boilerplate works. Note that, if no saved parameter data is found, and a guess is present, BBopt will use that, which is a good way of distributing your parameter values without including all your saved parameter data.
algs
BlackBoxOptimizer.algs
A dictionary mapping the valid algorithms for use in run to the pair (backend, kwargs)
of the backend and arguments to that backend that the algorithm corresponds to.
Supported algorithms are:
"serving"
(serving
backend) (used if run is never called)"random"
(random
backend)"tree_structured_parzen_estimator"
(hyperopt
backend)"adaptive_tpe"
(hyperopt
backend; but only Python 3+)"annealing"
(hyperopt
backend)"gaussian_process"
(scikit-optimize
backend)"random_forest"
(scikit-optimize
backend)"extra_trees"
(scikit-optimize
backend)"gradient_boosted_regression_trees"
(scikit-optimize
backend)"bask_gaussian_process"
(bayes-skopt
backend)"stochastic_radial_basis_function"
(pySOT
backend)"expected_improvement"
(pySOT
backend)"DYCORS"
(pySOT
backend)"lower_confidence_bound"
(pySOT
backend)"latin_hypercube"
(pySOT
backend)"symmetric_latin_hypercube"
(pySOT
backend)"two_factorial"
(pySOT
backend)"epsilon_max_greedy"
(mixture
backend)"epsilon_greedy"
(bandit
backend)"boltzmann_exploration"
(bandit
backend)"boltzmann_gumbel_exploration"
(bandit
backend) (the default meta_alg in run_meta)"openai"
(openai
backend)Additionally, there are also some algorithms of the form safe_<other_alg>
which use mixture
to defer to <other_alg>
if <other_alg>
supports the parameter definition functions you're using, otherwise default to a suitable replacement.
algs also includes the following pseudo-algorithms which defer to run_meta:
"any_fast"
(same as calling run_meta with a suite of algorithms selected for their speed except that some algorithms are ignored if unsupported parameter definition functions are used, e.g. normalvariate
for scikit-optimize
) (used if run is called with no args)"any_hyperopt"
(equivalent to calling run_meta with all hyperopt
algorithms)"any_skopt"
(equivalent to calling run_meta with all scikit-optimize
algorithms)"any_pysot"
(equivalent to calling run_meta with all pySOT
algorithms)Note: The bayes-skopt
backend is only available on Python 3.7+ and the pySOT
and openai
backends are only available on Python 3+.
run_meta
BlackBoxOptimizer.run_meta(algs, meta_alg="boltzmann_gumbel_exploration"
)
run_meta is a special version of run that uses the meta_alg algorithm to dynamically pick an algorithm from among the given algs. Both algs and meta_alg can use any algorithms in algs.
run_backend
BlackBoxOptimizer.run_backend(backend, *args, **kwargs)
The base function behind run. Instead of specifying an algorithm, run_backend lets you specify the specific backend you want to call and the parameters you want to call it with. Different backends do different things with the remaining arguments:
scikit-optimize
passes the arguments to skopt.Optimizer
,hyperopt
passes the arguments to fmin
,mixture
expects a distribution
argument to specify the mixture of different algorithms to use, specifically a list of (alg, weight)
tuples (and also admits a remove_erroring_algs
bool to automatically remove erroring algorithms),bayes-skopt
passes the arguments to bask.Optimizer
,pySOT
expects a strategy
(either a strategy class or one of "SRBF", "EI", "DYCORS", "LCB"
), a surrogate
(either a surrogate class or one of "RBF", "GP"
), and a design
(either an experimental design class or one of None, "latin_hypercube", "symmetric_latin_hypercube", "two_factorial"
), andopenai
expects an engine
(the name of the model to use), temperature
, max_retries
, and api_key
(otherwise uses OPENAI_API_KEY
env var).Note: The bayes-skopt
backend is only available on Python 3.7+ and the pySOT
and openai
backends are only available on Python 3+.
minimize
BlackBoxOptimizer.minimize(value)
Finish optimizing and set the loss for this run to value. To start another run, call run again.
maximize
BlackBoxOptimizer.maximize(value)
Same as minimize but sets the gain instead of the loss.
remember
BlackBoxOptimizer.remember(info)
Update the current run's "memo"
field with the given info dictionary. Useful for saving information about a run that shouldn't actually impact optimization but that you would like to have access to later (using get_best_run, for example).
plot_convergence
BlackBoxOptimizer.plot_convergence(ax=None
, yscale=None
)
Plot the running best gain/loss over the course of all previous trials. If passed, ax
should be the matplotlib axis to plot on and yscale
should be the scale for the y axis.
Run BBopt's keras
example to generate an example plot.
plot_history
BlackBoxOptimizer.plot_history(ax=None
, yscale=None
)
Plot the gain/loss at each point over the course of all previous trials. If passed, ax
should be the matplotlib axis to plot on and yscale
should be the scale for the y axis.
Run BBopt's keras
example to generate an example plot.
partial_dependence
BlackBoxOptimizer.partial_dependence(i_name, j_name=None
, sample_points=None
, n_samples=250
, n_points=40
)
Calls skopt.plots.partial_dependence
using previous trial data. The parameters i_name and j_name should be set to names of the parameters you want for the i and j arguments to skopt.plots.partial_dependence
.
plot_partial_dependence_1D
BlackBoxOptimizer.plot_partial_dependence_1D(i_name, ax=None
, yscale=None
, sample_points=None
, n_samples=250
, n_points=40
)
Plot the partial dependence of i_name on the given matplotlib axis ax
and with the given y axis scale yscale
. See partial_dependence for the meaning of the other parameters.
Run BBopt's keras
example to generate an example plot.
plot_evaluations
BlackBoxOptimizer.plot_evaluations(bins=20
)
Calls skopt.plots.plot_evaluations
using previous trial data.
Run BBopt's keras
example to generate an example plot.
plot_objective
BlackBoxOptimizer.plot_objective(levels=10
, n_points=40
, n_samples=250
, size=2
, zscale="linear"
)
Calls skopt.plots.plot_objective
using previous trial data.
Run BBopt's keras
example to generate an example plot.
plot_regret
BlackBoxOptimizer.plot_regret([ax, [true_minimum, [yscale]]])
Calls skopt.plots.plot_regret
using previous trial data.
Run BBopt's keras
example to generate an example plot.
get_skopt_result
BlackBoxOptimizer.get_skopt_result()
Gets an OptimizeResult
object usable by skopt.plots
functions. Allows for arbitrary manipulation of BBopt optimization results in scikit-optimize
including any plotting functions not natively supported by BBopt.
get_current_run
BlackBoxOptimizer.get_current_run()
Get information on the current run, including the values of all parameters encountered so far and the loss/gain of the run if specified yet.
get_best_run
BlackBoxOptimizer.get_best_run()
Get information on the best run so far. These are the parameters that will be used if run is not called.
get_data
BlackBoxOptimizer.get_data(print_data=False
)
Dump a dictionary containing "params"
—the parameters BBopt knows about and what random function and arguments they were initialized with—and "examples"
—all the previous data BBopt has collected. If print_data, pretty prints the data in addition to returning it.
data_file
BlackBoxOptimizer.data_file
The path of the file where BBopt is saving data to.
is_serving
BlackBoxOptimizer.is_serving
Whether BBopt is currently using the "serving"
algorithm.
tell_examples
BlackBoxOptimizer.tell_examples(examples)
Add the given examples as in get_data to memory, writing the new data to data_file. Must come before run if you want the new data to be included in the model for that run.
backend
BlackBoxOptimizer.backend
The backend object being used by the current BlackBoxOptimizer instance.
run_id
BlackBoxOptimizer.run_id
The id of the current run if started by the BBopt command-line interface.
Every BBopt parameter definition method has the form
bb.<random function>(<name>, <args>, **kwargs)
where
Important note: Once you bind a name to a parameter, you cannot change that parameter's options. Thus, if the options defining your parameters can vary from run to run, you must use a different name for each possible combination.
randrange
BlackBoxOptimizer.randrange(name, stop, **kwargs)
BlackBoxOptimizer.randrange(name, start, stop, step=1
, **kwargs)
Create a new parameter modeled by random.randrange(start, stop, step)
.
Backends which support randrange: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
randint
BlackBoxOptimizer.randint(name, a, b, **kwargs)
Create a new parameter modeled by random.randint(a, b)
, which is equivalent to random.randrange(a, b-1)
.
Backends which support randint: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
getrandbits
BlackBoxOptimizer.getrandbits(name, k, **kwargs)
Create a new parameter modeled by random.getrandbits(k)
, which is equivalent to random.randrange(0, 2**k)
.
Backends which support getrandbits: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
choice
BlackBoxOptimizer.choice(name, seq, **kwargs)
Create a new parameter modeled by random.choice(seq)
, which chooses an element from seq.
Backends which support choice: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, random
.
randbool
BlackBoxOptimizer.randbool(name, **kwargs)
Create a new boolean parameter, modeled by the equivalent of random.choice([False, True])
.
Backends which support randbool: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, random
.
sample
BlackBoxOptimizer.sample(name, population, k, **kwargs)
Create a new parameter modeled by random.sample(population, k)
, which chooses k elements from population.
By default, the ordering of elements in the result is random. If random ordering is not important and you're happy to have the same ordering as in population, BlackBoxOptimizer.unshuffled_sample
is recommended instead.
Backends which support sample: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, random
.
shuffle
BlackBoxOptimizer.shuffle(name, population, **kwargs)
Create a new parameter modeled by random.shuffle(population)
. A version that returns the shuffled list instead of shuffling it in place is also supported as BlackBoxOptimizer.shuffled
.
Backends which support shuffle: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, random
.
random
BlackBoxOptimizer.random(name, **kwargs)
Create a new parameter modeled by random.random()
, which is equivalent to random.uniform(0, 1)
except that 1
is disallowed.
Backends which support random: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
uniform
BlackBoxOptimizer.uniform(name, a, b, **kwargs)
Create a new parameter modeled by random.uniform(a, b)
, which uniformly selects a float in the range [a, b].
Backends which support uniform: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
loguniform
BlackBoxOptimizer.loguniform(name, min_val, max_val, **kwargs)
Create a new parameter modeled by
math.exp(random.uniform(math.log(min_val), math.log(max_val)))
which logarithmically selects a float between min_val and max_val.
Backends which support loguniform: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
normalvariate
BlackBoxOptimizer.normalvariate(name, mu, sigma, **kwargs)
Create a new parameter modeled by random.normalvariate(mu, sigma)
.
A shortcut for the standard normal distribution is also available via BlackBoxOptimizer.stdnormal
.
Backends which support normalvariate: hyperopt
, openai
, random
.
lognormvariate
BlackBoxOptimizer.lognormvariate(name, mu, sigma, **kwargs)
Create a new parameter modeled by random.lognormvariate(mu, sigma)
such that the natural log is a normal distribution with mean mu and standard deviation sigma.
Backends which support lognormvariate: hyperopt
, openai
, random
.
rand
BlackBoxOptimizer.rand(name, *shape, **kwargs)
Create a new parameter modeled by numpy.random.rand(*shape)
, which creates a numpy
array of the given shape with entries generated uniformly in [0, 1)
.
Backends which support rand: scikit-optimize
, hyperopt
, bayes-skopt
, pySOT
, openai
, random
.
randn
BlackBoxOptimizer.randn(name, *shape, **kwargs)
Create a new parameter modeled by numpy.random.randn(*shape)
, which creates a numpy
array of the given shape with entries generated according to a standard normal distribution.
Backends which support randn: hyperopt
, openai
, random
.
param
BlackBoxOptimizer.param(name, func, *args, **kwargs)
Create a new parameter modeled by the parameter definition function func with the given arguments. This function is mostly useful if you want to use a custom backend that implements parameter definition functions not included in BBopt by default.
BBopt's backend system is built to be extremely extensible, allowing anyone to write and register their own BBopt backends. The basic template for writing a BBopt backend is as follows:
from bbopt.backends.util import StandardBackend
class MyBackend(StandardBackend):
backend_name = "my-backend"
implemented_funcs = [
# list the random functions you support here
# (you don't need to include all random functions,
# only base random functions, primarily randrange,
# choice, uniform, and normalvariate)
...,
]
def setup_backend(self, params, **options):
# initialize your backend; you can use params
# to get the args for each param
def tell_data(self, new_data, new_losses):
# load new data points into your backend; new_data is
# a list of dictionaries containing data and new_losses
# is a list of losses for each of those data points
def get_next_values(self):
# return the values you want to use for this run as a dict
MyBackend.register()
MyBackend.register_alg("my_alg")
Once you've written a BBopt backend as above, you simply need to import it to trigger the register
calls and enable it to be used in BBopt. For some example BBopt backends, see BBopt's default backends (written in Coconut):