Gradient Descent and Numerical Optimization

Michael BrenndoerferOctober 25, 202643 min read

Learn how gradient descent updates parameters on quadratic objectives, how curvature and learning rates control convergence, and how to verify least-squares fits in NumPy.

Machine Learning from ScratchAll chapters
Part 3 of 15Model Fundamentals and Diagnostics
Chapter 30Gradient Descent and Numerical Optimization
Reading the full chapter · 10 sections · 43 min10 sections · 43 min

All 10 sections · 43 min read

Gradient Descent and Numerical OptimizationFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

Gradient Descent and Numerical Optimization

A decreasing loss can still leave the parameters far from their best values. A learning rate may make steady progress in one direction while barely moving in another; a larger rate can overshoot and diverge. On a quadratic objective, we can calculate these behaviors exactly and use them to check an implementation.

Gradient descent repeatedly measures the local slope and moves against it. For a differentiable objective with a nonzero gradient, a sufficiently small step in that direction decreases the objective. The gradient determines a direction; the learning rate determines the size of the displacement. Neither statement promises a global solution on an arbitrary nonconvex surface.

We will derive this update, compare two quadratics with controlled curvature, and contrast their paths with a Newton step from the same starting point. We will then fit a small least-squares model, check its gradient numerically, and examine batch, single-example, and mini-batch updates. The examples connect the objective from Sum of Squared Errors (SSE) with the coordinate changes introduced in standardization. The stopping rules will tell us what each run established.

Advantages

The update needs an objective and its gradient rather than a closed-form minimizer. We can therefore use it for smooth fitting problems such as logistic regression, using negative log-likelihood as the objective. For an unconstrained problem, the same update structure applies when we change the loss and compute its new gradient. Nondifferentiable objectives require an appropriate alternative, such as a subgradient or proximal method.

For the dense linear model used here, a full gradient costs O(np)O(np) operations for nn rows and pp parameters. A single-example or mini-batch update can use less data before changing the parameters. These first-order updates avoid forming a p×pp\times p Hessian, and data can be supplied in batches when storing the full design is inconvenient.

An iterative fit also produces a history that we can inspect. We can record the objective, gradient norm, parameter changes, and stopping reason. Computing these diagnostics has a cost, and an increasing objective can be expected during stochastic updates, so the appropriate interpretation depends on the algorithm.

Smooth penalties can be included in the objective and differentiated along with the data-fit term. Constraints need additional treatment, such as a projection or a solver designed for the feasible set; an unconstrained gradient step alone does not enforce them.

Disadvantages

Gradient descent uses local information. On a nonconvex objective, a small gradient can occur at a local minimum, a saddle, or a local maximum. A run may also diverge or reach its iteration limit. Its stopping status alone does not establish global optimality.

Curvature can make a stable run slow. For a positive-definite quadratic, the worst-case convergence rate depends on the Hessian condition number and the chosen learning rate. Large condition numbers make the best fixed-rate contraction approach one. The effect of rescaling features depends on their variances and correlations; it is not generally proportional to the change in their units.

We must also choose an update budget, stopping tolerances, and, for stochastic methods, a sampling rule and step schedule. Curvature sometimes provides a principled learning rate, while other problems need a line search or empirical tuning. Poor choices can cause divergence or stop a run before it reaches a useful fit.

Formula

Let θ∈Rp\theta\in\mathbb{R}^p be the parameter vector and let J(θ)J(\theta) be the real-valued objective to minimize. We begin with a local approximation to JJ, then use quadratic examples to determine exactly when repeated updates converge.

The entries of θ\theta are the quantities we fit. In the regression example they will be an intercept and slopes; the objective is half the mean squared prediction error. Unless stated otherwise, the derivations concern differentiable objectives on an unconstrained parameter space.

The first thing we need is a compact summary of how JJ responds to a nudge in any of the pp coordinate directions. That summary is the gradient, which collects all first partial derivatives into one vector:

∇J(θ)=[∂J∂θ1∂J∂θ2⋮∂J∂θp]\nabla J(\theta) = \begin{bmatrix} \dfrac{\partial J}{\partial \theta_1} \\[6pt] \dfrac{\partial J}{\partial \theta_2} \\ \vdots \\ \dfrac{\partial J}{\partial \theta_p} \end{bmatrix}

Read the vector one entry at a time and each entry asks the same question in a different coordinate: if we nudge only θj\theta_j by a tiny amount and hold the other p−1p-1 parameters fixed, how fast does JJ change, and in which direction? Stacking those answers into a column vector turns pp separate one-dimensional sensitivities into a single object that encodes both the steepest direction and the steepness itself. Two facts about that object carry everything that follows:

  1. For a nonzero gradient, its negative gives the steepest directional decrease among directions of unit Euclidean length. This is a first-order statement about a small displacement, not the best destination at an arbitrary finite distance.
  2. The gradient norm measures the first-order residual in the chosen units. A differentiable unconstrained local minimum has zero gradient, but a small norm alone does not measure distance to a minimizer or certify that a point is a minimum.

A direction alone does not tell us how far to move. The learning rate supplies that scale.

Gradients and parameter updates

Let dd be a direction vector and tt a scalar step multiplier. A first-order Taylor expansion around the current point says

J(θ+td)≈J(θ)+t ∇J(θ)⊤dJ(\theta + t d) \approx J(\theta) + t\, \nabla J(\theta)^{\top} d

The left side is the objective after moving by tdtd. The right side adds a first-order prediction of that change to the current objective. The dot product ∇J(θ)⊤d\nabla J(\theta)^{\top}d is the directional derivative along dd. When both vectors are nonzero, it is negative if their angle exceeds ninety degrees. Among unit Euclidean directions, Cauchy-Schwarz gives the smallest dot product at d=−∇J(θ)/∥∇J(θ)∥d=-\nabla J(\theta)/\|\nabla J(\theta)\|, provided the gradient is nonzero. Absorbing the step length into a positive scalar gives the gradient-descent update

θk+1=θk−η∇J(θk)\theta_{k+1} = \theta_k - \eta \nabla J(\theta_k)

where:

  • θk\theta_k: the parameter vector at iteration kk
  • θk+1\theta_{k+1}: the updated parameter vector after one step
  • η>0\eta > 0: the learning rate, controlling how far we travel along the negative gradient
  • ∇J(θk)\nabla J(\theta_k): the gradient of the objective evaluated at the current parameters, pointing in the direction of steepest increase
  • kk: the iteration counter

Reading it plainly: the new parameters are the old parameters, shifted a small distance against the gradient. In scalar form, component jj obeys θk+1,j=θk,j−η ∂J/∂θj\theta_{k+1,j} = \theta_{k,j} - \eta \, \partial J/\partial \theta_j, where θk+1,j\theta_{k+1,j} is the jj-th component after the update, θk,j\theta_{k,j} is the jj-th component before the update, η\eta is the learning rate, and ∂J/∂θj\partial J/\partial \theta_j is the partial derivative of the objective with respect to the jj-th parameter. This component form is often easier to reason about when we implement it.

The displacement has Euclidean length η∥∇J(θk)∥\eta\|\nabla J(\theta_k)\|: the learning rate multiplies the gradient rather than directly specifying a distance. For example, when fitting a likelihood model we minimize its negative log-likelihood and differentiate that objective. A local linear approximation need not remain accurate over a large displacement, so the step length requires a separate choice.

Gradients and parameter updates on a quadratic

For the scalar quadratic J(θ)=aθ2/2J(\theta)=a\theta^2/2 with a>0a>0, the gradient is aθa\theta. Substituting it into the update gives

