16  Gradient Descent

Almost every model you will ever fit is trained by one idea: compute the gradient, take a small step against it, repeat.

It is barely more than Section 12.4 plus a loop. What fills this chapter is everything that goes wrong — steps that overshoot, valleys that cause zig-zagging, gradients too expensive to compute exactly — and the fixes that turned a simple idea into the workhorse of machine learning.

16.1 Descent directions

A direction \(\mathbf{d}\) is a descent direction at \(\mathbf{x}\) if moving a little way along it decreases \(f\). By Equation 12.3 that means

\[ \nabla f(\mathbf{x})^\top\mathbf{d} < 0 \]

any direction making an angle greater than \(90°\) with the gradient. There are many. The obvious choice is the steepest one, \(\mathbf{d} = -\nabla f\), which is where the method gets its other name, steepest descent.

It is worth noticing early that “steepest” is a local claim, and locally steepest is not the same as fastest overall. That gap is the subject of half this chapter.

16.2 The gradient descent algorithm

\[ \mathbf{x}_{k+1} = \mathbf{x}_k - \eta\,\nabla f(\mathbf{x}_k) \tag{16.1}\]

The scalar \(\eta > 0\) is the step size, or in machine learning the learning rate. That is the whole algorithm.

gradient_descent <- function(grad, x0, eta, n) {
  path <- matrix(0, nrow = n + 1, ncol = length(x0))
  path[1, ] <- x0
  for (k in seq_len(n)) {
    path[k + 1, ] <- path[k, ] - eta * grad(path[k, ])
  }
  path
}

Start with the easy case: a circular bowl, where the gradient always points straight at the minimum.

f_bowl <- function(x, y) x^2 + y^2
grad_bowl <- function(p) c(2 * p[1], 2 * p[2])
path_bowl <- gradient_descent(grad_bowl, c(2, 2), 0.1, 12)
draw_contour(
  f_bowl, c(-2.6, 2.6), c(-2.6, 2.6),
  curves = list(path = path_bowl),
  points = path_bowl,
  xlab = "x", ylab = "y"
)
Figure 16.1: Gradient descent on \(x^2+y^2\) from \((2,2)\) with \(\eta = 0.1\). Every gradient points directly at the origin, so the path is a straight line and each step shrinks the distance by the same factor.

Now the ill-conditioned valley from Figure 12.2, where \(\kappa = 10\).

f_valley <- function(x, y) 10 * x^2 + y^2
grad_valley <- function(p) c(20 * p[1], 2 * p[2])
path_zz <- gradient_descent(grad_valley, c(1, 3), 0.09, 14)
draw_contour(
  f_valley, c(-1.3, 1.3), c(-0.5, 3.4),
  curves = list(path = path_zz),
  points = path_zz,
  xlab = "x", ylab = "y"
)
Figure 16.2: The same algorithm on \(10x^2 + y^2\) with \(\eta = 0.09\). The gradient points mostly across the valley rather than along it, so the path oscillates from wall to wall while creeping toward the minimum.
round(head(path_zz, 5), 5)
        [,1]    [,2]
[1,]  1.0000 3.00000
[2,] -0.8000 2.46000
[3,]  0.6400 2.01720
[4,] -0.5120 1.65410
[5,]  0.4096 1.35637

The \(x\) coordinate flips sign every step while shrinking; the \(y\) coordinate crawls down. Steepest descent is wasting most of its effort going sideways. This is the picture to keep — nearly every refinement below exists to fix it.

16.3 Choosing a step size

The single most consequential hyperparameter, and the failure modes at each end are completely different.

For a quadratic with curvature \(L\) in the steepest direction, the update multiplies each component by \((1 - \eta\lambda_i)\). Stability requires \(|1-\eta\lambda_i| < 1\) for every eigenvalue, which gives

\[ \eta < \frac{2}{L} \tag{16.2}\]

Above that, the largest-curvature direction is amplified rather than damped and the whole thing diverges. Here \(L = 20\), so the bound is \(\eta < 0.1\).

etas <- c(0.01, 2 / 22, 0.11)
paths <- lapply(etas, function(e) {
  gradient_descent(grad_valley, c(1, 3), e, 40)
})
losses <- lapply(paths, function(p) f_valley(p[, 1], p[, 2]))
sapply(losses, function(l) l[c(1, 6, 21, 41)])
          [,1]         [,2]         [,3]
[1,] 19.000000 1.900000e+01 1.900000e+01
[2,]  8.427397 2.554182e+00 6.266758e+01
[3,]  4.012633 6.205068e-03 1.469772e+04
[4,]  1.787840 2.026467e-06 2.160228e+07

