Roadmap / Optimizers & Schedules

✓

AdamW from Scratch

mediumoptimizers

The problem

AdamW (Adam with decoupled weight decay) is the optimizer behind practically every transformer in this roadmap. You will implement one step of it, exactly as PyTorch's torch.optim.AdamW does.

The idea, explained in detail on the theory page: every weight keeps two running averages, of its gradient (mm, the direction) and of its squared gradient (vv, the typical size). The step is their ratio, so every weight moves by about the learning rate, whatever the size of its gradient. Before the step, every weight is pulled a little towards zero (weight decay).

One step, in five lines

All operations are elementwise: every weight is handled on its own, with its own mm and vv.

1. Weight decay, a small pull of the parameters towards zero:

θ←θ−ηλθ\theta \leftarrow \theta - \eta \lambda \theta

2. Running average of the gradient:

m←β1m+(1−β1) gm \leftarrow \beta_1 m + (1 - \beta_1)\, g

3. Running average of the squared gradient:

v←β2v+(1−β2) g2v \leftarrow \beta_2 v + (1 - \beta_2)\, g^2

4. Bias correction, because mm and vv start at zero and are too small during the first steps:

m^=m1−β1t,v^=v1−β2t\hat m = \frac{m}{1 - \beta_1^t}, \qquad \hat v = \frac{v}{1 - \beta_2^t}

5. The step, applied to the parameters from line 1:

θ←θ−η m^v^+ϵ\theta \leftarrow \theta - \eta \, \frac{\hat m}{\sqrt{\hat v} + \epsilon}

where:

  • θ\theta (theta) is param and gg is grad;
  • mm and vv are the two running averages, all zeros before the first step;
  • η\eta (eta) is lr, λ\lambda (lambda) is weight_decay, ϵ\epsilon (epsilon) is eps, and (β1,β2)(\beta_1, \beta_2) are betas;
  • tt is the step number: 1,2,3,…1, 2, 3, \dots, and β1t\beta_1^t means β1\beta_1 to the power tt.

Example. Two parameters, param = [1.0, -2.0], with gradients of very different size, grad = [0.5, -50.0]. First step (t=1t = 1), with lr = 0.1, weight_decay = 0.01 and the default betas.

  1. Weight decay multiplies by 1−0.1⋅0.01=0.9991 - 0.1 \cdot 0.01 = 0.999: [0.999, -1.998].
  2. m=0.1⋅gm = 0.1 \cdot g = [0.05, -5.0].
  3. v=0.001⋅g2v = 0.001 \cdot g^2 = [0.00025, 2.5].
  4. Bias correction divides mm by 1−0.9=0.11 - 0.9 = 0.1 and vv by 1−0.999=0.0011 - 0.999 = 0.001: m^\hat m = [0.5, -50.0], v^\hat v = [0.25, 2500.0].
  5. m^/v^\hat m / \sqrt{\hat v} = [0.5 / 0.5, -50 / 50] = [1, -1], times lr: the step is [0.1, -0.1]. New param = [0.899, -1.898].

Both parameters moved by exactly lr, although the second gradient is 100 times larger. That is Adam's point: the step size does not depend on the scale of the gradient.

What to implement

adamw_step(param, grad, m, v, t, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01): return (new_param, new_m, new_v). The defaults are PyTorch's. Before the first step m and v are zeros and t = 1.

Requirements

  • Decoupled weight decay: it shrinks the parameters directly (line 1). It is not added to the gradient, so it never enters mm or vv.
  • Bias correction uses the step number tt, which starts at 1. With t=0t = 0, 1−β0=01 - \beta^0 = 0 and the correction divides by zero.
  • Do not modify the input arrays.

Hint

The example above is a good self-check: on the very first step, m^=g\hat m = g and v^=g2\hat v = g^2, so m^/v^\hat m / \sqrt{\hat v} is +1+1 or −1-1 for every parameter.

solution.py
Python idle

Press Run (⌘/Ctrl + Enter) to test your code.