Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Quasi-Newton Methods for Unconstrained Optimization

Reference: Sections 3.1 - 3.3 in Biegler (2010)

# Load required Python libraries.
import numpy as np
from scipy import linalg

# Seed the random number generator so this notebook is reproducible
# (Pyomo style guide, section 8).
np.random.seed(0)

Unconstrained Optimization with Approximate Hessian

A Newton-type method uses a symmetric curvature model Bk\mathbf B^k and solves Bkpk=−∇f(xk)\mathbf B^k\mathbf p^k=-\nabla f(\mathbf x^k). A positive-definite model gives a descent direction when the gradient is nonzero; an accepted update is xk+1=xk+αkpk\mathbf x^{k+1}=\mathbf x^k+\alpha_k\mathbf p^k. Unit-step modified Newton takes αk=1\alpha_k=1, but positive definiteness alone does not guarantee that this full step decreases the actual objective.

The examples below retain deliberate SR1 breakdowns. The detailed reference sections at the end explain the secant, signed update, curvature safeguards, and conditional rates. Biegler (2010), Algorithm 3.1 and §3.2, pp. 40–42.

For a differentiable objective and a nonzero gradient, a symmetric positive-definite model gives a descent direction. If its eigenvalues satisfy 0<m≤λj(B)≤M0<m\le\lambda_j(\mathbf B)\le M for all jj, the inverse Rayleigh bound gives

gTp=−gTB−1g≤−∥g∥2/M<0,∥p∥≤∥g∥/m.\mathbf g^T\mathbf p=-\mathbf g^T\mathbf B^{-1}\mathbf g\le-\|\mathbf g\|^2/M<0,\qquad\|\mathbf p\|\le\|\mathbf g\|/m.

This proves a negative initial slope. Differentiability gives f(x+αp)−f(x)=αgTp+o(α)f(\mathbf x+\alpha\mathbf p)-f(\mathbf x)=\alpha\mathbf g^T\mathbf p+o(\alpha), so sufficiently small positive steps decrease the objective. A full step can still overshoot. A scalar eigenvalue shift changes each eigenvalue by the same amount while preserving the eigenvectors. To enforce a floor ϵ>0\epsilon>0, the smallest nonnegative shift is δ=max⁡(0,ϵ−λmin⁡)\delta=\max(0,\epsilon-\lambda_{\min}). The diagram shows the shift geometry; the classroom example computes the shift from the original spectrum and the floor.

A symmetric indefinite model points uphill while a shifted model gives a negative initial slopeA common shift moves the spectrum to a specified positive floor

SR1 update is one way to approximate Bk\mathbf{B}^k.

Library of helper functions

## Check if any element of array is NaN
def check_nan(A):
    return np.sum(np.isnan(A))


## Calculate gradient with central finite difference
def my_grad_approx(x, f, eps1, verbose=False):
    """
    Calculate gradient of function my_f using central difference formula

    Inputs:
        x - point for which to evaluate gradient
        f - function to consider
        eps1 - perturbation size

    Outputs:
        grad - gradient (vector)
    """

    n = len(x)
    grad = np.zeros(n)

    if verbose:
        print("***** my_grad_approx at x = ", x, "*****")

    for i in range(0, n):

        # Create vector of zeros except eps in position i
        e = np.zeros(n)
        e[i] = eps1

        # Finite difference formula
        my_f_plus = f(x + e)
        my_f_minus = f(x - e)

        # Diagnostics
        if verbose:
            print("e[", i, "] = ", e)
            print("f(x + e[", i, "]) = ", my_f_plus)
            print("f(x - e[", i, "]) = ", my_f_minus)

        grad[i] = (my_f_plus - my_f_minus) / (2 * eps1)

    if verbose:
        print("***** Done. ***** \n")

    return grad


## Calculate Hessian using central finite difference
def my_hes_approx(x, grad, eps2):
    """
    Calculate Hessian of function my_f using central difference formula and my_grad

    Inputs:
        x - point for which to evaluate gradient
        grad - function to calculate the gradient
        eps2 - perturbation size (for Hessian NOT gradient approximation)

    Outputs:
        H - Hessian (matrix)
    """

    n = len(x)
    H = np.zeros([n, n])

    for i in range(0, n):
        # Create vector of zeros except eps in position i
        e = np.zeros(n)
        e[i] = eps2

        # Evaluate gradient twice
        grad_plus = grad(x + e)
        grad_minus = grad(x - e)

        # Notice we are building the Hessian by column (or row)
        H[:, i] = (grad_plus - grad_minus) / (2 * eps2)

    return H


## Linear algebra calculation
def xxT(u):
    """
    Calculates u*u.T to circumvent limitation with SciPy

    Arguments:
    u - numpy 1D array

    Returns:
    u*u.T

    Assume u is a nx1 vector.
    Recall: NumPy does not distinguish between row or column vectors

    u.dot(u) returns a scalar. This function returns an nxn matrix.
    """

    n = len(u)
    A = np.zeros([n, n])
    for i in range(0, n):
        for j in range(0, n):
            A[i, j] = u[i] * u[j]

    return A


## Analyze Hessian
def analyze_hes(B):
    print(B, "\n")

    l = linalg.eigvals(B)
    print("Eigenvalues: ", l, "\n")

Symmetric Rank 1 (SR1) Update

