PROMETHEUS OS
NATIVE AI OPERATING SYSTEM
[INIT] Prometheus OS loading...
[CORE] Neural interface initializing...
[AI] LLM router connecting...
[MEM] Memory graph loading...
[AGT] Agent chains on standby...
[SEC] Security protocols active...
[NET] Provider mesh established...
[UI] Workspace rendering...
[DONE] All systems nominal.
Welcome to PROMETHEUS OS.
READY · OFFLINE · SECURE · PRIVATE
🎨 THEME ENGINE
PROMETHEUS OS · 23 THEMES
Trial version: Dark + Light themes only.
Full theme access with PROMETHEUS OS license.
Dark theme active
Experience all 23 themes in PROMETHEUS OS

Optimization for Machine Learning

Intermediate 20 min· 4 exercises

Train models with gradient-based optimizers you understand from the update equations up: choose and diagnose learning rates, use SGD, momentum, RMSProp, Adam and AdamW, apply warmup and cosine schedules and regularization, and implement mini-batch gradient descent in NumPy.

By the end of this lesson you will be able to
  • Explain loss landscapes, convexity, conditioning and saddle points, and how curvature limits the learning rate.
  • Implement gradient descent and mini-batch SGD and explain the effect of batch size on gradient noise.
  • Write the update equations of momentum, RMSProp, Adam and AdamW and compute an update by hand.
  • Distinguish L2 regularization from decoupled weight decay and choose correctly between them.
  • Compute learning rates under linear warmup and cosine decay schedules.
  • Diagnose common optimization failures from loss, gradient-norm and learning-rate curves.

Before this: Calculus and Gradients: How Networks Learn, Linear Algebra for AI

Every trained model is the output of an optimizer. The architecture decides what a model could represent; the optimizer, its learning rate and its schedule decide what it actually ends up representing, how long that takes, and whether training diverges at step 40,000 after burning a week of GPU time. This lesson builds the optimizers that matter in practice, from plain gradient descent to AdamW, with their exact update equations, the geometry that motivates each one, the schedules used to train modern networks, and a from-scratch implementation you can run.

Intuition: hiking down in fog

You are on a mountainside in thick fog and want to reach the valley floor. You cannot see the valley; you can only feel the slope under your feet. The obvious strategy is to step downhill, re-check the slope, and repeat. That is gradient descent. The step length is the learning rate: too short and you will be walking all week; too long and you overshoot the valley and end up higher on the opposite slope. If the valley is a long, narrow ravine, the steepest direction points mostly across the ravine rather than along it, so you zig-zag; a heavy ball that builds up speed (momentum) or boots that adapt their stride per direction (Adam) do better. And if your slope readings come from a noisy instrument (a mini-batch), you need smaller steps toward the end.

Loss landscapes

Training minimizes an empirical risk

L(θ)=1N∑i=1Nℓ(f(xi;θ),yi),L(\boldsymbol\theta) = \frac{1}{N}\sum_{i=1}^{N} \ell\big(f(\mathbf{x}_i; \boldsymbol\theta), y_i\big),

where θ∈Rd\boldsymbol\theta \in \mathbb{R}^d are the parameters, ff is the model, ℓ\ell is the per-example loss (from the previous lesson) and NN is the number of training examples. The loss landscape is the surface of LL over parameter space. Near any point, the gradient ∇L\nabla L gives the slope and the Hessian H=∇2LH = \nabla^2 L (the d×dd \times d matrix of second derivatives) gives the curvature. The second-order Taylor expansion summarizes local geometry:

L(θ+δ)≈L(θ)+∇L⊤δ+12δ⊤Hδ.L(\boldsymbol\theta + \boldsymbol\delta) \approx L(\boldsymbol\theta) + \nabla L^\top \boldsymbol\delta + \tfrac12 \boldsymbol\delta^\top H \boldsymbol\delta.

The eigenvalues of HH (from the linear algebra lesson) are the curvatures along its eigenvector directions. The ratio of largest to smallest, the condition number κ=λmax⁡/λmin⁡\kappa = \lambda_{\max}/\lambda_{\min}, measures how elongated the valley is.

Convexity

A function is convex if the line segment between any two points on its graph lies on or above the graph: L(ta+(1−t)b)≤tL(a)+(1−t)L(b)L(t\mathbf{a} + (1-t)\mathbf{b}) \le tL(\mathbf{a}) + (1-t)L(\mathbf{b}) for t∈[0,1]t \in [0,1]. For twice-differentiable functions this is equivalent to the Hessian being positive semidefinite everywhere. Convexity's great gift: every local minimum is a global minimum, and for smooth convex functions gradient descent with a suitable learning rate is guaranteed to converge to the minimum value. Linear regression with squared error and logistic regression with cross-entropy are convex in their parameters.