Three regimes, in three columns:

\(\eta\) Behavior
\(0.01\) — too small stable, but the flat direction barely moves
\(0.0909\) — optimal fastest possible for this problem
\(0.11\) — too large past \(2/L\); the loss grows without bound
draw_line(
  x = 0:40,
  y = list(
    `eta = 0.01` = log10(losses[[1]]),
    `eta = 0.0909` = log10(losses[[2]]),
    `eta = 0.11` = log10(losses[[3]])
  ),
  points = FALSE,
  xlab = "iteration",
  ylab = "log10(loss)"
)
Figure 16.3: Loss against iteration for three step sizes, on a log scale. Too small converges slowly; too large diverges outright. The three lines end thirteen orders of magnitude apart after the same forty steps.

The optimal step size for a quadratic is

\[ \eta^* = \frac{2}{L + m} \]

which here is \(2/22 \approx 0.0909\) — just under the stability bound, and not a coincidence: the best you can do is damp the stiffest direction as hard as stability allows.

WarningWatch out

Equation 16.2 depends on \(L\), the largest Hessian eigenvalue, which you almost never know for a real model. In practice \(\eta\) is tuned, and the symptom of exceeding the bound is unmistakable: the loss increases, often to NaN within a few steps.

If your loss explodes, reduce the learning rate first. It is by far the most common cause.

16.4 Convergence

For a strongly convex, smooth function (Section 15.7) with optimal step size, the distance to the minimum shrinks by a constant factor each step:

\[ \|\mathbf{x}_k - \mathbf{x}^*\| \leq \left(\frac{\kappa - 1}{\kappa + 1}\right)^{k}\|\mathbf{x}_0 - \mathbf{x}^*\| \tag{16.3}\]

This is linear convergence: error falls geometrically, and the ratio is set entirely by the condition number.

kappa <- c(1, 2, 10, 100, 1000)
rate <- (kappa - 1) / (kappa + 1)
rbind(
  kappa = kappa,
  rate = round(rate, 4),
  steps_for_1e_3 = ceiling(log(1e-3) / log(pmax(rate, 1e-12)))
)
               [,1]   [,2]    [,3]     [,4]     [,5]
kappa             1 2.0000 10.0000 100.0000 1000.000
rate              0 0.3333  0.8182   0.9802    0.998
steps_for_1e_3    1 7.0000 35.0000 346.0000 3454.000

The last row is the headline. At \(\kappa = 1\) convergence is immediate; at \(\kappa = 10\) it takes about 35 steps to gain three digits; at \(\kappa = 1000\), roughly 3,500. Conditioning, not dimension, is what makes a problem slow.

This is why feature standardization helps so reliably. Rescaling variables to comparable ranges reduces \(\kappa\), and Equation 16.3 turns that directly into fewer iterations.

16.6 Momentum

Here is the fix for the zig-zag. Instead of stepping along the current gradient, accumulate a running average of past gradients and step along that:

\[ \begin{aligned} \mathbf{v}_{k+1} &= \beta\mathbf{v}_k + \nabla f(\mathbf{x}_k) \\ \mathbf{x}_{k+1} &= \mathbf{x}_k - \eta\,\mathbf{v}_{k+1} \end{aligned} \tag{16.4}\]

with \(\beta \in [0,1)\), typically \(0.9\). Setting \(\beta = 0\) recovers plain gradient descent.

The intuition is physical, and it is the reason for the name. A ball rolling down the valley keeps its momentum along the floor while the side-to-side pushes, alternating in sign, cancel out. Oscillating components average away; consistent components accumulate.

momentum_descent <- function(grad, x0, eta, beta, n) {
  path <- matrix(0, nrow = n + 1, ncol = length(x0))
  path[1, ] <- x0
  v <- rep(0, length(x0))
  for (k in seq_len(n)) {
    v <- beta * v + grad(path[k, ])
    path[k + 1, ] <- path[k, ] - eta * v
  }
  path
}
p_plain <- momentum_descent(grad_valley, c(1, 3), 0.02, 0, 40)
p_mom <- momentum_descent(grad_valley, c(1, 3), 0.02, 0.9, 40)
rbind(
  plain = f_valley(p_plain[, 1], p_plain[, 2])[c(11, 21, 41)],
  momentum = f_valley(p_mom[, 1], p_mom[, 2])[c(11, 21, 41)]
)
             [,1]     [,2]        [,3]
plain    3.978388 1.758295 0.343511399
momentum 2.879627 1.225979 0.001741788