θk+1=θk−η(aθk)=(1−ηa)θk\begin{aligned} \theta_{k+1} &= \theta_k - \eta (a\theta_k) \\ &= (1 - \eta a)\theta_k \end{aligned}

where:

  • θk\theta_k: the parameter at iteration kk
  • a>0a > 0: the curvature, the second derivative of JJ with respect to θ\theta
  • η>0\eta > 0: the learning rate
  • (1−ηa)(1 - \eta a): the signed multiplier of the scalar parameter at each update

The quantity 1−ηa1 - \eta a multiplies the parameter anew at every iteration, which means after kk steps

θk=(1−ηa)k θ0\begin{aligned} \theta_k &= (1 - \eta a)^k \, \theta_0 \end{aligned}

where θ0\theta_0 is the starting value at iteration zero.

For every starting value, convergence to zero occurs when ∣1−ηa∣<1|1-\eta a|<1, equivalently 0<η<2/a0<\eta<2/a. At η=1/a\eta=1/a, a single update reaches zero. For a nonzero starting value, η=2/a\eta=2/a produces constant-amplitude sign changes, and η>2/a\eta>2/a produces divergence. A start already at zero stays there for any finite learning rate. These exact recursions use the constant curvature of this quadratic; a general smooth objective needs a separate local or global analysis.

Learning rates, curvature, and feature scaling

The relevant step scale is reciprocal curvature. For a>0a>0, larger curvature narrows the stable interval 0<η<2/a0<\eta<2/a. This explains why a rate that works on one quadratic may be unstable on another. It does not imply that every small rate is equally efficient.

For a quadratic with a constant symmetric positive-definite Hessian HH, all eigenvalues satisfy 0<λmin⁡≤λi≤λmax⁡0<\lambda_{\min}\le\lambda_i\le\lambda_{\max}. Write the error relative to its unique minimizer in the orthonormal eigenvector basis of HH. Each error coordinate is multiplied by 1−ηλi1-\eta\lambda_i, so the Euclidean error contracts by at most ρ(η)=max⁡i∣1−ηλi∣\rho(\eta)=\max_i|1-\eta\lambda_i|. Convergence from every start requires 0<η<2/λmax⁡0<\eta<2/\lambda_{\max}. Balancing the two extreme multipliers gives the optimal fixed rate η=2/(λmin⁡+λmax⁡)\eta=2/(\lambda_{\min}+\lambda_{\max}) and its worst-case parameter-error contraction factor

κ−1κ+1,κ=λmax⁡λmin⁡\frac{\kappa - 1}{\kappa + 1}, \qquad \kappa = \frac{\lambda_{\max}}{\lambda_{\min}}

where:

  • κ\kappa: the condition number of the Hessian, defined as λmax⁡/λmin⁡\lambda_{\max}/\lambda_{\min}, which measures how differently the objective curves across directions
  • λmin⁡,λmax⁡\lambda_{\min}, \lambda_{\max}: the smallest and largest eigenvalues of the Hessian HH, the curvatures along the shallowest and steepest directions respectively
  • (κ−1)/(κ+1)(\kappa - 1)/(\kappa + 1): the worst-case per-iteration error contraction factor for the optimal fixed learning rate

When κ=1\kappa=1, this quadratic has equal curvature in every direction and the optimal fixed rate reaches the minimizer in one update. At κ=100\kappa=100, the factor is 99/101≈0.98019899/101\approx0.980198; the worst-case parameter error needs 116 updates for a tenfold reduction because (99/101)116<0.1(99/101)^{116}<0.1. This comparison uses relative parameter error and the optimal fixed rate. Objective gaps along an individual eigenmode contract by the square of that mode's parameter multiplier.

For SSE(θ)=∥y−Xθ∥2\mathrm{SSE}(\theta)=\|y-X\theta\|^2, differentiation gives gradient −2X⊤(y−Xθ)-2X^{\top}(y-X\theta) and Hessian 2X⊤X2X^{\top}X. Here XX has nn rows and pp columns, yy has nn entries, and θ\theta has pp entries. If XX has full column rank, the Hessian is positive definite and its spectral condition number equals the square of the design matrix's spectral condition number. Rescaling feature columns changes this geometry. Standardization often helps when unequal feature scales cause poor conditioning, but it does not remove collinearity or guarantee fast convergence. With rank deficiency, the Hessian is singular and the positive-condition-number formulas above do not apply.

Batch, stochastic, and mini-batch descent

When J(θ)=n−1∑i=1nℓi(θ)J(\theta)=n^{-1}\sum_{i=1}^n\ell_i(\theta) is a finite mean of differentiable example losses, differentiation gives ∇J(θ)=n−1∑i∇ℓi(θ)\nabla J(\theta)=n^{-1}\sum_i\nabla\ell_i(\theta). The full-batch method computes this complete average before an update. Its work is nn times the cost of one example gradient; for the dense linear model here that is O(np)O(np).

A single-example update uses one loss gradient. If a fresh index ii is sampled uniformly and independently of previous updates, its gradient at the current fixed parameters has conditional expectation equal to the full gradient. The update is

θk+1=θk−ηk∇ℓi(θk)\theta_{k+1} = \theta_k - \eta_k \nabla \ell_i(\theta_k)

A single-example gradient can vary around the full gradient, so individual updates need not decrease the full objective. Persistent noise near an optimum can leave fluctuations when the step stays constant. Positive schedules satisfying ηk→0\eta_k\to0, ∑kηk=∞\sum_k\eta_k=\infty, and ∑kηk2<∞\sum_k\eta_k^2<\infty are used in convergence analyses together with assumptions on the objective and noise. A smooth strongly convex objective, conditionally unbiased bounded-variance gradients, and stable iterates are one such setting; the schedule alone is not a guarantee for an arbitrary objective. Noise can also vanish at the solution, in which case convergence with a constant step is possible. Our implementation uses random reshuffling, whose adaptive updates are not automatically covered by the fresh-index unbiasedness calculation.

Mini-batch descent sits between the extremes, using a batch BkB_k of size bb:

θk+1=θk−ηkb∑i∈Bk∇ℓi(θk)\theta_{k+1} = \theta_k - \frac{\eta_k}{b} \sum_{i \in B_k} \nabla \ell_i(\theta_k)

At fixed parameters, averaging bb independent example gradients divides their covariance by bb. Sampling a subset without replacement has a different finite-population factor and gives zero sampling variance when the batch contains all nn examples. Batch size also changes the number of updates per pass and the available vectorization. We use 32 as one illustrative mini-batch size below; it is not a universal optimum.

Newton steps, stopping rules, and numerical checks

If curvature is the problem, we can use it. Rather than guessing a step size that survives the worst direction, expand the objective to second order around the current point:

J(θ+d)≈J(θ)+∇J(θ)⊤d+12d⊤H(θ) dJ(\theta + d) \approx J(\theta) + \nabla J(\theta)^{\top} d + \tfrac{1}{2} d^{\top} H(\theta)\, d

The Hessian H(θ)H(\theta) is the matrix of second partial derivatives. The new quadratic term models how the slope changes over the displacement dd. When this Hessian is positive definite, the quadratic approximation has a unique minimizer. Differentiating that approximation with respect to dd and setting its gradient to zero gives

∇J(θ)+H(θ) d=0⟹d=−H(θ)−1∇J(θ)\nabla J(\theta) + H(\theta)\, d = 0 \quad\Longrightarrow\quad d = -H(\theta)^{-1} \nabla J(\theta)

So the Newton update is

θk+1=θk−H(θk)−1∇J(θk)\theta_{k+1} = \theta_k - H(\theta_k)^{-1} \nabla J(\theta_k)

