Type at least 2 letters. Enter opens the first result, Esc closes.

    Optimization · Lesson 6 · Optimizers for training neural networks

    Adaptive methods: AdaGrad, RMSProp, Adam and AdamW

    Intermediate 70 min 4 interactive figures 5 videos Colab notebook

    Before this lesson: momentum as a running sum of gradients (lesson 04), minibatch noise (lesson 05), and diagonal preconditioning by feature scaling (lesson 02). Time: about 70 minutes, plus the notebook.


    Why this matters #

    Adam (Kingma and Ba 2015) and AdamW (Loshchilov and Hutter 2019) train almost every large neural network: language models (GPT-3 and LLaMA used AdamW with β2=0.95\beta_2 = 0.95), vision transformers, diffusion models and weather models such as GraphCast. Small details of the update decide whether a run is stable: the value of ε\varepsilon, the bias correction, and whether weight decay goes through the gradient. Interviewers ask exactly these details, and you need them to debug an unstable run or to resume one correctly. Lesson 07 adds the learning-rate schedules, warmup and clipping that complete the recipe.

    Notation. In this lesson and the next one we count steps with t=1,2,…t = 1, 2, \dots, as in the Adam paper and the PyTorch documentation. θ\theta are the parameters, gtg_t the (minibatch) gradient at step tt, η\eta the learning rate (lr), λ\lambda the weight-decay coefficient. Squares, square roots and divisions of vectors act per coordinate.

    1. Predict first #

    2. The picture #

    Moving averages remember the recent past #

    Momentum (lesson 04) and Adam both use an exponential moving average (EMA):

    mt=β mt−1+(1−β) gt,m0=0.m_t = \beta\, m_{t-1} + (1 - \beta)\, g_t, \qquad m_0 = 0 .

    Unroll it: mt=(1−β)(gt+βgt−1+β2gt−2+…)m_t = (1-\beta)\big(g_t + \beta g_{t-1} + \beta^2 g_{t-2} + \dots\big). The weight of a gradient decays by the factor β\beta per step. The weights (1−β)βj(1-\beta)\beta^j add up to almost 1, and most of the weight sits on the last 1/(1−β)1/(1-\beta) steps: about 10 steps for β=0.9\beta = 0.9, 1000 steps for β=0.999\beta = 0.999. Components that keep their sign add up; components that flip sign cancel.

    The zero start is a problem. At t=1t = 1, m1=(1−β)g1m_1 = (1-\beta)g_1: for β=0.9\beta = 0.9 this is only a tenth of the gradient. Dividing by 1−βt1 - \beta^t fixes this: m^t=mt/(1−βt)\hat m_t = m_t/(1 - \beta^t) is the bias-corrected average, and m^1=g1\hat m_1 = g_1.

    Interactive figure: loading…

    Slide β\beta and watch the two averages of the noisy gradients (grey dots). The orange EMA without correction starts near 0 and needs about 1/(1−β)1/(1-\beta) steps to catch up; the aqua corrected average is right from the first step. With jump at t = 50, a large β\beta reacts slowly: long memory means smooth but late. The bars show the weight of each past gradient in the last average; the dashed line marks 1/(1−β)1/(1-\beta) steps back.

    Quick check

    β=0.9\beta = 0.9, m0=0m_0 = 0 and the first gradient is g1=2g_1 = 2. What are m1m_1 and the bias-corrected m^1\hat m_1?

    One step size per coordinate #

    Take f(x)=12(x12+100x22)f(x) = \tfrac12(x_1^2 + 100x_2^2). The gradient is (x1,100x2)(x_1, 100x_2). Gradient descent is stable only for η<2/100\eta < 2/100 (lesson 01), so x1x_1 moves at most 2% of its distance per step. Now divide each coordinate of the step by the size of its own gradient, as Adam does. The factor 100 cancels, and both coordinates move at the same speed. In matrix form the step is −ηD∇f-\eta D\nabla f with a diagonal matrix DD: a diagonal preconditioner, like feature scaling in lesson 02, but learned from the gradients during training.

    A diagonal preconditioner can only rescale the coordinate axes. Rotate the same valley by 45°45°: the steep and the flat direction are now diagonal. Both coordinates of the gradient mix the two directions and have similar sizes, so DD is close to a multiple of the identity, and nothing cancels.

    Interactive figure: loading…

    Race four optimizers. Gradient descent (blue), momentum (orange), RMSProp (aqua, dashed) and Adam (aqua, solid) start from the same point (drag it). On the axis-aligned quadratic, Adam and RMSProp go almost straight to the minimum. Switch to rotated 45°: with the same learning rates they lose their advantage and stall, while gradient descent is unchanged. Then try the saddle (Adam escapes in a few steps, because it rescales the tiny gradient along x2x_2), Rosenbrock and the double well, and turn on gradient noise. Each optimizer has its own learning-rate slider; Default learning rates resets them.

    Quick check

    Adam, with a well-tuned learning rate, quickly solves f(x)=12(x12+100x22)f(x) = \tfrac12(x_1^2 + 100x_2^2). You rotate the problem by 45°45°. What happens?

    3. Definitions and main results #

    The exponential moving average #

    Definition. For β∈[0,1)\beta \in [0, 1), the EMA of a sequence g1,g2,…g_1, g_2, \dots is mt=βmt−1+(1−β)gtm_t = \beta m_{t-1} + (1-\beta) g_t with m0=0m_0 = 0. Unrolled:

    mt=(1−β)∑i=1tβt−igi.m_t = (1-\beta)\sum_{i=1}^{t} \beta^{t-i} g_i .

    The weights (1−β)βt−i(1-\beta)\beta^{t-i} sum to (1−β)(1+β+⋯+βt−1)=1−βt(1-\beta)(1 + \beta + \dots + \beta^{t-1}) = 1 - \beta^t. Their "average age" is ∑j≥0j(1−β)βj=β/(1−β)\sum_{j \ge 0} j(1-\beta)\beta^j = \beta/(1-\beta), so the memory is about 1/(1−β)1/(1-\beta) steps.

    Heavy-ball momentum in PyTorch form, bt=βbt−1+gtb_t = \beta b_{t-1} + g_t (lesson 04), is the same average without the factor 1−β1-\beta: bt=mt/(1−β)b_t = m_t/(1-\beta). With β=0.9\beta = 0.9 the momentum buffer is 10 times the EMA, which is why a constant gradient gives the effective step ηg/(1−β)\eta g/(1-\beta).

    AdaGrad, RMSProp and Adam #

    All state starts at 0. Here are the PyTorch rules (without weight decay) and their defaults.

    AdaGrad (Duchi, Hazan and Singer 2011; η=10−2\eta = 10^{-2}, ε=10−10\varepsilon = 10^{-10}):

    st=st−1+gt2,θt=θt−1−η gtst+ε.s_t = s_{t-1} + g_t^2, \qquad \theta_t = \theta_{t-1} - \eta\,\frac{g_t}{\sqrt{s_t} + \varepsilon} .

    sts_t only grows. With gradients of steady size ∣g∣|g|, st=∣g∣t\sqrt{s_t} = |g|\sqrt t, so the step decays like η/t\eta/\sqrt t. This is good for convex problems with sparse features (rare features keep large steps), but too fast a decay for long nonconvex training.

    RMSProp (Hinton's lecture notes, 2012; η=10−2\eta = 10^{-2}, α=0.99\alpha = 0.99, ε=10−8\varepsilon = 10^{-8}):

    vt=αvt−1+(1−α) gt2,θt=θt−1−η gtvt+ε.v_t = \alpha v_{t-1} + (1-\alpha)\, g_t^2, \qquad \theta_t = \theta_{t-1} - \eta\,\frac{g_t}{\sqrt{v_t} + \varepsilon} .

    The sum of AdaGrad becomes an EMA, so old gradients are forgotten and the step does not decay to zero. RMSProp has no bias correction: v1=0.01 g12v_1 = 0.01\,g_1^2, so v1=0.1∣g1∣\sqrt{v_1} = 0.1|g_1| and the first step is 10 η10\,\eta in every coordinate.

    Adam (Kingma and Ba 2015; η=10−3\eta = 10^{-3}, β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, ε=10−8\varepsilon = 10^{-8}):

    mt=β1mt−1+(1−β1) gt,m^t=mt/(1−β1t),vt=β2vt−1+(1−β2) gt2,v^t=vt/(1−β2t),θt=θt−1−η m^tv^t+ε.\begin{aligned} m_t &= \beta_1 m_{t-1} + (1-\beta_1)\, g_t, & \hat m_t &= m_t/(1-\beta_1^t),\\ v_t &= \beta_2 v_{t-1} + (1-\beta_2)\, g_t^2, & \hat v_t &= v_t/(1-\beta_2^t),\\ \theta_t &= \theta_{t-1} - \eta\,\frac{\hat m_t}{\sqrt{\hat v_t} + \varepsilon} . && \end{aligned}

    Adam is RMSProp plus momentum (mm) plus bias correction. m^t\hat m_t estimates the mean of the gradient, v^t\hat v_t the mean of its square (the "uncentered variance"), so the ratio m^t/v^t\hat m_t/\sqrt{\hat v_t} is like a signal-to-noise ratio per coordinate.

    Method State per parameter Bias correction Effective step for a steady gradient
    SGD none – η ∣g∣\eta\,\lvert g\rvert
    AdaGrad ss – η/t\eta/\sqrt t
    RMSProp vv no η\eta (first step 10η10\eta)
    Adam mm, vv yes η\eta

    Proposition 1 (bias correction) #

    If the gradients are stationary, E[gi]=gˉ\mathbb E[g_i] = \bar g and E[gi2]=sˉ\mathbb E[g_i^2] = \bar s for all ii, then E[mt]=(1−β1t) gˉ\mathbb E[m_t] = (1-\beta_1^t)\,\bar g and E[vt]=(1−β2t) sˉ\mathbb E[v_t] = (1-\beta_2^t)\,\bar s. So m^t\hat m_t and v^t\hat v_t are unbiased.

    Proof.

    1. Unroll from m0=0m_0 = 0: mt=(1−β1)∑i=1tβ1t−igim_t = (1-\beta_1)\sum_{i=1}^{t}\beta_1^{t-i} g_i.
    2. Take expectations: E[mt]=(1−β1) gˉ∑j=0t−1β1j\mathbb E[m_t] = (1-\beta_1)\,\bar g\sum_{j=0}^{t-1}\beta_1^{j}.
    3. Geometric sum: (1−β1)∑j=0t−1β1j=1−β1t(1-\beta_1)\sum_{j=0}^{t-1}\beta_1^j = 1 - \beta_1^t.
    4. The same steps with gi2g_i^2 and β2\beta_2 give E[vt]=(1−β2t) sˉ\mathbb E[v_t] = (1-\beta_2^t)\,\bar s. Dividing by 1−βt1 - \beta^t removes the bias that the zero start creates. ■\blacksquare

    (Kingma and Ba add a small error term when E[g2]\mathbb E[g^2] drifts slowly.)

    Why the correction matters most for vv. With β2=0.999\beta_2 = 0.999, 1−β2t≈t/10001 - \beta_2^t \approx t/1000 for small tt, so vtv_t is far too small for hundreds of steps. Without any correction, at t=1t = 1: m1=0.1 g1m_1 = 0.1\,g_1 and v1=0.001 ∣g1∣≈0.0316 ∣g1∣\sqrt{v_1} = \sqrt{0.001}\,|g_1| \approx 0.0316\,|g_1|, so the step is about 3.16 η3.16\,\eta. With the correction, m^1=g1\hat m_1 = g_1 and v^1=g12\hat v_1 = g_1^2, so the first step is

    η g1∣g1∣+ε≈η sign⁡(g1):\eta\,\frac{g_1}{|g_1| + \varepsilon} \approx \eta\,\operatorname{sign}(g_1):

    size η\eta in every coordinate, whatever the scale of the gradient.

    Two consequences (Kingma and Ba §2.1).

    • Scale invariance. Multiply all gradients by c>0c > 0: m^\hat m and v^\sqrt{\hat v} both grow by cc, and the step does not change (as long as ε\varepsilon is negligible).
    • Bounded step. By Jensen's inequality, ∣Eg∣≤Eg2|\mathbb E g| \le \sqrt{\mathbb E g^2}, so typically ∣Δθt∣≲η|\Delta\theta_t| \lesssim \eta. When the gradient sign is noisy, ∣m^∣|\hat m| is much smaller than v^\sqrt{\hat v}, and the step shrinks automatically.
    Interactive figure: loading…

    Type a gradient (or pick an example) and read the first three steps of each method, in units of η\eta. SGD copies the gradient: steps of 1000η1000\eta and 0.001η0.001\eta. RMSProp (light aqua) starts with 10η10\eta; Adam without correction (grey) starts with about 3.16η3.16\eta; Adam (aqua) moves by η\eta in both coordinates. With (100, 0.01), then flip, the coordinate whose sign flips moves back by only 5% of η\eta. Set ε=1\varepsilon = 1 to see Adam lose its adaptivity.

    Quick check

    At the first step g1=(1000,0.001)g_1 = (1000, 0.001). Adam with η=10−3\eta = 10^{-3} and the default ε=10−8\varepsilon = 10^{-8}: what is the step −Δθ1-\Delta\theta_1?

    The roles of β2\beta_2 and ε\varepsilon #

    β2\beta_2. vv remembers about 1/(1−β2)1/(1-\beta_2) steps: 1000 for 0.9990.999. A long memory gives a smooth estimate, but a slow one: after a sudden rise of the gradient scale, v^\hat v lags behind and the steps exceed η\eta for a while. This is one reason for loss spikes in large runs. Many LLM and weather-model recipes use β2=0.95\beta_2 = 0.95 (GPT-3, LLaMA, GraphCast), with a memory of about 20 steps.

    ε\varepsilon. If v^≫ε\sqrt{\hat v} \gg \varepsilon, it does nothing. If v^≪ε\sqrt{\hat v} \ll \varepsilon, the update is about (η/ε) m^(\eta/\varepsilon)\,\hat m: momentum SGD with step η/ε\eta/\varepsilon. So a large ε\varepsilon makes Adam less adaptive, and coordinates with gradients below ε\varepsilon are not rescaled.

    Quick check

    You train with Adam and set ε=1\varepsilon = 1, while the gradients have size about 10−310^{-3}. What does the optimizer behave like?

    Weight decay and L2 regularization #

    Weight decay shrinks the weights by a fixed factor at every step: θ←(1−ηλ)θ−η dt\theta \leftarrow (1 - \eta\lambda)\theta - \eta\, d_t, where dtd_t is the step direction that the optimizer computes from the loss. L2 regularization adds λ2∥θ∥2\tfrac{\lambda}{2}\|\theta\|^2 to the loss, so its gradient λθ\lambda\theta is added to gtg_t.

    Proposition 2 (L2 equals weight decay for SGD, not for Adam) #

    1. Plain SGD. The L2 step is θ−η(∇f+λθ)=(1−ηλ)θ−η∇f\theta - \eta(\nabla f + \lambda\theta) = (1 - \eta\lambda)\theta - \eta\nabla f: exactly weight decay with the same λ\lambda (PyTorch form). In Loshchilov and Hutter's form, θ←(1−λ′)θ−η∇f\theta \leftarrow (1 - \lambda')\theta - \eta\nabla f, the two agree for λ′=ηλ\lambda' = \eta\lambda (their Proposition 1).
    2. Adam. The L2 gradient λθ\lambda\theta enters mm and vv and is divided by v^\sqrt{\hat v}. When λθ\lambda\theta is small next to the gradient noise, v^\sqrt{\hat v} is about the RMS of the gradient, and the per-step shrinkage is about ηλ/(v^+ε)\eta\lambda/(\sqrt{\hat v} + \varepsilon). Weights with large gradients are decayed less. For any preconditioner that is not a multiple of II, no L2 coefficient reproduces weight decay (their Proposition 2).

    (With momentum, SGD + L2 and SGD + weight decay are also no longer identical, because PyTorch's L2 term passes through the momentum buffer. The difference is small for small ηλ\eta\lambda.)

    AdamW takes the decay out of the normalization (PyTorch form):

    θt=θt−1(1−ηλ)−η m^tv^t+ε,\theta_t = \theta_{t-1}(1 - \eta\lambda) - \eta\,\frac{\hat m_t}{\sqrt{\hat v_t} + \varepsilon},

    where mm and vv are computed from the raw gtg_t. Every weight shrinks by the same factor 1−ηλ1 - \eta\lambda per step. PyTorch's default is λ=10−2\lambda = 10^{-2}. The paper multiplies its decay by the schedule multiplier, not by the base learning rate; PyTorch multiplies it by lr. In both, the decay follows the learning-rate schedule.

    Interactive figure: loading…

    Two groups of weights, no reason to move. All weights start at 1 and see zero-mean gradient noise: group A with RMS 10 (solid lines), group B with a smaller RMS (dashed). Only the decay moves them on average. With AdamW (aqua) both groups shrink alike, like (1−ηλ)t(1 - \eta\lambda)^t. With Adam + L2 (magenta), group A barely shrinks and group B shrinks fast, like exp⁡(−ηλt/RMS)\exp(-\eta\lambda t/\text{RMS}). Change the RMS of group B and the decay λ\lambda.

    Quick check

    For which optimizer is adding λ2∥θ∥2\tfrac{\lambda}{2}\|\theta\|^2 to the loss exactly the same as decaying the weights by 1−ηλ1 - \eta\lambda per step?

    Adam as sign descent, and its memory cost #

    With β1=β2=0\beta_1 = \beta_2 = 0 and ε=0\varepsilon = 0, Adam's step is η gt/∣gt∣=ηsign⁡(gt)\eta\,g_t/|g_t| = \eta\operatorname{sign}(g_t): sign descent. With the usual β\beta's Adam is a smoothed version of it: each coordinate moves by about η\eta in the direction of its recent average sign. This view explains its robustness to gradient scale and connects it to Lion (Chen et al. 2023), which uses the sign of a momentum average directly, and to the matrix optimizers of lesson 12.

    Adam stores two extra numbers per parameter (mm and vv). With mixed precision a parameter costs about 16 bytes: fp16 weight and gradient (2 + 2), and fp32 master weight, mm and vv (4 + 4 + 4) (Rajbhandari et al. 2020). This optimizer state is what ZeRO and FSDP shard across GPUs.

    Adam does not always converge

    Reddi, Kale and Kumar (2018, Theorem 3) show that for any constant β1<β2\beta_1 < \sqrt{\beta_2} there is a stochastic convex problem on which Adam does not converge to the optimum. In their example a rare, large gradient points the right way, but it also raises vv and so is damped; the frequent small gradients point the wrong way and get large steps once vv has forgotten the large one. Their fix, AMSGrad, uses max⁡(v^t,v^t−1)\max(\hat v_t, \hat v_{t-1}). In practice, a large β2\beta_2 and a decaying learning rate avoid the problem, and Adam with these settings is the default. Zhang et al. (2022) show that Adam converges when β2\beta_2 is large enough for the problem.

    Answers to section 1. (1) Both move by about 10−310^{-3}: Adam's first step is ηsign⁡(g1)\eta\operatorname{sign}(g_1) in every coordinate (0.0010.001 and 0.000999990.00099999 with ε=10−8\varepsilon = 10^{-8}). (2) The weight with small gradients. Inside Adam the L2 gradient is divided by v^\sqrt{\hat v}, which is large for the weight with large gradients, so that weight is barely regularized. AdamW decays both by the same factor.

    4. Worked example #

    (a) Two Adam steps by hand (defaults, ε\varepsilon negligible). Gradients g1=(100,0.01)g_1 = (100, 0.01), then g2=(−100,0.01)g_2 = (-100, 0.01).

    • t=1t = 1: m1=0.1 g1=(10,0.001)m_1 = 0.1\,g_1 = (10, 0.001) and v1=0.001 g12=(10,10−7)v_1 = 0.001\,g_1^2 = (10, 10^{-7}). Corrected: m^1=m1/0.1=(100,0.01)\hat m_1 = m_1/0.1 = (100, 0.01) and v^1=v1/0.001=(104,10−4)\hat v_1 = v_1/0.001 = (10^4, 10^{-4}). Step =η (100/100, 0.01/0.01)=η (1,1)= \eta\,(100/100,\ 0.01/0.01) = \eta\,(1, 1). Without correction: m1/v1=(10/3.162, 0.001/0.000316)=(3.16,3.16)m_1/\sqrt{v_1} = (10/3.162,\ 0.001/0.000316) = (3.16, 3.16).
    • t=2t = 2: m2=0.9 m1+0.1 g2=(9−10, 0.0009+0.001)=(−1,0.0019)m_2 = 0.9\,m_1 + 0.1\,g_2 = (9 - 10,\ 0.0009 + 0.001) = (-1, 0.0019) and v2=0.999 v1+0.001 g22=(9.99+10, 0.999⋅10−7+10−7)=(19.99, 1.999⋅10−7)v_2 = 0.999\,v_1 + 0.001\,g_2^2 = (9.99 + 10,\ 0.999\cdot 10^{-7} + 10^{-7}) = (19.99,\ 1.999\cdot10^{-7}). With 1−β12=0.191 - \beta_1^2 = 0.19 and 1−β22=0.0019991 - \beta_2^2 = 0.001999: m^2=(−5.263,0.01)\hat m_2 = (-5.263, 0.01) and v^2=(104,10−4)\hat v_2 = (10^4, 10^{-4}). Step =η (−5.263/100, 0.01/0.01)=η (−0.0526,1)= \eta\,(-5.263/100,\ 0.01/0.01) = \eta\,(-0.0526, 1).

    Gradients that differ by a factor 10410^4 give the same step. The coordinate whose sign flips moves back by only 5% of η\eta: the momentum still remembers the first gradient.

    (b) RMSProp's first step. v1=0.01 g12v_1 = 0.01\,g_1^2, so v1=0.1∣g1∣\sqrt{v_1} = 0.1|g_1| and the step is η g1/(0.1∣g1∣)=10 ηsign⁡(g1)\eta\,g_1/(0.1|g_1|) = 10\,\eta\operatorname{sign}(g_1). With η=10−2\eta = 10^{-2} (the default) this is 0.10.1 per coordinate, whatever the gradient.

    (c) L2 against decoupled decay, one step. θ0=2\theta_0 = 2, g=1g = 1, η=0.1\eta = 0.1, λ=0.1\lambda = 0.1.

    • SGD + L2: θ1=2−0.1 (1+0.1⋅2)=2−0.12=1.88\theta_1 = 2 - 0.1\,(1 + 0.1 \cdot 2) = 2 - 0.12 = 1.88.
    • SGD + weight decay: θ1=2 (1−0.01)−0.1=1.98−0.1=1.88\theta_1 = 2\,(1 - 0.01) - 0.1 = 1.98 - 0.1 = 1.88. Same.
    • Adam + L2: the gradient becomes g′=1+0.2=1.2g' = 1 + 0.2 = 1.2; the first Adam step is ηsign⁡(g′)=0.1\eta\operatorname{sign}(g') = 0.1, so θ1=1.90\theta_1 = 1.90. The decay disappeared in the normalization.
    • AdamW: θ1=2 (1−0.01)−0.1⋅1=1.88\theta_1 = 2\,(1 - 0.01) - 0.1 \cdot 1 = 1.88.

    (d) Many steps. η=10−3\eta = 10^{-3}, λ=10−2\lambda = 10^{-2}, zero-mean gradient noise, 20,000 steps.

    • AdamW shrinks every weight by 1−ηλ=1−10−51 - \eta\lambda = 1 - 10^{-5} per step: (1−10−5)20000≈e−0.2≈0.82(1 - 10^{-5})^{20000} \approx e^{-0.2} \approx 0.82.
    • Adam + L2 shrinks by about ηλ/RMS\eta\lambda/\text{RMS} per step: 10−610^{-6} for noise RMS 10, so e−0.02≈0.98e^{-0.02} \approx 0.98; and 10−410^{-4} for RMS 0.1, so e−2≈0.14e^{-2} \approx 0.14.

    A simulation with 500 weights per group (notebook, exercise 4) gives mean weights of about 0.980.98 and 0.140.14 for Adam + L2, and about 0.820.82 for both groups with AdamW.

    5. In code #

    The NumPy function below follows the PyTorch formulas exactly. It reproduces the hand computations of section 4.

    import numpy as np
    
    def adam_steps(grads, lr=1e-3, b1=0.9, b2=0.999, eps=1e-8,
                   wd=0.0, decoupled=False, theta0=0.0, correct=True):
        """Adam / AdamW exactly as in torch.optim; returns the parameters after each step."""
        theta = np.full(len(grads[0]), theta0, dtype=float)
        m, v, out = np.zeros_like(theta), np.zeros_like(theta), []
        for t, g in enumerate(grads, start=1):
            g = np.asarray(g, dtype=float)
            if decoupled:
                theta = theta * (1 - lr * wd)          # AdamW: decay outside the normalization
            else:
                g = g + wd * theta                     # Adam(weight_decay=...): L2 via the gradient
            m = b1 * m + (1 - b1) * g
            v = b2 * v + (1 - b2) * g ** 2
            m_hat = m / (1 - b1 ** t) if correct else m
            v_hat = v / (1 - b2 ** t) if correct else v
            theta = theta - lr * m_hat / (np.sqrt(v_hat) + eps)
            out.append(theta.copy())
        return out
    
    # (a) two Adam steps, in units of lr
    th = adam_steps([[100.0, 0.01], [-100.0, 0.01]])
    print(np.round(-th[0] / 1e-3, 6), np.round(-(th[1] - th[0]) / 1e-3, 6))
    # [1.       0.999999] [-0.052632  0.999999]        (0.999999: the effect of eps = 1e-8)
    print(np.round(-adam_steps([[100.0, 0.01]], correct=False)[0] / 1e-3, 4))
    # [3.1623 3.1622]                                   without bias correction: about 3.16 lr
    
    # (b) RMSProp's first step: v_1 = 0.01 g^2
    g = np.array([100.0, 0.01]); v1 = 0.01 * g ** 2
    print(g / np.sqrt(v1))                     # [10. 10.]
    
    # (c) one step from theta = 2 with g = 1, lr = 0.1, weight decay 0.1
    print(2 - 0.1 * (1 + 0.1 * 2))                                            # SGD + L2: 1.88
    print(adam_steps([[1.0]], lr=0.1, wd=0.1, theta0=2.0)[0])                  # Adam + L2: [1.9]
    print(adam_steps([[1.0]], lr=0.1, wd=0.1, decoupled=True, theta0=2.0)[0])  # AdamW: [1.88]
    

    The same check with PyTorch, where these optimizers are used in practice:

    import torch
    
    for name, opt_class in [("SGD + L2", torch.optim.SGD), ("Adam + L2", torch.optim.Adam),
                            ("AdamW", torch.optim.AdamW)]:
        p = torch.tensor([2.0], requires_grad=True)
        opt = opt_class([p], lr=0.1, weight_decay=0.1)
        p.grad = torch.tensor([1.0])
        opt.step()
        print(f"{name:9s} theta_1 = {p.item():.4f}")   # 1.8800, 1.9000, 1.8800
    

    6. Where it shows up #

    • Large language models. AdamW with a linear warmup and a cosine or warmup–stable–decay schedule (lesson 07) is the standard pretraining recipe. GPT-3 used β1=0.9\beta_1 = 0.9, β2=0.95\beta_2 = 0.95; LLaMA used AdamW with β2=0.95\beta_2 = 0.95 and weight decay 0.1. Weight decay is usually applied to weight matrices only, not to biases and normalization gains.
    • ML for science. GraphCast trained with AdamW, β2=0.95\beta_2 = 0.95 and weight decay 0.1 (Lam et al. 2023). Diffusion models usually train with AdamW; DiT used a constant learning rate of 10−410^{-4}, no weight decay, and an EMA of the weights for sampling. Physics-informed networks often run Adam first and then switch to L-BFGS (lesson 10).
    • Distributed training. Adam's state (mm, vv) is twice the size of the model. ZeRO and FSDP shard it across GPUs; 8-bit optimizers store it in low precision.
    • Sparse features. AdaGrad's per-feature steps suit problems with rare features, such as embeddings and linear models for click prediction.
    • Beyond diagonal. Adam fixes only axis-aligned scaling. Shampoo, SOAP and Muon (lesson 12) precondition whole weight matrices.

    7. Common mistakes #

    • torch.optim.Adam(weight_decay=...) is not AdamW. It adds an L2 term to the gradient. Use torch.optim.AdamW (recent PyTorch versions also accept decoupled_weight_decay=True in Adam).
    • Forgetting that PyTorch's AdamW decay is lr * weight_decay per step. Halving the learning rate also halves the decay.
    • Bias correction with the wrong tt. If mm and vv restart at 0 but tt keeps counting (a global step counter), the correction is about 1 and the first steps are several times too large: 3.16 η3.16\,\eta at the first step, and up to about 6.6 η6.6\,\eta near step 12 for a constant gradient. Counting from t=0t = 0 divides by 1−β0=01 - \beta^0 = 0.
    • Resuming a run without the optimizer state. mm, vv and tt restart, v^\hat v again comes from a few gradients, and the first steps are about η\eta in every coordinate, as at a start without warmup.
    • ε\varepsilon in a different place. The paper's "efficient" form puts ε^\hat\varepsilon next to the uncorrected vt\sqrt{v_t}, so the same number means a different ε\varepsilon in libraries that use it.
    • Expecting Adam to fix any ill-conditioning. A rotated valley stays ill-conditioned for a diagonal preconditioner.
    • Comparing optimizers at one learning rate. Each method has its own good range (Adam around 10−410^{-4} to 10−310^{-3}, SGD often 10 to 100 times larger); compare each at its tuned value.

    8. Check yourself #

    1. What is the size of RMSProp's first step (α=0.99\alpha = 0.99, ε≈0\varepsilon \approx 0)? And of AdaGrad's first step?
      AnswerRMSProp: v1=0.01 g12v_1 = 0.01\,g_1^2, so v1=0.1 ∣g1∣\sqrt{v_1} = 0.1\,|g_1| and the step is 10 ηsign⁡(g1)10\,\eta\operatorname{sign}(g_1). AdaGrad: s1=g12s_1 = g_1^2, so the step is ηsign⁡(g1)\eta\operatorname{sign}(g_1).
    2. Adam sees the same gradient g=(3,−0.5)g = (3, -0.5) at every step. What is its step at step tt?
      Answermt=(1−β1t)gm_t = (1-\beta_1^t)g and vt=(1−β2t)g2v_t = (1-\beta_2^t)g^2 exactly, so m^t=g\hat m_t = g, v^t=g2\hat v_t = g^2, and the step is ηsign⁡(g)=η (1,−1)\eta\operatorname{sign}(g) = \eta\,(1, -1) at every tt (up to ε\varepsilon).
    3. With constant gradients of size ∣g∣|g|, how does AdaGrad's step change with tt? Why is this a problem for long training runs?
      Answerst=t g2s_t = t\,g^2, so the step is η∣g∣/tg2=η/t\eta|g|/\sqrt{t g^2} = \eta/\sqrt t. It keeps shrinking even when the problem would allow large steps, so long nonconvex runs slow down. RMSProp's EMA forgets old gradients and keeps the step near η\eta.
    4. Plain SGD with η=0.05\eta = 0.05 and an L2 coefficient 10−410^{-4}. Which weight decay gives the same updates, in PyTorch form and in Loshchilov–Hutter form?
      AnswerThe per-step factor is 1−ηλ=1−5⋅10−61 - \eta\lambda = 1 - 5\cdot10^{-6}. PyTorch form θ(1−ηλ)\theta(1 - \eta\lambda): λ=10−4\lambda = 10^{-4}. Loshchilov–Hutter form θ(1−λ′)\theta(1 - \lambda'): λ′=ηλ=5⋅10−6\lambda' = \eta\lambda = 5\cdot10^{-6}.
    5. On f(x)=12(x12+100x22)f(x) = \tfrac12(x_1^2 + 100x_2^2) from x0=(1,1)x_0 = (1, 1), Adam (exact gradients) keeps x1=x2x_1 = x_2 at every step. Why?
      AnswerScale invariance per coordinate. While x1=x2x_1 = x_2, the gradient of coordinate 2 is 100 times that of coordinate 1 at every step, so its mm and its v\sqrt v are both 100 times larger, and the two steps are equal (up to ε\varepsilon). By induction this holds at every step.
    6. A colleague restarts training from a checkpoint but forgets to load the optimizer state. What happens in the first steps?
      Answermm, vv and tt restart at 0. After bias correction the first step is about ηsign⁡(g)\eta\operatorname{sign}(g) in every coordinate, even for coordinates that had tiny steps before. This is like a start without warmup and can cause a loss spike. Load the state, or warm up again.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. How many steps does an EMA with coefficient β\beta remember?

    2. Write the Adam update with bias correction.

    3. Why does Adam need bias correction?

    4. What is the size of Adam's first step in each coordinate?

    5. What is the size of RMSProp's first step with α=0.99\alpha = 0.99?

    6. Why does Adam not fix a valley rotated by 45°45°?

    7. What does a large ε\varepsilon do to Adam?

    8. Why is L2 regularization not weight decay inside Adam?

    9. Write the AdamW update (PyTorch form).

    Open the review →

    9. Interview angle #

    • "Adam or AdamW?" — Adam's weight_decay is an L2 term through the gradient and is divided by v^\sqrt{\hat v}; AdamW decays every weight by 1−ηλ1 - \eta\lambda, outside the normalization. Use AdamW.
    • "Why bias correction?" — mm and vv start at 0 and underestimate by the factor 1−βt1 - \beta^t; without correction the first step is about 3.16 η3.16\,\eta with the default betas.
    • "What does Adam actually adapt to?" — The per-coordinate scale of the gradient: it is a diagonal preconditioner diag⁡(1/(v^+ε))\operatorname{diag}(1/(\sqrt{\hat v} + \varepsilon)), invariant to rescaling each coordinate. It does not adapt to rotations or correlations.
    • "Why β2=0.95\beta_2 = 0.95 for LLMs?" — A shorter memory (about 20 steps) lets v^\hat v follow sudden changes of the gradient scale, so the steps do not exceed η\eta for long; this reduces instabilities.
    • "What is Adam's memory cost?" — Two extra tensors per parameter; about 16 bytes per parameter with mixed precision, which is what ZeRO and FSDP shard.

    10. Practice #

    The notebook for this lesson runs in Colab. It has five exercises, each with a check cell and a folded solution:

    1. Implement AdaGrad, RMSProp, Adam and AdamW steps in NumPy, exactly as the PyTorch formulas, and test them against the hand computations of section 4.
    2. Bias correction on a constant gradient: show that m^t=g\hat m_t = g for every tt, and measure the step without correction.
    3. The diagonal preconditioner on an axis-aligned and on a rotated quadratic: compute the condition number after rescaling, and run Adam on both.
    4. Adam + L2 against AdamW: simulate two groups of weights and compare their mean weights after 20,000 steps.
    5. Train a softmax regression on the scikit-learn digits with SGD, momentum and Adam for the same number of steps, and compare them at several learning rates.

    11. Watch and read #

    The EMA and its memory of about 1/(1 − β) steps.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Why the zero start biases the average, and the division by 1 − β^t.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Dividing by the root of an average of squared gradients, with the picture of an elongated valley.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Adam as momentum plus RMSProp plus bias correction, and its default hyperparameters.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The first part compares SGD, momentum, AdaGrad, RMSProp and Adam on the same pictures.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play