def alg1_sr1(x0, calc_f, calc_grad, eps1, eps2, verbose=False, max_iter=1000):
    """
    Arguments:
        x0 - starting point
        calc_f - function that calculates f(x)
        calc_grad - function that calculates gradient(x)

    Outputs:
        x - iteration history of x (decision variables)
        f - iteration history of f(x) (objective value)
        p - iteration history of p (steps)
        B - Hessian approximation
    """

    # Allocate outputs as empty lists
    x = []
    f = []
    p = []
    grad = []
    B = []

    # Store starting point
    x.append(x0)
    k = 0

    flag = True

    print("Iter. \tf(x) \t\t||grad(x)|| \t||p|| \t\tmin(lambda)")

    while flag and k < max_iter:
        # Evaluate f(x) at current iteration
        f.append(calc_f(x[k]))

        # Evaluate gradient
        grad.append(calc_grad(x[k]))

        if check_nan(grad[k]):
            print("WARNING: gradient calculation returned NaN")
            break

        if verbose:
            print("\n")
            print("k = ", k)
            print("x = ", x[k])
            print("grad = ", grad[k])

        # Update Hessian approximation
        if k == 0:
            # Initialize with identity
            B.append(np.eye(len(x0)))

        else:
            # Change in x
            s = x[k] - x[k - 1]

            # Change in gradient
            y = grad[k] - grad[k - 1]

            # SR1 formulation
            u = y - B[k - 1].dot(s)
            denom = (u).dot(s)

            # Safeguard: skip test for the SR1 update.
            # Only apply the update when
            #     | (y - B^k s)^T s |  >=  C1 * ||s||^2,   C1 > 0
            # and otherwise leave B unchanged. See the "Quasi-Newton Methods"
            # lecture notes, Strategy 1 (SR1), and Biegler (2010), p. 43.
            # If the denominator is (near) zero the rank-one correction is an
            # arbitrarily large, arbitrarily ill-conditioned change built from
            # almost no new information. Note the test is two-sided: it looks at
            # the magnitude only, because -- unlike BFGS -- SR1 is allowed to
            # represent negative curvature.
            C1 = 1e-8

            if abs(denom) >= C1 * s.dot(s):
                # Formula: dB = u * u.T / (u.T * s) if u is a column vector.
                dB = xxT(u) / denom
                skipped = False
            else:
                # Skip the update: B[k] = B[k-1]
                dB = np.zeros((len(x0), len(x0)))
                skipped = True

            if verbose:
                print("s = ", s)
                print("y = ", y)
                print("SR1 update denominator, (y-B[k-1]*s).T*s = ", denom)
                print("SR1 update u = ", u)
                if skipped:
                    print("Skip test |denom| >= C1*||s||^2 failed: update skipped.")
                print("SR1 update u.T*u/(u.T*s) = \n", dB)

            B.append(B[k - 1] + dB)

        if verbose:
            print("B = \n", B[k])

        if check_nan(B[k]):
            print("WARNING: Hessian update returned NaN")
            break

        c = np.linalg.cond(B[k])
        if c > 1e12:
            flag = False
            print("Warning: Hessian approximation is near singular.")
            print("B[k] = \n", B[k])

        else:
            # Calculate step
            p.append(linalg.solve(B[k], -grad[k]))

            if verbose:
                print("p = ", p[k])

            # Take step
            x.append(x[k] + p[k])

            # Calculate norms
            norm_p = linalg.norm(p[k])
            norm_g = linalg.norm(grad[k])

            # Calculate eigenvalues of Hessian (for display only)
            ev = np.real(linalg.eigvals(B[k]))

            # print("k = ",k,"\t"f[k],"\t",norm_g,"\t",norm_p)
            print(
                k,
                "  \t{0: 1.4e} \t{1:1.4e} \t{2:1.4e} \t{3: 1.4e}".format(
                    f[k], norm_g, norm_p, np.min(ev)
                ),
            )

            # Check convergence criteria
            flag = (norm_p > eps1) and (norm_g > eps2)

            # Update iteration counter
            k = k + 1

    print("Done.")
    print("x* = ", x[-1])

    return x, f, p, B


print("Done.")
Done.

Test Case: Simple quadratic program

min⁡x  x12+(x2−1)2\min_{\mathbf{x}} ~~ x_1^2 + (x_2 -1)^2
def my_f_simple(x):
    return x[0] ** 2 + (x[1] - 1) ** 2


def my_grad_exact(x):
    return np.array([2 * x[0], 2 * (x[1] - 1)])

Near solution

Consider x0=[−0.1,0.2]T\mathbf{x}_0 = [-0.1, 0.2]^T

# Specify convergence criteria
eps1 = 1e-8
eps2 = 1e-4

# Create a Lambda (anonymous) function for gradient calculation
# calc_grad = lambda x : my_grad_approx(x,my_f_simple,1E-6,verbose=False)
calc_grad = lambda x: my_grad_exact(x)

# Specify starting point
x0 = np.array([-0.1, 0.2])

# Call optimization routine
x, f, p, B = alg1_sr1(
    x0, my_f_simple, calc_grad, eps1, eps2, verbose=False, max_iter=50
)

# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])

# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	 6.5000e-01 	1.6125e+00 	1.6125e+00 	 1.0000e+00
1   	 6.5000e-01 	1.6125e+00 	8.0623e-01 	 1.0000e+00
2   	 1.9259e-34 	2.7756e-17 	2.7595e-17 	 1.0000e+00
Done.
x* =  [-1.36642834e-17  1.00000000e+00]

SR1 Hessian approximation. B[k] =
[[1.01538462 0.12307692]
 [0.12307692 1.98461538]] 

Eigenvalues:  [1.+0.j 2.+0.j] 

True Hessian at x*. B =
[[2. 0.]
 [0. 2.]] 

Eigenvalues:  [2.+0.j 2.+0.j] 

Far from solution

Consider x0=[−100,500]T\mathbf{x}_0 = [-100, 500]^T

# Specify starting point
x0 = np.array([-100, 500])

# Call optimization routine
x, f, p, B = alg1_sr1(
    x0, my_f_simple, calc_grad, eps1, eps2, verbose=False, max_iter=50
)

# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])

# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	 2.5900e+05 	1.0178e+03 	1.0178e+03 	 1.0000e+00
1   	 2.5900e+05 	1.0178e+03 	5.0892e+02 	 1.0000e+00
2   	 3.4331e-27 	1.1719e-13 	7.2963e-14 	 1.0000e+00
Done.
x* =  [2.46138186e-14 1.00000000e+00]

SR1 Hessian approximation. B[k] =
[[ 1.03860989 -0.19266335]
 [-0.19266335  1.96139011]] 

Eigenvalues:  [1.+0.j 2.+0.j] 

True Hessian at x*. B =
[[2. 0.]
 [0. 2.]] 

Eigenvalues:  [2.+0.j 2.+0.j] 

Activity/Discussion

  • Does the number of iterations depend on the starting point for this problem?

  • How many iterations are needed for Newton’s method to converge for a positive definite quadratic program using exact second derivative information?

  • Why does the SR1 update not converge to the true Hessian?

Test Case: Example 2.19

Biegler’s two-variable test problem is

min⁡xf(x)=αe−β,\min_{\mathbf{x}} f(\mathbf{x})=\alpha e^{-\beta},

with u=x1−0.8u=x_1-0.8, v=x2−(a1+a2u2(1−u)1/2−a3u)v=x_2-(a_1+a_2u^2(1-u)^{1/2}-a_3u), α=−b1+b2u2(1+u)1/2+b3u\alpha=-b_1+b_2u^2(1+u)^{1/2}+b_3u, and β=c1v2(1−c2v)/(1+c3u2)\beta=c_1v^2(1-c_2v)/(1+c_3u^2). We use a=(0.3,0.6,0.2)\mathbf{a}=(0.3,0.6,0.2), b=(5,26,3)\mathbf{b}=(5,26,3), and c=(40,1,10)\mathbf{c}=(40,1,10).

The reported solution is x∗=(0.7395,0.3144)\mathbf{x}^*=(0.7395,0.3144) with f(x∗)=−5.0893f(\mathbf{x}^*)=-5.0893. The cells below recompute it rather than relying on the printed result. (Biegler, Example 2.19, p. 30.)

## Define Python function to calculate objective
def my_f_ex_2_19(x, verbose=False):
    """Evaluate function given above at point x

    Inputs:
        x - vector with 2 elements

    Outputs:
        f - function value (scalar)
    """
    # Constants
    a = np.array([0.3, 0.6, 0.2])
    b = np.array([5, 26, 3])
    c = np.array([40, 1, 10])

    # Intermediates. Recall Python indicies start at 0
    u = x[0] - 0.8
    s = np.sqrt(1 - u)
    s2 = np.sqrt(1 + u)
    v = x[1] - (a[0] + a[1] * u**2 * s - a[2] * u)
    alpha = (
        -b[0] + b[1] * u**2 * s2 + b[2] * u
    )  # September 5, 2018: changed 's' to 's2'
    beta = c[0] * v**2 * (1 - c[1] * v) / (1 + c[2] * u**2)
    f = alpha * np.exp(-beta)

    if verbose:
        print("##### my_f at x = ", x, "#####")
        print("u = ", u)
        print("sqrt(1-u) = ", s)
        print("sqrt(1+u) = ", s2)
        print("v = ", v)
        print("alpha = ", alpha)
        print("beta = ", beta)
        print("f(x) = ", f)
        print("##### Done. #####\n")

    return f

x0\mathbf{x}_0 somewhat near solution

Consider:

x0=[0.3,0.1]T\mathbf{x}_0 = [0.3, 0.1]^T
# Create a Lambda (anonymous) function for gradient calculation
calc_grad = lambda x: my_grad_approx(x, my_f_ex_2_19, 1e-6, verbose=False)

# Specify starting point
x0 = np.array([0.3, 0.1])

# Call optimization routine
x, f, p, B = alg1_sr1(
    x0, my_f_ex_2_19, calc_grad, eps1, eps2, verbose=False, max_iter=250
)

# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])

# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	-3.6022e-02 	8.3847e-01 	8.3847e-01 	 1.0000e+00
1   	-4.1276e-02 	4.7785e-01 	3.0223e-01 	 1.0000e+00
2   	-2.2301e+00 	2.1236e+01 	3.0865e-01 	-6.8856e+01
3   	-4.1260e-02 	4.4645e-01 	1.3053e-01 	-6.7478e+01
4   	-1.0354e-01 	1.1855e+00 	1.5062e-01 	-7.1805e+01
5   	-3.7115e-02 	3.9982e-01 	1.3096e-02 	-5.8783e+01
6   	-3.4039e-02 	3.6164e-01 	5.9865e-02 	-1.4365e+01
7   	-2.2398e-02 	2.0377e-01 	4.2988e-02 	-9.5209e+00
8   	-1.7133e-02 	1.1914e-01 	4.2522e-02 	-6.4394e+00
9   	-1.4292e-02 	5.7282e-02 	2.4810e-02 	-4.8015e+00
10   	-1.3542e-02 	2.5813e-02 	1.0353e-02 	-2.8612e+00
11   	-1.3373e-02 	1.0933e-02 	8.4091e-03 	-1.6117e+00
12   	-1.3325e-02 	7.7765e-04 	7.5906e-04 	-1.6030e+00
13   	-1.3324e-02 	1.7106e-05 	1.4129e-05 	-1.5761e+00
Done.
x* =  [0.80557705 0.96556999]

SR1 Hessian approximation. B[k] =
[[-1.48333929 -0.21589311]
 [-0.21589311 -1.07333658]] 

Eigenvalues:  [-1.57605489+0.j -0.98062097+0.j] 

True Hessian at x*. B =
[[-1.47003807 -0.20601966]
 [-0.20601966 -1.06561635]] 

Eigenvalues:  [-1.55650234+0.j -0.97915207+0.j] 

x0\mathbf{x}_0 far from solution

Consider:

x0=[−0.1,0.2]T\mathbf{x}_0 = [-0.1, 0.2]^T
# Specify starting point
x0 = np.array([-0.1, 0.2])

# Call optimization routine
x, f, p, B = alg1_sr1(
    x0, my_f_ex_2_19, calc_grad, eps1, eps2, verbose=False, max_iter=250
)

# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])

# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	-4.5540e-04 	9.2580e-03 	9.2580e-03 	 1.0000e+00
1   	-5.4879e-04 	1.0959e-02 	5.9578e-02 	-1.8395e-01
2   	-1.5604e-04 	3.4877e-03 	2.7627e-02 	-1.2568e-01
3   	-8.3235e-05 	1.9563e-03 	3.5000e-02 	-5.5390e-02
4   	-3.6006e-05 	9.0023e-04 	2.9559e-02 	-3.0011e-02
5   	-1.7097e-05 	4.4964e-04 	2.9246e-02 	-1.5078e-02
6   	-7.9092e-06 	2.1835e-04 	2.7389e-02 	-7.7836e-03
7   	-3.7242e-06 	1.0745e-04 	2.6349e-02 	-3.9664e-03
8   	-1.7531e-06 	5.2702e-05 	2.5208e-02 	-2.0268e-03
Done.
x* =  [-0.11328951 -0.05033867]

SR1 Hessian approximation. B[k] =
[[ 1.00141633e+00 -4.82365648e-02]
 [-4.82365648e-02  2.91945446e-04]] 

Eigenvalues:  [ 1.00373512+0.j -0.00202684+0.j] 

True Hessian at x*. B =
[[ 6.99111878e-05 -2.18302890e-04]
 [-2.18302890e-04 -7.02713991e-04]] 

Eigenvalues:  [ 0.00012733+0.j -0.00076013+0.j] 

Activity/Discussion

  • Classify each candidate solution.

  • Is the SR1 approximation always positive definite?

Broyden update with Cholesky factorization

As part of Algorithms Homework 3, you will adapt this example and implement the BFGS (Broyden-Fletcher-Goldfarb-Shanno) update formula.

You may decide to use the Cholesky factorization of Bk\mathbf{B}^{k},

Bk=Lk(Lk)T,\mathbf{B}^{k} = \mathbf{L}^{k} (\mathbf{L}^{k})^T,

to make your BFGS update more efficient. (This is not required). Let’s see how to do Cholesky factorization with SciPy.

## Create random P.D. and symmetric matrix
B_ = np.eye(3) + xxT(np.random.normal(0, 1, 3))
print("B = \n", B_, "\n")

## Perform Cholesky factorization.
# By default, lower=False and L is upper triangular. Either works here,
# but we prefer L to be lower triangular for convention.
L = linalg.cholesky(B_, lower=True)
print("L = \n", L, "\n")

## Reconstruct B
print("L*L.T = \n", L.dot(L.T), "\n")
B = 
 [[4.11188068 0.70589826 1.72654504]
 [0.70589826 1.16012579 0.39164906]
 [1.72654504 0.39164906 1.95792804]] 

L = 
 [[2.02777728 0.         0.        ]
 [0.3481143  1.01928515 0.        ]
 [0.85144708 0.09344604 1.10645097]] 

L*L.T = 
 [[4.11188068 0.70589826 1.72654504]
 [0.70589826 1.16012579 0.39164906]
 [1.72654504 0.39164906 1.95792804]] 

Reference: secant measurements and signed SR1

Use Euclidean vector norms and their induced matrix norm unless a weighted norm is specified.

