Each of the four methods in NMinimize
has built-in hooks you can use to get the current values through StepMonitor
. There are some great advantages to this approach over ones that hijack the user's objective function and only minor drawbacks:
- The regular
NMinimize
interface can be used.
- The user's objective function will be used as is, so that any analysis of the function normally done by
NMinimize
is not prevented by wrapping the objective function in shell that turns the function into a numeric black box.
- The hooks give direct access to the state of the method algorithm being used. One could hardly ask for more.
- The hooks are easy to access. Some post-processing is often needed, depending on the method. One might want further to apply
FindMinimum[]
to the results (not shown).
- The hooks are undocumented AFAIK, and perhaps they are subject to change. OTOH, the code is open to inspection and this approach can be adapted should new methods be added or old methods improved. I believe the current code has been stable for a fairly long time.
Note that NMaximize[f[x], x]
basically calls NMinimize[-f[x], x]
, so I will speak primarily in terms of minimization. The raw values you get with the following approaches will also be of -f[x]
, when using NMaximize[]
. In most examples below, which call NMaximize[]
, this is accounted for that the maximum is returned.
In NMinimize
the objective function consists of the user's function plus a penalty function. There are two values that it keeps track of, val
and fval
. The value initially optimized is val
, which equals fval
plus a penalty (often 0.
), where fval
is the value of the function. At the end of the method, there is post-processing of the results, sometimes using the equivalent of FindMinimum
to polish the results.
Each algorithm is different and there is not a uniform user-interface to them. Here are examples of each:
"DifferentialEvolution"
In this example foo
contains the most recent pools of points (vecs
) and values. One can use linked lists to keep track of each step (see "SimulatedAnnealing"
at the end).
(* "DifferentialEvolution" *)
TimeConstrained[
NMaximize[
{7 x - 4 x^2 + y - y^2, {x, y} ∈ Disk[]}, {x, y},
Method -> "DifferentialEvolution",
StepMonitor :>
If[ValueQ@Optimization`NMinimizeDump`fvals,
foo = {Optimization`NMinimizeDump`vals, Optimization`NMinimizeDump`vecs,
Optimization`NMinimizeDump`fvals}]],
0.1]
tolerance = 10^-7;
Last@ Sort@ Pick[Transpose[{-foo[[1]], Thread[{x, y} -> #] & /@ foo[[2]]}],
UnitStep[foo[[1]] - foo[[3]] - tolerance], 0]
(*
$Aborted
{3.31235, {x -> 0.870649, y -> 0.491204}}
*)
"NelderMead"
Like with "DifferentialEvolution"
, foo
contains the most recent pools of points (vecs
, the "simplex") and values.
(* "NelderMead" *)
TimeConstrained[
NMaximize[
{7 x - 4 x^2 + y - y^2, {x, y} ∈ Disk[]}, {x, y},
Method -> {"NelderMead",
"RandomSeed" -> 1 (* for reproducibility *)},
StepMonitor :>
If[ValueQ@Optimization`NMinimizeDump`fvals,
foo = {Optimization`NMinimizeDump`vals, Optimization`NMinimizeDump`vecs,
Optimization`NMinimizeDump`fvals}]],
0.05]
tolerance = 10^-7;
Last@ Sort@ Pick[Transpose[{-foo[[1]], Thread[{x, y} -> #] & /@ foo[[2]]}],
UnitStep[tolerance + foo[[3]] - foo[[1]]], 1]
(*
$Aborted
{3.31236, {x -> 0.871075, y -> 0.491156}}
*)
Note that each value val
has a penalty. Hence the need for a positive tolerance
, or all points would be rejected. (This would be cleaned up in post-processing, which was aborted by the time constraint.) As one can see below, the selected point above does not satisfy {x, y} ∈ Disk[]
. The user will have to decide how to treat the results in their particular case. (This applies to all methods, in fact.)
Norm[{x, y}] /. Last[%] // InputForm
(* 1.0000027734542223 *)
foo[[1]] - foo[[3]]
(* {5.21808*10^-8, 3.6511*10^-8, 5.48056*10^-8} *)
"RandomSearch"
In "RandomSearch"
results
is initialized to the pool of initial points. Each point is replaced by the result of a local minimizer (which may be specified with the "Method"
suboption to Method
). The post-processing code chooses the best result of the minimizer.
(* "RandomSearch" *)
TimeConstrained[
NMaximize[{7 x - 4 x^2 + y - y^2, {x, y} ∈ Disk[]}, {x, y},
Method -> "RandomSearch",
StepMonitor :>
If[ValueQ@Optimization`NMinimizeDump`results,
foo = Optimization`NMinimizeDump`results]],
0.2]
Select[foo, ! FreeQ[#, "Converged"] &]
(*
$Aborted
{{{-3.31236, {0.871072 -> 0.871072, 0.491155 -> 0.491155}}, {True, "Converged"}},
{{-3.31236, {0.871072 -> 0.871072, 0.491155 -> 0.491155}}, {True, "Converged"}},
{{-3.31236, {0.871072 -> 0.871072, 0.491155 -> 0.491155}}, {True, "Converged"}},
{{-3.31236, {0.871072 -> 0.871072, 0.491155 -> 0.491155}}, {True, "Converged"}},
{{-3.31236, {0.871072 -> 0.871072, 0.491155 -> 0.491155}}, {True, "Converged"}},
{{-3.31236, {0.871072 -> 0.871072, 0.491155 -> 0.491155}}, {True, "Converged"}}}
*)
"SimulatedAnnealing"
Simulated annealing works somewhat like the random search method in that it starts with a pool of initial points and processes each individually. It then does some post-processing and returns the best result found. As it processes each point, it keeps track of the best result of processing that point in the form
Optimization`NMinimizeDump`best = {val, vec, fval}
To accumulate all the results efficiently, I used link lists. I wrapped the list Optimization`NMinimizeDump`best
in an undefined head called hold
to make flattening the linked list in post-processing easier. Note it does not Hold[]
the results.
(* "SimulatedAnnealing" *)
TimeConstrained[
ClearAll[hold];
foo = {};
NMaximize[{7 x - 4 x^2 + y - y^2, {x, y} ∈ Disk[]}, {x, y},
Method -> "SimulatedAnnealing",
StepMonitor :>
If[ValueQ@Optimization`NMinimizeDump`best,
foo = {hold[Optimization`NMinimizeDump`best], foo}]],
0.02]
foo = Flatten@foo /. hold -> Identity;
tolerance = 0;
First@ Sort@ Pick[
Transpose[{-foo[[All, 1]], Thread[{x, y} -> #] & /@ foo[[All, 2]]}],
UnitStep[tolerance + foo[[All, 3]] - foo[[All, 1]]], 1]
(*
$Aborted
{3.25174, {x -> 0.753606, y -> 0.457427}}
*)
MaxIterations
, but it's not a constraint based on time. $\endgroup$