Choosing an Optimizer
Every optimization needs an algorithm and a criterion for stopping. ropt
supplies a default for both — SciPy's SLSQP, running until it converges — so
neither has to be configured for a first optimization. This page covers what to
change when the default is not appropriate.
The two settings live in different places: backend selects and configures
the algorithm, while optimizer sets the limits ropt enforces around
it. The full field reference is under
backend and
optimizer.
Selecting a method
A method is named by a "plugin/method" string:
The default is "scipy/default", which is SLSQP. The plugin part is the name a
backend registers itself under, and the method part is any name that backend
declares; both are case-insensitive, so "SciPy/SLSQP" and "scipy/slsqp" are
the same. The plugin part may be omitted when only one installed plugin provides
the method. See Method strings for the full
rules, and Optimizer Backends for the methods each
backend supports.
Options that belong to the algorithm itself, rather than to ropt, go in
options:
Note
Not every method supports every kind of constraint, and a mismatch is
rejected when the optimization starts rather than ignored; see
backend for what that error is. The
one case that is ignored silently is integer variables; see
Discrete and Mixed-Integer Variables.
Running the method in a separate process
Prefix the method with external/ to run the optimization algorithm in a
process of its own:
This SciPy example shows the syntax; the built-in SciPy backend does not need
process isolation. Some backend plugins may need it to run concurrently with
other optimizations. Starting a child process and passing evaluation requests
and results between processes adds overhead, so use external/ when isolation
is required.
ropt spawns a child process, creates the named backend there, and lets it
drive the optimization. The function and gradient evaluations still happen in
the original process: the child sends each set of variables back, the parent
evaluates it as usual, and the values are passed to the child. An error raised
in the child is re-raised in the parent. This is unrelated to evaluating in
parallel, which is about where your objective runs; see
Evaluating in Parallel.
This is useful when a backend cannot safely share a process with the rest of your program — for example one that crashes the interpreter, leaks memory, keeps state between optimizations, or links against native libraries that clash with your other dependencies.
It is also the answer for a backend that cannot run alongside anything else in
the same process. Some optimizers need a working directory of their own, write
to a file whose name is fixed, or keep state inside the library that a second
simultaneous optimization corrupts. What such a backend rules out is not merely a
second optimization of its own kind: changing the working directory applies to
the whole process, so it breaks another optimization's relative output path, and
any file your evaluation function opens by relative name, just as surely. Each
backend states in its own documentation whether this applies to it; where it
does, external/ is what lets it run alongside anything else, because the state
it needs is then its own. This matters as soon as optimizations overlap — see
Running Multiple Optimizations.
Two details differ from the other backends:
- The method must name the delegate in full, as
external/plugin/methodorexternal/method. Theexternal/prefix is removed and the rest is resolved like any other method string.externalis never selected implicitly, so it is used only when you ask for it by name. - The problem is sent to the child process, so everything describing it must be
serializable. The built-in plugins are, and so is any plugin class defined in
a module that can be imported. Only if you pass a plugin instance of a class
defined inside a function or a notebook do you need the optional
cloudpickleextra (see Installation). Without it the two differ in where they fail: a class defined inside a function cannot be sent at all, and is refused here with anExecutionError; a class defined in a notebook is sent by name, and the failure arrives from the child, which reports the name it could not find. Your objective function is never affected: it stays in this process.
Gradient-based or gradient-free
Most ropt methods are gradient-based, and since a black-box objective has no
analytic derivative, they rely on the estimated gradients described in
Stochastic Gradients. That estimate costs extra evaluations at
perturbed points on every iteration.
A gradient-free method such as differential_evolution avoids that entirely: it
evaluates many candidate points instead of following a gradient. It needs bounds
on every variable, ignores perturbation_magnitudes, and is the only choice
when any variable is an integer.
Limiting the length of an optimization
Four settings can stop an optimization, and they are split across the two sections:
| Setting | Section | Limits |
|---|---|---|
max_batches |
optimizer |
calls made to your evaluation function |
max_functions |
optimizer |
individual objective evaluations, across all batches |
max_iterations |
backend |
iterations of the algorithm itself |
convergence_tolerance |
backend |
the improvement below which the algorithm stops |
The division follows who enforces the limit. ropt counts batches and function
evaluations itself, so those belong to optimizer. Iterations and convergence
are the algorithm's own criteria — their exact meaning depends on the method,
and not every backend supports them — so they belong to backend.
CONFIG = {
"variables": {"variable_count": 5, "perturbation_magnitudes": 1e-6},
"optimizer": {"max_functions": 500},
"backend": {"method": "scipy/slsqp", "convergence_tolerance": 1e-8},
}
Which limit applies depends on which resource constrains the optimization. When
each evaluation is expensive, max_functions bounds the total number of
evaluations directly. When the evaluations within a batch are submitted
concurrently, max_batches bounds the number of sequential submissions, which
determines the elapsed time. A batch may contain many evaluations, so the two are
not interchangeable.
convergence_tolerance is compared against the quantities the optimizer works
with, which are scaled. An objective that has been divided by a large number
reaches any absolute tolerance sooner; see Objectives for how
scaling changes what the optimizer sees.
To stop on a condition of your own rather than a count, return True from the
result callback; that is covered in
Running Optimizations, with
examples/stopping.py
as the runnable version.
Output from the optimizer
Backends can report their own progress, which is disabled by default:
verbose determines whether output is produced at all. Where it goes is separate:
optimizer.stdout and optimizer.stderr capture it to files, resolved against
optimizer.output_dir. Capture rewires process-global state, so only one
optimization at a time can use it — leave those unset on optimizations that
overlap. See
optimizer for the details.
See also
- Watching results as they arrive, instead of reading optimizer output: Result Handlers.
- When an optimization stops earlier than expected, or returns no result at all: Troubleshooting.