Neural networks are not convex. Their landscapes contain many minima (often equivalent, because permuting hidden units gives the same function), flat regions, and saddle points: stationary points (∇L=0\nabla L = 0) that curve up in some directions and down in others, like a horse saddle, e.g. f(x,y)=x2−y2f(x, y) = x^2 - y^2 at the origin. In very high dimensions, a random stationary point is far more likely to be a saddle than a minimum, because a minimum requires every one of millions of curvatures to be positive. In practice, noisy gradients and momentum usually carry training past saddles, and the minima that large networks reach typically have similar, good loss values, so non-convexity is less catastrophic than it sounds. It does mean that results depend on initialization, data order and hyperparameters, which is why you train several seeds.

Gradient descent and the learning rate

Gradient descent (GD) repeats

θt+1=θt−η ∇L(θt),\boldsymbol\theta_{t+1} = \boldsymbol\theta_t - \eta\, \nabla L(\boldsymbol\theta_t),

with learning rate η>0\eta > 0.

Worked example: why the learning rate has a ceiling. Take L(w)=w2L(w) = w^2, so ∇L=2w\nabla L = 2w and the update is wt+1=wt−2ηwt=(1−2η) wtw_{t+1} = w_t - 2\eta w_t = (1 - 2\eta)\,w_t. Starting at w0=1w_0 = 1:

η\etafactor 1−2η1 - 2\etaw1,w2,w3w_1, w_2, w_3behaviour
0.10.80.8, 0.64, 0.512smooth convergence
0.500, 0, 0one-step convergence
0.9−0.8−0.8, 0.64, −0.512oscillating convergence
1.0−1−1, 1, −1bounces forever
1.1−1.2−1.2, 1.44, −1.728diverges

In general, for a quadratic with curvature (second derivative) λ\lambda, GD is stable only if η<2/λ\eta < 2/\lambda. With many directions, the largest curvature sets the ceiling, η<2/λmax⁡\eta < 2/\lambda_{\max}, while the smallest curvature sets the speed: progress along that direction shrinks the error by a factor ∣1−ηλmin⁡∣|1 - \eta\lambda_{\min}| per step. With the best fixed rate, the number of steps needed grows roughly in proportion to κ\kappa. For L(x,y)=12(x2+25y2)L(x, y) = \tfrac12(x^2 + 25y^2), the curvatures are 1 and 25, so η<0.08\eta < 0.08, and along xx each step only shrinks the error by a factor of at least 0.920.92. That is the narrow-ravine problem in numbers.

Stochastic gradient descent and mini-batches

Computing ∇L\nabla L over all NN examples per step is expensive. Stochastic gradient descent (SGD) instead uses a mini-batch B\mathcal{B} of BB random examples:

gt=1B∑i∈B∇θ ℓ(f(xi;θt),yi),θt+1=θt−η gt.\mathbf{g}_t = \frac{1}{B}\sum_{i \in \mathcal{B}} \nabla_{\boldsymbol\theta}\, \ell\big(f(\mathbf{x}_i;\boldsymbol\theta_t), y_i\big), \qquad \boldsymbol\theta_{t+1} = \boldsymbol\theta_t - \eta\,\mathbf{g}_t.

gt\mathbf{g}_t is an unbiased estimate of the full gradient, and by the central-limit reasoning of the probability lesson its noise standard deviation shrinks like 1/B1/\sqrt{B}. One pass over the data is an epoch. Trade-offs:

  • Larger batches use hardware better and give smoother gradients, but each example contributes less progress, and beyond some batch size returns diminish.
  • Smaller batches are noisier; that noise helps escape saddles and is often associated with better generalization, but requires smaller learning rates.
  • A common heuristic when scaling batch size kk-fold is to scale the learning rate kk-fold too (the linear scaling rule), combined with warmup; Goyal et al. (2017) used this to train ResNet-50 on ImageNet with 8,192-image batches.
  • When memory limits the batch, gradient accumulation sums gradients over several micro-batches before one update, emulating a larger batch.

Momentum

Momentum keeps a running velocity v\mathbf{v} that accumulates past gradients:

