Skip to content

← Optimizers from Scratch step 3 of 4

Hard Primitives

Implement Adam Optimizer Step

Adam keeps two running statistics for each parameter: m averages the gradients, and v averages their squares. The second statistic measures squared magnitude, not variance about the mean. Together they determine an update scale for each parameter.

Starting these buffers at zero makes their early values smaller. Adam compensates by dividing each updated buffer by a bias-correction factor that depends on the step number.

Your task

Implement adam_step(weights, gradients, m, v, t, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8) for floating-point tensors of matching shape. The supplied m and v are the uncorrected buffers from before this step. v is nonnegative. t is the step being performed, starting at 1; it is not the number of already completed steps.

Compute these elementwise operations in order:

new_m = beta1 * m + (1 - beta1) * gradients
new_v = beta2 * v + (1 - beta2) * gradients ** 2
m_hat = new_m / (1 - beta1 ** t)
v_hat = new_v / (1 - beta2 ** t)
new_weights = weights - lr * m_hat / (sqrt(v_hat) + eps)

Use nonnegative lr, decay coefficients in [0, 1), and positive eps. Add epsilon after the square root. Do not add weight decay or other optimizer variants. Return a Python dictionary with keys "new_weights", "new_m", and "new_v", each containing a same-shaped tensor. Return the uncorrected updated buffers, not m_hat and v_hat, so they can be passed into the next step. Do not modify the input tensors.

For a first step with weight 1, gradient 0.1, zero buffers, and default parameters, new_m is 0.01 and new_v is 0.00001. Bias correction gives m_hat=0.1 and v_hat=0.01. The new weight is approximately 0.999. On the next call, use the returned buffers and t=2; do not reset them.

The gradients are already supplied. This task performs one arithmetic update, not loss evaluation, gradient calculation, or an optimizer loop.