Adam From First Principles

4 min read

Contents

We have built two kinds of memory.

Momentum remembers which direction gradients have been pointing. RMSProp remembers how large they have been.

Adam uses both. But there is one more problem to solve before combining them: both memories start at zero.

Two questions for every parameter

Let be the current gradient for one neural-network parameter.

The first question is: what direction has this gradient usually pointed recently?

Keep a running average:

The second question is: how large has this gradient been recently, regardless of sign?

Keep a running average of squares:

The letters change from the previous posts to match the Adam paper. Here, holds the signed average and holds the squared average.

The two values control separate memory lengths.

Statistically, these estimate a first moment and a second raw moment. Those mean an average gradient and an average squared gradient. The second raw moment is not the variance; variance subtracts the square of the mean.

Xiaohei carrying two optimizer gauges, one for recent direction and one for recent size, toward the same weight update crank.

Adam is built from two memories: one for where gradients have pointed and one for how large they have been.

Why starting at zero matters

Suppose every gradient is exactly 5. Set and .

Then:

We are trying to estimate 5, but the estimate begins much smaller.

This is not evidence that the gradients are small. It is an effect of initializing the average at zero.

For a constant gradient , expand the recurrence:

The geometric sum simplifies to:

So divide by the missing weight:

At the first step, .

Apply the same correction to the squared average:

This is bias correction. It removes the attenuation caused by the zero initialization. Under a stationary gradient distribution, it corrects the corresponding expectation. It does not remove sampling noise or make a changing gradient history perfectly current.

Now combine direction and scale

The update is:

Read the fraction from top to bottom.

The numerator supplies a smoothed direction. The denominator scales it using recent gradient magnitude.

The learning rate still controls the overall update scale. Adam adapts updates; it does not remove the learning-rate choice.

Its name comes from adaptive moment estimation.

One complete example

This trains a linear neuron to learn . We use full-data gradients to keep the optimizer state easy to inspect. Adam can also use mini-batch gradients.

import numpy as np

X = np.linspace(-1, 1, 20)
Y = 2 * X + 1
theta = np.array([0.0, 0.0])  # [weight, bias]
m = np.zeros_like(theta)
v = np.zeros_like(theta)
alpha = 0.05
beta1, beta2, epsilon = 0.9, 0.999, 1e-8

for t in range(1, 201):
    # Compute both derivatives BEFORE changing either parameter.
    error = theta[0] * X + theta[1] - Y
    g = np.array([np.mean(error * X), np.mean(error)])

    # Update the two persistent memories.
    m = beta1 * m + (1 - beta1) * g
    v = beta2 * v + (1 - beta2) * g ** 2

    # t counts parameter updates, starting at 1.
    m_hat = m / (1 - beta1 ** t)
    v_hat = v / (1 - beta2 ** t)
    step = alpha * m_hat / (np.sqrt(v_hat) + epsilon)
    theta -= step

    if t in (1, 2, 3, 10, 50, 100, 200):
        cost = 0.5 * np.mean((theta[0] * X + theta[1] - Y) ** 2)
        print(f"update={t}, cost={cost:.6f}")
        print("gradient:", g)
        print("m:", m, "v:", v)
        print("m_hat:", m_hat, "v_hat:", v_hat)
        print("step:", step, "[weight, bias]:", theta, "\n")

The learning rate 0.05 is chosen for this small demonstration. The original paper suggests 0.001 as a default for its tested problems, alongside , , and .

For a larger network, each parameter array gets matching m and v arrays. Keep them between batches and epochs. Increment t once per optimizer update, not once for every layer.

The final weights should approach 2 and 1. More importantly, the prints show how raw gradients become corrected, scaled updates.

Final Mental Model

Adam is a rule, not a guarantee.

It does not guarantee the best validation performance, a global minimum, or successful training with any learning rate.

What it gives us is a precise way to combine two useful histories and correct their startup attenuation.

We now know how to choose an update direction and adapt its scale per parameter.

One question remains: should the overall learning rate stay the same from the first update to the last?

Further reading: Kingma and Ba, Adam: A Method for Stochastic Optimization, especially Algorithm 1 and the bias-correction derivation.

Share