vt+1=β vt+gt,θt+1=θt−η vt+1,\mathbf{v}_{t+1} = \beta\,\mathbf{v}_t + \mathbf{g}_t, \qquad \boldsymbol\theta_{t+1} = \boldsymbol\theta_t - \eta\,\mathbf{v}_{t+1},

with β\beta typically 0.9. In directions where gradients consistently agree (along the ravine floor), the velocity builds up to about 1/(1−β)=101/(1 - \beta) = 10 times a single gradient; in directions where they flip sign (across the ravine), they cancel. Momentum thus speeds up progress on ill-conditioned problems and averages out mini-batch noise. Nesterov momentum evaluates the gradient at the look-ahead point θt−ηβvt\boldsymbol\theta_t - \eta\beta\mathbf{v}_t, which often reduces overshoot.

RMSProp: per-parameter step sizes

Different parameters see gradients of wildly different magnitudes (embedding rows for rare tokens vs. common ones; first vs. last layers). RMSProp divides each coordinate's step by a running root-mean-square of its recent gradients:

st+1=ρ st+(1−ρ) gt2,θt+1=θt−ηst+1+ϵ gt,\mathbf{s}_{t+1} = \rho\,\mathbf{s}_t + (1 - \rho)\,\mathbf{g}_t^{2}, \qquad \boldsymbol\theta_{t+1} = \boldsymbol\theta_t - \frac{\eta}{\sqrt{\mathbf{s}_{t+1}} + \epsilon}\,\mathbf{g}_t,

where squares, square roots and division are elementwise, ρ≈0.9\rho \approx 0.9–0.990.99, and ϵ\epsilon (e.g. 10−810^{-8}) prevents division by zero. Coordinates with large gradients take smaller steps and vice versa, which partly undoes poor conditioning when it is aligned with the coordinate axes.

Adam and AdamW

Adam (Kingma and Ba, ICLR 2015) combines momentum (a first-moment estimate) with RMSProp-style scaling (a second-moment estimate) and corrects both for their initialization at zero. With m0=v0=0\mathbf{m}_0 = \mathbf{v}_0 = \mathbf{0} and step counter t=1,2,…t = 1, 2, \dots:

mt=β1mt−1+(1−β1) gtvt=β2vt−1+(1−β2) gt2m^t=mt/(1−β1t),v^t=vt/(1−β2t)θt=θt−1−η m^t/(v^t+ϵ)\begin{aligned} \mathbf{m}_t &= \beta_1 \mathbf{m}_{t-1} + (1 - \beta_1)\,\mathbf{g}_t \\ \mathbf{v}_t &= \beta_2 \mathbf{v}_{t-1} + (1 - \beta_2)\,\mathbf{g}_t^2 \\ \hat{\mathbf{m}}_t &= \mathbf{m}_t / (1 - \beta_1^t), \qquad \hat{\mathbf{v}}_t = \mathbf{v}_t / (1 - \beta_2^t) \\ \boldsymbol\theta_t &= \boldsymbol\theta_{t-1} - \eta\, \hat{\mathbf{m}}_t / (\sqrt{\hat{\mathbf{v}}_t} + \epsilon) \end{aligned}

Defaults from the paper: β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, ϵ=10−8\epsilon = 10^{-8}. Many large-language-model recipes lower β2\beta_2 (values around 0.95 are common) so the second-moment estimate reacts faster to gradient spikes.

Worked example: Adam's first step. One parameter, g1=0.5g_1 = 0.5, η=10−3\eta = 10^{-3}.

  1. m1=0.9⋅0+0.1⋅0.5=0.05m_1 = 0.9 \cdot 0 + 0.1 \cdot 0.5 = 0.05; v1=0.999⋅0+0.001⋅0.25=0.00025v_1 = 0.999 \cdot 0 + 0.001 \cdot 0.25 = 0.00025.
  2. Bias correction: m^1=0.05/(1−0.9)=0.5\hat m_1 = 0.05/(1 - 0.9) = 0.5; v^1=0.00025/(1−0.999)=0.25\hat v_1 = 0.00025/(1 - 0.999) = 0.25.
  3. Step: η⋅0.5/(0.25+10−8)≈10−3⋅1=10−3\eta \cdot 0.5/(\sqrt{0.25} + 10^{-8}) \approx 10^{-3} \cdot 1 = 10^{-3}.

Without bias correction, the step would have been 10−3⋅0.05/0.00025≈3.2×10−310^{-3} \cdot 0.05 / \sqrt{0.00025} \approx 3.2 \times 10^{-3}, distorted by the zero initialization. Note also that the step size was η\eta regardless of the gradient's magnitude: Adam's updates are roughly scale-invariant, so η\eta is approximately the maximum per-parameter change per step. That makes it much easier to tune than SGD across layers and architectures.