The displayed rule is the full Newton step, with step multiplier one. In practice we solve H(θk)d=−∇J(θk)H(\theta_k)d=-\nabla J(\theta_k) rather than explicitly forming an inverse. On a positive-definite quadratic this reaches the minimizer in one update in exact arithmetic. Local quadratic convergence for a nonquadratic objective is guaranteed under a sufficiently close start, a nonsingular Hessian at the solution, and a Hessian that is locally Lipschitz, meaning its changes are bounded proportionally to parameter displacement nearby. These sufficient conditions are stronger than smoothness alone.

A dense direct solve typically costs O(p3)O(p^3) arithmetic and stores a p×pp\times p matrix; forming the Hessian has an additional cost. Newton-type methods can also use iterative solves or Hessian-vector products, so explicit dense storage is not universal. An indefinite Hessian can produce an uphill direction. A shift must be large enough to make the modified Hessian positive definite; at a nonzero gradient that provides a descent direction. A line search can then shorten a descent step to achieve sufficient decrease. Shortening an uphill direction alone does not repair its sign.

For stopping, we need honest criteria rather than a fixed iteration count. Three are standard, and they answer different questions:

  • Absolute gradient norm ∥∇J(θk)∥<ϵg\|\nabla J(\theta_k)\| < \epsilon_g checks an approximate first-order stationarity condition in the current units.
  • Relative objective change ∣J(θk)−J(θk−1)∣/max⁡(1,∣J(θk−1)∣)<ϵJ|J(\theta_k) - J(\theta_{k-1})| / \max(1, |J(\theta_{k-1})|) < \epsilon_J asks whether progress has stalled.
  • Relative parameter change ∥θk−θk−1∥/max⁡(1,∥θk−1∥)<ϵθ\|\theta_k - \theta_{k-1}\| / \max(1, \|\theta_{k-1}\|) < \epsilon_\theta asks whether the iterates have settled.

Each can misfire alone. A tiny objective change may reflect a flat but nonoptimal plateau, and a small gradient norm may occur at a saddle. Report more than one diagnostic alongside the objective value at the returned point. A finite-difference estimate provides a separate check on a hand-derived gradient: it can expose a sign, normalization, or indexing mistake before we interpret the training curve.

Visualizing Gradient Descent on a Conditioned Quadratic

We use two diagonal quadratic objectives with known minima to inspect the update paths. For a two-entry parameter vector, define

J(θ)=12(a1θ12+a2θ22).J(\theta)=\tfrac12(a_1\theta_1^2+a_2\theta_2^2).

The positive coefficients a1a_1 and a2a_2 are the curvatures along the coordinate axes. The gradient is (a1θ1,a2θ2)(a_1\theta_1,a_2\theta_2), the Hessian is diag(a1,a2)\mathrm{diag}(a_1,a_2), and the unique minimizer is the origin. Here a1≥a2>0a_1\ge a_2>0, so the condition number is a1/a2a_1/a_2.

PythonIn [3]
CollapseExpand
import numpy as np

# Two diagonal quadratics: J(theta) = 0.5 * (a1 * theta1^2 + a2 * theta2^2)
# The Hessian is diag(a1, a2), so the condition number is a1 / a2 exactly.
well = np.array([2.0, 1.0])
ill = np.array([100.0, 1.0])


def objective(theta, a):
    """Scalar objective value for a diagonal quadratic."""
    return 0.5 * np.sum(a * theta**2)


def gradient(theta, a):
    """Exact gradient: component j is a_j * theta_j."""
    return a * theta


def run_gd(theta0, a, eta, steps):
    """Plain gradient descent, recording the full path for plotting."""
    theta = np.array(theta0, dtype=float)
    path = [theta.copy()]
    history = [objective(theta, a)]
    for _ in range(steps):
        theta = theta - eta * gradient(theta, a)
        path.append(theta.copy())
        history.append(objective(theta, a))
    return np.array(path), np.array(history)


theta0 = np.array([1.0, 1.0])

# Stable step for both problems: eta < 2 / lambda_max is required.
stability_info = []
for name, a in [("well", well), ("ill", ill)]:
    eta_max = 2.0 / a.max()
    stability_info.append((name, a, eta_max))
ConsoleOut [4]
CollapseExpand
well: curvature=[2. 1.], kappa=2, stability limit eta<1.000
ill: curvature=[100.   1.], kappa=100, stability limit eta<0.020
PythonIn [5]
CollapseExpand
# Use one common learning rate that is stable for the ill-conditioned problem,
# and observe how the two curvature vectors respond at that absolute rate.
eta = 1.0 / 100.0  # equals 1 / lambda_max of the ill-conditioned case
steps = 200
path_well, hist_well = run_gd(theta0, well, eta, steps)
path_ill, hist_ill = run_gd(theta0, ill, eta, steps)
ConsoleOut [6]
CollapseExpand
After 200 steps at eta=0.01:
  well-conditioned  J = 9.285e-03  ||theta|| = 1.351e-01
  ill-conditioned   J = 8.975e-03  ||theta|| = 1.340e-01

At this common step size, the final objectives are about 0.0092850.009285 for the well-conditioned problem and 0.0089750.008975 for the ill-conditioned problem. Their parameter norms are about 0.1351290.135129 and 0.1339800.133980, respectively. The ill-conditioned run is slightly closer in this experiment because its steep coordinate reaches zero after one update, while both shallow coordinates end at 0.99200≈0.1339800.99^{200}\approx0.133980. These results do not show faster convergence for the well-conditioned run. Here the objective is J(θ)=12(a1θ12+a2θ22)J(\theta) = \tfrac{1}{2}(a_1\theta_1^2 + a_2\theta_2^2), with:

  • θ1,θ2\theta_1, \theta_2: the two parameters
  • a1,a2a_1, a_2: the curvatures along the two axes
  • η=0.01\eta = 0.01: the learning rate
  • κ=a1/a2\kappa = a_1/a_2: the condition number
  • JJ: the objective value at the final iterate
  • ∥θ∥\|\theta\|: the Euclidean norm of the final parameter vector, which shrinks toward zero as the iterates approach the minimum

Each coordinate contracts by 1−ηaj1-\eta a_j per iteration. At η=0.01\eta=0.01, the well-conditioned factors are 0.980.98 and 0.990.99, while the ill-conditioned factors are 00 and 0.990.99. The shallow coordinate therefore moves identically in both runs; the difference in their final objectives comes from the small residual steep coordinate in the well-conditioned run. To assess the advantage available from conditioning, we would also need to compare appropriate step choices for each problem.

PythonIn [7]
CollapseExpand
# Prepare contour data once; the following cells only present these values.
plot_axis = np.linspace(-1.1, 1.1, 300)
contour_x, contour_y = np.meshgrid(plot_axis, plot_axis)
contour_well = 0.5 * (well[0] * contour_x**2 + well[1] * contour_y**2)
contour_ill = 0.5 * (ill[0] * contour_x**2 + ill[1] * contour_y**2)
levels_well = np.geomspace(0.001, 1.5, 12)
levels_ill = np.geomspace(0.001, 50.5, 14)
VisualizationOut [8]
CollapseExpand
Contour plot of the condition-number-2 quadratic with a curved path from (1,1) toward the origin, ending near (0.018,0.134).
Path from (1, 1) on the condition-number-2 quadratic at the common learning rate 0.01. The two coordinate multipliers are 0.98 and 0.99, so the path curves toward the vertical axis and remains away from the origin after two hundred updates.
VisualizationOut [9]
CollapseExpand
Elongated quadratic contours with a path that moves from (1,1) to (0,0.99), then follows the vertical axis to about (0,0.134).
Path from the same starting point on the condition-number-100 quadratic at learning rate 0.01. The steep coordinate reaches zero after one update; the shallow coordinate has the same 0.99 multiplier as in the other run, so later points lie on the vertical axis.

