Skip to content
  • Optimizers: Momentum, RMSProp, Adam · Deep Learning

Optimizers: Momentum, RMSProp, Adam

LearnViaNotes @learnvianotes· 3 min read

Plain SGD uses only the current gradient. Modern optimizers also remember past gradients — to build speed in consistent directions (momentum) and to give each parameter its own step size (RMSProp, Adam).

OptimizerUpdate (g = gradient)Idea
SGDθ ← θ − η·gStep against the gradient
Momentumv ← βv + g; θ ← θ − η·vA heavy ball: speeds up along consistent directions, damps zig-zag
NesterovGradient evaluated at the look-ahead point θ − ηβvMomentum that corrects earlier
RMSProps ← ρs + (1−ρ)g²; θ ← θ − η·g / (√s + ε)Divide by recent gradient size — per-parameter step
Adamm ← β₁m + (1−β₁)g; v ← β₂v + (1−β₂)g²; θ ← θ − η·m̂ / (√v̂ + ε)Momentum + RMSProp, with bias correction m̂ = m / (1−β₁ᵗ)
AdamWAdam + θ ← θ − η·λ·θ applied separatelyWeight decay decoupled from the adaptive step

Racing them on a narrow valley

f(x, y) = x² + 100y² is steep in y and flat in x — like many real loss surfaces. Plain SGD must keep its step below 0.01 to avoid bouncing out of the valley in y, and then crawls along x. All three start at (5, 1) and take 100 steps; the minimum is f = 0.

Python
import numpy as np

grad = lambda t: np.array([2 * t[0], 200 * t[1]])     # ∇ of f = x² + 100y²
f = lambda t: t[0] ** 2 + 100 * t[1] ** 2
start, steps = np.array([5.0, 1.0]), 100

def sgd(lr=0.009):                                    # above 0.01 it diverges in y
    t = start.copy()
    for _ in range(steps):
        t -= lr * grad(t)
    return f(t)

def momentum(lr=0.005, beta=0.9):
    t, v = start.copy(), np.zeros(2)
    for _ in range(steps):
        v = beta * v + grad(t)
        t -= lr * v
    return f(t)

def adam(lr=0.3, b1=0.9, b2=0.999, eps=1e-8):
    t, m, v = start.copy(), np.zeros(2), np.zeros(2)
    for k in range(1, steps + 1):
        g = grad(t)
        m = b1 * m + (1 - b1) * g
        v = b2 * v + (1 - b2) * g ** 2
        m_hat, v_hat = m / (1 - b1 ** k), v / (1 - b2 ** k)
        t -= lr * m_hat / (np.sqrt(v_hat) + eps)
    return f(t)

print(f"{sgd():.4f}")        # 0.6611 ← still far from the minimum
print(f"{momentum():.4f}")   # 0.0004
print(f"{adam():.4f}")       # 0.0007

In PyTorch

Python
import torch
from torch import nn

model = nn.Linear(10, 1)
opt_sgd = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, nesterov=True)
opt_adam = torch.optim.Adam(model.parameters(), lr=1e-3)
opt_adamw = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)

print(opt_adamw.defaults["betas"], opt_adamw.defaults["eps"])   # (0.9, 0.999) 1e-08
  1. optimizer.zero_grad()

    Clear old gradients — PyTorch accumulates them otherwise.

  2. loss.backward()

    Compute fresh gradients into each parameter's .grad.

  3. optimizer.step()

    Update the parameters using those gradients (and the optimizer's memory).

Adam vs AdamW

L2 regularisation adds λθ to the gradient. In Adam that term is then divided by √v̂, so parameters with large gradients get less regularisation — not what we want. AdamW applies the decay directly to the weights, outside the adaptive step. It's the default for Transformers.

ChooseWhen
AdamW (lr ≈ 1e-3 … 1e-4)Default for most work: Transformers, MLPs, fine-tuning
SGD + momentum 0.9 (lr ≈ 0.1) + scheduleCNNs on images, when you can tune — often generalises slightly better
AdamQuick prototypes; same as AdamW with weight_decay=0

Why does Adam need bias correction?

What does momentum do?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report