Weight decay and AdamW. L2 regularization adds λ2∥θ∥22\frac{\lambda}{2}\|\boldsymbol\theta\|_2^2 to the loss, contributing λθ\lambda\boldsymbol\theta to the gradient. For plain SGD this is identical to weight decay, i.e. shrinking the weights by a factor (1−ηλ)(1 - \eta\lambda) each step. For Adam it is not: the λθ\lambda\boldsymbol\theta term gets divided by v^\sqrt{\hat{\mathbf v}}, so weights with large gradient history are barely regularized. Loshchilov and Hutter's AdamW ("Decoupled Weight Decay Regularization") applies the decay directly to the weights, outside the adaptive scaling:

θt=θt−1−η(m^tv^t+ϵ+λ θt−1),\boldsymbol\theta_t = \boldsymbol\theta_{t-1} - \eta\left( \frac{\hat{\mathbf{m}}_t}{\sqrt{\hat{\mathbf{v}}_t} + \epsilon} + \lambda\,\boldsymbol\theta_{t-1} \right),

where gt\mathbf{g}_t inside m\mathbf{m} and v\mathbf{v} is the gradient of the unregularized loss. AdamW is the default optimizer for training transformers. Typical practice exempts biases and normalization-layer parameters from weight decay. (The formula above follows PyTorch's torch.optim.AdamW, where λ\lambda is multiplied by the full learning rate η\eta. In Loshchilov and Hutter's paper the decay is multiplied only by the schedule multiplier, not by the base step size, so the same nominal λ\lambda means different things across implementations.)

Learning-rate schedules