The second path reaches the vertical axis after one update. Both runs then have the same shallow-coordinate contraction, so different contour shapes alone do not establish a convergence-rate advantage at this common conservative learning rate.

VisualizationOut [10]
CollapseExpand
Logarithmic objective histories for two hundred updates. The ill-conditioned curve drops sharply after the first update, then both curves decrease gradually and finish near 0.009.
Objective histories at the common learning rate 0.01. The ill-conditioned objective falls about 3.75 decades, versus 2.21 for the well-conditioned objective, because its steep-coordinate contribution vanishes immediately. Both remaining shallow coordinates contract by 0.99, producing similar late-stage decreases.

The common rate deliberately gives both shallow coordinates the same multiplier. To see the convergence available to each problem, we now apply the same curvature-based rule, η=1/λmax⁡\eta=1/\lambda_{\max}, separately. This gives rates 0.5 and 0.01, respectively, while keeping the starting point and update budget fixed. The change in rate is explicit: we are comparing one common rate rule, rather than one common absolute rate.

PythonIn [11]
CollapseExpand
rate_well = 1.0 / well.max()
rate_ill = 1.0 / ill.max()
path_well_rule, hist_well_rule = run_gd(theta0, well, rate_well, steps)
path_ill_rule, hist_ill_rule = run_gd(theta0, ill, rate_ill, steps)
assert np.linalg.norm(path_well_rule[-1]) < np.linalg.norm(path_ill_rule[-1])
ConsoleOut [12]
CollapseExpand
Rule eta=1/lambda_max, same start and 200 updates:
  kappa=2:   eta=0.500, objective=1.936e-121, norm=6.223e-61
  kappa=100: eta=0.010, objective=8.975e-03, norm=1.340e-01

With this rule the well-conditioned shallow coordinate has multiplier 0.5, while the ill-conditioned one still has multiplier 0.99. This is a conditioning-dependent rate difference on the stated quadratics. Newton uses the full curvature matrix instead: since the Hessian is constant and positive definite, one full step reaches the minimizer in exact arithmetic.

PythonIn [13]
CollapseExpand
def newton_step(theta, a):
    """One Newton step using the exact diagonal Hessian."""
    H = np.diag(a)
    return theta - np.linalg.solve(H, gradient(theta, a))


theta_n = newton_step(theta0, ill)
ConsoleOut [14]
CollapseExpand
Newton from [1. 1.] on the ill-conditioned problem:
  one step -> theta = [0. 0.]
  objective = 0.000e+00
  gradient norm = 0.000e+00

The displayed Newton solution has zero objective and gradient to the reported numerical precision. The code solves a linear system with the exact diagonal Hessian. This example confirms the one-step algebra for this positive-definite quadratic; it does not establish one-step convergence for arbitrary objectives or immunity to floating-point conditioning.

Example

We can check the recurrence by hand, then compare its predicted iterates with the update loop.

Take the well-conditioned curvature a=(2,1)a = (2, 1), start at θ0=(1,1)\theta_0 = (1, 1), and use η=0.25\eta = 0.25. The gradient at the start is (2⋅1,1⋅1)=(2,1)(2\cdot 1, 1\cdot 1) = (2, 1), so the first update is

θ1=(1,1)−0.25 (2,1)=(0.5, 0.75)\theta_1 = (1,1) - 0.25\,(2,1) = (0.5,\, 0.75)

The objective falls from J(θ0)=12(2⋅12+1⋅12)=12(2+1)=1.5J(\theta_0) = \tfrac12(2\cdot 1^2 + 1\cdot 1^2) = \tfrac12(2 + 1) = 1.5 to J(θ1)=12(2⋅0.52+1⋅0.752)=12(0.5+0.5625)=12(1.0625)=0.53125J(\theta_1) = \tfrac12(2\cdot 0.5^2 + 1\cdot 0.75^2) = \tfrac12(0.5 + 0.5625) = \tfrac12(1.0625) = 0.53125, where θ0\theta_0 and θ1\theta_1 are the parameters before and after the first update, each term is 12ajθj2\tfrac12 a_j \theta_{j}^2 with aja_j the curvature and θj\theta_j the parameter along coordinate jj, and the intermediate sums make the arithmetic explicit. Continuing, we can factor the update per coordinate. For coordinate one the multiplier is 1−ηa1=1−0.25⋅2=1−0.5=0.51 - \eta a_1 = 1 - 0.25 \cdot 2 = 1 - 0.5 = 0.5; for coordinate two it is 1−ηa2=1−0.25⋅1=1−0.25=0.751 - \eta a_2 = 1 - 0.25 \cdot 1 = 1 - 0.25 = 0.75, where η\eta is the learning rate and a1,a2a_1, a_2 are the curvatures along the two coordinates. So after kk steps

θ1,k=0.5k,θ2,k=0.75k\begin{aligned} \theta_{1,k} &= 0.5^k, \\ \theta_{2,k} &= 0.75^k \end{aligned}

where θ1,k\theta_{1,k} and θ2,k\theta_{2,k} are the two coordinates at iteration kk. With this starting point and rate, coordinate two decays more slowly. After ten steps it is 0.7510≈0.05630.75^{10} \approx 0.0563, while coordinate one is 0.510≈0.000980.5^{10} \approx 0.00098. Here the smaller-curvature direction has the larger contraction multiplier. For another rate, we must compare both absolute multipliers rather than assuming that the smallest curvature always controls the error.

Now verify the Newton prediction. The Hessian is constant, so the step from θ0\theta_0 is

d=−H−1∇J(θ0)=−[0.5001][21]=−[0.5⋅2+0⋅10⋅2+1⋅1]=−[11]=[−1−1]\begin{aligned} d &= -H^{-1}\nabla J(\theta_0) \\ &= -\begin{bmatrix} 0.5 & 0 \\ 0 & 1 \end{bmatrix}\begin{bmatrix} 2 \\ 1 \end{bmatrix} \\ &= -\begin{bmatrix} 0.5 \cdot 2 + 0 \cdot 1 \\ 0 \cdot 2 + 1 \cdot 1 \end{bmatrix} \\ &= -\begin{bmatrix} 1 \\ 1 \end{bmatrix} \\ &= \begin{bmatrix} -1 \\ -1 \end{bmatrix} \end{aligned}

since H−1=diag(1/2,1)H^{-1} = \mathrm{diag}(1/2, 1) and the gradient is (2,1)(2,1), where:

  • HH: the Hessian with diagonal entries equal to the curvatures a1=2a_1 = 2 and a2=1a_2 = 1
  • H−1H^{-1}: the inverse of the Hessian, here diag(1/2,1)\mathrm{diag}(1/2, 1)
  • ∇J(θ0)\nabla J(\theta_0): the gradient at the starting point, here (2,1)(2, 1)
  • dd: the Newton step

Adding gives θ1=(1,1)+(−1,−1)=(0,0)\theta_1 = (1,1) + (-1,-1) = (0,0), the exact minimum, in a single step. Let us confirm these hand calculations with code so nothing rests on arithmetic we did in our heads.

PythonIn [15]
CollapseExpand
a = np.array([2.0, 1.0])
theta = np.array([1.0, 1.0])
eta = 0.25
first_three = [theta.copy()]
for k in range(3):
    grad = a * theta
    theta = theta - eta * grad
    first_three.append(theta.copy())