After 40 steps momentum has reduced the loss roughly two hundredfold further, at identical cost per step.

draw_contour(
  f_valley, c(-1.2, 1.2), c(-0.5, 3.4),
  curves = list(plain = p_plain, momentum = p_mom),
  curve_color = c(amds_gray, amds_colors[1]),
  xlab = "x", ylab = "y"
)
Figure 16.4: Plain gradient descent (gray) against momentum (colored), same step size and same starting point. Momentum damps the sideways oscillation and travels much further along the valley floor.

16.7 Stochastic gradient descent

For a loss summed over \(n\) observations,

\[ L(\boldsymbol{\theta}) = \frac{1}{n}\sum_{i=1}^{n}\ell_i(\boldsymbol{\theta}) \]

the exact gradient costs a pass over the entire dataset. With millions of observations that is one step per pass — unaffordable.

Stochastic gradient descent uses one randomly chosen observation instead:

\[ \boldsymbol{\theta}_{k+1} = \boldsymbol{\theta}_k - \eta\,\nabla\ell_{i_k}(\boldsymbol{\theta}_k) \]

The gradient is now wrong, but right on average — its expectation is the true gradient — so the steps are noisy yet drift the right way. In exchange each step costs \(1/n\) as much.

set.seed(11)
n <- 500
xd <- rnorm(n)
yd <- 2 + 3 * xd + rnorm(n, sd = 0.5)
Xd <- cbind(1, xd)
grad_i <- function(theta, i) {
  r <- drop(Xd[i, ] %*% theta) - yd[i]
  2 * r * Xd[i, ]
}
theta <- c(0, 0)
for (k in 1:2000) {
  i <- sample(n, 1)
  theta <- theta - 0.01 * grad_i(theta, i)
}
rbind(
  sgd = round(theta, 4),
  exact = round(coef(lm(yd ~ xd)), 4)
)
                 xd
sgd   2.0227 2.9980
exact 2.0102 3.0154

Close to the least squares solution, having touched 2,000 single observations rather than making 2,000 full passes.

WarningWatch out

SGD does not converge to the minimum and stay there — it rattles around in a neighborhood whose size is set by \(\eta\) and the gradient noise. Getting a precise answer requires decaying the learning rate, and the classical conditions are \(\sum_k \eta_k = \infty\) with \(\sum_k \eta_k^2 < \infty\): large enough to travel any distance, small enough that the noise dies out. A schedule like \(\eta_k \propto 1/k\) satisfies both.

16.8 Mini-batches

Between one observation and all of them sits the practical choice: average the gradient over a mini-batch of \(B\) observations.

Batch size Gradient noise Cost per step Steps per epoch
\(B = 1\) highest lowest \(n\)
\(1 < B < n\) moderate moderate \(n/B\)
\(B = n\) (full batch) none highest 1

Averaging \(B\) independent gradients reduces the noise standard deviation by \(\sqrt{B}\) — the same \(1/\sqrt{N}\) from Section 22.4. Note what that means: going from \(B=1\) to \(B=100\) costs a hundred times more per step and buys only a tenfold noise reduction. Diminishing returns are built into the arithmetic, which is why mini-batches are typically 32 to 512 rather than as large as memory allows.

Some gradient noise also appears to help, by discouraging the optimizer from settling into sharp narrow minima.

16.9 Adaptive methods

Plain gradient descent uses one \(\eta\) for every coordinate, which is a poor fit when they have wildly different curvatures — precisely the ill-conditioned case. Adaptive methods give each coordinate its own effective step size, scaled by the size of the gradients it has seen.

Method Idea
AdaGrad divide by the square root of the accumulated squared gradient
RMSProp same, but on an exponentially decaying average, so it does not stall
Adam RMSProp plus momentum, with bias correction

Adam is the default in deep learning. It is momentum and per-coordinate scaling combined — an approximation to using curvature information without ever forming a Hessian.

NoteIn machine learning

Adaptive methods are effectively estimating a diagonal approximation to the Hessian from the gradients they have already computed. That places them between plain gradient descent, which ignores curvature entirely, and Newton’s method, which uses all of it and cannot be afforded.

16.10 Newton’s method

Gradient descent uses a linear model of \(f\). Newton’s method uses a quadratic one — the second-order Taylor expansion from Equation 11.5 — and jumps straight to its minimum:

\[ \mathbf{x}_{k+1} = \mathbf{x}_k - \bigl[\nabla^2 f(\mathbf{x}_k)\bigr]^{-1}\nabla f(\mathbf{x}_k) \tag{16.5}\]

