One Learning Rate Doesn't Fit All#
In deep networks, different parameters have vastly different scales:
- Weights in early layers might have typical gradient magnitudes of
- Weights in later layers might have gradients of
- Embedding parameters for rare words get updated very infrequently (sparse gradients)
A single global learning rate cannot serve all parameters well. If is tuned for the fastest-changing parameters, the slow-changing ones barely move. If is large enough for the slow parameters, the fast ones oscillate or diverge.
Adaptive methods solve this by maintaining a per-parameter effective learning rate, automatically scaled by the historical magnitude of that parameter's gradient.
AdaGrad#
AdaGrad (Duchi et al., 2011) was the first widely-used adaptive method. It accumulates the squares of all past gradients:
where is elementwise multiplication and prevents division by zero.
How it works: Parameters that have historically received large gradients get their effective learning rate divided by a large number (slowing them down). Parameters with historically small gradients get divided by a small number (speeding them up).
The failure mode: is monotonically increasing — it never forgets. In the limit , and the effective learning rate goes to zero for all parameters. Training stalls. This is why AdaGrad fell out of favour for deep learning.
RMSProp#
RMSProp (Tieleman & Hinton, 2012) fixes AdaGrad's accumulating-divisor problem by replacing the sum with an exponential moving average (EMA):
where (typically ) controls the memory length. The EMA forgets old gradients — approximately the last steps influence .
RMSProp maintains adaptivity (each parameter gets its own scale) without the monotonic-stall problem. It became the default in many frameworks before Adam took over.
Adam#
AdamAdaptive Moment Estimation (optimizer) (Kingma & Ba, 2015) combines the best of both worlds:
- Momentum (Week 7) for the gradient direction:
- RMSProp for per-parameter scaling:
The full Adam update:
Bias correction: At initialisation, and . The EMA estimates start biased toward zero. Dividing by corrects this — at , exactly, and as the correction factor approaches 1.
Standard hyperparameters:
- (or )
- (momentum decay)
- (RMSProp decay)
These defaults work remarkably well across a wide range of deep learning problems — one reason Adam became the de facto standard.
Practice Defaults and What Adaptivity Hides#
When Adam works well:
- Sparse gradients (embeddings, NLP)
- Non-stationary objectives (GANs, RL — though care is needed)
- Default choice when you don't want to tune extensively
When to fall back to SGD + momentum:
- When you need the best possible generalisation on a well-understood problem
- Computer vision benchmarks (where SGD + momentum with careful tuning often beats Adam)
- When theoretical convergence guarantees matter
What adaptivity hides: Adam's per-parameter scaling can mask poor conditioning, obscuring the diagnosis. The theoretical convergence guarantees for Adam are weaker than for SGD — under certain adversarial constructions, Adam can diverge even on convex problems. The parameter needs to be close enough to 1 for the RMSProp term to provide a stable denominator.
Which component of Adam prevents the effective learning rate from vanishing for parameters with historically small gradients?
AdaGrad's main failure mode in deep learning is:
Browser lab#
Compare AdamAdaptive Moment Estimation (optimizer) vs SGD + momentum on a toy nonconvex 2D loss with an elongated valley. Plot iterate paths to visualise the adaptivity.
import numpy as np
import matplotlib.pyplot as plt
def f(w):
x, y = w[0], w[1]
return 0.5 * x**2 + 5.0 * y**2 + 2.0 * np.sin(x) * np.sin(y)
def grad(w):
x, y = w[0], w[1]
return np.array([x + 2.0 * np.cos(x) * np.sin(y),
10.0 * y + 2.0 * np.sin(x) * np.cos(y)])
def adam(w0, K, eta=0.1, beta1=0.9, beta2=0.999, eps=1e-8):
w = w0.copy(); m = np.zeros(2); v = np.zeros(2)
path = [w.copy()]; losses = [f(w)]
for t in range(1, K+1):
g = grad(w)
m = beta1 * m + (1 - beta1) * g
v = beta2 * v + (1 - beta2) * g**2
m_hat = m / (1 - beta1**t)
v_hat = v / (1 - beta2**t)
w = w - eta * m_hat / (np.sqrt(v_hat) + eps)
path.append(w.copy()); losses.append(f(w))
return np.array(path), np.array(losses)
def sgd_momentum(w0, K, eta=0.05, beta=0.9):
w = w0.copy(); v = np.zeros(2)
path = [w.copy()]; losses = [f(w)]
for _ in range(K):
v = beta * v - eta * grad(w)
w = w + v
path.append(w.copy()); losses.append(f(w))
return np.array(path), np.array(losses)
w0 = np.array([3.0, 2.0])
K = 150
path_adam, loss_adam = adam(w0, K)
path_sgd, loss_sgd = sgd_momentum(w0, K)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.plot(path_adam[:, 0], path_adam[:, 1], label="Adam", linewidth=1.5)
ax1.plot(path_sgd[:, 0], path_sgd[:, 1], label="SGD+momentum", linewidth=1.5)
ax1.plot(w0[0], w0[1], "ko", markersize=6, label="Start")
ax1.set_xlabel("w1"); ax1.set_ylabel("w2")
ax1.set_title("Iterate Paths"); ax1.legend()
ax2.plot(loss_adam, label="Adam"); ax2.plot(loss_sgd, label="SGD+momentum")
ax2.set_xlabel("Iteration"); ax2.set_ylabel("Loss")
ax2.set_title("Loss vs Iteration")
ax2.set_yscale("log"); ax2.legend(); ax2.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()