# Closed form prediction from the per-coordinate contraction factors
k = 10
pred = np.array([(1 - eta * a[0]) ** k, (1 - eta * a[1]) ** k])
gd_final = run_gd(np.array([1.0, 1.0]), a, eta, k)[0][-1]
assert np.allclose(gd_final, pred, rtol=1e-12, atol=1e-15)
assert np.allclose(first_three[1], [0.5, 0.75])
assert np.isclose(objective(first_three[1], a), 0.53125)
ConsoleOut [16]
CollapseExpand
After 10 steps the closed form gives theta = [0.00097656 0.05631351]
direct run gives theta       = [0.00097656 0.05631351]
Objective there: 1.587e-03
Newton one-step solution: [0. 0.]
iterate k=0: theta=[1. 1.], J=1.50000
iterate k=1: theta=[0.5  0.75], J=0.53125
iterate k=2: theta=[0.25   0.5625], J=0.22070
iterate k=3: theta=[0.125   0.42188], J=0.10461

The example also exposes a subtlety about learning rates. With η=0.25\eta = 0.25 both coordinates contract, and the objective decreases monotonically. Raise η\eta to 0.750.75 and coordinate one has multiplier 1−ηa1=1−0.75⋅2=1−1.5=−0.51 - \eta a_1 = 1 - 0.75 \cdot 2 = 1 - 1.5 = -0.5, so it oscillates in sign while shrinking; the objective still decreases each step but the path zig-zags. The multiplier for coordinate jj is 1−ηaj1 - \eta a_j, with η\eta the learning rate and aja_j the curvature along that coordinate. Raise η\eta to 1.251.25 and the multiplier for coordinate one is 1−1.25⋅2=1−2.5=−1.51 - 1.25 \cdot 2 = 1 - 2.5 = -1.5, magnitude greater than one, and the run diverges even though coordinate two still converges. The next cell confirms the threshold numerically.

PythonIn [17]
CollapseExpand
eta_tests = [0.25, 0.75, 1.25]
eta_results = []
for eta_test in eta_tests:
    p, h = run_gd(np.array([1.0, 1.0]), a, eta_test, 12)
    eta_results.append((eta_test, h[-1], p[-1]))
ConsoleOut [18]
CollapseExpand
eta=0.25: J after 12 steps = 5.018e-04, final theta = [0.    0.032]
eta=0.75: J after 12 steps = 5.960e-08, final theta = [0. 0.]
eta=1.25: J after 12 steps = 1.683e+04, final theta = [129.746   0.   ]
Stability requires eta < 2/2.0 = 1.0 for this problem.

The run with η=1.25\eta = 1.25 diverges because of the steep direction. On a positive-definite quadratic, an unbounded parameter eventually forces an unbounded objective. Over a finite budget, a lightly weighted coordinate can still look small in the objective while its parameter error matters. Inspect the parameter trajectory alongside the loss when that distinction is relevant.

Implementation in NumPy

We now implement least-squares fitting with four synthetic features and an explicit intercept. The design matrix contains a leading column of ones, so the parameter vector has five entries: the intercept followed by four slopes. Including that intercept is essential to this comparison: centering the features can then be absorbed into the intercept without changing the family of predictions. We compare gradient descent with NumPy's least-squares implementation, using the same design matrix and objective.

PythonIn [19]
CollapseExpand
import numpy as np

rng = np.random.default_rng(7)


def make_regression(n=200, noise=0.5, seed=0):
    """Four synthetic features and an explicit, unscaled intercept column."""
    rng = np.random.default_rng(seed)
    features = rng.normal(size=(n, 4))
    slopes = np.array([2.0, -1.0, 0.5, 0.0])
    y = features @ slopes + noise * rng.normal(size=n)
    X = np.column_stack([np.ones(n), features])
    beta = np.concatenate([[0.0], slopes])
    return X, y, beta


X_raw, y, beta_true = make_regression()


def sse_objective(theta, X, y):
    """Half the mean squared error for this linear design."""
    r = y - X @ theta
    return 0.5 * np.mean(r**2)


def sse_gradient(theta, X, y):
    """Analytic gradient of half the mean squared error."""
    r = y - X @ theta
    return -X.T @ r / len(y)


def full_batch_gd(X, y, eta=0.1, steps=200, tol=1e-10):
    """Full gradients; count gradient checks and reject nonfinite iterates."""
    theta = np.zeros(X.shape[1])
    hist = [sse_objective(theta, X, y)]
    gradient_checks = 0
    for _ in range(steps):
        g = sse_gradient(theta, X, y)
        gradient_checks += 1
        if not np.all(np.isfinite(g)):
            raise FloatingPointError(
                "Nonfinite gradient; inspect the learning rate."
            )
        if np.linalg.norm(g) < tol:
            break
        candidate = theta - eta * g
        with np.errstate(over="ignore", invalid="ignore"):
            value = sse_objective(candidate, X, y)
        if not np.all(np.isfinite(candidate)) or not np.isfinite(value):
            raise FloatingPointError(
                "Nonfinite iterate; reduce the learning rate."
            )
        theta = candidate
        hist.append(value)
    return theta, np.array(hist), gradient_checks
ConsoleOut [20]
CollapseExpand
design matrix: 200 rows, 5 columns
feature stds (intercept excluded): [0.973 0.946 1.027 1.042]
true coefficients: [ 0.   2.  -1.   0.5  0. ]
initial objective: 2.5285

The objective is half the mean squared error, J(θ)=∥y−Xθ∥2/(2n)J(\theta)=\|y-X\theta\|^2/(2n), where nn is the row count, yy contains the targets, and XθX\theta contains the predictions. Differentiating gives ∇J(θ)=−X⊤(y−Xθ)/n\nabla J(\theta)=-X^{\top}(y-X\theta)/n; the factor of two from the square cancels the one-half in the objective. Duplicating every row leaves this average and its gradient unchanged, although changing the data distribution can still change the curvature and a suitable learning rate. The corresponding sum of squared errors is SSE=2nJ\mathrm{SSE}=2nJ.

Before interpreting a fit, compare the analytic gradient with a finite difference. This is the numerical check introduced in the formula section; it can expose sign errors or missing factors without relying on the eventual training loss.

PythonIn [21]
CollapseExpand
def finite_difference_gradient(f, theta, h=1e-6):
    """Central difference estimate, one coordinate at a time."""
    theta = np.asarray(theta, dtype=float)
    grad = np.zeros_like(theta)
    for j in range(len(theta)):
        e = np.zeros_like(theta)
        e[j] = h
        grad[j] = (f(theta + e) - f(theta - e)) / (2 * h)
    return grad


theta_test = np.random.default_rng(11).normal(size=X_raw.shape[1])
g_analytic = sse_gradient(theta_test, X_raw, y)
g_numeric = finite_difference_gradient(
    lambda t: sse_objective(t, X_raw, y), theta_test
)
max_rel_err = np.max(
    np.abs(g_analytic - g_numeric) / np.maximum(1e-8, np.abs(g_numeric))
)
assert max_rel_err < 1e-5
ConsoleOut [22]
CollapseExpand
analytic gradient: [ 0.192819 -0.607492  1.941206 -1.096276 -0.270025]
numeric gradient:    [ 0.192819 -0.607492  1.941206 -1.096276 -0.270025]
Gradient check passed with max relative error 1.86e-09

With the gradient check passed, we fit the model and compare with np.linalg.lstsq, an established least-squares solver. This comparison checks the fitted coefficients and objective for this full-rank design.

