Skip to content
The Lumenary

Math laboratory

Gradient descent: rates, step sizes, Newton and least squares

Runs gradient descent with step 1/L on a convex quadratic and compares the gap f(x_k) - f* with the proved O(1/k) bound and the geometric rate for strongly convex functions, tries step sizes beyond 2/L, shows Newton's method solving a quadratic in one step, and fits a line to seeded noisy data by gradient descent. Numerical evidence.

Back to the labNumerical approximation
Gradient descent: rates, step sizes, Newton and least squaresPythonNumerical approximationComputationally tested
"""Gradient descent and Newton's method on convex quadratics and least squares.

For f(x) = (1/2) x^T Q x - b^T x with Q symmetric positive definite, the
gradient Q x - b is L-Lipschitz with L = largest eigenvalue of Q. The lesson
proves that gradient descent with step 1/L satisfies
    f(x_k) - f* <= L |x_0 - x*|^2 / (2k),
and for a quadratic with smallest eigenvalue mu > 0 the gap even shrinks
geometrically, at least as fast as (1 - mu/L)^(2k) times the initial gap
(stopped at k = 20, before rounding errors of size 1e-16 dominate). The program compares both bounds with the
actual gaps, shows what happens for step sizes 1/L, 1.9/L and 2.1/L, shows
Newton's method finishing in one step on a quadratic, and fits a straight line
to noisy data by gradient descent on the mean squared error, comparing the
result with the exact normal-equations solution.

Floating-point (numerical) evidence about specific problems.
"""

import numpy as np

Q = np.array([[3.0, 2.0], [2.0, 6.0]])
B = np.array([1.0, -2.0])
X0 = np.array([4.0, 3.0])

def f(x):
    """The quadratic (1/2) x^T Q x - b^T x."""
    return 0.5 * x @ Q @ x - B @ x

def grad(x):
    return Q @ x - B

def gradient_descent(x0, step, iters, gradient=grad):
    """Iterates x_{k+1} = x_k - step * gradient(x_k); returns all iterates."""
    xs = [np.array(x0, dtype=float)]
    for _ in range(iters):
        xs.append(xs[-1] - step * gradient(xs[-1]))
    return xs

def newton_step(x):
    """One Newton step x - Q^{-1} grad(x) for the quadratic."""
    return x - np.linalg.solve(Q, grad(x))

def line_data(seed, n=40):
    """Noisy samples of y = 2x + 1 for x in [0, 1]."""
    rng = np.random.default_rng(seed)
    x = np.linspace(0, 1, n)
    return x, 2 * x + 1 + 0.1 * rng.standard_normal(n)

def fit_line_gd(x, y, iters):
    """Least-squares line by gradient descent on the mean squared error."""
    a = np.column_stack([x, np.ones_like(x)])
    lip = 2 * np.linalg.eigvalsh(a.T @ a / len(x)).max()

    def g(w):
        return 2 * a.T @ (a @ w - y) / len(x)

    return gradient_descent(np.zeros(2), 1 / lip, iters, g)[-1]

def main():
    mu, lip = np.linalg.eigvalsh(Q)
    xstar = np.linalg.solve(Q, B)
    fstar = f(xstar)
    r2 = np.sum((X0 - xstar) ** 2)
    print(f"f(x) = x^T Q x / 2 - b^T x with eigenvalues mu = {mu:.0f},"
          f" L = {lip:.0f}")
    print("   k   gap f(x_k) - f*   bound L|x0-x*|^2/(2k)   (1-mu/L)^(2k) gap0")
    xs = gradient_descent(X0, 1 / lip, 100)
    for k in (1, 2, 5, 10, 20):
        gap = f(xs[k]) - fstar
        print(f"{k:4d}   {gap:14.3e}   {lip * r2 / (2 * k):21.3e}"
              f"   {(1 - mu / lip) ** (2 * k) * (f(X0) - fstar):17.3e}")
    print()
    print("step size   gap after 50 steps")
    for factor in (1.0, 1.9, 2.1):
        gap = f(gradient_descent(X0, factor / lip, 50)[-1]) - fstar
        print(f"{factor:.1f}/L      {gap:12.3e}")
    print()
    print("Newton: one step from x0 lands at x* with error",
          f"{np.abs(newton_step(X0) - xstar).max():.1e}")
    x, y = line_data(2024)
    a = np.column_stack([x, np.ones_like(x)])
    exact = np.linalg.solve(a.T @ a, a.T @ y)
    w = fit_line_gd(x, y, 2000)
    print(f"line fit: normal equations slope {exact[0]:.4f}, intercept"
          f" {exact[1]:.4f}")
    print(f"          gradient descent  slope {w[0]:.4f}, intercept"
          f" {w[1]:.4f}")
    print()
    print("Epistemic status: floating-point computations on specific problems.\n"
          "They illustrate the O(1/k) theorem (proved in the lesson and in\n"
          "Lean, from the descent and convexity inequalities) and the\n"
          "divergence for steps above 2/L; they do not prove them.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in