Multiplying by \(\mathbf{H}^{-1}\) undoes the stretch of the level sets: it turns an elongated valley back into a circular bowl, and then walks straight to the bottom.

On a quadratic that means it finishes in one step, whatever the condition number.

H <- matrix(c(20, 0, 0, 2), nrow = 2)
x0 <- c(1, 3)
x1 <- x0 - drop(solve(H, grad_valley(x0)))
rbind(start = x0, after_one_newton_step = x1)
                      [,1] [,2]
start                    1    3
after_one_newton_step    0    0

Exactly the minimum, from a starting point that took gradient descent dozens of steps to approach.

The catch is cost. The Hessian has \(p^2\) entries and solving with it costs \(O(p^3)\) (Section 5.14), so for \(p\) in the millions Newton’s method is unthinkable. It also requires \(\mathbf{H}\) to be positive definite; near a saddle it can step uphill.

16.11 Quasi-Newton methods

The compromise: build an approximation to \(\mathbf{H}^{-1}\) from the gradients you are already computing, without ever forming the Hessian.

BFGS is the standard, updating its estimate after each step from the change in gradient. L-BFGS (“limited memory”) stores only the last few updates instead of a full \(p \times p\) matrix, which makes it usable at large scale. R’s optim() provides both.

res <- optim(
  par = c(1, 3),
  fn = function(p) f_valley(p[1], p[2]),
  gr = grad_valley,
  method = "BFGS"
)
c(
  x = round(res$par[1], 8),
  y = round(res$par[2], 8),
  value = round(res$value, 10),
  gradient_evals = res$counts[["gradient"]]
)
             x              y          value gradient_evals 
             0              0              0              9 

Essentially exact in a handful of gradient evaluations.

Method Uses Cost per step Steps needed
Gradient descent \(\nabla f\) \(O(p)\) many, set by \(\kappa\)
Momentum / Adam \(\nabla f\) + history \(O(p)\) fewer
L-BFGS \(\nabla f\) + recent history \(O(mp)\) far fewer
Newton \(\nabla f\), \(\nabla^2 f\) \(O(p^3)\) very few
NoteIn machine learning

The choice tracks problem size. For a few thousand parameters — a GLM, a small likelihood — L-BFGS is excellent and is what optim() and glm() reach for. For a neural network with millions of parameters, only the \(O(p)\) methods are viable, which is why Adam and SGD dominate despite needing far more steps. It is a straight trade of steps against cost per step.

16.12 Summary

Idea Statement
Update \(\mathbf{x} \leftarrow \mathbf{x} - \eta\nabla f\)
Stability \(\eta < 2/L\)
Optimal step (quadratic) \(\eta^* = 2/(L+m)\)
Convergence rate \((\kappa-1)/(\kappa+1)\) per step
Momentum average past gradients; cancels oscillation
SGD one observation per step; right on average
Mini-batch noise falls as \(1/\sqrt{B}\)
Newton \(\mathbf{x} \leftarrow \mathbf{x} - \mathbf{H}^{-1}\nabla f\); one step on a quadratic
In R optim(..., method = "BFGS")

16.13 Exercises

1. For \(f(x) = x^2\), write the gradient descent update and find the \(\eta\) that converges fastest.