PythonIn [23]
CollapseExpand
# Closed-form reference
theta_ols = np.linalg.lstsq(X_raw, y, rcond=None)[0]

theta_gd, hist_gd, used = full_batch_gd(X_raw, y, eta=0.1, steps=2000)
coef_diff = np.max(np.abs(theta_ols - theta_gd))
ConsoleOut [24]
CollapseExpand
parameter updates: 252; full-gradient checks: 253
stopping reason: gradient tolerance
returned gradient norm: 9.357e-11
OLS: [-0.06483  1.96236 -0.99078  0.51876 -0.00477]
GD:  [-0.06483  1.96236 -0.99078  0.51876 -0.00477]
max abs difference: 7.83e-11
objective OLS 0.09391853 vs GD 0.09391853

The four feature columns are approximately standard normal, and the leading intercept column remains one. We multiply the first feature by 200 to change its units while leaving the available predictions unchanged: its slope can be divided by 200. A fixed learning rate of 0.1 becomes unstable on this design. We show only six such updates to keep the demonstration finite, then use 1/λmax⁡1/\lambda_{\max} for a longer stable run on the same badly scaled problem.

PythonIn [25]
CollapseExpand
X_bad = X_raw.copy()
X_bad[:, 1] = X_bad[:, 1] * 200.0

# Condition number of X^T X is the square of the design matrix condition number.
cond_raw = np.linalg.cond(X_raw)
cond_bad = np.linalg.cond(X_bad)

hessian_bad = X_bad.T @ X_bad / len(X_bad)
lambda_max_bad = np.linalg.eigvalsh(hessian_bad)[-1]
unsafe_eta = 0.1
theta_unsafe, hist_unsafe, checks_unsafe = full_batch_gd(
    X_bad, y, eta=unsafe_eta, steps=6
)
eta_bad = 1.0 / lambda_max_bad
theta_bad, hist_bad, used_bad = full_batch_gd(X_bad, y, eta=eta_bad, steps=2000)
assert unsafe_eta > 2.0 / lambda_max_bad
assert hist_unsafe[-1] > hist_unsafe[0]
assert np.all(np.isfinite(hist_bad))
ConsoleOut [26]
CollapseExpand
cond(X_raw) = 1.2, cond(X^T X) = 1.4
cond(X_bad) = 207.1, cond(X^T X) = 42870.3
Well-scaled objective: 0.093919; gradient checks: 253
Unsafe eta=0.1: objective after 6 updates = 1.612e+43
Bad-design stability ceiling: 5.281e-05
unsafe stopping reason: deliberate six-update budget
unsafe returned gradient norm: 1.105e+24
Stable eta=2.641e-05: objective = 0.616614; gradient checks: 2000
safe badly scaled stopping reason: update budget
safe badly scaled returned gradient norm: 9.840e-01

The six large-step updates increase the objective sharply because 0.1 exceeds this design's stability ceiling. The second badly scaled run remains finite at η=1/λmax⁡\eta=1/\lambda_{\max}, but makes slow progress in the shallow directions. Standardizing the four feature columns lets us use a much larger stable step. We leave the intercept column unchanged because its standard deviation is zero.

PythonIn [27]
CollapseExpand
mu = X_bad[:, 1:].mean(axis=0)
sd = X_bad[:, 1:].std(axis=0)
assert np.all(sd > 0)
X_scaled = np.column_stack([np.ones(len(X_bad)), (X_bad[:, 1:] - mu) / sd])

theta_scaled, hist_scaled, used_scaled = full_batch_gd(
    X_scaled, y, eta=0.1, steps=2000
)
cond_scaled = np.linalg.cond(X_scaled)
theta_scaled_in_bad_units = np.concatenate(
    [
        [theta_scaled[0] - mu @ (theta_scaled[1:] / sd)],
        theta_scaled[1:] / sd,
    ]
)
scaled_reference = np.linalg.lstsq(X_scaled, y, rcond=None)[0]
assert np.allclose(
    X_bad @ theta_scaled_in_bad_units, X_scaled @ theta_scaled, atol=1e-10
)
assert np.allclose(X_raw @ theta_ols, X_scaled @ scaled_reference, atol=1e-10)
assert np.isclose(
    hist_scaled[-1], sse_objective(theta_ols, X_raw, y), atol=1e-10
)
ConsoleOut [28]
CollapseExpand
Standardized: cond(X)=1.1, J=0.093919, gradient checks=234
standardized stopping reason: gradient tolerance
standardized returned gradient norm: 9.087e-11
Badly scaled, stable step: cond(X)=207.1, J=0.616614, gradient checks=2000
Same OLS optimum: 0.093919
Prediction-map max error: 8.88e-16

With an explicit intercept, standardization preserves the set of possible linear predictions. To map standardized slopes back, divide each by its feature standard deviation; subtract the mean-weighted slopes from the standardized intercept. The assertions above verify that this mapping preserves predictions and that the two reference fits attain the same least-squares optimum. The finite-budget badly scaled gradient-descent run can still be farther from that optimum. All 200 rows here are a synthetic training set; in an evaluated model, estimate scaling statistics on its training split and reuse them unchanged for validation and test data.

Learning rate from the Hessian spectrum

For this full-rank quadratic, the Hessian spectrum gives the stable interval 0<η<2/λmax⁡0<\eta<2/\lambda_{\max}. With our half-mean-squared-error objective the Hessian is X⊤X/nX^{\top}X/n, where XX is the n×pn\times p design and λmax⁡\lambda_{\max} is its largest eigenvalue. The next cell computes the eigenvalues and prints η=2/(λmin⁡+λmax⁡)\eta=2/(\lambda_{\min}+\lambda_{\max}), where λmin⁡\lambda_{\min} is the smallest eigenvalue. This rate minimizes the worst-case Euclidean parameter-error contraction for a fixed step on this quadratic.

The update budget trades computation against closeness to the optimizer. Stopping early can also change a model's generalization behavior; its relation to regularization depends on the model and update rule. Tolerances need to match the units and scale of the parameterization and objective. A small absolute gradient threshold is an algorithmic condition, not a universal accuracy measure.

PythonIn [29]
CollapseExpand
def hessian_spectrum(X):
    """Eigenvalues of the least-squares Hessian X^T X / n."""
    H = X.T @ X / len(X)
    return np.sort(np.linalg.eigvalsh(H))


ev_scaled = hessian_spectrum(X_scaled)
ev_bad = hessian_spectrum(X_bad)
eta_opt_scaled = 2.0 / (ev_scaled[0] + ev_scaled[-1])
eta_opt_bad = 2.0 / (ev_bad[0] + ev_bad[-1])
ConsoleOut [30]
CollapseExpand
scaled eigenvalues: [0.9371 0.9771 1.     1.0073 1.0785]
  kappa 1.15, eta_max 1.8545, eta_opt 0.9923
unscaled eigenvalues: [8.800000e-01 8.900000e-01 1.090000e+00 1.190000e+00 3.787083e+04]
  kappa 42870.3, eta_max 0.000053, eta_opt 0.000053

The badly scaled problem has both a small stable-rate ceiling and a large eigenvalue ratio. At the optimal fixed rate, the worst-case parameter-error contraction is (κ−1)/(κ+1)(\kappa-1)/(\kappa+1), where κ=λmax⁡/λmin⁡\kappa=\lambda_{\max}/\lambda_{\min}. Its factor is therefore much closer to one than the standardized problem's factor. This compares worst-case relative error across starting directions; a particular run also depends on which eigendirections are present in its initial error.

The eigenvalues make the conditioning gap visible. The standardized design has a spectrum clustered near one, while the badly scaled design spans several orders of magnitude. That spread worsens the best worst-case fixed-rate contraction on this quadratic.

