An optimization can stop for three importantly different reasons:
| Outcome | status |
converged |
Typical termination reasons |
|---|---|---|---|
| A tolerance was satisfied | "converged" |
TRUE |
abs_tol, rel_tol,
grad_tol, ginf_tol, step_tol
|
| A hard limit was exhausted | "budget_exhausted" |
FALSE |
max_iter, max_fn,
max_gr, max_fg
|
| The optimization failed | "failed" |
FALSE |
a non-finite callback or a failed line search |
The result contract also reserves status = "terminated"
for other terminal reasons.
A limit often marks the intended end of a bounded run. It leaves
convergence unestablished. Inspect status and
converged for the broad outcome and
terminate$what and terminate$val for the
detailed reason. This article shows how to choose among the controls and
interpret tolerance, budget, and failure outcomes.
Choose stopping controls
Each control answers a different question:
| Control | What it detects | Main limitation | Quantity required |
|---|---|---|---|
abs_tol / rel_tol
|
Little change in the objective | Stagnation need not mean stationarity | Objective |
grad_tol / ginf_tol
|
A small gradient | Stationary points include saddles and maxima; absolute thresholds depend on scaling | Gradient |
step_tol |
Little movement in the parameters | May reflect poor scaling or a rejected step | Parameter update |
max_iter |
An outer iteration limit | Iterations can contain different amounts of work | None |
max_fn / max_gr /
max_fg
|
A callback cost limit | Says nothing about convergence | None |
Out of the box, mize() checks for small absolute and
relative changes in the objective and for a small parameter update; all
three tolerances use sqrt(.Machine$double.eps).
Gradient-based stopping is disabled. A run is capped at 100 outer
iterations, while max_fn, max_gr, and
max_fg do not impose finite run-wide limits unless you set
them. Those run-wide callback limits are separate from the controls
applied within an individual line search.
For a general smooth problem, enable a gradient criterion when its scale is meaningful, retain callback or iteration limits as safety bounds, and treat objective-change or step tolerances as evidence of stagnation rather than proof of stationarity.
We’ll use the two-dimensional Rosenbrock function, which has a
minimum at c(1, 1) where the function equals zero.
Iteration limit
max_iter bounds the number of optimizer iterations.
Disable the tolerances when the purpose of an example is to demonstrate
that limit:
res <- mize(
rb0,
rb_fg,
max_iter = 10,
abs_tol = NULL,
rel_tol = NULL,
grad_tol = NULL,
ginf_tol = NULL,
step_tol = NULL
)
result_summary(res)
#> status terminate iter nf ng objective
#> 1 budget_exhausted max_iter 10 18 18 2.5526Iteration counts are method-specific: one method may perform much more work inside an iteration than another. For comparisons, report callback counts and elapsed time as appropriate. Equal iteration limits do not imply equal computational effort.
Function-change tolerances
abs_tol compares consecutive function values and is
reached when abs(fn_old - fn_new) < abs_tol.
res <- mize(
rb0,
rb_fg,
abs_tol = 1e-8,
rel_tol = NULL,
grad_tol = NULL,
ginf_tol = NULL,
step_tol = NULL
)
result_summary(res)
#> status terminate iter nf ng objective
#> 1 converged abs_tol 38 50 50 4.60648e-11At a mize() call, rel_tol defaults to the
value supplied for abs_tol. Set rel_tol = NULL
explicitly when you want the absolute criterion alone; similarly, set
abs_tol = NULL when isolating relative change.
Relative change is calculated as
abs(fn_old - fn_new) / min(abs(fn_old), abs(fn_new)). If
the denominator is zero, the relative change is zero when the values are
equal and Inf otherwise.
Consecutive objective values can become similar because a run has approached a solution or because its steps have become too small. The following one-dimensional quadratic isolates that distinction. Its minimizer is 10, and a deliberately small constant step produces little relative change while the parameter remains far away:
slow_minimum <- 10
slow_fg <- list(
fn = function(x) (x - slow_minimum)^2,
gr = function(x) 2 * (x - slow_minimum)
)
relative_result <- mize(
0,
slow_fg,
method = "SD",
line_search = "constant",
step0 = 1e-4,
max_iter = 5,
abs_tol = NULL,
rel_tol = 1e-3,
grad_tol = NULL,
ginf_tol = NULL,
step_tol = NULL
)| Reason | Relative change | Parameter | Distance from 10 |
|---|---|---|---|
| rel_tol | 4e-04 | 0.004 | 9.996 |
The relative criterion has answered its specific question: recent objective values changed little. The distance column shows why that observation alone does not establish that the parameter is near the minimizer. Pair it with a stationarity criterion when that is the conclusion the run must support.
Gradient tolerances
grad_tol tests the Euclidean (2-)norm of the current
gradient.
res <- mize(
rb0,
rb_fg,
abs_tol = NULL,
rel_tol = NULL,
grad_tol = 1e-3,
ginf_tol = NULL,
step_tol = NULL
)
result_summary(res)
#> status terminate iter nf ng objective
#> 1 converged grad_tol 37 49 49 4.52167e-10A small gradient indicates approximate stationarity. It does not distinguish a minimum from a saddle point or maximum, and an absolute threshold depends on the scaling of both the parameters and the objective. Gradient norms are still commonly used when comparing optimizers; see, for example, Nocedal and co-workers.
ginf_tol instead tests the infinity norm: the largest
absolute component of the gradient. This is another common choice,
particularly for larger problems; see the conjugate-gradient paper by
Hager and Zhang.
res <- mize(
rb0,
rb_fg,
abs_tol = NULL,
rel_tol = NULL,
grad_tol = NULL,
ginf_tol = 1e-3,
step_tol = NULL
)
result_summary(res)
#> status terminate iter nf ng objective
#> 1 converged ginf_tol 37 49 49 4.52167e-10Checking either gradient tolerance requires a gradient at the current position. A cached current gradient avoids an additional callback; otherwise the check requests one. The method and line search determine whether the value is already available.
Step tolerance
step_tol detects when the Euclidean distance between
consecutive parameter vectors is small.
res <- mize(
rb0,
rb_fg,
abs_tol = NULL,
rel_tol = NULL,
grad_tol = NULL,
ginf_tol = NULL,
step_tol = 1e-4
)
result_summary(res)
#> status terminate iter nf ng objective
#> 1 converged step_tol 38 50 50 4.60648e-11Some methods can reject an iteration and restart from the same
parameters with different optimizer state. step_tol ignores
that restart. Because the criterion uses an absolute Euclidean distance,
rescaling one parameter can also change when it triggers even when the
underlying path describes the same problem.
Evaluation budgets
When function or gradient callbacks dominate runtime, their counts
can be a useful proxy for work. The relative costs still depend on the
problem, and methods and line searches differ in which callbacks they
need and can reuse. Accepted Hessian and inverse-Hessian callbacks are
reported separately as nh and nhi; they are
observational fields and are not governed by max_fn,
max_gr, or max_fg.
max_fn caps function evaluations, max_gr
caps gradient evaluations, and max_fg caps their combined
total. The following runs use the same problem and disable every
ordinary tolerance so that each budget supplies the terminal
condition:
budget_names <- c("max_fn", "max_gr", "max_fg")
run_with_budget <- function(budget) {
arguments <- list(
par = rb0,
fg = rb_fg,
abs_tol = NULL,
rel_tol = NULL,
grad_tol = NULL,
ginf_tol = NULL,
step_tol = NULL
)
arguments[[budget]] <- 10
do.call(mize, arguments)
}
budget_runs <- setNames(
lapply(budget_names, run_with_budget),
budget_names
)| Control | Reason | Function calls | Gradient calls | Total calls |
|---|---|---|---|---|
| max_fn | max_fn | 10 | 10 | 20 |
| max_gr | max_gr | 10 | 10 | 20 |
| max_fg | max_fg | 5 | 5 | 10 |
The separate limits allow ten evaluations of each callback in this example; the combined limit stops after ten callbacks in total. The table reports the actual work because an iteration can consume different numbers of callbacks.
Each actual callback consumes the applicable budget; reuse of a
cached value does not. Once a callback budget is exhausted,
mize makes no later callback covered by that cap. This is
why a final objective or requested gradient diagnostic can be absent
when its value was not already available.
When several criteria are active
All non-NULL criteria are active.
terminate$what identifies the terminal condition reported
by the optimizer; it does not imply that every other measure was far
from its threshold. Hard callback limits are checked before a covered
callback is made, so the configured cap is respected. When the final
permitted callback itself establishes convergence or produces a
non-finite result, that observed tolerance or failure is reported. The
Convergence section of the
mize() reference gives the complete contract.
Failure result
A non-finite callback produces a failure outcome. Here the objective
is finite, but the gradient callback returns NaN:
bad_fg <- list(
fn = function(x) sum(x^2),
gr = function(x) rep(NaN, length(x))
)
failure_result <- mize(c(1, 1), bad_fg)
failure_result[c("status", "message", "terminate")]
#> $status
#> [1] "failed"
#>
#> $message
#> [1] "Failed: gr_inf encountered."
#>
#> $terminate
#> $terminate$what
#> [1] "gr_inf"
#>
#> $terminate$val
#> [1] InfThe gr_inf reason identifies the non-finite gradient as
the immediate problem. Begin failure triage with message
and terminate, then inspect available progress or
line-search diagnostics for the callback and iteration that produced
it.
Checking tolerances less often
By default, mize checks tolerances every iteration.
Function-change tolerances need a function value at the current
parameters, and gradient tolerances need a current gradient. When the
method or line search has not already produced the needed value,
checking can add a callback; when it has, the cached value is
reused.
check_conv_every changes the cadence of tolerance
detection. For example, this run tests the gradient criterion only every
five iterations:
res <- mize(
rb0,
rb_fg,
abs_tol = NULL,
rel_tol = NULL,
grad_tol = 1e-3,
ginf_tol = NULL,
step_tol = NULL,
check_conv_every = 5
)
result_summary(res)
#> status terminate iter nf ng objective
#> 1 converged grad_tol 40 52 52 4.147e-18A larger interval can delay recognition of a satisfied tolerance and
therefore overshoot it. Callback budgets remain hard:
max_fn, max_gr, and max_fg are
enforced when a covered callback would be made. Non-finite callback
results and line-search no-step failures are also handled independently
of the ordinary tolerance cadence.
If verbose = TRUE or store_progress = TRUE,
log_every controls how often the corresponding summary is
emitted or stored. It defaults to check_conv_every and must
be an integer multiple of it; otherwise mize uses
check_conv_every for both.
Best and last results
Stopping and point selection answer different questions. A tolerance,
limit, or failure decides when a run ends. When objective values are
available for best-result tracking, mize returns the
observed optimizer state with the lowest objective, even if the stopping
event occurred at a later state.
| Question | Fields | Interpretation |
|---|---|---|
| Which point is the optimization result? |
par, f
|
The returned best point and its objective |
| Which names state that choice explicitly? |
best_par, best_f
|
Aliases for par and f
|
| Which point ended the optimizer loop? |
last_par, last_f
|
The final optimizer state before any best-result restoration |
| Why did the run end? |
status, converged,
message, terminate
|
The stopping event; if detected before restoration, it is associated with the last state or transition |
| Where are returned gradient norms evaluated? |
g2n, ginfn
|
At par, not necessarily at
last_par
|
When objective tracking is active, use par or
best_par for the lowest observed objective. Use
last_par when you need the parameters associated with the
terminal optimizer state. If no objective value establishes best-result
tracking, mize may instead compare observed infinity norms
of the gradient.
The following constructed run makes the distinction visible. Its
constant step moves from a lower objective to a stationary point with a
higher objective. The gradient tolerance stops the run at the stationary
point, but mize returns the lower-objective starting
point.
separating_fg <- list(
fn = function(x) -3 * x^3 + 5 * x^2 - x,
gr = function(x) -9 * x^2 + 10 * x - 1
)
separated_result <- mize(
0,
separating_fg,
method = "SD",
line_search = "constant",
step0 = 1,
max_iter = 1,
abs_tol = 0,
rel_tol = NULL,
grad_tol = NULL,
ginf_tol = 0,
step_tol = NULL,
store_progress = TRUE
)
point_comparison <- data.frame(
Point = c("Returned best", "Terminal"),
Parameter = c(separated_result$best_par, separated_result$last_par),
Objective = c(separated_result$best_f, separated_result$last_f),
`Gradient infinity norm` = c(
separated_result$ginfn,
separated_result$terminate$val
),
check.names = FALSE
)
knitr::kable(point_comparison, align = c("l", "r", "r", "r"))| Point | Parameter | Objective | Gradient infinity norm |
|---|---|---|---|
| Returned best | 0 | 0 | 1 |
| Terminal | 1 | 1 | 0 |
Here terminate$val = 0 is the gradient norm that
satisfied ginf_tol at last_par = 1. The
returned ginfn = 1 is evaluated at par = 0.
These values describe different points, so they do not conflict.
Cached function or gradient information can make the best and last
results differ even when every tolerance is disabled. A hard callback
budget can also prevent a final observation. In that case, an objective
result field can be absent, and last_f can be
NA when last_par differs from
par. Check that the required field is present and
non-missing before using it programmatically.
See also
- Getting started introduces status and result fields at the first optimization call.
- Choosing methods and tuning explains method-specific work and line-search controls.
- Stateful optimization applies these termination controls in a caller-owned loop.