Concept units

AdamW: steps that adapt

Intermediate

Read these first if they are new to you

The idea in one paragraph

AdamW is the most common way to take gradient-descent steps in modern networks. For each weight, it keeps a running average of recent gradients and a running average of their squares. It steps in the direction of the average gradient, divided by the typical gradient size. So every weight moves at about the same rate, whether its gradients are huge or tiny. Separately, AdamW shrinks every weight a little toward zero at each step. That shrinking is called weight decay, and AdamW keeps it out of the averages.

One learning rate cannot suit every weight

Plain gradient descent moves each weight by the learning rate times its gradient. In a network, gradients of different weights can differ by factors of thousands. Suppose one weight's gradient is 100 and another's is 0.01. With a learning rate of 0.001, the first moves by 0.1 and the second by 0.00001. A learning rate small enough for the first weight leaves the second one almost frozen.

The same problem appears in a long, narrow valley of the loss. The steep direction needs small steps. The shallow direction needs large ones.

Try it. Both paths start at the same point on a long, narrow valley, whose lowest point is the cross. Predict first: which path reaches the cross first? Then move the slider to watch them step.

Plain gradient descent and Adam in a long, narrow valley
plain gradient descent, η = 0.18Adam, η = 0.2

Plain gradient descent zigzags across the steep direction and creeps along the shallow one. Its learning rate cannot be raised much: above 0.2 it diverges in the steep direction. Adam sizes each direction's step separately. The loss is L = 0.05u2 + 5v2, with u across the page and v up the page.
Answer

Adam. After 60 steps it is within about 0.04 of the cross, with a loss near 0.006. Plain gradient descent has settled the steep direction but still has a gap of about 2.7 in the shallow direction, with a loss near 0.36.

Adam divides the average gradient by its typical size

Adam keeps two numbers for every weight, updated at every step from the new gradient g:

  • m, a running average of the gradient: m ← 0.9m + 0.1g;
  • v, a running average of the squared gradient: v ← 0.999v + 0.001g2.

A running average of this kind keeps most of its old value and mixes in a little of the new one. So it remembers recent values and slowly forgets old ones. The numbers 0.9 and 0.999 are Adam's usual settings, called β1 and β2.

Both averages start at 0, which makes them too small for the first steps. Adam corrects this by dividing by 1 − 0.9t and 1 − 0.999t at step t. The corrected values are written m̂ and v̂. The step is then:

step = η × m̂ / (√v̂ + ε)

√v̂ is the typical size of this weight's gradient. Dividing by it cancels the gradient's scale. ε is a tiny number, usually 10−8, that prevents dividing by zero.

Worked example: the first step for a huge and a tiny gradient

Take η = 0.001. One weight has gradient 100; another has gradient 0.01.

  1. Gradient 100: m = 0.1 × 100 = 10 and v = 0.001 × 1002 = 10. Corrected: m̂ = 10 / 0.1 = 100 and v̂ = 10 / 0.001 = 10,000, so √v̂ = 100. Step = 0.001 × 100 / 100 = 0.001.
  2. Gradient 0.01: m̂ = 0.01 and √v̂ = 0.01. Step = 0.001 × 0.01 / 0.01 = 0.001.

Both weights move by 0.001, although their gradients differ by a factor of 10,000. Plain gradient descent would have moved them by 0.1 and 0.00001.

The averaging also damps noise. If a weight's gradient keeps flipping sign, m stays near zero and the steps stay small. If the gradient keeps the same sign, the steps stay at about η:

adam_by_hand.py

import numpy as np

# eps is the ε above. 1e-8 means 0.00000001.
def adam_steps(grads, lr=0.001, b1=0.9, b2=0.999, eps=1e-8):
    m = v = 0.0                            # both averages start at 0
    for t, g in enumerate(grads, start=1):  # t counts the steps from 1
        m = b1 * m + (1 - b1) * g          # average gradient
        v = b2 * v + (1 - b2) * g * g      # average squared gradient
        m_hat = m / (1 - b1 ** t)          # correct the start-at-zero bias
        v_hat = v / (1 - b2 ** t)
        print(f"g = {g:6.2f}   step = {lr * m_hat / (np.sqrt(v_hat) + eps):.6f}")

adam_steps([5.0, 5.0, 5.0, 5.0])      # steady gradient
print()
adam_steps([5.0, -5.0, 5.0, -5.0])    # gradient that keeps flipping
g =   5.00   step = 0.001000
g =   5.00   step = 0.001000
g =   5.00   step = 0.001000
g =   5.00   step = 0.001000

g =   5.00   step = 0.001000
g =  -5.00   step = -0.000053
g =   5.00   step = 0.000336
g =  -5.00   step = -0.000053

The printed step is the amount subtracted from the weight. PyTorch's Adam gives the same first step for the worked example:

