EasyOptimization

Implement AdamW (Decoupled Weight Decay)

Optimization

Easy

Problem

Perform one AdamW step without bias correction. Update the first and second moments:

m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t

v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2

Then apply the adaptive update and decoupled weight decay:

w_t = w_{t-1} - \eta\frac{m_t}{\sqrt{v_t}+\varepsilon} - \eta\lambda w_{t-1}

Here, w contains parameters, g contains gradients, m and v are moment arrays, \eta is lr, \lambda is weight_decay, and \varepsilon is eps. Return new_w, new_m, and new_v in a dictionary of NumPy arrays.

Theory

Neural networks with millions of parameters can memorize training data instead of learning general patterns. Weight decay is a regularization technique that discourages the model from relying on any single parameter being very large.

The idea: at every training step, shrink all the weights slightly toward zero. If a weight is large, it gets penalized more. If a weight is near zero, it barely changes. This keeps the overall magnitude of the network's parameters in check.

Mathematically, weight decay adds a penalty proportional to the weight itself:

w_t = w_{t-1} - \eta \cdot \lambda \cdot w_{t-1}


L2 Regularization vs. Weight Decay

These two terms are often used interchangeably, but they are not the same thing when combined with adaptive optimizers like Adam. This distinction is the entire reason AdamW exists.

L2 regularization adds a penalty to the loss function:

L_{\text{total}} = L_{\text{original}} + \frac{\lambda}{2} \sum w_i^2

When you compute the gradient of this modified loss, the gradient becomes:

g_t^{\text{L2}} = g_t + \lambda \cdot w_{t-1}

The regularization term \lambda \cdot w_{t-1} gets mixed into the gradient. With vanilla SGD, this is equivalent to weight decay. But with Adam, it is not.

Weight decay directly shrinks the parameters, completely separate from the gradient:

w_t = w_{t-1} - \eta \cdot \lambda \cdot w_{t-1} - \eta \cdot \text{(gradient-based update)}

The decay happens independently, not through the gradient.


Why L2 + Adam Breaks

With Adam, the gradient gets divided by \sqrt{v_t}, where v_t is the running average of squared gradients. This is what gives Adam its adaptive learning rates.

When you use L2 regularization with Adam, the regularization term \lambda \cdot w gets mixed into the gradient before the adaptive scaling happens:

  1. Modified gradient: g_t^{\text{L2}} = g_t + \lambda \cdot w_{t-1}
  2. Second moment: v_t = \beta_2 v_{t-1} + (1 - \beta_2)(g_t^{\text{L2}})^2
  3. Update: w_t = w_{t-1} - \eta \cdot \frac{m_t}{\sqrt{v_t} + \epsilon}

The problem: the weight decay signal (\lambda \cdot w) is being divided by \sqrt{v_t}. For parameters with large gradients, v_t is large, so the decay effect is weakened. For parameters with small gradients, v_t is small, so the decay effect is amplified.

This means:


AdamW: The Fix

AdamW (proposed by Loshchilov and Hutter, 2019) solves this by decoupling weight decay from the gradient update. The weight decay is applied directly to the parameters, completely bypassing the adaptive scaling.

The three steps:

Step 1: Update first moment (same as Adam)

m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t

Step 2: Update second moment (same as Adam)

v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2

Step 3: Parameter update (different from Adam)

w_t = w_{t-1} - \eta \cdot \lambda \cdot w_{t-1} - \eta \cdot \frac{m_t}{\sqrt{v_t} + \epsilon}

Notice the update has two separate terms:

The weight decay now affects every parameter equally (proportional to its magnitude), regardless of its gradient history. This is the "decoupled" part.


A Concrete Example

Parameters: w = [1.0, -2.0], moments: m = [0.0, 0.0], v = [0.0, 0.0], gradient: g = [0.3, -0.7], \eta = 0.01, \lambda = 0.1, \beta_1 = 0.9, \beta_2 = 0.999

Step 1 (first moment):

Step 2 (second moment):

Step 3 (parameter update for w_1):

The weight decay (0.001) and gradient update (0.0316) are independent. The decay does not get scaled by the second moment.


Why It Matters in Practice

AdamW has become the default optimizer for training Transformers and large language models:

The practical benefits:


Special Case: Zero Weight Decay

When weight decay = 0, AdamW is identical to standard Adam. The weight decay term vanishes:

w_t = w_{t-1} - 0 - \eta \cdot \frac{m_t}{\sqrt{v_t} + \epsilon}

This makes AdamW a strict generalization of Adam. You can always use AdamW and set \lambda = 0 to recover Adam behavior.

Examples

Example 1

Input
w = [1.0, -2.0], m = [0.0, 0.0], v = [0.0, 0.0], grad = [0.3, -0.7], lr = 0.01, beta1 = 0.9, beta2 = 0.999, weight_decay = 0.1, eps = 1e-8
Output
{"new_w": [0.967377, -1.966377], "new_m": [0.03, -0.07], "new_v": [0.00009, 0.00049]}
Explanation
Adam's adaptive step and the independent decay term both change each parameter.

Example 2

Input
w = [5.0], m = [0.1], v = [0.01], grad = [0.2], lr = 0.01, beta1 = 0.9, beta2 = 0.999, weight_decay = 0.05, eps = 1e-8
Output
{"new_w": [4.986516], "new_m": [0.11], "new_v": [0.01003]}

Example 3

Input
w = [1.0, 2.0], m = [0.1, 0.2], v = [0.01, 0.04], grad = [0.0, 0.0], lr = 0.01, beta1 = 0.9, beta2 = 0.999, weight_decay = 0.1, eps = 1e-8
Output
{"new_w": [0.989995, 1.988995], "new_m": [0.09, 0.18], "new_v": [0.00999, 0.03996]}

Hints

  1. Update new_m and new_v with their exponential moving averages.
  2. Subtract both lr * new_m / (np.sqrt(new_v) + eps) and lr * weight_decay * w.

Requirements

Constraints

Starter Code

import numpy as np

def adamw_step(w: list, m: list, v: list, grad: list, lr: float = 0.001, beta1: float = 0.9, beta2: float = 0.999, weight_decay: float = 0.01, eps: float = 1e-8) -> dict:
    """
    Returns a dictionary with new_w, new_m, and new_v.
    """
    # Write code here
    pass

Test Cases

CaseMatches
First step with weight decaypublic
Single parameterpublic
Zero gradientpublic