Optimization
Language revisions
- C++20
- Concepts separate objective, gradient and Hessian-action requirements, so a solver states exactly what it consumes.
- C++23
- std::expected reports non-convergence as a value rather than an exception or a sentinel.P0323R12
- C++26
- std::linalg supplies the dense factorizations used inside trust-region subproblem solves.P1673R13
Definition
This entry treats the implementation of the methods whose theory is in Optimization: line-search methods, quasi-Newton updates, and trust-region methods, for the unconstrained smooth problem
Motivation
The theory gives rates; the implementation determines whether they are achieved. Three decisions account for most of the difference between a solver that works and one that stalls.
The first is the globalisation strategy, without which Newton and quasi-Newton steps diverge from ordinary starting points. The second is the curvature approximation: storing a dense Hessian is infeasible above , and limited-memory updates change the storage from to . The third is the termination criterion, which must be scale-invariant or the solver's tolerance means something different for every problem.
Formal formulation
The Wolfe conditions define an acceptable step along a descent direction :
with , typically and . The first is sufficient decrease, the second sufficient curvature; together they guarantee that the BFGS update preserves positive definiteness.
The BFGS update, with and , is
approximating the inverse Hessian, and it is positive definite whenever , which the curvature condition guarantees. The limited-memory form stores the last pairs and applies by the two-loop recursion at cost without forming .
The trust-region subproblem is
and the region is updated from the agreement ratio
expanding when is near 1 and contracting when it is small. The advantage over line search is that need not be positive definite, so genuine negative curvature is exploited rather than modified away.
C++ representation
The requirement hierarchy, so that each solver consumes only what it needs:
#include <concepts>
#include <cstddef>
#include <span>
namespace hpc {
template <class F>
concept Objective = requires(const F& f, std::span<const double> x) {
{ f.dimension() } -> std::convertible_to<std::size_t>;
{ f.value(x) } -> std::convertible_to<double>;
};
template <class F>
concept GradientObjective = Objective<F> &&
requires(const F& f, std::span<const double> x, std::span<double> g) {
f.gradient(x, g);
};
// Evaluating value and gradient together is usually much cheaper than
// separately, since they share intermediate quantities.
template <class F>
concept FusedObjective = GradientObjective<F> &&
requires(const F& f, std::span<const double> x, std::span<double> g) {
{ f.value_and_gradient(x, g) } -> std::convertible_to<double>;
};
} // namespace hpc
The result type reports why the solver stopped, since "did not converge" is not one condition:
#include <cstddef>
namespace hpc {
enum class termination {
gradient_tolerance, // ‖∇f‖ below tolerance: success
step_tolerance, // step below tolerance: possibly a flat region
objective_tolerance, // no decrease: possibly at rounding floor
iteration_limit,
evaluation_limit,
line_search_failure, // no acceptable step: bad gradient or non-smoothness
};
struct optimize_result {
termination reason;
std::size_t iterations;
std::size_t evaluations;
double objective;
double gradient_norm;
};
} // namespace hpc
The L-BFGS two-loop recursion, which is the computational core:
#include <cstddef>
#include <span>
#include <vector>
namespace hpc {
// Applies the limited-memory inverse-Hessian approximation to q, in place.
// Θ(mn) operations, Θ(mn) storage, no matrix formed.
inline void two_loop_recursion(std::span<double> q,
std::span<const std::vector<double>> s,
std::span<const std::vector<double>> y,
std::span<const double> rho,
double gamma)
{
const std::size_t m = s.size();
std::vector<double> alpha(m);
for (std::size_t i = m; i-- > 0;) {
double sq = 0.0;
for (std::size_t j = 0; j < q.size(); ++j) { sq += s[i][j] * q[j]; }
alpha[i] = rho[i] * sq;
for (std::size_t j = 0; j < q.size(); ++j) { q[j] -= alpha[i] * y[i][j]; }
}
for (double& qi : q) { qi *= gamma; }
for (std::size_t i = 0; i < m; ++i) {
double yq = 0.0;
for (std::size_t j = 0; j < q.size(); ++j) { yq += y[i][j] * q[j]; }
const double beta = rho[i] * yq;
for (std::size_t j = 0; j < q.size(); ++j) {
q[j] += (alpha[i] - beta) * s[i][j];
}
}
}
} // namespace hpc
Implementation
The initial scaling is applied at each iteration and is worth a substantial factor in iteration count; omitting it is the most common defect in L-BFGS implementations.
Curvature pairs with are rejected rather than stored, since they would destroy the positive definiteness of the approximation. The rejection is recorded, since a high rejection rate indicates a non-smooth or badly scaled problem.
The line search implements the strong Wolfe conditions by bracketing followed by cubic interpolation, not by pure backtracking: backtracking satisfies only Armijo, and BFGS requires the curvature condition.
Termination is scale-invariant:
with an additional test on relative step and objective change. An absolute gradient tolerance is meaningless, since scaling by scales the gradient by the same factor.
Derivatives are supplied by Automatic Differentiation where available, and checked against finite differences in a random direction at the starting point during development. A gradient error is indistinguishable from a hard problem from the solver's point of view, and the check costs one extra evaluation.
Complexity
| Operation | Time | Space | Remarks |
|---|---|---|---|
| Gradient descent iteration | Θ(n) + cost(∇f) | Θ(n) | Θ(κ log 1/ε) iterations |
| L-BFGS iteration, memory m | Θ(mn) + cost(∇f) | Θ(mn) | Superlinear in practice |
| BFGS iteration, dense | Θ(n²) + cost(∇f) | Θ(n²) | Infeasible above n ≈ 10⁴ |
| Newton iteration, dense | Θ(n³) + cost(∇²f) | Θ(n²) | Quadratic near the solution |
| Truncated Newton (CG–Steihaug) | Θ(k · cost(∇²f·v)) | Θ(n) | k inner CG iterations |
| Line search, cubic interpolation | 2–5 evaluations | Θ(n) | Strong Wolfe |
| Trust-region subproblem, dogleg | Θ(n²) or Θ(mn) | Θ(n) | Approximate solution suffices |
Performance considerations
The dominant cost is nearly always the objective and gradient evaluation, so the profitable
optimisations are in the user's function and in reducing the number of evaluations. A fused
value-and-gradient evaluation, which shares intermediates, is typically 30–50 percent cheaper
than two separate calls and is the reason FusedObjective exists in the concept hierarchy
above.
Memory m in L-BFGS trades storage against iteration count. Values between 5 and 20 are
standard; beyond 20 the additional pairs contribute little and the per-iteration
cost grows. The choice is made by measurement on representative problems.
Vectors are allocated once for the whole solve, not per iteration. The two-loop recursion above touches data per iteration in a streaming pattern, so its cost is bandwidth rather than arithmetic, and keeping the history vectors contiguous matters.
Conditioning dominates everything else. A diagonal rescaling that reduces by four orders of magnitude reduces gradient-descent iterations by the same factor, and no implementation change compares. Where the problem's natural scaling is known, applying it is the first optimisation.
Limitations
Non-smooth objectives break the line search: the Wolfe conditions may be unsatisfiable, and the solver reports a line-search failure that is a property of the problem rather than a defect. Subgradient and proximal methods are the correct treatment and converge far more slowly.
Quasi-Newton methods assume a smooth objective with a positive-definite Hessian near the solution. Applied to a problem with indefinite curvature, they either modify the curvature implicitly and converge slowly, or require the trust-region formulation.
Termination on a small gradient does not certify a minimum: it certifies a stationary point, which may be a saddle. Checking requires second-order information.
Inaccurate gradients (from finite differences, from a loosely converged inner solve, or from a stochastic estimate) impose an accuracy floor on the solution and cause line-search failures near it. The floor is roughly the square root of the gradient's relative error, which is why finite-difference gradients limit the achievable accuracy to about .
Related entries
Optimization for the theory; Automatic Differentiation for derivatives; Numerical Linear Algebra for the inner solves; Analysis for convergence.
References
- J. Nocedal and S. J. Wright, Numerical Optimization, 2nd ed., Springer, 2006.
- D. Liu and J. Nocedal, On the Limited Memory BFGS Method for Large Scale Optimization, Mathematical Programming 45, 1989.
- A. Conn, N. Gould and P. Toint, Trust-Region Methods, SIAM, 2000.
- J. Moré and D. Thuente, Line Search Algorithms with Guaranteed Sufficient Decrease, ACM TOMS 20(3), 1994.