For a nonzero accepted displacement, s=xk+1−xk\mathbf s=\mathbf x^{k+1}-\mathbf x^k and y=∇f(xk+1)−∇f(xk)\mathbf y=\nabla f(\mathbf x^{k+1})-\nabla f(\mathbf x^k). For f∈C2(D)f\in C^2(D) on an open DD containing the full segment {xk+ts:0≤t≤1}\{\mathbf x^k+t\mathbf s:0\le t\le1\}, the componentwise FTC gives the exact identity

y=[∫01∇2f(xk+ts)dt]s\mathbf y=\left[\int_0^1\nabla^2f(\mathbf x^k+t\mathbf s)dt\right]\mathbf s

motivates Bk+1s=y\mathbf B^{k+1}\mathbf s=\mathbf y. It fixes the action on one direction, not every matrix entry. A symmetric n×nn\times n matrix has n(n+1)/2n(n+1)/2 free entries and a nonzero secant gives nn independent scalar equations. For example, s=e1\mathbf s=\mathbf e_1, y=(2,1)T\mathbf y=(2,1)^T gives B+=[211c]\mathbf B^+=\begin{bmatrix}2&1\\1&c\end{bmatrix} with free cc; positive definiteness adds c>1/2c>1/2.

For u=y−Bs\mathbf u=\mathbf y-\mathbf B\mathbf s, signed SR1 is

B+=B+uuTuTs,uTs≠0.\mathbf B^+=\mathbf B+\frac{\mathbf u\mathbf u^T}{\mathbf u^T\mathbf s},\qquad \mathbf u^T\mathbf s\ne0.

It has the form B+σwwT\mathbf B+\sigma\mathbf w\mathbf w^T with σ=sign⁡(uTs)\sigma=\operatorname{sign}(\mathbf u^T\mathbf s) and w=u/∣uTs∣\mathbf w=\mathbf u/\sqrt{|\mathbf u^T\mathbf s|}. A formula using only +wwT+\mathbf w\mathbf w^T cannot represent negative denominators. Multiplying by s\mathbf s cancels the denominator and gives B+s=Bs+u=y\mathbf B^+\mathbf s=\mathbf B\mathbf s+\mathbf u=\mathbf y.

To see why the zero-denominator case is an obstruction, suppose a nonzero symmetric rank-one correction σwwT\sigma\mathbf w\mathbf w^T satisfies σw(wTs)=u≠0\sigma\mathbf w(\mathbf w^T\mathbf s)=\mathbf u\ne0. Then u\mathbf u is parallel to w\mathbf w and wTs≠0\mathbf w^T\mathbf s\ne0, so uTs=σ(wTs)2≠0\mathbf u^T\mathbf s=\sigma(\mathbf w^T\mathbf s)^2\ne0. Thus u≠0\mathbf u\ne0 with uTs=0\mathbf u^T\mathbf s=0 cannot be repaired by that rank-one form.

There are three cases: if u=0\mathbf u=0, keep the existing matrix; if u≠0\mathbf u\ne0 but uTs=0\mathbf u^T\mathbf s=0, no symmetric rank-one secant correction exists; if the denominator is small but nonzero, a large correction can amplify errors. Nocedal and Wright’s scale-aware safeguard accepts only if

∣uTs∣≥r∥u∥∥s∥,0<r<1.|\mathbf u^T\mathbf s|\ge r\|\mathbf u\|\|\mathbf s\|,\quad0<r<1.

Biegler gives the alternative threshold ∣uTs∣≥C1∥s∥2|\mathbf u^T\mathbf s|\ge C_1\|\mathbf s\|^2. Neither test enforces a positive sign; SR1 can represent indefinite curvature. A trust-region method can use that curvature, whereas a basic line-search step requires a descent direction.

Direction coverage also matters for recovering a Hessian. The precise nonlinear SR1 result uses uniform linear independence and its other convergence/safeguard hypotheses, not merely a different-looking sequence of steps. See Nocedal and Wright, Theorem 6.2, p. 149. Biegler’s short independence remark on p. 43 should be read with those hypotheses.

Sources: Biegler (2010), Eqs. (3.8)–(3.10), pp. 42–43; Nocedal and Wright (2006), §6.2, Eqs. (6.24) and (6.26), pp. 144–149.

A scalar secant measures change in gradient over accepted displacementThe same measured direction permits different symmetric matrix actions elsewherePositive and negative secant projections determine the sign of the rank-one correction

Reference: BFGS, curvature safeguards, and implementation alternatives

With B≻0\mathbf B\succ0, s≠0\mathbf s\ne\mathbf0 and sTy>0\mathbf s^T\mathbf y>0,

B+=B+yyTsTy−BssTBsTBs.\mathbf B^+=\mathbf B+\frac{\mathbf y\mathbf y^T}{\mathbf s^T\mathbf y} -\frac{\mathbf B\mathbf s\mathbf s^T\mathbf B}{\mathbf s^T\mathbf B\mathbf s}.

Multiplication by s\mathbf s gives Bs+y−Bs=y\mathbf B\mathbf s+\mathbf y-\mathbf B\mathbf s=\mathbf y. The correction has rank at most two. The positive curvature condition is necessary for any positive-definite secant matrix: sTy=sTB+s>0\mathbf s^T\mathbf y=\mathbf s^T\mathbf B^+\mathbf s>0.

