Optimisers Mathematical Deep Dive

P6.dl-architectures.04 · Audience: it-ml · Prerequisites: Training Loop and Optimisers

The SGD derivation from the stochastic objective, momentum as an exponential moving average with its heavy-ball origin, Adam with the full bias-correction proof, and learning-rate schedule formulas — mathematical details for IT/ML practitioners, with pencil-and-paper exercises, NumPy programming tasks, and a 5-question self-test.

Prerequisite: Training Loop and Optimisers. This page assumes familiarity with expectations, the chain rule, and geometric series. All derivations are self-contained.

Step 1 / 4SGD — Full Derivation

Stochastic Gradient Descent minimises an objective by iteratively taking steps in the direction of the negative gradient of a stochastic estimate of the loss.

Problem: minimise the expected loss over the data distribution:

L(θ)=E(x,y)D[(fθ(x),y)]\mathcal{L}(\theta) = \mathbb{E}_{(x,y)\sim\mathcal{D}}\big[\ell(f_\theta(x), y)\big]

Because we cannot compute the expectation over the full dataset at each step, we approximate it with a mini-batch of size B:

^L(θ)=1B(xi,yi)Bθ(fθ(xi),yi)\hat{\nabla}\mathcal{L}(\theta) = \frac{1}{B}\sum_{(x_i,y_i)\in\mathcal{B}} \nabla_\theta\, \ell(f_\theta(x_i), y_i)

SGD update rule:

θt+1=θtη^L(θt)\theta_{t+1} = \theta_t - \eta \cdot \hat{\nabla}\mathcal{L}(\theta_t)

Why this works (gradient descent intuition). If the loss surface were quadratic, the exact gradient is κθ and the update converges geometrically to 0 as long as η < 2/κ:

L=12κθ2    θt+1=(1ηκ)θt\mathcal{L} = \tfrac{1}{2}\kappa\theta^2 \;\Rightarrow\; \theta_{t+1} = (1-\eta\kappa)\,\theta_t

Convergence conditions:

ConditionResultNotes
η < 2/κ (convex quadratic)Exact convergence to minimumκ = Lipschitz constant of the gradient (curvature)
η → 0 but Σ ηt = ∞, Σ ηt² < ∞Convergence in expectation (stochastic setting)Robbins–Monro conditions — satisfied by 1/t schedule
η fixedConverges to noise ball around minimum; size ∝ η·σ²Most common in practice; σ² = variance of stochastic gradient

Worked SGD trace on L(w) = w² (gradient 2w, minimum at 0) with the defaults w₀ = 3.0 and η = 0.1. Each step multiplies w by (1 − 2η) = 0.8, so the loss contracts by 0.8² = 0.64 per step:

StepwL(w) = w²
03.000009.00000
12.400005.76000
21.920003.68640
31.536002.35930
41.228801.50995
50.983040.96637

After 20 steps: w = 3.0 × 0.8²⁰ ≈ 0.0346, loss ≈ 0.0012. Convergence speed is controlled by η·κ where κ = 2 (curvature of w²). Current product: 0.200 (< 1: converges; ≥ 1: may diverge).

Exercises

Work these on paper or in the scratch console at the bottom of the page. Each exercise carries its worked solution behind a reveal, so check yourself as you go. (Hands-on PyTorch training rungs arrive with the VN-056 training clinic — coming soon.)

Maths exercises (pencil and paper)

M1 — One Adam step. Given g=0.1g = 0.1, m0=0m_0 = 0, v0=0v_0 = 0, β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, ε=108\varepsilon = 10^{-8}, α=0.001\alpha = 0.001, t=1t = 1: compute the Adam weight update at t=1t = 1 for a single parameter θ=0.5\theta = 0.5.

Recall: m1=β10+(1β1)gm_1 = \beta_1 \cdot 0 + (1-\beta_1) \cdot g, v1=β20+(1β2)g2v_1 = \beta_2 \cdot 0 + (1-\beta_2) \cdot g^2, then bias-correct and update.