adam_torch.py

import torch

# Two weights whose gradients differ in size by a factor of 10,000.
w = torch.tensor([1.0, 1.0], requires_grad=True)
opt = torch.optim.Adam([w], lr=0.001)   # an Adam optimizer that updates w

w.grad = torch.tensor([100.0, 0.01])    # set the gradients by hand
opt.step()                              # take one Adam step
print(1.0 - w.detach())   # how far each weight moved
tensor([0.0010, 0.0010])

torch.tensor makes a PyTorch array, called a tensor. With requires_grad=True, the tensor keeps a gradient in .grad, as Automatic gradients in PyTorch explains. w.detach() gives the same values without gradient tracking, so the output holds only the values.

Weight decay shrinks every weight a little at every step

Weight decay multiplies every weight by a number just below 1 at each step. A weight that the loss does not need drifts toward zero. This keeps weights small, which tends to help a model work on new examples, not only on the ones it trained on. The strength is a number λ (lambda), often between 0.01 and 0.1.

AdamW applies the decay beside the Adam step, not through it. The W in the name stands for this "decoupled" weight decay. One AdamW step is:

wnew = w × (1 − ηλ) − η × m̂ / (√v̂ + ε)

AdamW keeps the shrinking out of the gradient averages
gradient g averages m and v Adam step η × m̂ / √v̂ weight w shrink w × (1 − ηλ) new weight shrunk − step blue: from the loss green: weight decay
The weight never enters the averages m and v. So every weight shrinks by the same fraction, ηλ, whatever its gradients look like.

The older way added λw to the gradient instead, before the averages. For plain gradient descent the two ways are the same. For Adam they are not. The added term would also be divided by √v̂, so weights with large gradients would hardly shrink, and weights with small gradients would shrink a lot. Loshchilov and Hutter proposed decoupling the decay in a paper published in 2019. In their tests it gave better results than Adam with the older decay, and AdamW has since become the usual choice.

Worked example: decay alone

Take η = 0.01, λ = 0.1 and a weight w = 2 whose gradient is zero. The Adam step is zero, so only the decay acts: wnew = 2 × (1 − 0.01 × 0.1) = 2 × 0.999 = 1.998.

Your turn. With η = 0.001 and λ = 0.01, a weight with zero gradient is multiplied by what each step? About how many steps halve it?

Answer

By 1 − 0.00001 = 0.99999. Halving takes about ln 2 / 0.00001 ≈ 69,000 steps. Weight decay is gentle; it matters over a long run.

adamw_decay.py

import torch

w = torch.tensor([2.0], requires_grad=True)
opt = torch.optim.AdamW([w], lr=0.01, weight_decay=0.1)

w.grad = torch.zeros(1)   # no pull from the loss at all
opt.step()
print(w.item())           # 2 * (1 - 0.01 * 0.1) = 1.998
1.9980000257492065

.item() turns the one-number tensor into a plain Python number. The printed value is 1.998 up to the rounding of 32-bit numbers. PyTorch's AdamW defaults are lr=0.001, betas=(0.9, 0.999), eps=1e-08 and weight_decay=0.01.

Details this page skips

"About η per step" holds only while a weight's gradients are steady; noisy gradients give smaller steps. Many programs turn weight decay off for biases and for the weights of normalization layers, by giving those parameters their own settings. PyTorch multiplies λ by the current learning rate, so a learning-rate schedule that lowers η also weakens the decay. Other libraries may scale the decay differently, so check before comparing settings.

Check yourself

  1. With η = 0.01, a weight's gradient is 0.5 on Adam's first step. How far does Adam move it? How far would plain gradient descent move it?

    Answer

    Adam: about 0.01, because the first step is η times g / |g|. Plain gradient descent: 0.01 × 0.5 = 0.005.

  2. A weight's gradient alternates +3, −3, +3, −3 for many steps. Does Adam move it a lot or a little? Why?

    Answer

    A little. The running average m of the gradient stays near zero, while √v̂ stays near 3, so each step is small. The weight is not being pushed consistently one way.

  3. Using AdamW with η = 0.002 and λ = 0.05, a weight is 4 and its Adam step this time is +0.002 (subtracted). What is the new weight?

    Answer

    Shrink: 4 × (1 − 0.002 × 0.05) = 4 × 0.9999 = 3.9996. Then subtract the step: 3.9996 − 0.002 = 3.9976.

  4. Why would adding λw to the gradient decay unevenly under Adam?

    Answer

    Adam divides everything in the gradient by √v̂. A weight with large gradients has a large √v̂, so its decay term is divided by a large number and nearly vanishes. AdamW avoids this by applying the decay outside the division.

Go further

Optional background, for readers who want more than this page.

Where this shows up

Units that build on this one