Why Momentum?#
Recall Week 4's ill-conditioned problem: with . GD with makes rapid progress along (steep direction) but crawls along (shallow direction). The iterates zigzag down a narrow valley.
Momentum fixes this by adding inertia. Instead of the gradient alone determining the step, a fraction of the previous step's direction is retained:
where is the velocity and is the momentum coefficient.
The zigzag: oscillations in the steep direction cancel out when averaged over time, while progress in the shallow direction accumulates. The result is a smoother, faster trajectory.
Heavy-Ball Momentum#
The heavy-ball method (Polyak, 1964) is the classic momentum formulation:
Unrolling the velocity update reveals it as an exponential moving average:
Recent gradients have weight , while gradients steps ago have weight . The effective "memory" of momentum is roughly steps — with , about 10 steps; with , about 100 steps.
Practical choices for :
- : recovers plain GD/SGDStochastic Gradient Descent
- : standard setting — good for most problems
- : longer memory, useful when gradients are very noisy (SGD with small batch)
- : very long memory — used in Adam's first-moment estimate
Physical analogy: A ball rolling down a hill. The gradient is the slope (instantaneous force), momentum keeps the ball rolling in the direction it's already going. If the slope direction changes slowly, the ball accelerates; if it oscillates (zigzag valley), the oscillations cancel.
Interpreting Momentum#
For a 1D quadratic , the momentum iteration becomes a second-order linear recurrence:
The eigenvalues of this recurrence determine convergence. The optimal and for a quadratic with curvature are:
The resulting convergence factor is , compared to for plain GD. For , GD needs iterations for a constant-factor reduction; momentum needs — a 10× speedup.
Nesterov Accelerated Gradient#
Nesterov (1983) proposed a subtle but important modification:
The difference: the gradient is evaluated at — the point we would reach if we continued with the current velocity — instead of at the current position . This "look-ahead" gradient provides a correction before we commit to the velocity direction.
For convex quadratics, Nesterov achieves the provably optimal convergence rate: for smooth convex (vs GD's ) and for strongly convex.
In deep learning practice, the distinction between heavy-ball and Nesterov momentum is often minor — both work well, and the choice is usually dictated by framework defaults.
Convergence Benefit#
For a convex -smooth function, Nesterov's accelerated gradient achieves:
Compare to GD: . The acceleration improves the denominator from to — a quadratic improvement in the number of iterations. To reach accuracy , GD needs iterations; Nesterov needs .
When does momentum actually help?
- Ill-conditioned convex problems: large speedup
- Well-conditioned problems: little benefit (GD is already fast)
- Nonconvex problems (deep nets): empirical benefit from dampening oscillations and escaping shallow regions — though theoretical guarantees are weaker
- SGD with momentum: reduces variance in the update direction, acting as a variance-reduction technique alongside the adaptive step-size role
If $\\beta = 0$, momentum SGD reduces to plain ___.
The heavy-ball momentum update v_{k+1} = β v_k − η ∇f(w_k) can be seen as:
Browser lab#
Compare GD, momentum, and Nesterov on an ill-conditioned 2D quadratic (). Plot iterate trajectories and loss vs iteration.
import numpy as np
import matplotlib.pyplot as plt
Q = np.diag([100.0, 1.0])
def f(w): return 0.5 * (w @ Q @ w)
def grad(w): return Q @ w
L = 100.0; mu = 1.0
eta_gd = 1.0 / L
eta_mom = 4.0 / (np.sqrt(L) + np.sqrt(mu))**2
beta_mom = ((np.sqrt(L/mu) - 1) / (np.sqrt(L/mu) + 1))**2
w0 = np.array([0.5, 10.0])
K = 200
def run_gd(w0, K, eta):
w = w0.copy(); path = [w.copy()]; losses = [f(w)]
for _ in range(K):
w = w - eta * grad(w)
path.append(w.copy()); losses.append(f(w))
return np.array(path), np.array(losses)
def run_momentum(w0, K, eta, beta, nesterov=False):
w = w0.copy(); v = np.zeros(2)
path = [w.copy()]; losses = [f(w)]
for _ in range(K):
if nesterov:
g = grad(w + beta * v)
else:
g = grad(w)
v = beta * v - eta * g
w = w + v
path.append(w.copy()); losses.append(f(w))
return np.array(path), np.array(losses)
path_gd, loss_gd = run_gd(w0, K, eta_gd)
path_mom, loss_mom = run_momentum(w0, K, eta_mom, beta_mom, nesterov=False)
path_nes, loss_nes = run_momentum(w0, K, eta_mom, beta_mom, nesterov=True)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.plot(path_gd[:, 0], path_gd[:, 1], alpha=0.6, label="GD")
ax1.plot(path_mom[:, 0], path_mom[:, 1], alpha=0.6, label="Momentum")
ax1.plot(path_nes[:, 0], path_nes[:, 1], alpha=0.6, label="Nesterov")
ax1.plot(0, 0, "kx", markersize=8)
ax1.set_xlabel("w1"); ax1.set_ylabel("w2")
ax1.set_title("Iterate Paths"); ax1.legend()
ax2.semilogy(loss_gd, label="GD"); ax2.semilogy(loss_mom, label="Momentum")
ax2.semilogy(loss_nes, label="Nesterov")
ax2.set_xlabel("Iteration"); ax2.set_ylabel("Loss")
ax2.set_title("Loss vs Iteration"); ax2.legend(); ax2.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()