Advanced Optimizers: SGD, Momentum, RMSProp, Adam
Objectives
Section titled “Objectives”After this lesson, you will be able to:
- Identify the three fundamental weaknesses of standard gradient descent: slow progress through ill-conditioned ravines, oscillations driven by mini-batch noise, and a single shared learning rate across heterogeneous parameters.
- Formulate the update equations for Momentum, AdaGrad, RMSProp, and Adam, explaining the role of each constituent term.
- Explain why Adam requires bias correction, demonstrating its impact with step-1 calculations.
- Choose appropriate optimizers, base learning rates, and learning rate schedules when approaching new machine learning tasks.
Prerequisites
Section titled “Prerequisites”- Gradient Descent, in Machine Learning (Learned)
Intuition
Section titled “Intuition”Imagine rolling a heavy ball down a steep, narrow ravine whose floor gently slopes toward one end. The ball does not roll straight down the valley floor. It oscillates back and forth against the steep valley walls several times before gradually progressing along the bottom. Unlike gradient descent, however, a physical ball possesses momentum: the longer it rolls in a consistent direction, the faster it travels.
Standard gradient descent lacks momentum entirely. At each step, it only evaluates the instantaneous local slope under its feet. Consider a synthetic toy ravine to observe this limitation:
The minimum sits at . Curvature is 1 along and 100 along , yielding condition number : the valley is 10 times longer than it is wide (). We initialize optimization at .
Three Weaknesses of Standard Gradient Descent
Section titled “Three Weaknesses of Standard Gradient Descent”1. Narrow ravines. The steep slope along forces a tight step bound . Setting near the stability boundary, distance to the valley floor along scales by each step: oscillates wildly back and forth across the ravine floor (zigzagging). Along , distance scales by : each iteration advances by only 1.8%. Standard GD requires 369 iterations to enter a 0.01 radius around the origin, spending nearly all its steps crawling along the valley floor.
2. Mini-batch noise. In stochastic mini-batch settings, gradient estimates computed on small subsets fluctuate, causing search paths to jitter randomly. With fixed , SGD wanders perpetually around the minimum rather than settling at the bottom.
3. A single global learning rate for all parameters. At the starting point, the gradient vector is : the gradient along is almost 19 times larger than along . Under a single shared , parameters with steep gradients take massive strides while flat parameters crawl, despite the flat parameter needing to cover far greater distance (from to 0). In deep networks with millions of parameters, gradient magnitudes span several orders of magnitude.
Two Remedial Ideas
Section titled “Two Remedial Ideas”- Retain directional history. Accumulate historical gradients into a persistent “velocity”. Along directions where gradients point consistently, velocity accumulates (accelerating down the ravine). Along directions where gradients oscillate signs, opposing components cancel out (damping transversal oscillation). This is Momentum.
- Individualize learning rates. Scale each parameter’s step inversely by its typical historical gradient magnitude. Steep parameters are damped; flat parameters are boosted. This is the core mechanism of AdaGrad and RMSProp.
Adam unifies both concepts. The objective of this lesson is: engineer gradient update rules that navigate narrow ravines rapidly, dampen stochastic noise, and eliminate manual per-parameter learning rate tuning.
Theory
Section titled “Theory”Unified Notation
Section titled “Unified Notation”All algorithms in this lesson build directly on gradient descent. We index iterations starting from :
where is the gradient computed at parameters . Standard GD is expressed as . The optimizers below maintain internal auxiliary vectors of matching dimension, known as the optimizer state, initialized to .
From RMSProp onward, vector squares, square roots, and divisions operate element-wise: denotes the vector of squared elements , rather than a scalar dot product.
Momentum: Adding Velocity
Section titled “Momentum: Adding Velocity”Momentum (Polyak, 1964), also known as the heavy-ball method, maintains a persistent velocity vector :
The coefficient is the momentum factor, typically set to . The first equation retains fraction of previous velocity and adds the current gradient. The second equation updates parameters along velocity rather than raw gradient. When , , recovering standard GD.
What does velocity remember? Expanding recursively down to :
Velocity is an exponentially decayed sum of all past gradients. Consider two illustrative regimes:
- Consistently aligned gradients (along the valley floor), : the sum forms a geometric series , converging to . For , steps become 10 times longer than standard GD.
- Alternating gradients (across the ravine walls), : the sum evaluates to , converging to . Opposing components cancel out by roughly half.
On our toy ravine with , Momentum enters the 0.01 error ball in 111 steps versus 369 for GD. The trade-off is inertial overshoot: the heavy ball surges down the valley, overshooting the minimum to before reversing.
Alternative literature formulates velocity as an exponential moving average (EMA): . This scales velocity down by , meaning both formulations produce identical trajectories if the latter scales up by . Always verify which convention a codebase uses before adopting learning rates.
Nesterov accelerated gradient computes the gradient at the anticipated lookahead position rather than the current parameters:
If momentum is about to overshoot the basin, the lookahead gradient detects the opposing slope early and applies braking force. Scikit-learn’s MLPClassifier enables this by default (nesterovs_momentum=True).
AdaGrad and RMSProp: Per-Parameter Learning Rates
Section titled “AdaGrad and RMSProp: Per-Parameter Learning Rates”AdaGrad (Duchi, Hazan, Singer, 2011) accumulates squared gradients per parameter, dividing parameter updates by the square root of historical sum:
The scalar prevents division by zero. Parameter receives effective learning rate : historical steepness scales steps down, while rarely occurring sparse features receive larger steps. However, accumulator strictly increases monotonically, eventually causing effective learning rates to decay toward zero and freezing training before convergence.
RMSProp (Hinton, 2012) resolves AdaGrad’s premature freezing by replacing cumulative sums with an exponential moving average, allowing older gradients to be forgotten:
The decay rate is typically 0.9 or 0.99 (PyTorch default: 0.99). Here represents the root mean square (RMS) of recent gradients. If gradient magnitudes hover around , then and parameter step sizes evaluate to : roughly length regardless of raw gradient magnitude. At our toy ravine start, the gradient along is 19 times larger than , yet RMSProp advances both dimensions by equal step lengths.
A key property: scaling the loss function by scalar multiplies gradients by and by , leaving effective step sizes completely invariant. In standard GD, steps scale by , breaking stability.
At step 1, because , , giving . With , this evaluates to , inflating the initial step length to 10 times . Adam addresses this start-up artifact directly.
Adam: Combining Momentum with Adaptive Learning Rates
Section titled “Adam: Combining Momentum with Adaptive Learning Rates”Adam (Adaptive Moment Estimation; Kingma and Ba, 2015) maintains two concurrent exponential moving averages: first moments (like Momentum) and uncentered second moments (like RMSProp):
Vector estimates the expected gradient (first moment), while estimates the expected squared gradient (second moment). Because Adam tracks two state vectors, it requires auxiliary memory twice the parameter count. Both moments are corrected for zero-initialization bias:
The exponent denotes the power of time step . Default hyperparameters across the original paper, PyTorch, and scikit-learn are: , , , .
Why bias correction is essential. Because moving averages initialize at , early estimates are severely biased toward zero. At step 1 on our ravine with :
Uncorrected is only 10% of true gradient, while is shrunk to of true magnitude. Without correction, their ratio evaluates to : the denominator is suppressed far more aggressively than the numerator, blowing up the first step to . After bias correction, and , restoring unit ratio so each parameter steps exactly .
Why dividing by corrects bias. Assuming gradient remains constant , unrolling the recursion:
The raw average is scaled by ; dividing by this term recovers unbiased expectation . While dissipates its bias within tens of steps, recovers slowly: , meaning uncorrected is only 10% of true scale after 100 steps. Without correction, early steps remain amplified for hundreds of iterations.
On our ravine, Adam () converges in 193 steps—faster than GD (369), but behind Momentum (111) and RMSProp (103). Adam is not universally the fastest optimizer; its advantage lies in exceptional stability across diverse gradient scales.
Learning Rate Schedules
Section titled “Learning Rate Schedules”Under mini-batch noise, maintaining constant causes parameters to bounce around the minimum. Decaying over time stabilizes convergence:
- Step decay: Multiplies by 0.1 at fixed epoch milestones.
- Cosine decay: Decays from to along a cosine half-cycle—gentle at the start and finish, steepest in the middle.
- Warmup: Linearly ramps from near zero to during initial steps before decaying. This prevents unstable updates when parameter weights and optimizer moments are uncalibrated. Transformer and LLM training universally pair warmup with cosine decay.
Over total steps , cosine decay follows:
At , ; at , .
Practical Recommendations
Section titled “Practical Recommendations”| Setting | Recommended Starting Point |
|---|---|
| Deep neural networks, new problems | Adam (). Most forgiving baseline |
| Transformers and LLMs | AdamW with warmup and cosine decay |
| Computer vision CNNs (tuned) | SGD with Momentum 0.9 (or Nesterov) and step/cosine decay |
| Small linear models, tabular data | Standard solvers (Normal equation, L-BFGS in scikit-learn) |
AdamW (Loshchilov and Hutter, 2019) decouples weight decay from gradient updates. In standard Adam with L2 penalty, regularizing gradients are divided by , inadvertently penalizing large-gradient weights less. AdamW subtracts decay term directly outside the adaptive fraction, ensuring uniform shrinkage across all parameters.
Limitations
Section titled “Limitations”- No single optimizer dominates all benchmarks. On simple quadratic surfaces, Momentum and RMSProp often outperform Adam. Wilson et al. (2017) observed that well-tuned SGD with momentum can yield better test generalization than adaptive methods.
- First-order limitations. All four methods remain strictly first-order gradient algorithms, mitigating but not eliminating condition number bottlenecks. Feature scaling remains essential.
- Non-convex landscapes. Complex networks feature non-convex saddle points and local minima where global convergence guarantees do not hold.
Visualization
Section titled “Visualization”The diagram below plots loss contours for and the first 60 optimization steps starting from :
The second code cell animates this comparison across all four optimizers alongside log-loss trajectories.
Code Examples
Section titled “Code Examples”The three cells below run independently.
The first cell implements all four optimizers from scratch in NumPy, optimizing the toy ravine from until reaching a 0.01 error radius or 1,000 steps. A second run scales the loss by 10 (scale = 10) holding all learning rates constant.
Four optimizers on a narrow ravine
import numpy as np
c = np.array([1.0, 100.0]) # curvature along w1 and w2: a narrow valley
def run(name, eta, scale=1.0, beta1=0.9, beta2=0.999, rho=0.99, eps=1e-8): w = np.array([-8.0, 1.5]) # starting point v, s = np.zeros(2), np.zeros(2) # first and second moment for t in range(1, 1001): g = scale * c * w # gradient of scale * 0.5 * (w1^2 + 100 w2^2) if name == "GD": w = w - eta * g elif name == "Momentum": v = beta1 * v + g # velocity: accumulates past gradients w = w - eta * v elif name == "RMSProp": s = rho * s + (1 - rho) * g**2 # mean of squares w = w - eta * g / (np.sqrt(s) + eps) else: # Adam v = beta1 * v + (1 - beta1) * g s = beta2 * s + (1 - beta2) * g**2 v_hat = v / (1 - beta1**t) # bias correction s_hat = s / (1 - beta2**t) w = w - eta * v_hat / (np.sqrt(s_hat) + eps) dist = np.linalg.norm(w) # distance to the minimum (0, 0) if dist < 0.01 or dist > 1e6: break status = "diverged" if dist > 1e6 else f"{t:4d} steps" return status, scale * 0.5 * np.sum(c * w**2) # L when stopping
settings = [("GD", 0.018), ("Momentum", 0.018), ("RMSProp", 0.1), ("Adam", 0.1)]for scale in [1.0, 10.0]: # scale = 10: valley walls 10 times steeper print(f"scale = {scale:.0f}") for name, eta in settings: status, loss = run(name, eta, scale) print(f" {name:8s} eta = {eta}: {status}, L = {loss:.1e}")The first run downloads Python, about 10 MB.
With scale = 1, iteration counts match theory: GD requires 369, Momentum 111, RMSProp 103, and Adam 193 steps. When scale = 10, curvature along inflates to 1000, reducing GD’s stability threshold to ; consequently, causes both GD and Momentum to diverge. In contrast, RMSProp and Adam converge in the exact same number of steps, demonstrating scale invariance.
The second cell plots 100-step trajectory paths across contour ellipses (left) alongside log-loss convergence curves (right).
Optimization paths on contour plots
import matplotlib.pyplot as pltimport numpy as np
c = np.array([1.0, 100.0]) # L = 0.5 * (w1^2 + 100 w2^2), as above
def path(name, eta, steps=100, beta1=0.9, beta2=0.999, rho=0.99): w, v, s = np.array([-8.0, 1.5]), np.zeros(2), np.zeros(2) points = [w] for t in range(1, steps + 1): g = c * w if name == "GD": step = g elif name == "Momentum": v = beta1 * v + g step = v elif name == "RMSProp": s = rho * s + (1 - rho) * g**2 step = g / (np.sqrt(s) + 1e-8) else: # Adam, with bias correction v = beta1 * v + (1 - beta1) * g s = beta2 * s + (1 - beta2) * g**2 step = (v / (1 - beta1**t)) / (np.sqrt(s / (1 - beta2**t)) + 1e-8) w = w - eta * step points.append(w) return np.array(points)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))W1, W2 = np.meshgrid(np.linspace(-9, 3, 300), np.linspace(-2, 2, 300))levels = np.geomspace(0.05, 150, 12) # levels of L, evenly spaced in logax1.contour(W1, W2, 0.5 * (W1**2 + 100 * W2**2), levels, colors="0.8")for name, eta in [("GD", 0.018), ("Momentum", 0.018), ("RMSProp", 0.1), ("Adam", 0.1)]: p = path(name, eta) loss = 0.5 * (p[:, 0]**2 + 100 * p[:, 1]**2) # L at each step ax1.plot(p[:, 0], p[:, 1], ".-", ms=3, lw=1, label=name) ax2.semilogy(loss, label=name) print(f"{name:8s}: L after 100 steps = {loss[-1]:.1e}")ax1.plot(0, 0, "kx", ms=10) # minimumax1.set(xlabel="w1", ylabel="w2", title="Paths, 100 steps")ax2.set(xlabel="step", ylabel="loss", title="Loss (log scale)")ax1.legend(loc="lower left")plt.show()The first run downloads Python, about 10 MB.
After 100 iterations, RMSProp loss drops to , Momentum reaches , Adam sits at , and GD at . Momentum’s loss curve exhibits ripples from transversal bouncing. Adam smoothly hugs the axis without zigzagging. Note RMSProp’s large step-1 jump (length 1.0 rather than 0.1), confirming our theoretical analysis.
The third cell trains an MLPClassifier neural network (32 hidden neurons, ~2,400 parameters) on load_digits (1,797 handwritten digits) across 20 epochs with batch size 64 and , comparing SGD, Momentum, and Adam.
SGD, Momentum, and Adam on load_digits
import warnings
import matplotlib.pyplot as pltfrom sklearn.datasets import load_digitsfrom sklearn.exceptions import ConvergenceWarningfrom sklearn.model_selection import train_test_splitfrom sklearn.neural_network import MLPClassifier
# 20 epochs is deliberately few, so silence the "not converged" warningwarnings.filterwarnings("ignore", category=ConvergenceWarning)
X, y = load_digits(return_X_y=True) # 8x8 digit images, pixels 0..16X_train, X_test, y_train, y_test = train_test_split( X / 16, y, test_size=0.25, random_state=0) # scale pixels to [0, 1]
optimizers = { "SGD": dict(solver="sgd", momentum=0.0), "Momentum": dict(solver="sgd", momentum=0.9, nesterovs_momentum=False), "Adam": dict(solver="adam"), # beta1 = 0.9, beta2 = 0.999 by default}for name, options in optimizers.items(): # same network, same initialisation, same eta = 0.01, 64 images per batch model = MLPClassifier(hidden_layer_sizes=(32,), batch_size=64, learning_rate_init=0.01, max_iter=20, random_state=0, **options) model.fit(X_train, y_train) plt.plot(range(1, 21), model.loss_curve_, marker=".", label=name) loss, acc = model.loss_curve_[-1], model.score(X_test, y_test) print(f"{name:8s}: loss = {loss:.4f}, test acc = {acc:.3f}")
plt.xlabel("epoch")plt.ylabel("training loss")plt.yscale("log")plt.legend()plt.show()The first run downloads Python, about 10 MB.
After 20 epochs: SGD reaches loss 1.4786 with test accuracy 0.680; Momentum achieves loss 0.1815 and test accuracy 0.947; Adam drives loss down to 0.0321 with test accuracy 0.971.
Common Pitfalls
Section titled “Common Pitfalls”Quick Check
Section titled “Quick Check”Summary and Further Reading
Section titled “Summary and Further Reading”Key Takeaways:
- Standard GD crawls when condition number is large because steep directions constrain . On our ravine, GD requires 369 iterations.
- Momentum maintains velocity , accelerating consistent directions by while damping transversal oscillations, at the cost of inertial overshoot.
- RMSProp scales updates by the square root of exponentially decayed squared gradients, keeping step lengths near and conferring loss-scale invariance. AdaGrad accumulates sums indefinitely, freezing prematurely.
- Adam tracks both first and second moments, applying bias correction to prevent initial step blowup. Recommended default: Adam (or AdamW) with and a learning rate schedule.
Further Reading:
- Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Bài 8: Gradient Descent (phần 2/2). Clear Vietnamese introduction to Momentum and Nesterov with animated illustrations.
- Zhang, A., Lipton, Z. C., Li, M., Smola, A. J. (2023). Dive into Deep Learning, Chapter 12: Optimization Algorithms. Sections 12.6 through 12.11 provide derivations and code using matching notation.
- Goh, G. (2017). Why Momentum Really Works. Distill. Visual, interactive breakdown of momentum dynamics on quadratic surfaces.
- Ruder, S. (2016). An overview of gradient descent optimization algorithms. arXiv:1609.04747. Comprehensive survey of modern optimization methods.
- Original Papers: Kingma & Ba (2015), Adam: A Method for Stochastic Optimization; Duchi et al. (2011), Adaptive Subgradient Methods (AdaGrad); Hinton et al. (2012), RMSProp; Loshchilov & Hutter (2019), Decoupled Weight Decay Regularization (AdamW).
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About