A minimalistic and easy-to-use Python module that efficiently searches for a global minimum of an expensive black-box function (e.g. optimal hyperparameters of simulation, neural network or anything that takes significant time to run). User needs to provide a function, a search domain (ranges of each input parameter) and a total number of function calls available. A code scales well on multicore CPUs and clusters: all function calls are divided into batches and each batch is evaluated in parallel.
A mathematical method behind the code is described in this arXiv note (there were few updates to the method recently): https://arxiv.org/pdf/1605.00998.pdf
Don't forget to cite this note if you are using method/code.
(a) - demo function (unknown to a method).
(b) - running a procedure using 15 evaluations.
(c) - running a procedure using 30 evaluations.
You can install it with uv (run uv init first if you don't have an existing project yet):
uv add git+https://github.com/paulknysh/blackboxor, with pip, into a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
pip install git+https://github.com/paulknysh/blackboxTo work on blackbox itself, clone the repo and run uv sync. Testing, linting, and formatting are done in one command:
make sureCI runs these steps automatically on every push and pull request (with
ruff format --check instead of ruff format).
Simply needs to be wrapped into a Python function.
def fun(par):
...
return outputpar is a vector of input parameters (a Python list), output is a scalar value to be minimized.
import blackbox as bb
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 1) ** 2
if __name__ == "__main__":
result = bb.minimize(
f=fun, # given function
domain=[[-5, 5], [-5, 5]], # ranges of each parameter
budget=20, # total number of function calls available
batch=4, # number of calls that will be evaluated in parallel
)
# best result (x and function value)
print(result["best_x"])
print(result["best_f"])
# the entire history of evaluations
# print(result["all_xs"])
# print(result["all_fs"])Important:
- All function calls are divided into batches and each batch is evaluated in parallel. Total number of batches is
ceil(budget/batch)(the budget is automatically rounded up to a multiple ofbatch). The value ofbatchshould correspond to the number of available computational units. - An optional parameter
executor = ...should be specified withinbb.minimize()in case when custom parallel engine is used (ipyparallel, dask.distributed, pathos etc).executormust be a callable (e.g. a class or factory) that, when called with no arguments, returns a context-managing object exposing amapmethod (it is invoked aswith executor() as e:). The default ismultiprocessing.Pool. - The default
executorismultiprocessing.Pool, which pickles the objective functionfto send it to workers. This meansfmust be picklable: alambdaor a closure defined in a REPL/notebook will raise aPicklingError. Use a top-level function (as in the example above) or pass a custom serial executor /dask/ipyparallelexecutor that avoids pickling.
bb.minimize() returns a dictionary with the following keys:
"best_x"- best iteration"best_f"- corresponding function value"all_xs"- all iterations"all_fs"- corresponding function values
MIT