Show answer — M1

m1=(10.9)0.1=0.01000m_1 = (1-0.9)\cdot 0.1 = \mathbf{0.01000}

v1=(10.999)0.01=0.0000100v_1 = (1-0.999)\cdot 0.01 = \mathbf{0.0000100}

m^1=m1/(10.9)=0.10000\hat{m}_1 = m_1/(1-0.9) = \mathbf{0.10000} =g= g (bias-corrected back to gg)

v^1=v1/(10.999)=0.0100000\hat{v}_1 = v_1/(1-0.999) = \mathbf{0.0100000} =g2= g^2 (bias-corrected back to g2g^2)

Δθ=αv^1+εm^1=0.0010.0100000+108×0.10000=0.0010000\Delta\theta = \dfrac{\alpha}{\sqrt{\hat{v}_1}+\varepsilon}\,\hat{m}_1 = \dfrac{0.001}{\sqrt{0.0100000}+10^{-8}} \times 0.10000 = \mathbf{0.0010000}

θnew=0.50.00100000.499000\theta_{\text{new}} = 0.5 - 0.0010000 \approx \mathbf{0.499000}

M2 — Step-decay LR at t = 100. Compute α(100)=α0γt/s\alpha(100) = \alpha_0 \cdot \gamma^{\lfloor t/s \rfloor} for α0=0.1\alpha_0 = 0.1, s=30s = 30, γ=0.5\gamma = 0.5.

Show answer — M2

100/30=3\lfloor 100/30 \rfloor = \mathbf{3} (three full drops have occurred)

α(100)=0.1×0.53=0.1×0.125=0.0125\alpha(100) = 0.1 \times 0.5^3 = 0.1 \times 0.125 = \mathbf{0.0125}

M3 — Bias-correction proof: show that m^1=g1\hat{m}_1 = g_1 when β1=0.9\beta_1 = 0.9. Starting from m0=0m_0 = 0, after one step: m1=β1m0+(1β1)g1m_1 = \beta_1 \cdot m_0 + (1-\beta_1) \cdot g_1. Show that m^1=m1/(1β11)=g1\hat{m}_1 = m_1/(1-\beta_1^1) = g_1.

Show answer — M3

m1=0.90+(10.9)g1=0.1g1m_1 = 0.9 \cdot 0 + (1-0.9)\cdot g_1 = \mathbf{0.1\, g_1}

m^1=m11β11=0.1g110.9=0.1g10.1=g1\hat{m}_1 = \dfrac{m_1}{1-\beta_1^1} = \dfrac{0.1\, g_1}{1-0.9} = \dfrac{0.1\, g_1}{0.1} = \mathbf{g_1}

The bias-correction exactly cancels the (1β1)(1-\beta_1) factor that underweights the first gradient, recovering the unbiased estimate.

Programming exercises (NumPy)

Try each exercise in your own Python environment (or the scratch console), then reveal the reference solution.

P1 — Implement one Adam step in NumPy.

import numpy as np