VisualizationOut [31]
CollapseExpand
Log-scale comparison of Hessian eigenvalues for standardized and badly scaled designs.
Eigenvalues of the least-squares Hessian for the standardized and badly scaled designs. The badly scaled spectrum spans several orders of magnitude, matching its large condition number and slow first-order convergence.

Stochastic and mini-batch methods

The next implementation reshuffles the training rows each epoch and updates using the mean gradient of the current batch. We use a nonempty design, nonnegative integer epoch budgets, positive integer batch sizes, a positive initial rate, and nonnegative decay. We count per-example gradient contributions used for updates, and separately note that objective evaluation and diagnostic checks also cost work. A final short mini-batch is divided by its actual number of rows.

PythonIn [32]
CollapseExpand
def sgd(X, y, eta0=0.05, epochs=30, decay=None, seed=0, batch_size=None):
    """SGD with optional mini-batch and learning-rate decay.

    Returns parameters, one objective per epoch, and example contributions.
    """
    rng = np.random.default_rng(seed)
    n, p = X.shape
    theta = np.zeros(p)
    b = 1 if batch_size is None else batch_size
    hist, evals = [], 0
    for epoch in range(epochs):
        order = rng.permutation(n)
        for start in range(0, n, b):
            idx = order[start : start + b]
            eta = eta0 if decay is None else eta0 / (1 + decay * evals / n)
            g = -X[idx].T @ (y[idx] - X[idx] @ theta) / len(idx)
            theta = theta - eta * g
            evals += len(idx)
        hist.append(sse_objective(theta, X, y))
    return theta, np.array(hist), evals


theta_sgd, hist_sgd, evals_sgd = sgd(
    X_scaled, y, eta0=0.1, epochs=40, decay=1.0, batch_size=1, seed=3
)
theta_mb, hist_mb, evals_mb = sgd(
    X_scaled, y, eta0=0.1, epochs=40, decay=1.0, batch_size=32, seed=3
)
full_batch_evals = len(X_scaled) * (len(hist_scaled) - 1)
ConsoleOut [33]
CollapseExpand
SGD       : J = 0.093933, update contributions = 8000
mini-batch: J = 0.104950, update contributions = 8000
full batch: J = 0.093919, update contributions = 46600
SGD and mini-batch stopping reason: completed the 40-epoch budget
SGD returned full-gradient norm: 5.436e-03
mini-batch returned full-gradient norm: 1.485e-01

These are results for the stated data, learning rates, sampling seed, and finite budgets. They do not prove that one variant always reaches a target objective with fewer operations. The decay rule changes the step according to the number of examples already used for updates; it is a choice for this illustration, not a general convergence certificate.

The horizontal axis below counts per-example gradient contributions in parameter updates. For full-batch descent it increases by nn per update; the stochastic curves are recorded after each epoch of nn contributions. This omits the extra cost of evaluating full objectives, terminal gradient checks, and implementation overhead, so it is not a wall-clock comparison.

PythonIn [34]
CollapseExpand
eval_sgd = np.arange(1, len(hist_sgd) + 1) * len(X_scaled)
eval_mb = np.arange(1, len(hist_mb) + 1) * len(X_scaled)
eval_full = np.arange(len(hist_scaled)) * len(X_scaled)
VisualizationOut [35]
CollapseExpand
Logarithmic objective versus per-example update-gradient contributions for full-batch, mini-batch, and single-example updates on the same standardized training design.
Objective versus per-example gradient contributions in updates for full-batch, mini-batch, and single-sample SGD on the standardized design. The displayed trajectories use the stated rates and budgets; the count excludes objective monitoring and other overhead.

Optional: Newton on least squares

On the full-rank least-squares design, the Hessian X⊤X/nX^{\top}X/n is constant and positive definite. We can therefore demonstrate the full Newton step directly. The implementation below is specific to this quadratic and does not add a line search or a logistic-loss example. General nonquadratic objectives need the safeguards discussed above.

PythonIn [36]
CollapseExpand
def newton_ls(X, y, steps=5):
    """Newton on full-rank least squares, with one history entry per update."""
    H = X.T @ X / len(X)
    theta = np.zeros(X.shape[1])
    hist = [sse_objective(theta, X, y)]
    for _ in range(steps):
        g = sse_gradient(theta, X, y)
        if np.linalg.norm(g) < 1e-10:
            break
        theta = theta - np.linalg.solve(H, g)
        hist.append(sse_objective(theta, X, y))
    return theta, np.array(hist)


theta_newton, hist_newton = newton_ls(X_bad, y, steps=5)
ConsoleOut [37]
CollapseExpand
Newton on ill-conditioned LS: J = 0.0939185272, steps used 1
Newton objective history: [2.52846336 0.09391853]
Newton solution: [-0.065  0.01  -0.991  0.519 -0.005]
Gradient norm at the solution: 3.03e-14
Newton stopping reason: gradient tolerance
coefficients match OLS: True

The printed gradient norm and reference comparison quantify the numerical error. One update reaches the least-squares solution to the reported precision on this design. The exact-arithmetic result is independent of the condition number, while the accuracy of a floating-point linear solve can still depend on scaling and conditioning.

Key Parameters

Below are the main parameters that affect how gradient-based optimization behaves, with guidance on how to select them.

  • eta scales the negative gradient. For the positive-definite quadratics here, convergence from every start requires 0<η<2/λmax⁡0<\eta<2/\lambda_{\max}, and 2/(λmin⁡+λmax⁡)2/(\lambda_{\min}+\lambda_{\max}) minimizes the worst-case fixed-rate parameter contraction. General objectives need an appropriate step-selection rule.
  • steps caps the update count. More iterations can reduce optimization error in a stable run; they do not repair an unstable rate. Early stopping can regularize some fitting procedures, but the optimization examples here do not evaluate that effect on held-out data.
  • tol is the absolute gradient-norm threshold used by this illustrative implementation. Interpret it in the current objective and parameter units, and report the returned residual and budget.
  • batch_size is the number of examples used per update. It changes gradient noise, work per update, and updates per epoch; 32 is the setting demonstrated here.
  • decay controls the displayed schedule η=η0/(1+c m/n)\eta=\eta_0/(1+c\,m/n), where mm counts per-example gradient contributions already used. The code uses this example-count clock, which differs from counting parameter updates.

Before increasing the budget or loosening a tolerance, check the gradient calculation and the response to the learning rate. A decay schedule and a batch size should be assessed together under a stated budget and sampling rule.

Practical Implications

Gradient descent is useful when a differentiable objective is available and first-order updates suit the data and parameter dimensions. A dense linear-model gradient can be cheaper to compute and store than its Hessian. A smooth penalty can be differentiated along with the loss; a nonsmooth penalty or a constraint needs an update rule that handles that additional structure.

Curvature-aware methods are also worth considering when their matrix operations are affordable. Dense Newton steps and quasi-Newton approximations can reduce the number of updates on some problems, but iteration count alone does not measure total cost. Their behavior depends on the objective, derivatives, safeguards and starting point.

On a nonconvex problem, report the objective, gradient norm, stopping reason and initialization. These diagnostics help describe the returned point, but they do not generally prove that it is a local or global minimum. A budget-limited or divergent run must be distinguished from a small-residual termination.

Best Practices

For the positive-definite quadratic examples, the spectrum gives an explicit stable interval and the optimal worst-case fixed rate. For a general objective, local curvature may change as the parameters move. A line search or another step-selection rule can assess proposed displacements; a short successful trial alone does not prove that one fixed rate will remain suitable throughout the run.