A fixed learning rate is rarely best. Two ideas dominate:

  • Warmup: start near zero and increase linearly over the first WW steps. Early in training, Adam's second-moment estimates are unreliable and the network is far from any reasonable region, so large steps can blow up; warmup is standard for transformers.
  • Decay: reduce the rate later so the noisy SGD iterates settle into a minimum. Step decay (divide by 10 at fixed epochs) was common for CNNs; cosine decay (popularized by Loshchilov and Hutter's SGDR paper) is now the default for most large models.

Linear warmup followed by cosine decay from ηmax⁡\eta_{\max} to ηmin⁡\eta_{\min} over SS total steps:

ηt={ηmax⁡ tWt<Wηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡ ⁣(πt−WS−W))W≤t≤S\eta_t = \begin{cases} \eta_{\max}\, \dfrac{t}{W} & t < W \\[2mm] \eta_{\min} + \tfrac12(\eta_{\max} - \eta_{\min})\left(1 + \cos\!\left(\pi \dfrac{t - W}{S - W}\right)\right) & W \le t \le S \end{cases}

Worked example. ηmax⁡=3×10−4\eta_{\max} = 3 \times 10^{-4}, ηmin⁡=0\eta_{\min} = 0, W=1,000W = 1{,}000, S=10,000S = 10{,}000.

  • t=500t = 500: warmup, η=3×10−4⋅0.5=1.5×10−4\eta = 3\times10^{-4} \cdot 0.5 = 1.5 \times 10^{-4}.
  • t=5,500t = 5{,}500: progress (5500−1000)/9000=0.5(5500 - 1000)/9000 = 0.5, cos⁡(π/2)=0\cos(\pi/2) = 0, η=12⋅3×10−4=1.5×10−4\eta = \tfrac12 \cdot 3\times10^{-4} = 1.5\times 10^{-4}.
  • t=7,750t = 7{,}750: progress 0.750.75, cos⁡(0.75π)≈−0.7071\cos(0.75\pi) \approx -0.7071, η≈1.5×10−4×0.2929≈4.39×10−5\eta \approx 1.5\times10^{-4} \times 0.2929 \approx 4.39\times10^{-5}.

Variants include warmup-stable-decay (a long constant phase, then a short decay, convenient when you do not know the total step count in advance) and one-cycle schedules.

Regularization through the optimizer's eyes

Regularization reduces overfitting by preferring simpler parameter settings.

  • L2 / weight decay penalizes ∥θ∥22\|\boldsymbol\theta\|_2^2; it shrinks all weights proportionally and, as a MAP estimate, corresponds to a Gaussian prior on the weights.
  • L1 penalizes ∥θ∥1\|\boldsymbol\theta\|_1; its (sub)gradient λ sign(θ)\lambda\,\mathrm{sign}(\theta) pushes every weight toward zero by a constant amount, so the penalized optimum has many weights exactly zero (sparsity, feature selection). Solvers that actually land on those zeros use soft-thresholding (proximal gradient) or coordinate descent; plain subgradient steps only jitter around zero. It corresponds to a Laplace prior.
  • Implicit regularizers include early stopping (stop when validation loss stops improving), the noise of small-batch SGD, dropout, data augmentation, and the learning-rate schedule itself.

Implementing gradient descent for linear regression

Model: y^=Xw+b\hat{\mathbf y} = X\mathbf w + b with X∈RN×dX \in \mathbb{R}^{N \times d}. Loss: mean squared error L=1N∥Xw+b−y∥2L = \frac{1}{N}\|X\mathbf w + b - \mathbf y\|^2. Gradients (using the calculus lesson's matrix rules), with residual r=Xw+b−y\mathbf r = X\mathbf w + b - \mathbf y:

∇wL=2NX⊤r,∂L∂b=2N∑iri.\nabla_{\mathbf w} L = \frac{2}{N} X^\top \mathbf r, \qquad \frac{\partial L}{\partial b} = \frac{2}{N}\sum_{i} r_i.
import numpy as np

rng = np.random.default_rng(0)
N, d = 1000, 3
X = rng.normal(size=(N, d)) * np.array([1.0, 10.0, 0.1])   # badly scaled features
w_true, b_true = np.array([2.0, -0.3, 5.0]), 1.0
y = X @ w_true + b_true + rng.normal(scale=0.5, size=N)

# Standardize features: fixes conditioning (try removing this and see GD crawl)
mu, sd = X.mean(0), X.std(0)
Xs = (X - mu) / sd

def minibatch_sgd(X, y, lr=0.01, epochs=30, batch=32, wd=0.0):
    w, b = np.zeros(X.shape[1]), 0.0
    for epoch in range(epochs):
        perm = rng.permutation(len(y))
        for i in range(0, len(y), batch):
            idx = perm[i:i + batch]
            r = X[idx] @ w + b - y[idx]                 # residuals (B,)
            gw = 2 * X[idx].T @ r / len(idx)            # (d,)
            gb = 2 * r.mean()
            w -= lr * (gw + wd * w)                     # L2 / weight decay
            b -= lr * gb
        if epoch % 10 == 0:
            print(epoch, round(np.mean((X @ w + b - y) ** 2), 4))
    return w, b

w, b = minibatch_sgd(Xs, y)
w_orig = w / sd                                         # undo standardization
b_orig = b - (mu / sd) @ w
print("SGD   :", w_orig.round(3), round(b_orig, 3))
A = np.c_[X, np.ones(N)]                                # closed-form check
print("lstsq :", np.linalg.lstsq(A, y, rcond=None)[0].round(3))

The loop shuffles each epoch, computes residuals and gradients on a mini-batch, and updates. Standardizing features matters: in the raw data the feature scales differ by a factor of 100, so curvatures differ by about 10410^4 and any single learning rate is either unstable for one feature or glacial for another. After standardization the condition number is near 1. Running it, the first two weights and the bias agree with lstsq to about two decimals (1.976 vs 1.975, −0.297 vs −0.297, 1.019 vs 1.018). The third weight lands at about 5.24 against lstsq's 5.18: that feature has a tiny scale (standard deviation 0.1), so the data pin its coefficient down only loosely (even the exact least-squares answer is 0.18 away from the true 5.0), and SGD's constant-rate jitter in standardized units is multiplied by 1/0.1=101/0.1 = 10 when converted back. Decaying the learning rate, or a few full-batch steps at the end, closes the gap. Try lr=0.05: training still converges, but the final jitter is visibly larger.

Comparing optimizers on a ravine

import numpy as np

curv = np.array([0.01, 1.0])                    # L = 0.5*(0.01 x^2 + y^2), kappa = 100
grad = lambda th: curv * th

def run(update, steps=100):
    th, state = np.array([10.0, 1.0]), {}
    for t in range(1, steps + 1):
        th = update(th, grad(th), state, t)
    return 0.5 * (curv * th**2).sum()

def gd(th, g, s, t, lr=1.9):                    # just under 2 / lambda_max
    return th - lr * g

def momentum(th, g, s, t, lr=0.5, beta=0.9):
    s["v"] = beta * s.get("v", 0) + g
    return th - lr * s["v"]

def adamw(th, g, s, t, lr=0.3, b1=0.9, b2=0.999, eps=1e-8, wd=0.0):
    s["m"] = b1 * s.get("m", 0) + (1 - b1) * g
    s["v"] = b2 * s.get("v", 0) + (1 - b2) * g**2
    mhat, vhat = s["m"] / (1 - b1**t), s["v"] / (1 - b2**t)
    return th - lr * (mhat / (np.sqrt(vhat) + eps) + wd * th)

for name, f in [("GD", gd), ("Momentum", momentum), ("Adam", adamw)]:
    print(f"{name:9s} final loss {run(f):.2e}")

The starting loss is 1.0 and the condition number is 100. GD runs just under its maximum stable rate ($1.9 < 2/1)andstillonlyreachesabout) and still only reaches about 1.1 \times 10^{-2}$ after 100 steps, because along the shallow xx direction each step shrinks the error by just 1−1.9×0.01=0.9811 - 1.9 \times 0.01 = 0.981. Momentum reaches about 1.0×10−51.0 \times 10^{-5} and Adam about 1.5×10−51.5 \times 10^{-5}: three orders of magnitude better. Momentum wins by accumulating velocity along the consistent shallow direction; Adam wins by rescaling each coordinate so both directions move at a similar rate. The learning rates were tuned by hand for each method, as you would in practice; results on a toy quadratic do not transfer one-to-one to deep networks, but the mechanisms are the same.

Common pitfalls

  • Learning rate too high or too low. Loss that explodes or becomes nan within a few hundred steps usually means too high; a loss that falls painfully slowly usually means too low. Run a short learning-rate sweep on a log scale (e.g. 10−510^{-5} to 10−210^{-2}) before long runs.
  • Using Adam with L2 in the loss when you meant weight decay. Use AdamW (decoupled decay) and exclude biases and norm parameters.
  • No warmup for transformers. Early instability or loss spikes often disappear with a few hundred to a few thousand warmup steps.
  • Unscaled features for linear models and MLPs on tabular data: standardize first.
  • Forgetting to shuffle or reusing the same batch order makes SGD gradients biased toward data ordering.
  • Changing batch size without retuning the learning rate. The best learning rate depends on batch size.
  • Judging by training loss alone. Track validation loss; use early stopping or checkpoint selection.

In practice

  • Default starting point for deep networks: AdamW, β1=0.9\beta_1 = 0.9, β2\beta_2 between 0.95 and 0.999, weight decay around 0.01 to 0.1, linear warmup, cosine decay, and gradient clipping at a global norm of 1.0. Then sweep the peak learning rate, which matters most.
  • SGD with momentum (often Nesterov) and step or cosine decay remains competitive for many vision models and is cheaper in memory: Adam stores two extra values per parameter, which adds up for large models.
  • Log the learning rate, loss, gradient norm and update-to-weight ratio every step; most training failures are visible in these four curves before they become fatal.
  • For fine-tuning pretrained models, use learning rates one or two orders of magnitude lower than for pretraining and short warmups; adapters such as LoRA tolerate somewhat higher rates because only small matrices are trained.
  • Save optimizer state along with weights in checkpoints; resuming Adam without its moment estimates behaves like a fresh, unwarmed start.

Key takeaways

  • Gradient descent is stable only when the learning rate is below 2 divided by the largest curvature, and its speed is limited by the smallest curvature.
  • Mini-batch gradients are unbiased but noisy estimates whose noise falls like one over the square root of the batch size.
  • Momentum accelerates progress along consistent directions; RMSProp and Adam rescale steps per parameter.
  • AdamW decouples weight decay from the adaptive scaling and is the default optimizer for transformers.
  • Linear warmup followed by cosine decay is the standard learning-rate schedule for large models.
  • Feature scaling, learning-rate sweeps and monitoring gradient norms prevent most optimization failures.

Terms in this lesson

Learning rate
The scalar step size that multiplies the update direction in a gradient-based optimizer.
Convex function
A function for which the segment between any two points on its graph lies on or above the graph, so every local minimum is global.
Saddle point
A point with zero gradient where the function curves upward in some directions and downward in others.
Condition number
The ratio of the largest to the smallest curvature (Hessian eigenvalue), measuring how elongated a loss valley is.
Stochastic gradient descent
Gradient descent that estimates the gradient on a random mini-batch of examples at each step.
Momentum
An optimizer technique that accumulates an exponentially decaying sum of past gradients and steps along it.
Adam
An adaptive optimizer that scales bias-corrected first-moment estimates of the gradient by the square root of bias-corrected second-moment estimates.
AdamW
A variant of Adam that applies weight decay directly to the parameters instead of adding an L2 term to the gradient.
Warmup
A schedule phase that increases the learning rate gradually from near zero at the start of training.

Check your understanding

  1. 1.

    A quadratic loss has Hessian eigenvalues 0.5 and 40. What is the largest learning rate for which plain gradient descent is stable?

  2. 2.

    On Adam's very first step (t = 1) with default β values and ε negligible, what is the magnitude of each parameter's update?

  3. 3.

    You train a transformer with Adam and add 0.01·||θ||² to the loss for regularization. What is the most important issue?

  4. 4.

    With linear warmup over 2,000 steps to a peak of 1e-3, then cosine decay to 0 by step 12,000, what is the learning rate at step 7,000?

  5. 5.

    Which regularizer tends to drive many weights exactly to zero?

Exercises

Work each problem before opening the solution.

  1. Exercise 1Core

    Run three steps of momentum (β=0.9\beta = 0.9, η=0.1\eta = 0.1, v0=0v_0 = 0) on L(w)=w2L(w) = w^2 starting from w0=1w_0 = 1. Report vv and ww after each step, and compare with plain GD at the same learning rate.

    Show solution

    Gradient g=2wg = 2w.

    Step 1: g=2g = 2; v1=0.9⋅0+2=2v_1 = 0.9 \cdot 0 + 2 = 2; w1=1−0.1⋅2=0.8w_1 = 1 - 0.1 \cdot 2 = 0.8.

    Step 2: g=1.6g = 1.6; v2=0.9⋅2+1.6=3.4v_2 = 0.9 \cdot 2 + 1.6 = 3.4; w2=0.8−0.34=0.46w_2 = 0.8 - 0.34 = 0.46.

    Step 3: g=0.92g = 0.92; v3=0.9⋅3.4+0.92=3.98v_3 = 0.9 \cdot 3.4 + 0.92 = 3.98; w3=0.46−0.398=0.062w_3 = 0.46 - 0.398 = 0.062.

    Plain GD: w=0.8,0.64,0.512w = 0.8, 0.64, 0.512. Momentum gets much closer in three steps because the velocity accumulates. (On this well-conditioned problem it will next overshoot past zero: g=0.124g = 0.124, v4=3.706v_4 = 3.706, w4=0.062−0.3706=−0.3086w_4 = 0.062 - 0.3706 = -0.3086; momentum trades speed for some oscillation.)

  2. Exercise 2Core

    Compute two steps of Adam by hand for one parameter with η=0.01\eta = 0.01, β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, ϵ≈0\epsilon \approx 0, gradients g1=2g_1 = 2 and g2=−1g_2 = -1, starting at θ0=0\theta_0 = 0.

    Show solution

    Step 1: m1=0.2m_1 = 0.2, v1=0.001⋅4=0.004v_1 = 0.001 \cdot 4 = 0.004. m^1=0.2/0.1=2\hat m_1 = 0.2/0.1 = 2, v^1=0.004/0.001=4\hat v_1 = 0.004/0.001 = 4. Update =0.01⋅2/2=0.01= 0.01 \cdot 2/2 = 0.01, so θ1=−0.01\theta_1 = -0.01.

    Step 2: m2=0.9⋅0.2+0.1⋅(−1)=0.08m_2 = 0.9 \cdot 0.2 + 0.1 \cdot (-1) = 0.08. v2=0.999⋅0.004+0.001⋅1=0.004996v_2 = 0.999 \cdot 0.004 + 0.001 \cdot 1 = 0.004996. Bias corrections: 1−0.92=0.191 - 0.9^2 = 0.19, 1−0.9992=0.0019991 - 0.999^2 = 0.001999. m^2=0.08/0.19≈0.4211\hat m_2 = 0.08/0.19 \approx 0.4211; v^2=0.004996/0.001999≈2.4992\hat v_2 = 0.004996/0.001999 \approx 2.4992, v^2≈1.5809\sqrt{\hat v_2} \approx 1.5809. Update =0.01⋅0.4211/1.5809≈0.002664= 0.01 \cdot 0.4211/1.5809 \approx 0.002664. θ2=−0.01−0.002664≈−0.01266\theta_2 = -0.01 - 0.002664 \approx -0.01266.

    Note that even though g2g_2 is negative, θ\theta keeps decreasing: momentum from the larger first gradient still dominates.

  3. Exercise 3Core

    Write a Python function lr_at(step, peak, warmup, total, min_lr=0.0) implementing linear warmup followed by cosine decay, and print the learning rate at steps 0, 500, 1000, 5500 and 10000 for peak 3×10−43 \times 10^{-4}, warmup 1,000 and total 10,000.

    Show solution
    import math
    
    def lr_at(step, peak, warmup, total, min_lr=0.0):
        if step < warmup:
            return peak * step / warmup
        progress = min(1.0, (step - warmup) / max(1, total - warmup))
        return min_lr + 0.5 * (peak - min_lr) * (1 + math.cos(math.pi * progress))
    
    for s in (0, 500, 1000, 5500, 10000):
        print(s, f"{lr_at(s, 3e-4, 1000, 10000):.2e}")
    

    Output: step 0 gives 0.00e+00, 500 gives 1.50e-04, 1000 gives 3.00e-04 (peak), 5500 gives 1.50e-04 (halfway through the cosine), and 10000 gives 0.00e+00. The min(1.0, ...) clamp keeps the rate at min_lr if training runs past total, and max(1, ...) avoids division by zero when warmup equals total.

  4. Exercise 4Challenge

    Extend the lesson's linear-regression code to logistic regression on a synthetic binary dataset (two Gaussian blobs in 2-D). Implement mini-batch training with AdamW written from scratch (no libraries beyond NumPy), use the binary cross-entropy loss with a numerically stable formulation, and report training accuracy. Derive the gradient you use.

    Show solution

    Model: pi=σ(xi⊤w+b)p_i = \sigma(\mathbf x_i^\top \mathbf w + b). Binary cross-entropy for logit zz and label yy: ℓ=−[ylog⁡σ(z)+(1−y)log⁡(1−σ(z))]\ell = -[y\log\sigma(z) + (1-y)\log(1-\sigma(z))], which simplifies to the stable form ℓ=max⁡(z,0)−yz+log⁡(1+e−∣z∣)\ell = \max(z, 0) - yz + \log(1 + e^{-|z|}). Its derivative is ∂ℓ/∂z=σ(z)−y\partial\ell/\partial z = \sigma(z) - y (the binary version of "prediction minus target"), so ∇w=1BX⊤(p−y)\nabla_{\mathbf w} = \frac1B X^\top(\mathbf p - \mathbf y) and ∂/∂b=mean(p−y)\partial/\partial b = \mathrm{mean}(\mathbf p - \mathbf y).

    import numpy as np
    
    rng = np.random.default_rng(1)
    X = np.r_[rng.normal([-1, -1], 1.0, (500, 2)), rng.normal([1.5, 1.0], 1.0, (500, 2))]
    y = np.r_[np.zeros(500), np.ones(500)]
    
    def bce(z, y):
        return np.mean(np.maximum(z, 0) - y * z + np.log1p(np.exp(-np.abs(z))))
    
    theta = np.zeros(3)                                  # w1, w2, b
    m, v = np.zeros(3), np.zeros(3)
    lr, b1, b2, eps, wd, t = 0.05, 0.9, 0.999, 1e-8, 0.01, 0
    Xb = np.c_[X, np.ones(len(X))]
    decay_mask = np.array([1.0, 1.0, 0.0])                # no decay on bias
    for epoch in range(20):
        perm = rng.permutation(len(y))
        for i in range(0, len(y), 64):
            idx = perm[i:i + 64]
            z = Xb[idx] @ theta
            p = 1 / (1 + np.exp(-z))
            g = Xb[idx].T @ (p - y[idx]) / len(idx)
            t += 1
            m = b1 * m + (1 - b1) * g
            v = b2 * v + (1 - b2) * g**2
            mhat, vhat = m / (1 - b1**t), v / (1 - b2**t)
            theta -= lr * (mhat / (np.sqrt(vhat) + eps) + wd * decay_mask * theta)
    z = Xb @ theta
    print("loss", round(bce(z, y), 4), "acc", ((z > 0) == y).mean())
    

    The bias is excluded from weight decay through decay_mask, following common practice. The blobs overlap somewhat, so accuracy is high but not perfect: with this seed the script prints a loss of about 0.108 and accuracy 0.958 (other seeds give similar values). Decay is decoupled: wd * theta is added outside the adaptive fraction, which is what distinguishes AdamW from Adam with an L2 penalty.

Further reading

Go further

Sources

Fact-checked and last reviewed 2026-11-05

Stay in the loop
Prometheus OS™ · Native AI OS · Built for the future © 2025 - 2026