\(f' = 2x\), so \(x_{k+1} = x_k - 2\eta x_k = (1-2\eta)x_k\). Convergence needs \(|1-2\eta| < 1\), i.e. \(0 < \eta < 1\), and the factor is smallest — zero — at \(\eta = 0.5\).

etas1 <- c(0.1, 0.5, 0.9, 1.1)
sapply(etas1, function(e) (1 - 2 * e)^5)
[1]  0.32768  0.00000 -0.32768 -2.48832

At \(\eta = 0.5\) it lands exactly on the minimum in one step. That is Newton’s method in disguise: \(H^{-1} = 1/2\), so the optimal step for a one-dimensional quadratic is the Newton step.

2. Run gradient descent on the valley with \(\eta = 0.05\) and count steps to reach \(f < 10^{-3}\).

p05 <- gradient_descent(grad_valley, c(1, 3), 0.05, 400)
l05 <- f_valley(p05[, 1], p05[, 2])
which(l05 < 1e-3)[1] - 1
[1] 44

Slower than the optimal \(\eta = 0.0909\) would be. With \(\eta = 0.05\) the \(x\) factor is \(|1-1| = 0\) — that direction is solved immediately — but the \(y\) factor is \(1 - 0.1 = 0.9\), and the flat direction now sets the pace.

A step size can be excellent for one direction and poor for another. That is precisely what adaptive methods try to fix.

3. Show that momentum with \(\beta = 0\) is plain gradient descent.

With \(\beta = 0\), Equation 16.4 gives \(\mathbf{v}_{k+1} = \nabla f(\mathbf{x}_k)\), so the update becomes \(\mathbf{x}_{k+1} = \mathbf{x}_k - \eta\nabla f(\mathbf{x}_k)\) — exactly Equation 16.1.

a <- momentum_descent(grad_valley, c(1, 3), 0.05, 0, 10)
b <- gradient_descent(grad_valley, c(1, 3), 0.05, 10)
all.equal(a, b)
[1] TRUE

4. Verify that Newton’s method solves any quadratic in one step, using \(f(x,y) = 3x^2 + 5y^2 - 2x + 4y\).

\(\nabla f = (6x - 2,\; 10y + 4)\) and \(\mathbf{H} = \operatorname{diag}(6, 10)\), constant.

gq <- function(p) c(6 * p[1] - 2, 10 * p[2] + 4)
Hq <- diag(c(6, 10))
start <- c(5, -7)
step1 <- start - drop(solve(Hq, gq(start)))
rbind(step1 = round(step1, 8), exact = c(1 / 3, -2 / 5))
           [,1] [,2]
step1 0.3333333 -0.4
exact 0.3333333 -0.4
round(gq(step1), 10)
[1] 0 0

One step from an absurd starting point lands exactly on \((1/3, -2/5)\), and the gradient there is zero. Newton’s method is exact on a quadratic because the quadratic model is the function — no approximation error to iterate away.

5. Compare SGD against full-batch gradient descent on the regression data above, at equal numbers of observation touches. Run SGD from several seeds. What do you notice?

grad_full <- function(theta) {
  r <- drop(Xd %*% theta) - yd
  2 * drop(crossprod(Xd, r)) / n
}
th_full <- c(0, 0)
for (k in 1:20) th_full <- th_full - 0.1 * grad_full(th_full)

sgd_run <- function(seed, eta_fn) {
  set.seed(seed)
  th <- c(0, 0)
  for (k in 1:10000) {
    th <- th - eta_fn(k) * grad_i(th, sample(n, 1))
  }
  th
}
exact <- unname(coef(lm(yd ~ xd)))
err <- function(th) sqrt(sum((th - exact)^2))

fixed <- sapply(1:3, sgd_run, eta_fn = function(k) 0.01)
decay_eta <- function(k) 0.05 / (1 + k / 500)
decay <- sapply(1:3, sgd_run, eta_fn = decay_eta)
round(c(
  full_batch = err(th_full),
  sgd_fixed = apply(fixed, 2, err),
  sgd_decay = apply(decay, 2, err)
), 4)
full_batch sgd_fixed1 sgd_fixed2 sgd_fixed3 sgd_decay1 sgd_decay2 sgd_decay3 
    0.0460     0.0780     0.0698     0.0955     0.0371     0.0086     0.0388 

All four use the same budget: 10,000 observation touches, which is 20 full passes.

The instructive part is the spread. Full batch is deterministic — run it again and you get the identical answer. Fixed-step SGD gives a different answer every seed, scattered around the solution rather than converging to it, because a constant \(\eta\) leaves a noise floor it cannot get below. That is exactly the warning in Section 16.7, made concrete.

Decaying the step size shrinks the scatter, as the classical conditions promise.

Note what this exercise does not show: SGD beating full batch. With \(n = 500\) and a well-conditioned problem, 20 full passes is plenty, and SGD’s noise is a pure cost. Its advantage appears when \(n\) is large enough that even a handful of full passes is unaffordable — then the comparison is 10,000 noisy steps against two or three exact ones, and the noisy steps win easily. The method is not better; it is better per unit of compute, and only once that unit is scarce.

6. Increase the condition number to 100 by using \(f = 100x^2 + y^2\) and observe the effect on convergence.

grad_100 <- function(p) c(200 * p[1], 2 * p[2])
p100 <- gradient_descent(grad_100, c(1, 3), 2 / 202, 200)
l100 <- 100 * p100[, 1]^2 + p100[, 2]^2
c(
  predicted_rate = (100 - 1) / (100 + 1),
  observed_ratio = round(l100[101] / l100[100], 4)^0.5
)
predicted_rate observed_ratio 
     0.9801980      0.9802041 

The per-step contraction matches Equation 16.3’s \((\kappa-1)/(\kappa+1) = 0.980\) — the loss is the squared distance, hence the square root. Ten times worse conditioning means roughly ten times as many iterations, which is the practical content of Equation 16.3.