A stronger quantitative test sTy≥C2∥s∥2\mathbf s^T\mathbf y\ge C_2\|\mathbf s\|^2 can reject small positive curvature as well as zero or negative curvature. It does not, by itself, bound the matrix norm. With B=I\mathbf B=\mathbf I, s=e1\mathbf s=\mathbf e_1, and y=(1,a)T\mathbf y=(1,a)^T, the scalar test is always sTy=1\mathbf s^T\mathbf y=1, but

B+=[1aa1+a2]\mathbf B^+=\begin{bmatrix}1&a\\a&1+a^2\end{bmatrix}

has an arbitrarily large largest eigenvalue as ∣a∣|a| grows. Additional bounds on measurements and updates matter.

Powell damping. Let b=sTBs>0b=\mathbf s^T\mathbf B\mathbf s>0 and choose β∈(0,1)\beta\in(0,1), commonly 0.2. Define yˉ=θy+(1−θ)Bs\bar{\mathbf y}=\theta\mathbf y+(1-\theta)\mathbf B\mathbf s. Taking

θ={1,sTy≥βb,(1−β)bb−sTy,sTy<βb\theta=\begin{cases}1,&\mathbf s^T\mathbf y\ge\beta b,\\ \dfrac{(1-\beta)b}{b-\mathbf s^T\mathbf y},&\mathbf s^T\mathbf y<\beta b\end{cases}

makes sTyˉ≥βb\mathbf s^T\bar{\mathbf y}\ge\beta b; in the second case equality holds. The update then fits the damped measurement, not the original secant. The prepared picture below shows the interpolation and the permitted curvature half-space.

Inverse form. For H=B−1\mathbf H=\mathbf B^{-1}, ρ=1/(sTy)\rho=1/(\mathbf s^T\mathbf y),

H+=(I−ρsyT)H(I−ρysT)+ρssT.\mathbf H^+=(\mathbf I-\rho\mathbf s\mathbf y^T)\mathbf H (\mathbf I-\rho\mathbf y\mathbf s^T)+\rho\mathbf s\mathbf s^T.

For any nonzero z\mathbf z, write w=(I−ρysT)z\mathbf w=(\mathbf I-\rho\mathbf y\mathbf s^T)\mathbf z. Then

zTH+z=wTHw+ρ(sTz)2>0.\mathbf z^T\mathbf H^+\mathbf z=\mathbf w^T\mathbf H\mathbf w+\rho(\mathbf s^T\mathbf z)^2>0.

If the second term is zero, w=z≠0\mathbf w=\mathbf z\ne0, so the first term is positive. Otherwise the second term is positive. This proves positive definiteness without an eigenvalue decomposition. In exact arithmetic the inverse update equals the inverse of the direct BFGS update.

A dense inverse update still stores O(n2)O(n^2) numbers and its matrix-vector step costs O(n2)O(n^2); avoiding a solve is not avoiding a matrix. A Cholesky-factor update is another option: update the factor and use triangular solves instead of refactorizing a full new matrix. Finite precision still matters. For the existing Hilbert-matrix conditioning demonstration, see Linear Algebra Review and SciPy Basics, section D2. The example is not duplicated here. Positive definiteness maintained algebraically does not by itself establish safe conditioning in floating point.

Weighted least-change characterizations distinguish the updates: an appropriate weighted Frobenius formulation for the Hessian gives DFP; the corresponding inverse formulation gives BFGS. The norm and weighting hypotheses are essential (Nocedal and Wright, Eqs. (6.9)–(6.17), pp. 138–140). The broader Broyden family and its parameterization are discussed by Al-Baali, Spedicato and Maggioni (2014), pp. 942–943; BFGS is the zero-parameter member in their convention.

Sources: Biegler (2010), Eqs. (3.18)–(3.21), pp. 44–45, and the errata for Eq. (3.23) if using its alternative rank-two vectors; Nocedal and Wright (2006), pp. 137–141; Dennis and Schnabel (1996), Lemma 9.2.1, p. 199, and Eq. (9.2.10), p. 201. These are mathematical reference formulas; the BFGS programming exercise above remains an exercise.

The two BFGS corrections cancel the old matrix action and leave the measured gradient differencePowell damping changes the secant measurement to enforce positive measured curvatureThe zero sets of the two nonnegative quadratic-form terms intersect only at the origin

Reference: directional accuracy and superlinear convergence

For a nonterminated convergent sequence xk→x∗\mathbf x^k\to\mathbf x^* with ek=∥xk−x∗∥>0e_k=\|\mathbf x^k-\mathbf x^*\|>0, Q-superlinear convergence means

∀ϵ>0, ∃k0∈N:∀k≥k0,ek+1≤ϵek.\forall\epsilon>0,\ \exists k_0\in\mathbb N:\quad\forall k\ge k_0,\quad e_{k+1}\le\epsilon e_k.

Equivalently, ek+1/ek→0e_{k+1}/e_k\to0. Finite termination requires no quotient. Linear and quadratic rates have their primary definitions in the Newton reference.

Illustrative linear, superlinear and quadratic error sequences

Let f∈C2(D)f\in C^2(D) on an open neighborhood DD of x∗\mathbf x^*, ∇f(x∗)=0\nabla f(\mathbf x^*)=0, and ∇2f(x∗)≻0\nabla^2f(\mathbf x^*)\succ0. Suppose unit-step iterates converge to x∗\mathbf x^*, with symmetric positive-definite Bk\mathbf B^k, Bkpk=−gk\mathbf B^k\mathbf p^k=-\mathbf g^k and nonzero errors/steps before termination. Superlinear convergence is equivalent to

∥(Bk−∇2f(xk))pk∥∥pk∥→0.\frac{\|(\mathbf B^k-\nabla^2f(\mathbf x^k))\mathbf p^k\|}{\|\mathbf p^k\|}\to0.

Using ∇2f(x∗)\nabla^2f(\mathbf x^*) instead gives the same limit by Hessian continuity. The condition concerns the directions used. For G=diag⁡(1,2)\mathbf G=\operatorname{diag}(1,2), B=diag⁡(1,10)\mathbf B=\operatorname{diag}(1,10) and p=e1\mathbf p=\mathbf e_1, the directional error is zero despite ∥B−G∥=8\|\mathbf B-\mathbf G\|=8.

Two matrices agree on the chosen step but have different actions in an unmeasured direction

Proof mechanism. Write ek=xk−x∗\mathbf e_k=\mathbf x^k-\mathbf x^*, Gk=∇2f(xk)\mathbf G^k=\nabla^2f(\mathbf x^k) and pNk=−(Gk)−1gk\mathbf p_N^k=-(\mathbf G^k)^{-1}\mathbf g^k. Hessian continuity and nondegeneracy give a bounded inverse and ek+pNk=o(∥ek∥)\mathbf e_k+\mathbf p_N^k=o(\|\mathbf e_k\|). A Lipschitz Hessian strengthens this to O(∥ek∥2)O(\|\mathbf e_k\|^2), but that stronger assumption is unnecessary for the equivalence.

Subtracting the two solves gives

pk−pNk=(Gk)−1(Gk−Bk)pk.\mathbf p^k-\mathbf p_N^k=(\mathbf G^k)^{-1}(\mathbf G^k-\mathbf B^k)\mathbf p^k.

Under directional accuracy, the difference is o(∥pk∥)o(\|\mathbf p^k\|). Hence

∥ek+1∥≤rk∥ek∥+ηk∥pk∥,rk,ηk→0.\|\mathbf e_{k+1}\|\le r_k\|\mathbf e_k\|+\eta_k\|\mathbf p^k\|, \quad r_k,\eta_k\to0.

Since ∥pk∥≤∥ek∥+∥ek+1∥\|\mathbf p^k\|\le\|\mathbf e_k\|+\|\mathbf e_{k+1}\|, rearranging for ηk<1\eta_k<1 gives

∥ek+1∥∥ek∥≤rk+ηk1−ηk→0.\frac{\|\mathbf e_{k+1}\|}{\|\mathbf e_k\|}\le\frac{r_k+\eta_k}{1-\eta_k}\to0.

Conversely, superlinear convergence gives pk=−ek+o(∥ek∥)\mathbf p^k=-\mathbf e_k+o(\|\mathbf e_k\|) and thus ∥pk∥/∥ek∥→1\|\mathbf p^k\|/\|\mathbf e_k\|\to1. Subtracting the Newton-step remainder then gives pk−pNk=o(∥ek∥)\mathbf p^k-\mathbf p_N^k=o(\|\mathbf e_k\|). Multiply by the bounded Gk\mathbf G^k and divide by ∥pk∥\|\mathbf p^k\| to recover directional accuracy. Finite termination is handled separately; the displayed ratios describe a nonterminated sequence.

A strict minimum alone is insufficient. For f(x)=x4f(x)=x^4 with nonzero iterates, exact Newton has Bk=f′′(xk)B^k=f''(x^k), so its directional error is identically zero, yet xk+1=2xk/3x^{k+1}=2x^k/3 is only linear. Its limiting Hessian is zero. This exposes the missing nondegeneracy hypothesis in an unqualified reading of Biegler’s Theorem 3.1; Nocedal and Wright’s Theorem 3.7 states it explicitly.

For standard BFGS, the local superlinear theorem assumes convergence, a nondegenerate minimizer, the bound ∥∇2f(x)−∇2f(x∗)∥≤L∥x−x∗∥\|\nabla^2f(\mathbf x)-\nabla^2f(\mathbf x^*)\|\le L\|\mathbf x-\mathbf x^*\| near x∗\mathbf x^* for some L>0L>0, summable iterate errors, and the specified line-search/update framework. Wolfe searches test the unit step first and use a sufficient-decrease parameter strictly below 1/21/2 when invoking the unit-step admissibility result. A finite gradient-norm plot does not prove those assumptions or a convergence rate. The shared Rosenbrock comparison below is one numerical experiment; its unmodified SR1 run deliberately stops on an ascent direction.

Sources: Nocedal and Wright (2006), Theorems 3.6–3.7, pp. 46–47; Assumption 6.2, p. 156, and Theorem 6.6, p. 158; Biegler (2010), Theorems 3.1–3.2, pp. 42 and 44, read with the explicit nondegenerate-minimum hypothesis.

Reference: storage and derivative-acquisition costs

Full dense BFGS stores n2n^2 values. Fixed-memory L-BFGS retains mm recent secant pairs, or 2mn2mn values, plus workspace and base scaling. Its two-loop recursion computes the inverse action without forming an n×nn\times n matrix (Nocedal and Wright, Algorithm 7.4, pp. 177–178). At float64 precision, n=106n=10^6, m=10m=10 means 8 TB for one dense matrix versus 160 MB for the pairs, in decimal units. This is storage accounting, not a timing benchmark.

Discarding pairs means fixed-memory L-BFGS does not inherit the full BFGS superlinear theorem. Linear-rate results for limited memory impose additional smoothness, curvature and algorithm hypotheses; memory size alone establishes no universal rate. See Liu and Nocedal (1989), Algorithm 2.1, p. 506, and Theorem 7.1, p. 524, with the discussion on p. 526.

When only function values are available, derivative cost depends on the stencil and reuse policy. Forward differences reuse f(x)f(\mathbf x) and require n+1n+1 distinct points for a gradient. A complete forward second-difference Hessian also needs x+2hei\mathbf x+2h\mathbf e_i and x+hei+hej\mathbf x+h\mathbf e_i+h\mathbf e_j for i<ji<j:

fij≈f(x+hei+hej)−f(x+hei)−f(x+hej)+f(x)h2.f_{ij}\approx\frac{f(\mathbf x+h\mathbf e_i+h\mathbf e_j)-f(\mathbf x+h\mathbf e_i)-f(\mathbf x+h\mathbf e_j)+f(\mathbf x)}{h^2}.

Including the reused points gives 1+2n+n(n−1)/21+2n+n(n-1)/2 evaluations, or 861 at n=40n=40. A central Hessian stencil uses 1+2n21+2n^2 distinct points, or 3201 at n=40n=40; its mixed entries use four sign combinations per pair. These counts assume a dense Hessian, shared points, and one common step size; sparsity, automatic derivatives, constraints, noise and domain restrictions change the practical choice. The earlier rough n2/2n^2/2 estimate was an order-of-growth comparison, not an exact call count.

BFGS avoids Hessian acquisition, but finite-difference gradients can have an error floor that prevents the exact-derivative asymptotic theorem from applying. The notebook’s intentionally nonfinite-derivative examples remain useful checks of domain restrictions.

Additional reading: Nocedal and Wright (2006), §8.1, pp. 195–203, on derivative approximation; Biegler (2010), §3.3, pp. 42–46, on quasi-Newton implementation. Factorization/error demonstrations are kept in the existing linear-algebra reference pages rather than copied here.

A full dense matrix stores n-squared entries while limited memory stores recent displacement and gradient-difference vectors

Shared Rosenbrock experiment: complete settings and outcomes

The handout comparison uses the existing canonical figure implementation. Its model is

f(x)=100(x2−x12)2+(1−x1)2,x0=(−1.2,1)T.f(\mathbf x)=100(x_2-x_1^2)^2+(1-x_1)^2,\qquad \mathbf x^0=(-1.2,1)^T.

The derivatives used by the experiment are

∇f=[−400x1(x2−x12)−2(1−x1)200(x2−x12)],∇2f=[1200x12−400x2+2−400x1−400x1200].\nabla f=\begin{bmatrix}-400x_1(x_2-x_1^2)-2(1-x_1)\\200(x_2-x_1^2)\end{bmatrix},\qquad \nabla^2f=\begin{bmatrix}1200x_1^2-400x_2+2&-400x_1\\-400x_1&200\end{bmatrix}.

All methods use the same wolfe_step routine: test α=1\alpha=1 first, halve the trial value as needed, with c1=10−4c_1=10^{-4} and c2=0.9c_2=0.9, and at most sixty trial reductions. The run records the gradient norm, uses threshold 10-11 and at most sixty accepted steps. On an exhausted search this demonstration routine returns its final trial; that returned value is not automatically a Wolfe certificate. These are finite experiment settings, not the hypotheses of an unconditional convergence theorem.

Newton uses the exact Hessian, adding (10−8−λmin⁡+10−3)I(10^{-8}-\lambda_{\min}+10^{-3})\mathbf I if λmin⁡<10−8\lambda_{\min}<10^{-8}. BFGS begins with B0=I\mathbf B^0=\mathbf I and updates only if sTy>10−12\mathbf s^T\mathbf y>10^{-12}. SR1 also starts from I\mathbf I, permits either denominator sign, and stops if ∣uTs∣<10−14|\mathbf u^T\mathbf s|<10^{-14}; it has no curvature repair. Before searching, the implementation rejects a nonnegative slope for its BFGS/SR1 solve. Steepest descent uses p=−g\mathbf p=-\mathbf g.

MethodAccepted stepsFinal stored plot gradient normOutcome
Modified Newton221e-16 (display floor)gradient threshold reached
BFGS371.82305e-13gradient threshold reached
SR1314.1664ascent direction rejected
Steepest descent603.31254iteration cap reached

The stored plot history floors values at 10-16 to support logarithmic display; it does not assert an exact derivative value of 10-16. The shared SR1 run’s negative-denominator update creates indefinite curvature (eigenvalues approximately -642 and 6); the next slope is approximately +0.26, so the descent-based routine rejects that direction. The norm trace remains finite and does not certify a minimum or an asymptotic rate. Source code, settings and this table are available together so the comparison can be reproduced without changing the implementation’s ownership.

Finite Rosenbrock residual histories and the intentional unmodified SR1 ascent failure