def adam_step(theta, g, m, v, t, alpha=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
    """Perform one Adam update.

    Parameters
    ----------
    theta : np.ndarray  Current parameters.
    g     : np.ndarray  Gradient at current step.
    m     : np.ndarray  First moment (EMA of gradients).
    v     : np.ndarray  Second moment (EMA of squared gradients).
    t     : int         Current step (1-based).

    Returns
    -------
    tuple[np.ndarray, np.ndarray, np.ndarray]
        (theta_new, m_new, v_new)
    """
    # TODO: implement Adam
    pass

theta = np.array([0.5, -0.3])
g     = np.array([0.1, -0.2])
m     = np.zeros(2)
v     = np.zeros(2)
print(adam_step(theta, g, m, v, t=1))
Show solution — P1
def adam_step(theta, g, m, v, t, alpha=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
    m_new = beta1 * m + (1 - beta1) * g
    v_new = beta2 * v + (1 - beta2) * g**2
    m_hat = m_new / (1 - beta1**t)
    v_hat = v_new / (1 - beta2**t)
    theta_new = theta - alpha / (np.sqrt(v_hat) + eps) * m_hat
    return theta_new, m_new, v_new

theta_new, m_new, v_new = adam_step(theta, g, m, v, t=1)
print(theta_new)   # [0.499   -0.299  ] — very small step at t=1

P2 — Implement a step-decay learning rate schedule.

import numpy as np

def step_decay_schedule(t_max, alpha0=0.1, step_size=30, gamma=0.5):
    """Return LR values for steps 0..t_max.

    Parameters
    ----------
    t_max     : int    Total number of steps.
    alpha0    : float  Initial learning rate.
    step_size : int    Number of steps between decays.
    gamma     : float  Decay factor per drop.

    Returns
    -------
    np.ndarray  LR at each step.
    """
    # TODO: compute alpha(t) = alpha0 * gamma^floor(t/step_size) for t in range(t_max+1)
    pass

lrs = step_decay_schedule(150)
print(lrs[:5], "...", lrs[30], "...", lrs[90])
Show solution — P2
def step_decay_schedule(t_max, alpha0=0.1, step_size=30, gamma=0.5):
    t = np.arange(t_max + 1)
    return alpha0 * gamma ** np.floor(t / step_size)

lrs = step_decay_schedule(150)
print(lrs[:5])         # [0.1 0.1 0.1 0.1 0.1]
print(lrs[30])         # 0.05  (first drop at t=30)
print(lrs[90])         # 0.0125  (three drops: 0.1 × 0.5³)

P3 — Implement a cosine annealing schedule.

import numpy as np

def cosine_annealing(t_max, alpha_max=0.1, alpha_min=0.0):
    """Return cosine-annealed LR values for steps 0..t_max.

    Parameters
    ----------
    t_max     : int    Total number of steps (one cycle).
    alpha_max : float  Peak learning rate.
    alpha_min : float  Minimum learning rate.

    Returns
    -------
    np.ndarray  LR at each step.
    """
    # TODO: alpha(t) = alpha_min + 0.5*(alpha_max - alpha_min)*(1 + cos(pi*t/t_max))
    pass

lrs = cosine_annealing(150)
print(lrs[0], lrs[75], lrs[150])   # should be ~alpha_max, ~midpoint, ~alpha_min
Show solution — P3
def cosine_annealing(t_max, alpha_max=0.1, alpha_min=0.0):
    t = np.arange(t_max + 1)
    return alpha_min + 0.5 * (alpha_max - alpha_min) * (1 + np.cos(np.pi * t / t_max))

lrs = cosine_annealing(150)
print(lrs[0])     # 0.1  (= alpha_max)
print(lrs[75])    # 0.05 (= midpoint)
print(lrs[150])   # 0.0  (= alpha_min)

Self-Test — 5 Questions

Write your answer before revealing.

Q1 — What does the second moment v_t in Adam track?

Q2 — At step t = 1, what is the bias-correction factor for m when β₁ = 0.9?

Q3 — What is the effective learning rate in Adam for a parameter whose gradient has been consistently ≈ 0.3 for many steps? (α = 0.001)

Q4 — In step decay, what happens to the LR at t = 2s?

Q5 — Why does cosine annealing outperform step decay in practice?


Cross-references: Training Loop and Optimisers (base module) · Regularisation and Normalisation — dropout and LayerNorm, the two regularisation techniques built into every Transformer encoder layer. Hands-on PyTorch training rungs arrive with the VN-056 training clinic (coming soon).

Ask the mentor about this module

Ask a question about this content. The mentor explains and grounds its answer in what you are studying; asking is recorded as a learning signal, not a grade.

Ctrl/Cmd + Enter to send

Try it yourself

A scratch console for this page's ideas — ungraded, nothing you run here is recorded.

Scratch console

A scratch console with the scientific stack (pandas, numpy, scikit-learn). Runs on the server — no network, resource-limited and measured.

Output appears here.