Advanced Optimizers: Momentum, AdaGrad, RMSProp, Adam
Plain SGD knows only one thing — the current gradient. These four optimizers each add memory, and each memory fixes a specific way vanilla gradient descent gets stuck.
Lecture 7's gradient descent works, but it has known, specific failure modes: it oscillates across narrow ravines, stalls at saddle points, and crawls across flat regions — all because it treats every direction and every timestep identically. Every optimizer in this lecture is a targeted fix for exactly one of those specific failures, not a wholesale replacement of the idea.
- Identify the specific failure modes of plain SGD: narrow-ravine oscillation, saddle points, and vanishing progress along flat directions.
- Derive the Momentum update and explain it physically as a ball accumulating velocity.
- Derive the AdaGrad update and explain how per-parameter accumulated squared gradients equalize progress across axes of different curvature.
- Derive the RMSProp update as a fix for AdaGrad's ever-shrinking learning rate.
- Derive the Adam update, explain why bias correction is needed, and state its default hyperparameters.
- Hand-compute one update step under each optimizer from the same starting point and compare the resulting step sizes.
1. Why Go Beyond Plain SGD?
Lecture 7 gave us the basic rule \(w \leftarrow w - \eta\nabla_w L\). It works, but it treats every parameter identically and remembers nothing about the past. On the simple, symmetric bowl \(L(w)=(w-3)^2\) used throughout Lectures 7–8, that is fine. Real loss surfaces for deep networks are far less friendly, and plain SGD struggles in three specific ways:
- Narrow ravines. If the surface curves steeply in one direction and gently in another, a learning rate large enough to make progress along the gentle direction causes the update to oscillate back and forth across the steep direction.
- Saddle points. Points where the gradient is (near) zero but which are not minima — common in high-dimensional loss surfaces — can stall plain gradient descent almost indefinitely.
- Flat directions. Along directions where the gradient is consistently small, plain SGD takes tiny, painfully slow steps.
Momentum, AdaGrad, RMSProp, and Adam each patch one or more of these problems by giving the optimizer memory — some running statistic of past gradients that shapes the current step.
2. Momentum
"Updating of weights depends not only on the current gradient but also on the previous gradient accumulated till time \(t\). Avoids local minima and saddle points."
We use the velocity form of momentum, which will be our consistent convention throughout this lecture:
$$v_t = \beta v_{t-1} - \eta\,\nabla_w L \qquad\qquad w \leftarrow w + v_t$$with typical \(\beta=0.9\). \(v_t\) is a running "velocity" — an exponentially-weighted accumulation of past (negative, scaled) gradients. Physically, this is exactly a ball rolling downhill: gravity (the gradient) keeps accelerating it, but it also carries momentum from where it has already been rolling. Momentum's effect is twofold — it accelerates movement in directions where the gradient has consistently pointed the same way, and it damps oscillation in directions where the gradient keeps flip-flopping (the contributions partially cancel in the running average).
3. AdaGrad
Imagine a loss surface where the gradient increases weakly in one direction and strongly in another. AdaGrad adapts the learning rate per parameter to equalize progress across such axes.
\(G_t\) accumulates the sum of squared past gradients for that parameter (\(\epsilon\) is a tiny constant, e.g. \(10^{-8}\), preventing division by zero). Consider the two axes in the instructor's motivating picture:
- Weak-gradient axis — gradients stay small, so \(G_t\) stays small, so dividing by \(\sqrt{G_t+\epsilon}\) (a small number) speeds up the update along that axis.
- Strong-gradient axis — gradients are large, \(G_t\) grows large, and dividing by \(\sqrt{G_t+\epsilon}\) (a large number) slows down the update along that axis.
The net effect is that AdaGrad automatically equalizes the rate of progress across parameters with very different curvature — exactly the narrow-ravine problem from Section 1.
\(G_t\) only ever grows (it is a running sum, never decayed), so the effective learning rate \(\eta/\sqrt{G_t+\epsilon}\) keeps shrinking monotonically. On long training runs this can shrink the step size so much that training effectively stalls, long before the model has converged.
4. RMSProp
RMSProp fixes exactly that downside. Instead of an ever-growing sum, it keeps an exponential moving average of squared gradients — old gradients decay away instead of accumulating forever:
$$G_t = \beta G_{t-1} + (1-\beta)(\nabla_w L)^2 \qquad\qquad w \leftarrow w - \frac{\eta}{\sqrt{G_t+\epsilon}}\,\nabla_w L$$with typical \(\beta=0.9\). The update rule is identical in form to AdaGrad's — same per-parameter normalization by accumulated squared gradient — but because \(G_t\) is now an EMA rather than a running sum, it can go back down as well as up, tracking the recent gradient magnitude rather than the entire history. This keeps the effective learning rate from decaying to zero and makes RMSProp far more usable for long training runs.
5. Adam
Adam is "a combination of RMSProp and Stochastic GD with momentum."
Adam maintains two running statistics per parameter: a first-moment estimate \(m_t\) (momentum-like — an EMA of the raw gradient) and a second-moment estimate \(v_t\) (RMSProp-like — an EMA of the squared gradient):
$$m_t = \beta_1 m_{t-1} + (1-\beta_1)\nabla_w L \qquad\qquad v_t = \beta_2 v_{t-1} + (1-\beta_2)(\nabla_w L)^2$$Both \(m_0\) and \(v_0\) are initialized to zero. That creates a subtle problem: early on, the EMA is still "warming up" and is biased toward zero (e.g. after one step, \(m_1=(1-\beta_1)\nabla_w L\) is much smaller in magnitude than the true gradient when \(\beta_1\) is close to 1). Adam corrects this with bias correction:
$$\hat m_t = \frac{m_t}{1-\beta_1^{\,t}} \qquad\qquad \hat v_t = \frac{v_t}{1-\beta_2^{\,t}}$$Dividing by \(1-\beta_1^t\) (which is small for small \(t\) and approaches 1 as \(t\) grows) inflates the early, zero-biased estimates back up to roughly the right scale, while leaving later estimates almost unchanged. The final update is:
$$w \leftarrow w - \eta\,\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}$$\(\beta_1=0.9,\ \ \beta_2=0.999,\ \ \epsilon=10^{-8}\) — these defaults work well across an enormous range of problems, which is a large part of why Adam is the most widely used optimizer in practice today.
6. Worked Comparison: One Update Step, Four Ways
Take the same quadratic loss from Lecture 7, \(L(w)=(w-3)^2\), so \(\nabla_w L = 2(w-3)\), starting from \(w=0\) with \(\eta=0.1\). At \(w=0\), \(\nabla_w L = 2(0-3)=-6\) for every optimizer below. To make the comparison meaningful, we assume each optimizer already has some accumulated history from previous steps (the values below are hypothetical, chosen purely to illustrate how each formula behaves — not a claim that one optimizer is always fastest). Every optimizer's arithmetic is worked in full below.
\(w=0,\ \eta=0.1,\ L(w)=(w-3)^2\), so \(\nabla_w L(0) = 2(0-3) = \mathbf{-6}\). Every optimizer below starts from this same gradient — the differences that follow come entirely from each optimizer's memory of past gradients, not from the current gradient itself.
$$w_{\text{new}} = 0 - 0.1\times(-6) = 0+0.6 = \mathbf{0.6}$$
First accumulate the velocity, then take the step:
$$v_t = 0.9\times0.5 - 0.1\times(-6) = 0.45+0.6 = \mathbf{1.05}$$
$$w_{\text{new}} = w + v_t = 0+1.05 = \mathbf{1.05}$$
Larger step than plain SGD, because the assumed prior velocity (0.5) already pointed the same way as the current gradient step — momentum adds the two together.
First update the accumulated sum of squared gradients, then divide the step by its square root:
$$G_t = 0.2 + (-6)^2 = 0.2+36 = \mathbf{36.2} \qquad \sqrt{G_t+\epsilon}\approx\sqrt{36.2}\approx\mathbf{6.017}$$
$$w_{\text{new}} = 0 - \frac{0.1}{6.017}\times(-6) \approx 0+0.0997 = \mathbf{0.0997}$$
Much smaller step than SGD's 0.6 — the single large gradient (−6) squared to 36 and dominated \(G_t\), immediately shrinking the effective learning rate from 0.1 down to roughly \(0.1/6.017\approx0.0166\).
First moment (momentum-like EMA of the raw gradient):
$$m_t = 0.9\times0.3 + 0.1\times(-6) = 0.27-0.6 = \mathbf{-0.33}$$
Second moment (RMSProp-like EMA of the squared gradient):
$$v_t = 0.999\times0.1 + 0.001\times(-6)^2 = 0.0999+0.036 = \mathbf{0.1359}$$
Bias-correct both, dividing by \(1-\beta^t\) at step \(t=2\) (\(1-0.9^2=0.19\) and \(1-0.999^2=0.001999\)):
$$\hat m_t = \frac{-0.33}{0.19} \approx \mathbf{-1.7368} \qquad\qquad \hat v_t = \frac{0.1359}{0.001999} \approx \mathbf{67.98},\quad \sqrt{\hat v_t}\approx\mathbf{8.245}$$
Final update:
$$w_{\text{new}} = 0 - 0.1\times\frac{-1.7368}{8.245} \approx 0+0.0211 = \mathbf{0.0211}$$
Notice how small the two bias-correction denominators (0.19, 0.001999) are at \(t=2\) — dividing by them inflates \(m_t\) and \(v_t\) substantially, which is exactly the point: without bias correction, these early-step estimates would still be biased toward their zero initialization and the step would be even smaller than shown here.
You can replay the same four computations interactively below — useful for testing yourself before moving on:
These four numbers are not a general ranking of "best to worst" optimizer — they depend entirely on the assumed history we plugged in (\(v_{t-1}, G_{t-1}, m_{t-1}, v_{t-1}\) for momentum/AdaGrad/Adam respectively). The point of the exercise is mechanical: given identical current gradients, each optimizer's memory reshapes the step differently. In real training, these histories emerge naturally from the trajectory taken so far.
7. Common Pitfalls
- Forgetting bias correction in a from-scratch Adam implementation. Without it, the first several updates are systematically too small, which can look like the optimizer is "warming up slowly" when it's really just a bug.
- Reusing AdaGrad's accumulator across an entire long training run. Because \(G_t\) never decays, AdaGrad is a poor choice once training runs for many epochs — prefer RMSProp or Adam.
- Treating \(\eta\) as the only thing that matters. With Adam, the effective per-parameter step size is \(\eta/(\sqrt{\hat v_t}+\epsilon)\), not \(\eta\) alone — tuning \(\eta\) without understanding this can be confusing.
- Assuming a fancier optimizer always trains faster. Adam converges quickly in a huge range of practical settings, but plain SGD with momentum, tuned carefully, sometimes generalizes better on some vision benchmarks — the "best" optimizer is empirical, not universal.
8. Summary
- Momentum accumulates a velocity from past gradients (\(v_t=\beta v_{t-1}-\eta\nabla_w L\)), accelerating consistent directions and damping oscillation.
- AdaGrad divides each parameter's step by the square root of its accumulated squared gradients, equalizing progress across axes of different curvature — but its learning rate only ever shrinks.
- RMSProp fixes that by using an exponential moving average of squared gradients instead of a running sum.
- Adam combines momentum (first-moment EMA) with RMSProp (second-moment EMA), plus bias correction to fix the zero-initialization problem — defaults \(\beta_1=0.9,\beta_2=0.999,\epsilon=10^{-8}\).
- All four optimizers still ultimately compute and use \(\nabla_w L\) from backpropagation (Lecture 8) — they only change how that gradient is turned into a step.
9. Code: Four Optimizers From Scratch
The script below implements plain SGD, Momentum, AdaGrad, RMSProp, and Adam as small, self-contained functions, and reproduces the exact worked comparison numbers from Section 6.
import numpy as np
def grad_L(w):
return 2 * (w - 3) # L(w) = (w-3)^2, same loss as Lecture 7
w0, lr, eps = 0.0, 0.1, 1e-8
g = grad_L(w0) # -6.0 at w=0
# ---- Plain SGD ----
w_sgd = w0 - lr * g
# ---- Momentum (v_t = beta*v_{t-1} - lr*grad ; w <- w + v_t) ----
beta = 0.9
v_prev = 0.5
v = beta * v_prev - lr * g
w_mom = w0 + v
# ---- AdaGrad (G_t = G_{t-1} + grad^2) ----
G_prev = 0.2
G = G_prev + g ** 2
w_ada = w0 - (lr / np.sqrt(G + eps)) * g
# ---- Adam (bias-corrected first & second moments) ----
beta1, beta2 = 0.9, 0.999
m_prev, v2_prev, t = 0.3, 0.1, 2
m = beta1 * m_prev + (1 - beta1) * g
v2 = beta2 * v2_prev + (1 - beta2) * g ** 2
m_hat = m / (1 - beta1 ** t)
v2_hat = v2 / (1 - beta2 ** t)
w_adam = w0 - lr * m_hat / (np.sqrt(v2_hat) + eps)
print(f"gradient at w=0: {g:.4f}")
print(f"Plain SGD -> w_new = {w_sgd:.4f}")
print(f"Momentum -> w_new = {w_mom:.4f} (v_t={v:.4f})")
print(f"AdaGrad -> w_new = {w_ada:.4f} (G_t={G:.4f})")
print(f"Adam -> w_new = {w_adam:.4f} (m_hat={m_hat:.4f}, v_hat={v2_hat:.4f})")
⬇ Download lecture-09-optimizers.py More resources for this lecture →