When gradient noise persists near an optimum, smaller or decaying steps can reduce fluctuations. Constant steps can still be appropriate, and some problems have vanishing gradient noise at the solution. Inspect trajectories over a stated budget and consider repeated sampling seeds; a flat noisy curve alone is not a convergence proof.

Check the analytic gradient at several representative parameter values against a numerical approximation. A discrepancy can expose a sign, normalization or indexing error, although a finite set of checks is not a proof. During fitting, record enough information to distinguish small gradients, tiny steps, numerical failure and an exhausted budget.

Data Requirements and Preprocessing

For unpenalized linear least squares with an intercept, invertible feature scaling and centering preserve the prediction family after the corresponding coefficient transformation. They can improve conditioning when unequal feature scales are the cause. They do not remove exact dependence between columns, and equal variances can coexist with strong correlations and a large condition number. A suitably chosen gradient-descent step can converge without standardization.

Estimate preprocessing statistics on the training portion used for fitting and apply the fitted transformation to held-out data. If a penalty is defined in coefficient units, decide how it should transform when those units change: applying the same coefficient penalty after rescaling generally changes the fitting objective. Inspect unusual values and feature scales in the context of the data rather than treating scaling as a substitute for model checks.

A full gradient can be accumulated across smaller data blocks before taking one update, so full-batch descent does not require the entire dataset to remain in memory. Stochastic methods update from individual examples or subsets. Fresh independent sampling and random reshuffling are different procedures; the fixed-parameter unbiasedness result does not by itself cover the adaptive gradients produced during a reshuffled epoch.

Common Pitfalls

A growing or oscillating objective on a fixed quadratic can indicate an unsuitable step. Other causes include derivative mistakes and nonfinite arithmetic. The examples show an explicit instability ceiling, but observations on a general objective need to be interpreted with its changing curvature and update rule.

A sign reversal changes a descent update into an ascent update whenever the gradient is nonzero. Omitting a positive scalar factor can instead act like changing the learning rate; other implementation errors can change the direction or even make the array operations invalid. Numerical checks should test the actual derivative and normalization used by the loop.

Stagnation can arise from a tiny step, finite precision, poor conditioning, or approach to a stationary point. A small parameter change and a small objective change do not distinguish these cases on their own. The gradient residual and stated assumptions provide additional information; even all three diagnostics together do not generally certify a global optimum.

Computational Considerations

For the dense linear model, a full-gradient update uses O(np)O(np) work, a single-example update O(p)O(p), and a mini-batch update O(bp)O(bp). These costs exclude full-objective monitoring and depend on the model's per-example derivative cost. Independent batch averaging reduces covariance by 1/b1/b at fixed parameters; the statistical and hardware trade-offs should be assessed for the actual sampling scheme and implementation.

The examples store the full design for simplicity. A first-order implementation can accumulate a full gradient or process updates from a batch while retaining the parameter vector. A dense least-squares Hessian occupies O(p2)O(p^2) storage, takes O(np2)O(np^2) work to form directly, and typically takes O(p3)O(p^3) work to factor; these are costs of the dense formulation rather than all Newton-type methods.

A solver such as scipy.optimize.minimize exposes the BFGS quasi-Newton method for smooth objectives. Compare such a solver with first-order alternatives using the achieved residual, objective, work and memory requirements, rather than assuming a dimension cutoff gives one method a universal advantage. For this chapter's least-squares example, np.linalg.lstsq provides the reference fit.

Performance and Deployment Considerations

The training objective measures fitting on the training data. A lower value does not guarantee better held-out performance. Keep optimization diagnostics separate from an evaluation of the fitted model on data and a metric chosen for the application.

Report the criterion that stopped the run, its tolerances, the final objective and gradient norm, and the available budget. Relative objective and parameter changes can supplement the gradient residual. If termination occurred because improvement was small, say so; do not silently treat that condition as proof that the minimizer was reached.

Repeated seeds can show how much a stochastic run varies under its sampling procedure. Logs can also reveal changes in the observed trajectory, but they do not identify a unique cause such as data drift or an incorrect step size. Investigate the data, derivatives and update rule together when behavior changes.

Summary

Gradient descent uses the gradient to construct a local update. On a positive-definite quadratic, eigenvalue analysis determines the stable interval and the best worst-case fixed-rate contraction. The scalar recurrence θk=(1−ηa)kθ0\theta_k=(1-\eta a)^k\theta_0 makes convergence, sign alternation and divergence explicit. These exact statements require the stated quadratic conditions; a general nonconvex objective needs additional analysis.

The examples also show why the comparison setup matters. One common absolute rate gave the two quadratics identical shallow-coordinate progress; applying the same reciprocal-curvature rate rule separately exposed a much faster well-conditioned run. In least squares, keeping an explicit intercept made standardized and unscaled reference fits equivalent, while their finite-budget gradient-descent progress differed. Newton reached the quadratic solution to the reported numerical precision with one update.

Batch and stochastic variants use different amounts of data per update and require explicit sampling and work-count conventions. A numerical gradient check can catch implementation mistakes, and stopping diagnostics describe what a run achieved. Curvature, feature coordinates, numerical arithmetic and gradient noise all belong in that interpretation; a termination message alone is not a certificate of global optimality.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about gradient descent, curvature, learning-rate stability, and numerical optimization checks.

Gradient Descent and Numerical Optimization

Question 1 of 80 of 8 completed
For J(θ)=aθ²/2 with fixed a>0, what is the full range of constant learning rates that guarantees convergence to the minimum from every starting value?
Join the discussionAsk a question or share what you learned.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

A different angle
All writing
Machine Learning

Maximum Likelihood Estimation

Learn maximum likelihood estimation from scratch, including Bernoulli and Gaussian estimators, log-likelihood, score, curvature, and NumPy/SciPy implementation.

51 min read
Machine Learning · Vol. 2

Gauss-Markov Assumptions for OLS Regression

Covers the Gauss-Markov assumptions that underpin linear regression. Topics include the five key assumptions, how to test them, consequences of violations.

17 min read
Machine Learning · Vol. 2

Normalization: Feature Scaling with Min-Max Implementation

Covers normalization in machine learning, covering min-max scaling, proper train-test split implementation, when to use normalization vs standardization.

14 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026gradientdescent, author = {Michael Brenndoerfer}, title = {Gradient Descent and Numerical Optimization}, year = {2026}, url = {https://mbrenndoerfer.com/writing/gradient-descent-numerical-optimization-from-scratch}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-28} }
APAAcademic
Michael Brenndoerfer (2026). Gradient Descent and Numerical Optimization. Retrieved from https://mbrenndoerfer.com/writing/gradient-descent-numerical-optimization-from-scratch
MLAAcademic
Michael Brenndoerfer. "Gradient Descent and Numerical Optimization." 2026. Web. October 28, 2026. <https://mbrenndoerfer.com/writing/gradient-descent-numerical-optimization-from-scratch>.
CHICAGOAcademic
Michael Brenndoerfer. "Gradient Descent and Numerical Optimization." Accessed October 28, 2026. https://mbrenndoerfer.com/writing/gradient-descent-numerical-optimization-from-scratch.
HARVARDAcademic
Michael Brenndoerfer (2026) 'Gradient Descent and Numerical Optimization'. Available at: https://mbrenndoerfer.com/writing/gradient-descent-numerical-optimization-from-scratch (Accessed: October 28, 2026).
SimpleBasic
Michael Brenndoerfer (2026). Gradient Descent and Numerical Optimization. https://mbrenndoerfer.com/writing/gradient-descent-numerical-optimization-from-scratch
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy