Mathematics for AI

Optimization

The gradient tells you which way to move. Optimization decides how far to move, and it is the difference between a model that trains and one that never does.

Read these first

On this page 9
  1. Why you should care
  2. What going wrong looks like
  3. The narrow gully problem
  4. Two fixes you have already felt
  5. Learning from a spoonful
  6. The honest part
  7. Where you have already used it
  8. Remember this
  9. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

Optimization is deciding how big a step to take, once you already know which way to go.

Think about reversing a car into a parking space. The back is drifting left, so the wheel needs to go right. You worked that out without effort.

How far to turn it is the hard part. Turn too much and you swing across the whole space. Turn too little and you shuffle back and forth for five minutes.

Why you should care

The previous lesson gave you the direction to move. This lesson is the other half, and in practice it is the half that decides whether training works.

The size of the step has a name: the learning rate. It is the single setting that ruins the most training runs.

Set it too large and the model gets worse and worse until the numbers stop being numbers. Set it too small and training takes a week to do a morning's work.

What going wrong looks like

   step too big                         step too small

   \    third        first  /           \                    /
    \                     /              \                  /
     \        second     /                \    • • • •      /
      \_________________/                  \________________/

   every jump lands further              many tiny steps,
   up the opposite side                  hardly any progress

With a step that is too large, you leap past the bottom and land higher up the other side. Next step you leap back, higher still. The model gets worse every round, which is a strange sight the first time you see it.

With a step that is too small, everything is stable and nothing happens. The loss inches down and your electricity bill does not.

The narrow gully problem

Real landscapes are not shaped like a bowl. They are shaped like a long, narrow gully: very steep across, almost flat along its length.

   looking down at the gully floor

      steep    |||||||||||||||||||||||||||    steep
      wall     |    ← you are here        |    wall
               |    the bottom is that    |
               |    way, far along  →     |

Now you have a problem. The steep walls force you to take small steps, or you bounce off them. But the direction you actually need to travel is the flat one, where small steps get you nowhere.

This is why plain step-by-step descent is slow on real problems, and the effect is not small. In the Developer block, two hundred steps cover only about half the ground needed.

Two fixes you have already felt

Momentum: let it roll. Push a bicycle and let go. It keeps moving. It does not stop dead in every dip in the road.

Momentum gives the search a memory of where it has been going. Along the flat gully floor, every step points the same way, so the movement builds up and gets faster. Across the gully, the bounces point opposite ways and cancel each other out.

A different step size for each setting. Think of the shower taps from the last lesson. The hot tap is fierce and the cold tap is feeble. You do not turn them by the same amount.

Modern optimisers watch how strongly each setting has been pushing recently. Settings that get pushed hard get smaller turns. Settings that barely move get larger ones. The most common one is called Adam, and it does both this and momentum together.

Learning from a spoonful

There is one more idea and it comes from statistics.

Working out the direction using every training example is accurate and slow. Using a handful of examples is rough and fast.

The rough direction is good enough. It points close to the true one. In a fixed amount of time, many rough steps beat one perfect step.

That handful is called a batch. Small batches are fast and jittery. Large batches are smooth and expensive. Almost everyone lands somewhere in the middle.

The honest part

There is no formula for the learning rate. People will tell you rules of thumb, and the rules of thumb are starting points, not answers. You try a few, watch the loss, and adjust. This is genuinely how it is done, at every laboratory on earth.

Getting stuck in a small dip is not the main danger. Books warn about it constantly. In a landscape with millions of directions, a point that is uphill in every single one is extremely rare.

The real trouble is different. Vast flat regions where the ground barely tilts. Places that go down one way and up another. Progress there is slow rather than blocked, which feels the same from the inside and is not.

Training loss going up is normal in short bursts. Each step judges the model on a different handful of examples. Look at the trend over hundreds of steps, not at any single one.

Where you have already used it

  • Reversing into a parking space, where you steer, look, correct, and steer less the second time.
  • Tuning an old radio dial, sweeping wide first and then finer.
  • Adding salt to a dish, in smaller pinches as you get closer.
  • A shower tap, where the same turn does far more on some fittings than others.

Remember this

  • The gradient gives direction. Optimization gives the size of the step.
  • Too large a step makes things worse; too small wastes time. There is no formula, so you try.
  • Momentum builds speed along a consistent direction, and adaptive methods give each setting its own step size.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install numpy

Everything below runs on a CPU in a second or two. The test problem is a two-dimensional bowl, stretched so that one direction is 500 times steeper than the other. That stretch is what makes real optimisation hard, and two dimensions are enough to see it.

Three optimisers on a narrow valley

valley.py
import numpy as np

A = np.array([1.0, 500.0])        # curvature on each axis: a long, very narrow valley
START = np.array([-5.0, 2.0])

def loss(p):
    return 0.5 * float(A @ (p * p))

def grad(p):
    return A * p

def plain(lr):
    def step(p, g, t, s):
        return p - lr * g
    return step

def momentum(lr, beta=0.9):
    def step(p, g, t, s):
        s["v"] = beta * s.get("v", np.zeros_like(p)) + g
        return p - lr * s["v"]
    return step

def adam(lr, b1=0.9, b2=0.999, eps=1e-8):
    def step(p, g, t, s):
        s["m"] = b1 * s.get("m", np.zeros_like(p)) + (1 - b1) * g
        s["v"] = b2 * s.get("v", np.zeros_like(p)) + (1 - b2) * g * g
        m_hat = s["m"] / (1 - b1 ** t)        # bias correction: without it the first steps stall
        v_hat = s["v"] / (1 - b2 ** t)
        return p - lr * m_hat / (np.sqrt(v_hat) + eps)
    return step

def run(step, steps=200):
    p, s = START.copy(), {}
    for t in range(1, steps + 1):
        p = step(p, grad(p), t, s)
    return p

print(f"start          : loss {loss(START):9.4f}   at [{START[0]:7.4f}, {START[1]:7.4f}]")
for name, step in (("plain    ", plain(0.0039)),
                   ("momentum ", momentum(0.002)),
                   ("adam     ", adam(0.05))):
    p = run(step)
    print(f"{name}      : loss {loss(p):9.6f}   at [{p[0]:7.4f}, {p[1]:7.4f}]")
Output
start          : loss 1012.5000   at [-5.0000,  2.0000]
plain          : loss  2.618703   at [-2.2885,  0.0001]
momentum       : loss  0.000611   at [-0.0349, -0.0000]
adam           : loss  0.003032   at [-0.0779,  0.0001]

Every one of those learning rates was tuned to be near the best available for that method. Plain descent is not being handicapped.

Look at the final positions, not only the losses. Plain descent finished at y = 0.0001, so the steep direction was solved within a few steps. It finished at x = -2.2885. Two hundred steps covered a little over half the distance along the flat direction.

That is the narrow gully from the Beginner block. Plain descent has one step size for both axes. The steep axis caps that step size, and the flat axis then starves.

Momentum reached x = -0.0349. Steps along the flat direction all point the same way. So s["v"] accumulates, and the effective step grows toward roughly 1 / (1 - beta) times the base rate. Across the valley, consecutive gradients point in opposite directions and cancel.

Adam reached x = -0.0779, using a learning rate more than twelve times the largest one plain descent tolerates. Dividing by sqrt(v_hat) normalises each coordinate by its own recent gradient size. A fierce coordinate and a feeble one then get comparable steps.

The bias correction lines matter more than they look. m and v start at zero, so early estimates are biased toward zero. Without dividing by 1 - b1**t and 1 - b2**t, the first hundred or so steps are far too small. Delete those two lines and rerun to see it.

Where the divergence threshold comes from

stepsize.py
import numpy as np

A = np.array([1.0, 500.0])
START = np.array([-5.0, 2.0])

def loss(p):
    return 0.5 * float(A @ (p * p))

def run(lr, steps=200):
    p = START.copy()
    for _ in range(steps):
        p = p - lr * (A * p)
    return p

print("stability limit for plain descent here is 2 / 500 = 0.004")
for lr in (0.0002, 0.002, 0.0039, 0.0041, 0.01):
    val = loss(run(lr))
    tag = "diverged" if not np.isfinite(val) or val > 1e6 else f"{val:12.6f}"
    print(f"  lr {lr:<7}: loss after 200 steps {tag}")
Output
stability limit for plain descent here is 2 / 500 = 0.004
  lr 0.0002 : loss after 200 steps    11.538862
  lr 0.002  : loss after 200 steps     5.612115
  lr 0.0039 : loss after 200 steps     2.618703
  lr 0.0041 : loss after 200 steps diverged
  lr 0.01   : loss after 200 steps diverged

This is not a rule of thumb. It is arithmetic.

On a quadratic, the largest usable learning rate is exactly 2 / L, where L is the largest curvature. Here L = 500, so the threshold is 0.004. At 0.0039 training works. At 0.0041 it blows up, and 0.0002 either side of the line is the whole difference.

A real loss surface has a curvature that changes as you move, so the threshold moves with it. That is why the honest procedure is to increase the rate until training breaks, then back off. It is also why a run can be stable for ten thousand steps and then explode.

A loss of nan is almost always this. Before you go looking for bad data, divide the learning rate by ten and rerun.

Why a small batch is good enough

batchnoise.py
import numpy as np

rng = np.random.default_rng(0)
X = rng.normal(size=(256, 3))
true_w = np.array([2.0, -1.0, 0.5])
y = X @ true_w + rng.normal(0.0, 0.5, size=256)
w = np.zeros(3)                       # gradients measured at the starting point

def gradient_on(idx):
    Xb, yb = X[idx], y[idx]
    return 2.0 * Xb.T @ (Xb @ w - yb) / len(idx)

full = gradient_on(np.arange(256))
print("full-batch gradient:", np.round(full, 4))
print()
print("  batch   average agreement   average size error")
for bs in (1, 4, 16, 64, 256):
    agree, err = [], []
    for _ in range(500):
        idx = rng.choice(256, size=bs, replace=False)
        g = gradient_on(idx)
        agree.append(float(g @ full / (np.linalg.norm(g) * np.linalg.norm(full))))
        err.append(float(np.linalg.norm(g - full) / np.linalg.norm(full)))
    print(f"  {bs:>5}   {np.mean(agree):>16.3f}   {np.mean(err):>17.3f}")
Output
full-batch gradient: [-3.9733  1.8482 -0.7943]

  batch   average agreement   average size error
      1              0.508               1.618
      4              0.802               0.893
     16              0.941               0.448
     64              0.987               0.209
    256              1.000               0.000

Agreement of 1.0 would mean pointing in exactly the same direction as the true gradient.

A batch of 16 out of 256 already agrees at 0.941, using one sixteenth of the compute. That is the whole argument for mini-batch training, in one row.

Read the error column too. It falls roughly by half each time the batch size is multiplied by four. That is the square-root law from statistics. Doubling batch size buys you a factor of 1.41, not 2. Compute cost doubles regardless.

Note that these gradients are all measured at the same starting point. In real training the parameters move between batches, and the noise interacts with that movement. The scaling law survives, but it is not the whole picture.

Common mistakes

1. Tuning the learning rate last. It matters more than architecture on most projects. Sweep it first, over powers of ten, then refine.

2. Using Adam with weight_decay and expecting L2 regularisation. In torch.optim.Adam the decay is added to the gradient, then divided by sqrt(v). Its strength therefore varies per parameter. Use torch.optim.AdamW, which applies decay directly to the weights.

3. Changing batch size without changing the learning rate. Larger batches give less noisy gradients and can take larger steps. The common starting point is to scale the rate linearly with batch size, with a warmup period.

4. No warmup on a transformer. Adam's v estimate is unreliable in the first few hundred steps, which produces huge early steps. A linear warmup over 500 to 2000 steps is standard. Skipping it is a common cause of an early nan.

5. Reading a jagged loss curve as a bug. Every step sees different data. Plot a running mean over 50 or 100 steps before drawing conclusions.

6. Leaving the learning rate constant to the end. Decaying it late lets the model settle instead of rattling around the minimum. Cosine decay is the usual default and costs nothing to add.

Try it yourself

In valley.py, change beta in momentum from 0.9 to 0.99 while keeping the same learning rate. Predict what happens before running it. A longer memory means larger effective steps, and there is a point past which it destabilises.

Then change A to np.array([1.0, 5.0]), a much rounder bowl, and rerun all three optimisers with their current rates. Watch the gap between plain descent and the others shrink. The advantage of momentum and Adam is a response to a badly stretched landscape, not a free upgrade.

What to learn next

Researcher — Mathematics and papers.

The problem and the classical rates

Minimise f(theta) over theta in R**n, with access to grad_f. Convergence guarantees depend entirely on which assumptions hold.

AssumptionsMethodRate
convex, L-smoothGD, eta = 1/L`f(x_t) - f* = O(L
convex, L-smoothNesterov`O(L
mu-strongly convex, L-smoothGD, eta = 1/Llinear, factor (1 - 1/kappa) per step
mu-strongly convex, L-smoothNesterovlinear, factor (1 - 1/sqrt(kappa))
L-smooth, non-convexGD`min_t
L-smooth, non-convex, stochasticSGD, decaying eta`E[

kappa = L / mu is the condition number. For the quadratic in the Developer block, L = 500 and mu = 1, so kappa = 500. Plain descent contracts the flat direction by 1 - eta * mu per step; with eta capped at 2/L, that is 1 - 2/kappa. Reaching a fixed accuracy therefore needs O(kappa) steps, while Nesterov needs O(sqrt(kappa)). The measured gap in that experiment is exactly this, and it widens as the landscape stretches.

The stability threshold eta < 2/L comes from the descent lemma; the derivation is in calculus.

Nesterov's lower bound (1983) says no first-order method beats O(1/t**2) on this class. Acceleration is therefore optimal, not an incremental gain.

Momentum

Polyak's heavy ball (1964) and Nesterov's accelerated gradient (1983) differ in where the gradient is evaluated:

heavy ball:  v_{t+1} = beta v_t + grad_f(x_t)          x_{t+1} = x_t - eta v_{t+1}
Nesterov:    v_{t+1} = beta v_t + grad_f(x_t - eta beta v_t)
             x_{t+1} = x_t - eta v_{t+1}

Nesterov evaluates the gradient at the look-ahead point, which damps overshoot and yields the accelerated rate on convex problems. Heavy ball achieves the same rate on quadratics. On general smooth strongly convex functions it can fail to converge at all (Lessard, Recht and Packard, 2016).

The effective step along a consistently-signed direction is amplified by 1 / (1 - beta), so beta = 0.9 multiplies it by roughly 10. That is why momentum and learning rate must be retuned together.

Adaptive methods

Adam (Kingma and Ba, 2015):

m_t = b1 m_{t-1} + (1 - b1) g_t                first moment, an average of recent gradients
v_t = b2 v_{t-1} + (1 - b2) g_t**2             second moment, elementwise
m_hat = m_t / (1 - b1**t)                      bias correction
v_hat = v_t / (1 - b2**t)
theta_t = theta_{t-1} - eta * m_hat / (sqrt(v_hat) + eps)

Bias correction is not cosmetic. With b2 = 0.999 and v_0 = 0, the uncorrected v_t is low by a factor of 1 - 0.999**t, which is about 0.001 at t = 1. Ignoring it produces enormous or vanishing early steps depending on which correction is dropped.

Three results are worth carrying:

  • Adam can fail to converge on convex problems. Reddi, Kale and Kumar (2018) constructed an online convex counterexample where the exponential moving average of v forgets a rare large gradient. AMSGrad, which uses a running maximum of v, fixes it in theory and is rarely used in practice.
  • Decoupled weight decay. Loshchilov and Hutter (2019) showed that adding lambda * theta to the gradient inside Adam gives per-parameter-scaled decay, which is not L2 regularisation. AdamW applies theta <- theta - eta * lambda * theta separately. This is now the default for transformer training.
  • eps placement matters. Adding it inside the square root instead of outside changes behaviour for small gradients, and implementations differ. Check the source before porting a hyperparameter between frameworks.

Adam is scale-invariant per coordinate. That is why it tolerates badly conditioned and heterogeneous parameter groups, such as embeddings and layer norms alongside dense weights.

On well-conditioned vision problems, well-tuned SGD with momentum still frequently generalises better (Wilson et al., 2017).

Batch size and gradient noise

For a mini-batch of size B drawn i.i.d., the gradient estimator is unbiased with covariance Sigma / B. The relative error therefore falls as 1/sqrt(B), which is the trend measured in the Developer block.

McCandlish et al. (2018) formalised the trade-off with the gradient noise scale B_crit = tr(Sigma) / (grad.T @ H @ grad), approximately tr(Sigma) / ||grad||**2. Below B_crit, doubling the batch roughly halves the number of steps needed. Above it, extra batch size buys almost nothing and wastes compute. B_crit grows during training as the gradient shrinks, which is the basis for batch-size ramps.

Goyal et al. (2017) established the linear scaling rule: multiply the learning rate by k when multiplying the batch by k. With a gradient warmup, they trained ImageNet at batch 8192 without accuracy loss.

The rule breaks down at very large batch sizes. LARS and LAMB (You et al., 2017, 2020) use layerwise trust ratios to push further.

The relevant scaling under SGD is the ratio eta / B, which controls the size of the stationary noise ball. Two runs with the same ratio behave similarly, which is a useful invariant when changing hardware.

Schedules

Warmup exists because early curvature estimates are unreliable. For Adam specifically, v_hat has high variance in the first steps; Liu et al. (2020) analysed this and proposed RAdam as a principled alternative to a hand-set warmup. Empirically, linear warmup over 500 to 4000 steps remains the standard for transformers. Its absence is a leading cause of early divergence.

Decay shrinks the noise ball as training proceeds. Cosine decay (Loshchilov and Hutter, 2017) is the common default. Fixed-budget schedules must be planned in advance, which motivated later work on schedule-free and warmup-stable-decay approaches.

Smith and Le (2018) argued that two moves are near-equivalent: decaying the learning rate, and increasing the batch size. Both reduce the same noise term. The choice between them is about hardware utilisation.

What actually stops progress

Local minima are not the problem. Dauphin et al. (2014) argued from random matrix theory that critical points in high dimensions are overwhelmingly saddles. Requiring all n Hessian eigenvalues to be positive is exponentially unlikely. Ge et al. (2015) showed noisy SGD escapes strict saddles — those with at least one strictly negative eigenvalue — in polynomial time.

Plateaus and conditioning are the problem. Long regions of small gradient dominate wall-clock time, and the condition number sets the step count.

Not every minimum is equally good. Keskar et al. (2017) reported that large-batch training finds sharper minima that generalise worse, though Dinh et al. (2017) showed sharpness as usually defined is not reparameterisation-invariant, so the argument needs care. Sharpness-Aware Minimisation (Foret et al., 2021) optimises the worst case within a neighbourhood. It improves generalisation, at roughly double the compute.

Cohen et al. (2021) documented the edge of stability. Full-batch gradient descent drives the largest Hessian eigenvalue to about 2/eta, then oscillates there. The loss keeps falling, non-monotonically. Classical smooth-optimisation theory does not describe this regime, and it is the regime real training occupies.

References

  • Nesterov, Y. Lectures on Convex Optimization, 2nd ed., Springer, 2018.
  • Polyak, B. T. "Some methods of speeding up the convergence of iteration methods." USSR Comp. Math. 4(5), 1964.
  • Robbins, H., Monro, S. "A Stochastic Approximation Method." Ann. Math. Statist. 22(3), 400–407, 1951.
  • Kingma, D. P., Ba, J. "Adam: A Method for Stochastic Optimization." ICLR, 2015. arXiv:1412.6980
  • Reddi, S. J., Kale, S., Kumar, S. "On the Convergence of Adam and Beyond." ICLR, 2018. arXiv:1904.09237
  • Loshchilov, I., Hutter, F. "Decoupled Weight Decay Regularization." ICLR, 2019. arXiv:1711.05101
  • Loshchilov, I., Hutter, F. "SGDR: Stochastic Gradient Descent with Warm Restarts." ICLR, 2017. arXiv:1608.03983
  • Goyal, P., et al. "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour." arXiv:1706.02677, 2017.
  • McCandlish, S., Kaplan, J., Amodei, D., et al. "An Empirical Model of Large-Batch Training." arXiv:1812.06162, 2018.
  • Smith, S. L., Le, Q. V. "Don't Decay the Learning Rate, Increase the Batch Size." ICLR, 2018. arXiv:1711.00489
  • Dauphin, Y., et al. "Identifying and attacking the saddle point problem." NeurIPS, 2014. arXiv:1406.2572
  • Keskar, N. S., et al. "On Large-Batch Training for Deep Learning." ICLR, 2017. arXiv:1609.04836
  • Foret, P., Kleiner, A., Mobahi, H., Neyshabur, B. "Sharpness-Aware Minimization." ICLR, 2021. arXiv:2010.01412
  • Wilson, A. C., et al. "The Marginal Value of Adaptive Gradient Methods in Machine Learning." NeurIPS, 2017. arXiv:1705.08292
  • Lessard, L., Recht, B., Packard, A. "Analysis and Design of Optimization Algorithms via Integral Quadratic Constraints." SIAM J. Optim. 26(1), 2016.
  • Cohen, J. M., et al. "Gradient Descent on Neural Networks Typically Occurs at the Edge of Stability." ICLR, 2021. arXiv:2103.00065
  • Liu, L., et al. "On the Variance of the Adaptive Learning Rate and Beyond" (RAdam). ICLR, 2020. arXiv:1908.03265

What to learn next