Math Relearn/ Learning/ Module 07

Optimization: how models learn

Convexity, gradient descent, stochastic gradients, Adam, constraints and regularisation β€” the machine that actually turns data into parameters. Every knob you will ever turn is a control on this machine.

On this page
    Track 3 Β· Learning

    07 Β· Optimization: how models learn

    ⏱ ~1 h 50πŸ“Ž NumPy🎯 Choose and tune an optimizer for a reason, not by ritual.

    Training a model is an optimisation problem: pick the parameters that make the loss smallest, and everything else you touch β€” learning rate, batch size, $\lambda$, warmup, architecture β€” is a knob on that one problem. This module hands you the map of the machine: what each knob changes mathematically, which knobs are safe to turn, and which ones people turn by ritual without knowing what they do.

    By the end you can

    • State the training objective and separate parameters $\theta$ from hyperparameters $\lambda$, $\eta$, batch size, depth.
    • Test convexity on sight, and say exactly what convexity buys (local minima are global; convergence proofs exist).
    • Derive the gradient descent update from a first-order Taylor expansion, and say what the learning rate does to a quadratic β€” including the exact condition $0 \lt \eta \lt 2$.
    • Explain what momentum, RMSProp and Adam each add, why Adam needs bias correction, and when plain SGD + momentum is still the better choice.
    • Write the Lagrangian of a constrained problem, read the KKT conditions, and derive ridge regression as a constrained least-squares problem.
    • Diagnose a training run that will not converge β€” ill-conditioning, exploding or vanishing gradients, learning rate β€” before reaching for exotic explanations.

    Why this module exists

    Module 03 gave you the derivative and the update rule. That is a tool; this module is the trade. Knowing how to compute $\nabla J$ does not tell you why your loss flatlined at 2.3, why adding momentum fixed it, why Adam needs a warmup, or why a hundred times more epochs did not help. Those questions have mathematical answers, and they are all answers about optimisation.

    The second reason is vocabulary. When a paper says "we use decoupled weight decay with a cosine schedule and 1k warmup steps", it is naming three specific mechanisms on the machine you are about to learn. Once you own the machine, those sentences stop being incantations and start being configuration β€” and you can reason about whether the configuration suits your problem.

    The third reason is honesty about scale: ML is approximate optimisation. Nobody in practice finds the global optimum of a neural network, and nobody needs to. What you need is a $\theta$ that is good enough and that generalises β€” which is why this module keeps one eye on Module 05's statistics the whole way through.

    Pass 1

    Remember β€” retrieve before you read

    Six questions, cold, before any scrolling

    Cold questions β€” answer from memory, in writing

    1. Write the gradient descent update rule from memory. What do $\theta$ and $\eta$ each stand for?
    2. On a quadratic $J(\theta) = \frac{1}{2}a\theta^2$, gradient descent converges for which range of $\eta$? Derive it in one line.
    3. Momentum keeps a velocity $v$. What does $\beta$ control, and roughly how many steps does $\beta = 0.9$ look back over?
    4. What single property of a function makes all its local minima global? Give the defining inequality.
    5. Why is $\lambda$ called a regulariser? Which term of the objective does it multiply, and what is it trading against?
    6. "The problem is ill-conditioned." What does that mean, in terms of a matrix's eigenvalues β€” and what does it do to gradient descent?

    Model answers

    Only open these after your own answers are written down.

    1. The gradient descent update rule
    $\theta_{k+1} = \theta_k - \eta\,\nabla J(\theta_k)$. $\theta$ are the parameters β€” everything the gradient can change (weights, biases). $\eta$ is the learning rate β€” a hyperparameter multiplied in by hand, not learned: it converts slope units into parameter units. One step is the first-order Taylor model evaluated at its own minimum.
    2. When does GD provably converge on a quadratic?
    On $J(\theta)=\frac{1}{2}a\theta^2$ the update is $\theta_{k+1}=(1-\eta a)\theta_k$, so it converges iff $|1-\eta a| \lt 1$, i.e. $0 \lt \eta \lt 2/a$. With $a=1$: $0 \lt \eta \lt 2$. The error is multiplied by $(1-\eta a)$ every step β€” the whole convergence story of gradient descent on a quadratic is one number you can compute.
    3. What does momentum's $\beta$ do?
    $\beta$ sets the memory. The velocity $v \leftarrow \beta v - \eta\nabla J$ is an exponential moving average of past gradients, with weights $(1-\beta)\beta^{k}$ that sum to $1$ β€” an effective look-back window of about $1/(1-\beta)$ steps. $\beta=0.9$ β‰ˆ 10 steps, $\beta=0.99$ β‰ˆ 100. Large $\beta$ smooths noise and accelerates along consistent directions; it also makes the optimizer slow to react to a change of direction.
    4. What makes a problem convex?
    Convexity of the objective over a convex feasible set. A function is convex when the straight segment between any two points on its graph stays above the graph: $f(\alpha x + (1-\alpha)y) \le \alpha f(x) + (1-\alpha)f(y)$ for all $\alpha\in[0,1]$. Equivalent for a smooth function: the Hessian is positive semidefinite everywhere. The payoff: every local minimum is a global one.
    5. Why is $\lambda$ called a regulariser?
    It regularises the solution, not the fit. $\lambda\Omega(\theta)$ is added to the empirical risk to restrict which solutions are acceptable β€” preferring small or sparse $\theta$ over the one that merely minimises training error. $\lambda=0$ recovers pure data fitting; larger $\lambda$ trades training fit for a simpler, more stable, better-generalising model (Module 05's bias–variance trade in algebraic form).
    6. What "ill-conditioned" means
    The eigenvalues span a huge range. $\kappa = \lambda_{\max}/\lambda_{\min}$ large. Geometrically the loss is a long narrow valley: the gradient in the steep direction is enormous compared with the flat one, so any $\eta$ small enough to survive the steep wall crawls along the valley floor. Numerically, small perturbations get amplified by $\kappa$ β€” answers lose digits.
    7. What does Adam add on top of momentum?
    A per-parameter step size. Momentum smoothes the gradient (a first moment); Adam additionally keeps a running mean of the squared gradients (a second moment) and divides by it β€” so each parameter gets its own effective learning rate. Plus bias correction, because both running averages start at zero.
    8. Newton's method, in one line β€” and its price
    $\theta \leftarrow \theta - H^{-1}\nabla J(\theta)$, with $H$ the Hessian. The price: $H$ is $d\times d$, costs $O(d^2)$ to store and $O(d^3)$ to invert, and $d$ can be a billion. Beautiful on small problems, impossible as stated at scale β€” which is why the whole second-order family exists.
    Pass 2

    Understand β€” the shape of the idea

    Ten sections: the objective, convexity, descent, noise, adaptive steps, second order, constraints, regularisation, conditioning, and the map
    Training a model is an optimisation problem: everything you tune is a knob on that problem, and this module is the wiring diagram.

    1 Β· The problem statement

    Strip a training script to its mathematics and one line remains. The parameters you search for are $\theta$; the loss of a single example is $L$; the model is $f(x;\theta)$; the penalty $\Omega$ discourages silly solutions.

    $$J(\theta) = \frac{1}{n}\sum_{i=1}^{n} L\big(f(x_i;\theta),\, y_i\big) + \lambda\,\Omega(\theta)$$ empirical risk (fit the data) + λ·regulariser (stay sane) β€” two roles, one scalar to minimise

    Read it as two factions. The sum is the empirical risk: the average error on the $n$ samples you actually have. It pulls $\theta$ toward whatever reproduces the training data, including its noise. The second term is the regulariser: a penalty whose only job is to keep the solution tame β€” small weights, sparse weights, bounded complexity. $\lambda$ is the dial between the two factions, and it is set by you, not by the gradient.

    That distinction is the whole parameter/hyperparameter split. Parameters $\theta$ are learned by the optimiser β€” they move every step. Hyperparameters are outside the gradient: $\lambda$, the learning rate $\eta$, the batch size, the number of layers, the number of epochs, $\beta$. Nothing in $J$ can tell you the right learning rate, because the learning rate is not a variable of $J$. Choosing them is a second, outer optimisation β€” usually by cross-validation or a schedule you write by hand (Module 05 covers the honest version of that loop).

    ML is approximate optimisation β€” on purpose

    You almost never need the global optimum, and for a deep network you cannot have it. What you need is a $\theta$ that is good enough and that generalises: a lower test error, not a lower training error. The regulariser $\lambda\Omega(\theta)$ and the early-stopped trajectory of Section 8 both exist to stop you optimising the training set into the ground. A "worse" optimum that generalises beats a "better" one that does not β€” the statistical reason is Module 05's bias–variance story.

    Two and a half centuries on the same line

    1669 β€” Newton's method of fluxions: solve equations by iterating a local linear model. 1795 β€” Gauss, aged 18, takes the least-squares problem of fitting an orbit to noisy observations and shows it has a unique best answer; it is convex, it is a linear system, and it is arguably the first modern optimisation problem. 1951 β€” Robbins and Monro publish stochastic approximation: minimise using noisy estimates of the gradient with a step size that shrinks. That is SGD, with a proof. 2014 β€” Kingma and Ba combine momentum with per-parameter step scaling and call it Adam; it is still the default almost everywhere. Everything between those dates is engineering on this one line.

    2 Β· Convexity: the property that makes everything easy

    A set is convex if the straight segment between any two of its points stays inside it. A function is convex if the same is true one level up: the segment joining two points on the graph stays above the graph. In words: the bowl holds water. Formally, for every $\alpha$ between 0 and 1:

    $$f\big(\alpha x + (1-\alpha)y\big) \le \alpha f(x) + (1-\alpha) f(y), \qquad \alpha \in [0,1]$$ value at the interpolated point ≀ interpolated value β€” the chord never dips under the curve

    The picture to sketch: draw a bowl, mark two points on its rim, draw the straight chord between them, and note the chord is always the pessimistic one β€” it over-estimates the function in between. The equivalent "epigraph" reading: if you shade everything above the graph, that shaded region is a convex set. Convexity of the function is convexity of a set, just lifted one dimension.

    Why convex β‡’ local minima are global. Suppose $\theta^\star$ is a local minimum but not global, so some point $\theta'$ has $J(\theta') \lt J(\theta^\star)$. Take the segment between them: convexity says every point on it satisfies $J(\alpha\theta^\star + (1-\alpha)\theta') \le \alpha J(\theta^\star) + (1-\alpha)J(\theta')$, and the right side is strictly less than $J(\theta^\star)$ for any $\alpha \in (0,1)$. But those points are inside the neighbourhood of $\theta^\star$ β€” contradicting it being a local minimum. No dip, no hidden basin, no local trap.

    Examples that matter in ML. Linear regression is convex: $J(w)=\frac{1}{n}\|Xw-y\|^2$ has Hessian $\frac{2}{n}X^\top X$, which is positive semidefinite because for any vector $v$, $v^\top X^\top X v = \|Xv\|^2 \ge 0$ β€” a Module 02 fact doing a Module 07 job. Logistic regression is convex too (its Hessian is $\frac{1}{n}X^\top W X$ with $W$ a diagonal matrix of positive weights β€” positive semidefinite for the same reason). A neural network is not convex: composing non-linearities with the parameters, not just with the input, destroys the property. Neither is matrix factorisation, low-rank approximation, or any problem where the unknown sits inside a product of matrices.

    ProblemConvex?Why / why not
    Linear regression (least squares)YesHessian $\frac{2}{n}X^\top X$ is positive semidefinite
    Logistic regressionYesLog-loss of a linear scorer; Hessian $X^\top W X$ with $W \succeq 0$
    Ridge / Lasso regressionYesAdding $\lambda\|w\|^2$ (convex) or $\lambda\|w\|_1$ (convex) keeps it convex
    Softmax regression (multi-class)YesCross-entropy of a linear map
    Two-layer neural netNoWeights multiply each other; the loss surface folds
    Matrix factorisation / embeddingsNo$UV^\top$ is bilinear in the unknowns; each factor alone is fine

    Convex combinations show up again immediately. A convex combination is a weighted average of points with non-negative weights summing to 1 β€” the $\alpha x + (1-\alpha)y$ of the definition. Momentum's velocity is a convex combination of past gradients; Adam's first moment $m_t$ is exactly a convex combination of all past gradients; the mixture interpretation of a regulariser interpolates between the data's answer and zero. When you see $\sum_k c_k v_k$ with $c_k \ge 0$ and $\sum c_k = 1$, read it as "a weighted blend of the $v_k$", and the result stays inside the hull of them.

    Convexity is what makes a proof possible

    Every convergence guarantee you will read β€” "$f$ converges at rate $O(1/k)$", "Adam converges under these conditions", "this method reaches an $\varepsilon$-accurate solution in $m$ iterations" β€” is proved on a convex (usually strongly convex) problem. The proof needs the global-minimum-is-local-minimum property to conclude that any stationary point found is the answer. That is why convex optimisation is a whole literature and why its statements sound so confident: on convex problems, "converges to a local minimum" means "converges, period". When a paper claims a rate for a non-convex loss, read the fine print β€” the guarantee is usually about reaching a stationary point, not a minimum.

    3 Β· Gradient descent, derived and understood

    Module 03 gave you the update rule; here it is derived, so it stops being a memorised line. Start from the first-order Taylor expansion β€” the local linear model of $J$ near $\theta$:

    $$J(\theta+\Delta) \approx J(\theta) + \nabla J(\theta)^\top \Delta$$ where you are + the tilt under your feet Γ— the step you take

    The second term is $\|\nabla J\|\|\Delta\|\cos\phi$, maximally negative when the step points opposite the gradient. So the best linear decrease is $\Delta = -\eta\nabla J$ with $\eta$ the step length β€” and substituting back gives the update rule by construction, not by fiat:

    $$\theta_{k+1} = \theta_k - \eta\,\nabla J(\theta_k)$$ move opposite the local steepest ascent, then re-linearise β€” that "then" is why the step must be small

    The learning-rate regimes, all four of which you will see in a career. Too small: the loss falls, but the linear model is re-evaluated far slower than necessary β€” you crawl, and a budget of steps gets you nowhere. Just right: each step buys a healthy fraction of the remaining error, and the loss falls geometrically. Too large but still stable: the iterates oscillate β€” you overshoot the valley floor, land on the other wall, get flung back. Past the stability limit: divergence β€” the loss climbs, doubles, hits overflow, and prints nan.

    The complete, checkable result. Take the simplest possible convex function, $J(\theta)=\frac{1}{2}\theta^2$, whose gradient is $\theta$. The update is a multiplication by a constant:

    $$J(\theta)=\tfrac12\theta^2 \;\Rightarrow\; \theta_{k+1}=(1-\eta)\theta_k \;\Rightarrow\; \theta_k=(1-\eta)^k\theta_0, \qquad \text{converges iff } |1-\eta| \lt 1$$ so 0 < Ξ· < 2: convergent, and Ξ· = 1 lands on the minimum in a single step

    There is nothing approximate about that statement β€” it is an equality, and you can check it by hand. $\eta = 0.9$ shrinks the error by $0.1$ per step; $\eta = 1.5$ flips the sign and shrinks by $0.5$ (zig-zagging but converging); $\eta = 2$ flips the sign with no shrinkage β€” a perfect oscillation forever; $\eta = 2.1$ multiplies the error by $1.1$ each step and runs away. Pass 3 sweeps $\eta$ and draws exactly this curve. The general statement replaces $1$ with the curvature: on a quadratic with Hessian eigenvalues in $[\lambda_{\min},\lambda_{\max}]$, the same one-number argument gives the stability threshold $\eta \lt 2/\lambda_{\max}$ β€” and Section 9's $\kappa=\lambda_{\max}/\lambda_{\min}$ is the ratio between the step you can afford and the step the flat directions want.

    Convergence rates, stated not proved. On a strongly convex, smooth loss (Hessian eigenvalues in $[\mu, L]$ with $0 \lt \mu \le L$) and $\eta \le 1/L$, the error shrinks by a constant factor each step: the distance to the minimum is multiplied by at most $\left(1-\eta\mu\right)$ β€” linear (geometric) convergence, which means "halve the error every $c$ steps" forever. With convexity but no strong convexity you only get sublinear convergence: the objective gap falls like $O(1/k)$, so ten times more steps buys one extra digit. Non-convex losses get neither statement β€” only "the gradient norm gets small". And $\kappa = L/\mu$ is baked into the constant: a worse-conditioned problem converges more slowly no matter how carefully you pick $\eta$.

    The gradient points downhill locally, not at the minimum

    $-\nabla J$ is the best direction for a tiny step. It does not aim at the minimum, it does not know where the minimum is, and on a curved valley it famously points mostly across the valley rather than along it β€” the zig-zag that motivates momentum, rescaling and second-order methods. When you debug a trajectory, ask first "is my linear model valid at this step size?", not "is my loss broken?".

    4 Β· Batch, mini-batch, stochastic

    The gradient of the average is the average of the gradients. If $J(\theta)=\frac{1}{n}\sum_i L_i(\theta)$, then $\nabla J = \frac{1}{n}\sum_i \nabla L_i$ β€” which means you may trade exactness for cost:

    $$\nabla J(\theta) = \frac{1}{n}\sum_{i=1}^{n}\nabla_\theta L_i(\theta) \qquad\text{vs}\qquad g_B(\theta) = \frac{1}{B}\sum_{i \in B}\nabla_\theta L_i(\theta)$$ full gradient over all n samples vs mini-batch gradient over B of them β€” g_B is unbiased: E[g_B] = βˆ‡J

    Batch uses all $n$ samples: an exact gradient, zero noise, and a cost proportional to $n$ per step. Stochastic uses $B=1$: a single sample's gradient β€” biased in the large, but an unbiased estimator at every point, and $n$ times cheaper per step. Mini-batch is everything real training does: $B$ between about 32 and 512, mostly to make the estimate less noisy and to use the GPU, which is a matrix-multiply engine that idles below a few thousand elements.

    Batch size $B$Gradient qualityCost per stepPer epoch
    $1$ (SGD)Extremely noisy β€” one sample's opinion$O(d)$$n$ cheap steps
    $32$–$512$ (mini-batch)Noisy but directionally useful; GPU-efficient$O(Bd)$$n/B$ steps
    $n$ (full batch)Exact, deterministic$O(nd)$one step

    The unit of progress is the epoch β€” one full pass over the $n$ samples. Compare methods by epochs as well as by steps, or you will rank a method that saw 40 times less data as faster. Shuffling at every epoch is not decoration: if batches are not i.i.d. samples, consecutive gradients are correlated, the effective batch size collapses, and the optimizer can chase a sequence of single-class or single-source batches. This bites hardest exactly where data arrives in order β€” time series, per-user records, files sorted by directory.

    The noise is a feature β€” the honest version

    Standard intuition, worth holding while knowing it is an active research area rather than a theorem: the noise in $g_B$ is small only in the directions where samples agree. Sharp minima β€” where the loss climbs steeply in most directions β€” are places a noisy step escapes, while broad flat minima absorb it. So SGD's noise acts as a filter that prefers flat regions, and flat minima tend to generalise better (they tolerate the shift from training data to test data). The same noise doubles as an implicit regulariser: it effectively caps how tightly you can fit the training set, in the same spirit as $\lambda\Omega(\theta)$ but without writing the penalty down. What is not in dispute: the noise is real, it slows convergence on any single objective, and it is why the step-size schedule of Section 5 exists β€” you shrink $\eta$ to let the iterates settle into a minimum the noise would otherwise kick them out of.

    Practical sizes. Start at 32–128 for anything you iterate on quickly; go to 256–512 when the GPU is idle (utilisation below ~80%) and memory allows; go larger only when you have a schedule that scales with it, because the generalisation behaviour changes with $B$. Doubling the batch size means you can roughly double the learning rate and halve the epochs β€” the interesting part is the "roughly", which is empirical and problem-dependent, not a law.

    5 Β· Momentum, Nesterov, RMSProp, Adam

    Plain gradient descent reacts to each gradient as if it were the whole truth. All four optimisers below are answers to the same complaint: that estimate is noisy, badly scaled, and cheap to improve.

    Momentum. Keep a velocity β€” an exponential moving average of past gradients β€” and step along it instead of the raw gradient:

    $$v \leftarrow \beta v - \eta\,\nabla J(\theta), \qquad \theta \leftarrow \theta + v$$ Ξ² β‰ˆ 0.9 (0.99 for noisy problems): the ball remembers which way it was already rolling

    The physical analogy is literal: $v$ is a velocity and $\beta$ is friction. On a narrow curved valley, plain GD zig-zags because the across-the-valley component flips sign every step while the along-the-valley component is tiny. Momentum averages away the flipping component (they cancel in the running average) and accumulates the consistent one (they add). The "look-back window" follows from the weights: $v_t = -\eta\sum_{k\ge0}\beta^k g_{t-k}$, and $\sum_{k\ge0}(1-\beta)\beta^k = 1$ normalises those weights β€” so the average lives over about $1/(1-\beta)$ steps. $\beta=0.9$ β‰ˆ 10 steps, $\beta=0.95$ β‰ˆ 20, $\beta=0.99$ β‰ˆ 100. Two consequences worth knowing: a constant gradient produces a steady-state velocity $\eta/(1-\beta)\cdot\nabla J$ β€” so momentum does scale your effective step by $1/(1-\beta)$; and a changing gradient takes $1/(1-\beta)$ steps to be fully forgotten, which is why a large $\beta$ makes an optimizer sluggish to correct course (and why warmup exists, below).

    Nesterov is momentum with the gradient evaluated where you are about to land: compute $g$ at $\theta+v$ instead of $\theta$. It is the same averaging with better lookahead, and it typically converges a few steps sooner. One sentence is enough β€” you will meet it as nesterov=True.

    AdaGrad and RMSProp: per-parameter step sizes. The second complaint is scaling: parameters with rare, large gradients (the vocabulary of a sparse feature, say) need small steps, while frequent small-gradient parameters need large ones. AdaGrad divides each coordinate's step by the running root of squared gradients. RMSProp fixes AdaGrad's fatal flaw β€” its accumulator only ever grows, so the effective learning rate decays to zero and learning stops β€” by making the accumulation exponential rather than cumulative:

    $$s_t = \gamma\, s_{t-1} + (1-\gamma)\, g_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_t}+\varepsilon}\, g_t$$ divide each coordinate by the RMS of its recent gradients β€” steep directions get shrunk, flat ones don't

    Adam = momentum (first moment $m$) + RMSProp (second moment $v$) + one correction. Both running averages start at zero, and with decay $\beta_1 = 0.9$ the estimate is heavily biased toward zero in early steps β€” the update would be far too small exactly when you know least. Dividing by the unbiased normaliser $1-\beta^t$ removes exactly that bias, because $\mathbb{E}[\beta^t m_0] = \beta^t$:

    $$m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t, \qquad v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2$$ first moment (mean gradient) and second moment (mean squared gradient), both exponential averages
    $$\hat m_t = \frac{m_t}{1-\beta_1^{\,t}}, \qquad \hat v_t = \frac{v_t}{1-\beta_2^{\,t}}, \qquad \theta_{t+1} = \theta_t - \eta\,\frac{\hat m_t}{\sqrt{\hat v_t}+\varepsilon}$$ divide by t=1 the correction is 1/(1-β₁)=10 for m, and the step stays sane; without it the early steps are starved

    Defaults are $\beta_1=0.9$, $\beta_2=0.999$, $\varepsilon=10^{-8}$, $\eta=10^{-3}$ β€” and they are good defaults, which is most of why Adam won.

    • AdamW β€” decoupled weight decay: apply $w \leftarrow (1-\eta\lambda)w$ to the weights directly, not by folding $\lambda$ into the gradient. With Adam, folded-in L2 gets divided by $\sqrt{s_t}$, so "the same $\lambda$" shrinks different parameters by different amounts. Decoupling makes weight decay mean what it says. This is the correct default for networks.
    • Lion (2023) β€” update by the sign of the momentum, nothing else: one hyperparameter-free rule that often matches Adam at lower memory cost. Sign updates are a natural fit with quantisation.
    • Sophia (2023) β€” a diagonal second-order (Hessian-based) preconditioner with clipping, which reacts to curvature rather than to gradient magnitude, and reports better sample efficiency on language models.
    • 8-bit optimisers β€” store the state in 8-bit integers with block-wise quantisation, cutting optimizer memory by ~4Γ— at almost no quality cost: the state is three vectors the size of the model, which matters when the model is large.
    ChooseWhenWhy
    SGD + momentum ($\beta=0.9$)Convolutional vision models; when you want a generalisation claim to be defensibleLess state, fewer moving parts, and its flat-minima bias is the behaviour you are relying on
    Adam / AdamWTransformers, language models, anything with sparse or badly scaled gradients, anything you want to just workPer-parameter steps absorb bad scaling; robust out of the box; the default practice
    L-BFGS (Section 6)Small convex problems, full-batch objectives, hyperparameter searchesUses curvature; no learning-rate ritual needed; matches the closed form (Pass 3, Block 4)

    Schedules. The learning rate is a function of time, not a constant. Step decay: drop $\eta$ by 10Γ— every fixed number of epochs β€” crude and effective. Cosine: decay $\eta$ from a peak to near zero along half a cosine wave β€” the modern default for fine-tuning, because it spends most of the run at a small step where fine detail gets settled. Warmup: ramp $\eta$ linearly from ~0 to the peak over the first 500–2000 steps. Warmup matters because of Section 9: early in training the gradients are uninformative and, crucially, the second moments $v_t$ have not been estimated yet β€” a few large early gradients can poison $v_t$ for the rest of the run (the running average with $\beta_2=0.999$ forgets on a 1000-step timescale), so the safest way to survive estimation error is to be small while it is worst.

    "Momentum is just a bigger learning rate" β€” no, but it is close enough to be dangerous

    With a constant gradient, momentum's steady-state step is exactly $\eta/(1-\beta)$ β€” ten times bigger at $\beta=0.9$. That is why "turn on momentum and keep the learning rate" silently multiplies your effective step size. What momentum is not is a scalar rescaling: on a curved loss it also rotates the effective direction toward the consistent gradient component, which is the whole point. Practical rule: when you switch on momentum, either keep the learning rate (and know you moved it ~10Γ—) or divide it by $1/(1-\beta)$ to stay comparable.

    6 Β· Second-order methods, honestly

    Newton's method uses all three terms of the second-order Taylor expansion β€” value, slope, and curvature β€” by solving the local quadratic model exactly:

    $$\theta \leftarrow \theta - H^{-1}\nabla J(\theta), \qquad H_{ij} = \frac{\partial^2 J}{\partial\theta_i \partial\theta_j}$$ the same linear solve you used for the normal equations in Module 02 β€” now once per iteration

    Why it is beautiful, in two sentences. First, scale invariance: $H^{-1}$ rescales each direction by its own curvature, so a valley that is $10^6$ times steeper in one direction than another gets equalised instead of demanding a compromise step size β€” it is a preconditioner computed from the true local geometry. Second, quadratic convergence near a minimum: once you are close enough that the quadratic model is accurate, one step lands on its minimum, which is the minimum; the error squares every iteration, so three or four steps buy full double precision.

    Why it is not the default at scale, in two numbers. $H$ has $d^2$ entries β€” for $d = 10^9$ parameters that is $10^{18}$ numbers, far beyond any memory on Earth β€” and inverting or solving it costs $O(d^3)$, worse still. Even forming a matrix-vector product with $H$ requires second-order derivatives you would rather not store. So Newton's method lives where $d$ is small and the objective is smooth: the inner loops of classical scientific computing, small convex models, and β€” the part you will actually meet β€” as the idea behind every method below.

    • Quasi-Newton (L-BFGS). Never form $H$. Instead keep the last $m$ (say 10) gradient and position changes and build a low-rank approximate inverse Hessian from them, implicitly, in $O(md)$ memory. L-BFGS is what scikit-learn reaches for on small convex problems, it needs no learning-rate tuning, and Pass 3 Block 4 shows it hitting the closed-form ridge solution to $10^{-9}$ in 24 iterations.
    • Natural gradient. The clever reframe: rather than the geometry of the parameter space, use the geometry of the model distribution. Two parameter settings that barely change the predicted probabilities are close, however far apart the raw parameters are. The metric that says so is the Fisher information matrix $F = \mathbb{E}[\nabla\log p\,\nabla\log p^\top]$, and the natural gradient step is $F^{-1}\nabla J$ instead of $H^{-1}\nabla J$. The idea β€” precondition by a statistic of the model, not by the raw curvature β€” is the direct ancestor of modern variance-reduced and diagonal preconditioning methods (K-FAC, Shampoo, SOAP), and it explains why Adam's $1/\sqrt{v}$ feels like it works: it is a crude diagonal stand-in for exactly this.

    7 Β· Constrained optimisation and Lagrange multipliers

    So far the constraint was invisible: minimise $J(\theta)$ over all of $\mathbb{R}^d$. Real problems add requirements β€” weights bounded, budget spent, outputs summing to one, margin at least 1. The idea that makes constraints tractable is geometric, and it is the picture to keep:

    The picture: tangent contours

    At a constrained optimum, the contours of $J$ are tangent to the boundary of the feasible set. If they merely crossed, you could slide along the constraint and lower $J$ β€” so you would not be at an optimum. Tangent means the two normals are parallel: $\nabla J$ and $\nabla g$ (the constraint's outward normal) point along the same line. That parallelism is the Lagrange condition β€” it is what $\nabla J = -\mu\,\nabla g$ says, in one line, in any number of dimensions.

    Turn the picture into algebra with the Lagrangian: the objective with the constraint folded in as a term whose strength is a new unknown, the multiplier $\mu$. For an inequality constraint $g(\theta) \le 0$:

    $$\mathcal{L}(\theta,\mu) = J(\theta) + \mu\, g(\theta)$$ ΞΌ is the price of the constraint: how much the optimum would improve if you loosened it by one unit

    The KKT conditions are the full statement of optimality for a constrained problem. Stated plainly, at the solution $\theta^\star$:

    $$\text{feasibility: } g(\theta^\star) \le 0 \qquad \text{stationarity: } \nabla J(\theta^\star) + \mu\,\nabla g(\theta^\star) = 0 \qquad \text{complementary slackness: } \mu\, g(\theta^\star) = 0, \quad \mu \ge 0$$ the constraint holds; the gradients balance; and either the constraint is tight (g = 0) or it is free (ΞΌ = 0) β€” never both active

    Complementary slackness is the one people forget and the one that does the work: $\mu g = 0$ with both factors non-negative forces one of them to vanish. Either the constraint is active β€” you are sitting exactly on the boundary β€” and then $\mu$ can be positive, meaning the constraint is genuinely costing you; or it is inactive β€” the unconstrained optimum was already feasible β€” and then $\mu = 0$, and the constraint may be deleted without changing the answer.

    The primal–dual story, one paragraph. For every constrained ("primal") problem there is a dual problem, obtained by asking: what is the best lower bound on $J(\theta^\star)$ that I can certify by choosing a $\mu$? Minimising over $\theta$ with $\mu$ fixed gives a bound; the dual problem maximises that bound over $\mu$. Weak duality always holds β€” the dual value never exceeds the primal optimum, so any dual-feasible $\mu$ certifies that you cannot do better than its bound. Strong duality β€” no gap at all, dual optimum = primal optimum β€” holds for convex problems under mild conditions (Slater's: a strictly feasible point exists). That is why convexity buys more than convergence: it guarantees a certificate. And it is what makes the kernel trick possible, next.

    Payoff (a): ridge regression, with the algebra. Constrained least squares with a budget on the weight norm, and its unconstrained twin:

    $$\min_w\ \frac{1}{n}\|Xw-y\|^2 \quad \text{subject to} \quad \|w\|^2 \le t$$ the constraint is active exactly when the least-squares solution is too big for the budget t

    Write the Lagrangian, differentiate, and solve. Four steps, all mechanical β€” but they are the correspondence between "$\lambda$ in scikit-learn" and "a budget you could actually state to a stakeholder":

    $$\mathcal{L}(w,\mu) = \tfrac{1}{n}\|Xw-y\|^2 + \mu\big(\|w\|^2 - t\big) \;\Rightarrow\; \nabla_w \mathcal{L} = \tfrac{2}{n}X^\top(Xw-y) + 2\mu w = 0$$ stationarity: set the gradient to zero β€” the same normal equations as Module 02, with one extra term
    $$\big(X^\top X + n\mu I\big) w = X^\top y \qquad\Longrightarrow\qquad w(\mu) = \big(X^\top X + n\mu I\big)^{-1} X^\top y$$ this IS ridge regression: Ξ» = ΞΌ, and larger t (looser budget) β‡’ smaller Ξ»

    Compare with the ridge objective $\frac{1}{n}\|Xw-y\|^2 + \lambda\|w\|^2$, whose stationarity condition is $(X^\top X + n\lambda I)w = X^\top y$ β€” identical, with $\lambda = \mu$. So the constrained and penalised forms trace out the same curve of solutions; $\lambda$ and the radius $t$ are two dials on one control. Read off the correspondence: if $\|w_{\text{OLS}}\|^2 \le t$, the constraint is slack, $\mu = 0$ and $\lambda = 0$ β€” no shrinkage at all; as $t$ shrinks, $\mu$ (and hence $\lambda$) must grow to push the solution in; at $t \to 0$ you shrink toward zero. Complementary slackness $\mu(\|w\|^2 - t) = 0$ says exactly this: for every $t$ below the unconstrained norm, the budget binds and the solution sits on the sphere of radius $\sqrt{t}$.

    Payoff (b): the SVM and why its dual buys kernels. The maximum-margin classifier is a constrained quadratic program: maximise the margin $\|w\|^2$ subject to $y_i(w^\top x_i + b) \ge 1$ for every sample. Its dual has one multiplier $\mu_i$ per training sample, and two features of that dual explain everything about SVMs. First: samples with $\mu_i = 0$ contribute nothing to the solution β€” only those with $\mu_i \gt 0$ matter, and they are exactly the points on or inside the margin: the support vectors. The model is defined by a handful of points, not by the dataset. Second: the dual objective and the solution depend on the data only through inner products $x_i^\top x_j$ β€” so you may replace them by a kernel $k(x_i,x_j) = \phi(x_i)^\top\phi(x_j)$ and implicitly optimise in a feature space you never wrote down. That is the kernel trick, and it is available because the problem was convex and its dual was tractable.

    Payoff (c): projection and resource allocation. When the feasible set is simple, the cheapest solver is projected gradient descent: take the step, then project back onto the set β€” clip a parameter to $[-c,c]$, renormalise a weight vector to unit norm, mask a budget. When the set is not simple, the multiplier $\mu$ becomes a price: allocate resource where $\nabla J$ is largest per unit of constraint, which is how capacity, rate limits and budgets get optimised in systems too. The vocabulary transfers directly β€” "dual", "multiplier", "price", "shadow cost" β€” from optimisation into the systems work you already know.

    8 Β· Regularisation as an optimisation choice

    Section 1 introduced $\lambda\Omega(\theta)$ as a term. Here are the three versions you will actually use, and each one is a different answer to the same question: which solution among the many that fit the data equally well do you prefer?

    $$J_{L_2}(w) = \frac{1}{n}\|Xw-y\|^2 + \lambda\sum_j w_j^2, \qquad \nabla_w\,\lambda\|w\|^2 = 2\lambda w$$ L2 / weight decay: the gradient it adds is proportional to the weight itself β€” a multiplicative shrink every step

    L2 pulls every coefficient toward zero in proportion to its size, so big coefficients shrink hardest. Because the gradient $2\lambda w$ vanishes as $w \to 0$, a coefficient gets close to zero but never exactly there β€” the update is $w \leftarrow (1-2\eta\lambda)w$ plus the data term, an exponential decay that only reaches zero in the limit. (Pass 3 Block 3 prints the smallest surviving L2 coefficient: $9 \times 10^{-4}$ β€” small, and not zero.)

    $$J_{L_1}(w) = \frac{1}{n}\|Xw-y\|^2 + \lambda\sum_j |w_j|, \qquad \partial|w_j| = \operatorname{sign}(w_j) \text{ for } w_j \ne 0,\ \in[-1,1] \text{ at } w_j = 0$$ L1: not differentiable at zero β€” a kink β€” which is precisely what produces exact zeros

    L1 produces sparse solutions: exact zeros, and therefore feature selection. Two arguments, both worth owning. The subgradient argument: $|w|$ is not differentiable at 0 β€” its subdifferential there is the whole interval $[-1,1]$ β€” so the data gradient must exceed $\lambda$ in magnitude before the coefficient can leave zero. Weak signals never clear that hurdle and are set exactly to zero. The geometric argument: draw the contours of the data-fitting term and the constraint set $\|w\|_1 \le t$, which is a diamond with its corners on the axes. When a tilted ellipse first touches the diamond, it very often touches at a corner β€” and a corner is a point where all coordinates but one are zero. The $\ell_2$ ball is a circle with no corners, so contact is generically at a non-axis point and no coefficient is exactly zero.

    Getting the gradient wrong here is the standard bug: $\operatorname{sign}(w)$ is a subgradient, and plain subgradient steps on L1 oscillate around zero rather than settling on it. The correct simple algorithm is proximal gradient (ISTA): take the gradient step for the smooth part, then apply the proximal operator of the $\ell_1$ term, which is a soft threshold:

    $$w_j \leftarrow \operatorname{sign}(z_j)\,\max\big(|z_j| - \eta\lambda,\ 0\big), \qquad z = w - \eta\,\nabla\Big(\tfrac{1}{n}\|Xw-y\|^2\Big)$$ shrink by Ξ·Ξ», then clamp to zero β€” the max() is what makes exact zeros appear
    • Elastic net β€” both penalties: $\lambda_1\|w\|_1 + \lambda_2\|w\|^2$. Sparsity with stable selection among correlated features, where pure L1 picks one of a correlated group at random and L2 keeps all of them.
    • Early stopping β€” regularise by not finishing. The trajectory itself limits effective capacity: after $k$ steps an iterative solver has mostly fitted the directions with large eigenvalues (the ones it can reach fastest), and stopping before convergence acts like an $\ell_2$ penalty with $\lambda$ decreasing in $k$. It is implicit regularisation β€” the optimiser is doing the constraint without writing it down.
    • Dropout β€” regularise by randomly deleting units each step: the surviving sub-network must fit without any single unit, which pushes against brittle co-adaptation. Data augmentation β€” regularise by making the training distribution fatter: every rotated or cropped image is a new constraint on the solution. Same idea, different faces: shrink the hypothesis space, or fill the data space.
    Choosing $\lambda$ is a statistical decision, not an optimisation one

    $\lambda$ does not appear inside the gradient's job; it is chosen by the outer loop of Module 05 β€” cross-validation over a grid, and the honest choice of metric. The correspondence from Section 7 gives it a second, more communicable reading: $\lambda$ is a constraint radius, so "we capped the weight norm at $\sqrt{t}$" is a sentence you can defend, while "$\lambda = 0.01$" is a sentence you can only defend by showing the validation curve.

    9 Β· Numerical reality: conditioning, and why your model might be untrainable

    Sometimes the model is fine, the loss is right, the code is correct β€” and training still crawls. The reason is usually arithmetic, not ideas.

    $$\kappa(A) = \frac{\lambda_{\max}(A)}{\lambda_{\min}(A)} \qquad \kappa(X^\top X) = \kappa(X)^2$$ the condition number β€” ratio of extreme eigenvalues; and the square law that makes least squares worse than it looks

    $\kappa = 1$ means every direction is equally curved β€” a circular bowl, one learning rate fits all. Large $\kappa$ means the loss is a long narrow valley: the gradient in the steep direction is $\kappa$ times the one in the flat direction, so any $\eta$ that survives the steep wall crawls along the valley floor. The number to remember is the square law: $\kappa(X^\top X) = \kappa(X)^2$, because the eigenvalues of $X^\top X$ are the squares of the singular values of $X$. Two features on scales $1$ and $100$ give $\kappa(X) \approx 100$ and $\kappa(X^\top X) \approx 10^4$ β€” your optimisation problem is ten thousand times worse-scaled than your data looked. The second symptom is numerical: solving $X^\top X w = X^\top y$ loses roughly $\log_{10}\kappa$ decimal digits to rounding, which is why the normal equations are avoided in serious linear algebra code (Module 02).

    The cheapest fix in all of ML: scale the features. Standardising each column to zero mean and unit variance makes the singular values cluster near 1, $\kappa \approx 1$, and one learning rate serves every coordinate. Pass 3's exercise measures a 5000Γ— speed-up from scaling alone β€” no algorithm change, no tuning, one line of preprocessing.

    Ill-conditioning is not a bug in your code or your model; it is a property of the coordinates you expressed the problem in β€” and rescaling is the fix that costs nothing.

    Deep networks add a second failure of the same family: gradients along the chain. Backpropagation multiplies one factor per layer (Module 03, Section 7), and the product of many numbers that are each a bit below 1 is a number that is essentially 0 β€” vanishing gradients, where early layers never learn. The product of many a bit above 1 is overflow β€” exploding gradients, where one step destroys the weights and prints nan. Same mechanism, opposite sign, and the mitigations are all attempts to keep those per-layer factors near 1:

    • Initialisation. Weights must be drawn with variance matched to the layer's width, so signals neither blow up nor die in the forward pass and the gradients behave in the backward one. Xavier/Glorot: variance $\propto 1/\text{fan-in}$, derived for linear and sigmoid/tanh layers. He: variance $\propto 2/\text{fan-in}$ β€” twice as large, to compensate for the ReLU units that it zeroes out.
    • Normalisation layers (BatchNorm, LayerNorm): re-centre and re-scale activations to a controlled distribution each step, so the next layer always sees inputs of sane magnitude β€” which keeps gradients in the non-saturating range.
    • Residual connections: $y = F(x) + x$ gives the gradient a clean additive path β€” $\partial L/\partial x = \partial L/\partial y + \ldots$ β€” so the signal can flow backwards through hundreds of layers without being multiplied by anything.
    • Gradient clipping: rescale the whole gradient to norm at most $c$ when it spikes. It treats the symptom, deliberately, because sometimes the symptom is all you need.
    "My loss is NaN" β€” check in this order, always

    In practice this is almost never an exotic bug. In order of likelihood: (1) the learning rate β€” halve it; if the NaN disappears, you were past the stability limit of Section 3, and the real fix is a smaller $\eta$ or a warmup. (2) Un-normalised or un-preprocessed inputs β€” a feature with a scale of $10^6$ multiplied through fifty layers gives infinities long before it gives a bug report; this is Section 9's conditioning problem in its most violent form. (3) A numerically unsafe loss β€” $\log(0)$ from a probability that underflowed, a division by zero, an overflow in a softmax without max-subtraction. Only when all three are excluded should you go looking for the exotic explanation: an inverted gradient, a poisoned checkpoint, a corrupted dataloader. The first two fixes cost minutes; the third costs days.

    10 Β· The map

    Every knob you turn in training, translated into what it changes mathematically. Keep this table: it is the answer to "what will halving this number actually do?"

    KnobWhat it changes mathematicallyTell-tale when wrong
    Learning rate $\eta$Step size along $-\nabla J$; stability requires $\eta \lt 2/\lambda_{\max}$Too small: flat loss for ages. Too large: oscillation, then nan
    Batch size $B$Gradient noise (variance $\propto 1/B$) and throughput per epochToo small: noisy floor, no convergence. Too large: wasted GPU, poor generalisation
    Momentum $\beta$Effective smoothing window $1/(1-\beta)$ steps, and effective step $\times 1/(1-\beta)$Too large: sluggish correction. Too small: you still have plain GD
    Regularisation $\lambda$Size of the complexity constraint ($\ell_1$ or $\ell_2$) on $\theta$ β€” the radius $t$ of Section 7Too small: overfit (train β‰ͺ test). Too large: underfit (both high)
    Feature scalingConditioning $\kappa$ of $X^\top X$ β€” the geometry the optimiser must traverseConvergence needs an absurdly tiny $\eta$; coefficients unstable
    Optimizer familyThe preconditioner: identity (GD), moving average (momentum), diagonal second moment (Adam), curvature model (L-BFGS, Newton)One parameter crawls while another oscillates β†’ you need per-parameter steps
    Depth / architectureWhether $J$ is convex at all, and the product of per-layer gradient factorsTrainable shallow, untrainable deep β†’ vanishing gradients, use residuals/norms
    Pass 3

    Implement β€” check the math against the machine

    Type these; do not copy. Then change one number and predict the result

    Block 1 Β· GD vs SGD vs momentum on one anisotropic quadratic

    Same loss, three optimisers. The quadratic is rotated so the valley is not axis-aligned β€” that is what makes plain GD zig-zag, and what makes the difference between the methods visible.

    import numpy as np
    import matplotlib.pyplot as plt
    
    # anisotropic quadratic, rotated so plain GD visibly zig-zags
    th = np.deg2rad(30)
    R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
    A = R @ np.diag([1.0, 50.0]) @ R.T          # eigenvalues 1 and 50 -> kappa = 50
    J  = lambda p: 0.5 * p @ A @ p
    gJ = lambda p: A @ p
    print("eigs:", np.round(np.linalg.eigvalsh(A), 6), " kappa:", np.linalg.cond(A))
    
    # SGD data: n per-sample quadratics whose MEAN is exactly J
    rng = np.random.default_rng(0)
    n = 40
    a = rng.uniform(0.6, 1.4, n); a *= 1.0 / a.mean()   # rescale so the means hit exactly
    b = rng.uniform(40.0, 60.0, n); b *= 50.0 / b.mean()
    Ai = np.array([R @ np.diag([a[i], b[i]]) @ R.T for i in range(n)])
    print("mean per-sample Hessian == A:", np.allclose(Ai.mean(0), A))
    
    def run(kind, eta, steps=20000, tol=1e-6, seed=0):
        p, v = np.array([2.0, -1.5]), np.zeros(2)
        r = np.random.default_rng(seed)
        curve, hit = [J(p)], None
        for k in range(1, steps + 1):
            if kind == "momentum":
                v = 0.9 * v - eta * gJ(p); p = p + v
            elif kind == "gd":
                p = p - eta * gJ(p)
            else:                                   # sgd: one sample's gradient
                p = p - eta * (Ai[r.integers(n)] @ p)
            curve.append(J(p))
            if hit is None and J(p) < tol: hit = k
        return np.array(curve), hit
    
    for kind, eta in [("gd", 0.02), ("sgd", 0.005), ("momentum", 0.02)]:
        curve, hit = run(kind, eta)
        print(f"{kind:9s} eta={eta:<6} iterations to J<1e-6: {hit}")
        # gd 324 | sgd 1293 (~32 epochs of 40 samples) | momentum 141

    Three honest readings of that line. Momentum needs 141 steps where plain GD needs 324 β€” the same $\eta$, roughly 2.3Γ— fewer iterations, because the consistent valley-floor direction accumulates while the across-valley zig-zag cancels. SGD needs 1293 steps β€” but each of its steps used one sample where the others used 40, so in data terms SGD used 1293 samples to GD's 12960: the trade of Section 4, quantified. And note what the tolerance means: SGD's noise keeps it from settling far below $10^{-6}$, which is the noise floor β€” the same feature Section 4 calls implicit regularisation, seen as a floor on the achievable training loss.

    Block 2 Β· A learning-rate sweep, against the exact threshold

    import numpy as np
    import matplotlib.pyplot as plt
    
    # 1-D quadratic: J(theta) = 0.5 theta^2  ->  theta_{k+1} = (1-eta) theta_k
    etas = np.logspace(-3, np.log10(4.0), 400)
    final1 = np.abs(0.5 * (1 - etas) ** 100)          # closed form after 100 steps
    thr1 = etas[final1 < 0.5].max()
    print("1-D : largest grid eta still converging:", round(thr1, 4), "(theory: 2.0)")
    
    # same experiment on a 2-D anisotropic quadratic: threshold set by the curvature
    th = np.deg2rad(30); R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
    A = R @ np.diag([1.0, 50.0]) @ R.T
    lam_max = np.linalg.eigvalsh(A).max()
    p0 = np.array([2.0, -1.5])
    
    def gd2d(eta, steps=100):
        p = p0.copy()
        with np.errstate(over='ignore', invalid='ignore'):   # diverging eta overflows on purpose
            for _ in range(steps): p = p - eta * (A @ p)
            return 0.5 * p @ p
    
    final2 = np.array([gd2d(e) for e in etas])
    thr2 = etas[final2 < 0.5 * p0 @ A @ p0].max()
    print("2-D : largest grid eta still converging:", round(thr2, 4),
          "(theory 2/lambda_max =", round(2 / lam_max, 4), ")")
    
    plt.semilogx(etas, np.maximum(final1, 1e-20), label="1-D: J = 0.5 theta^2")
    plt.semilogx(etas, np.maximum(final2, 1e-20), label="2-D: lambda_max = 50")
    plt.axvline(2.0, ls='--', c='k'); plt.axvline(2 / lam_max, ls=':', c='r')
    plt.xlabel("learning rate eta"); plt.ylabel("J after 100 steps")
    plt.legend(); plt.show()
    
    for e in [0.1, 1.0, 1.5, 1.9, 2.1]:                # the exact factor, five times
        print(f"eta={e:<4} |1-eta| = {abs(1-e):.2f}  -> {'converges' if abs(1-e) < 1 else 'DIVERGES'}")

    The measured thresholds land where the theory puts them: $1.973$ against a theoretical $2.0$ (the grid's largest point below 2), and $0.0396$ against $2/\lambda_{\max} = 0.04$. The second number is the important one β€” the same method on the same kind of problem has a different threshold once the curvature changes. That is why "the learning rate" is not a property of an algorithm: it is a property of the algorithm and the loss, joined by $\lambda_{\max}$. It is also why Adam's per-parameter division by $\sqrt{v_t}$ helps so much: it removes the dependence on scale, direction by direction.

    Block 3 Β· L1 vs L2: what "sparsity" actually means

    A synthetic regression with a genuinely sparse truth: 5 of 60 coefficients are non-zero. Fit it twice, once with each penalty, and count exact zeros.

    import numpy as np
    from sklearn.linear_model import Lasso, Ridge
    
    rng = np.random.default_rng(1)
    n, d = 200, 60
    X = rng.normal(size=(n, d))
    X = (X - X.mean(0)) / X.std(0)                       # standardise: one step size for all
    w_true = np.zeros(d)
    support = rng.choice(d, 5, replace=False)
    w_true[support] = [2.0, -3.0, 1.5, -1.0, 2.5]
    y = X @ w_true + 0.3 * rng.normal(size=n)
    
    L = np.linalg.eigvalsh(2 / n * X.T @ X).max()        # Lipschitz constant of the gradient
    step = 1.0 / L
    grad = lambda w: (2 / n) * X.T @ (X @ w - y)
    soft = lambda z, t: np.sign(z) * np.maximum(np.abs(z) - t, 0.0)
    lam = 0.15
    
    w2 = np.zeros(d)                                     # L2: plain gradient steps
    for _ in range(4000):
        w2 -= step * (grad(w2) + 2 * lam * w2)
    
    w1 = np.zeros(d)                                     # L1: proximal gradient (ISTA)
    for _ in range(4000):
        w1 = soft(w1 - step * grad(w1), step * lam)
    
    print("L1 zeros:", d - np.count_nonzero(w1), " support found:", np.sort(np.flatnonzero(w1)))
    print("L2 zeros:", d - np.count_nonzero(w2), " smallest |w|:", f"{np.abs(w2).min():.4f}")
    lasso = Lasso(alpha=lam / 2, fit_intercept=False, max_iter=50000, tol=1e-10).fit(X, y)
    ridge = Ridge(alpha=n * lam, fit_intercept=False).fit(X, y)
    print("max |w_L1 - Lasso|:", f"{np.abs(w1 - lasso.coef_).max():.2e}",
          " max |w_L2 - Ridge|:", f"{np.abs(w2 - ridge.coef_).max():.2e}")

    Printed: 55 exact zeros from L1, and the five survivors are exactly the true support $\{4, 32, 37, 40, 56\}$ β€” a clean recovery. Zero exact zeros from L2: the smallest coefficient is $9\times10^{-4}$, small and not null, exactly as Section 8's argument predicts ($2\lambda w$ vanishes as $w$ does). Both match scikit-learn to $5\times10^{-12}$ or better, which also verifies the scaling correspondence between the two objectives: Lasso minimises $\frac{1}{2n}\|Xw-y\|^2 + \alpha\|w\|_1$, so our $\lambda$ is its $2\alpha$; Ridge minimises $\|Xw-y\|^2 + \alpha\|w\|^2$, so its $\alpha$ is our $n\lambda$. Get those factors wrong and your "same" model silently isn't β€” a real and common bug.

    Block 4 Β· When a quasi-Newton method is the right tool

    import numpy as np
    from scipy.optimize import minimize
    
    # the ridge problem of Block 3: min_w (1/n)||Xw-y||^2 + lam||w||^2
    rng = np.random.default_rng(1)
    n, d = 200, 60
    X = rng.normal(size=(n, d)); X = (X - X.mean(0)) / X.std(0)
    w_true = np.zeros(d); support = rng.choice(d, 5, replace=False)
    w_true[support] = [2.0, -3.0, 1.5, -1.0, 2.5]
    y = X @ w_true + 0.3 * rng.normal(size=n)
    lam = 0.15
    
    f  = lambda w: np.mean((X @ w - y) ** 2) + lam * w @ w
    gf = lambda w: (2 / n) * X.T @ (X @ w - y) + 2 * lam * w
    
    w_cf = np.linalg.solve(X.T @ X + n * lam * np.eye(d), X.T @ y)   # closed form
    
    res = minimize(f, np.zeros(d), jac=gf, method='L-BFGS-B',
                   options=dict(maxiter=2000, ftol=1e-16, gtol=1e-12))
    print(f"L-BFGS: {res.nit} iterations, {res.nfev} gradients,"
          f"  max |w - closed form| = {np.abs(res.x - w_cf).max():.2e}")
    
    L = np.linalg.eigvalsh(2 / n * X.T @ X + 2 * lam * np.eye(d)).max()
    w = np.zeros(d); k = 0
    while np.abs(w - w_cf).max() > 1e-8 and k < 100000:   # plain GD to the same accuracy
        w -= (1 / L) * gf(w); k += 1
    print(f"plain GD: {k} iterations,  max |w - closed form| = {np.abs(w - w_cf).max():.2e}")

    L-BFGS reaches the closed-form answer to $2.7\times10^{-9}$ in 24 iterations and 35 gradient evaluations, where tuned plain gradient descent needs 83 β€” about 3.5Γ— more, on a problem whose condition number is only 5.74. Scale that gap up to a badly conditioned problem and the ratio explodes. The lesson is not "always use L-BFGS"; it is the ordering: on a small convex problem with a closed form, the closed form wins outright (one solve, no iterations); when there is no closed form and the problem is small enough to hold its curvature implicitly, quasi-Newton wins; at billion-parameter scale neither is even representable, which is where Sections 5's Adam and Section 6's diagonal preconditioners come back in.

    Exercise Β· predict the conditioning number, then earn the speed-up
    • Build a badly scaled regression: one feature from $N(0,1)$, one from $N(0,100^2)$, $n=500$. Before running anything, write down your prediction for $\kappa(X)$ and for $\kappa(X^\top X)$ β€” use the singular value picture from Module 02: the singular values are the feature scales, roughly, and $\kappa(X^\top X)=\kappa(X)^2$.
    • Measure: np.linalg.cond(X) and np.linalg.cond(X.T @ X), then the ratio $\kappa(X^\top X)/\kappa(X)^2$. Measured: $101.4$ and $10291$, ratio $1.000$ β€” the square law, to three digits.
    • Race plain GD both ways with $\eta = 1/L$, $L$ the gradient's Lipschitz constant, and count iterations to $\|w - w_{\text{true}}\| \lt 0.05$. Measured: raw features β€” $L = 2.05\times10^4$, $\eta = 4.9\times10^{-5}$, 26923 iterations; standardised β€” $L = 2.18$, $\eta = 0.459$, 5 iterations. A 5000Γ— speed-up from one line of preprocessing, with the algorithm untouched.
    • Explain it in one sentence each way: why the raw run is slow (the $\eta$ allowed by the steep direction is $\lambda_{\max}$-limited, while the useful progress is made in the $\lambda_{\min}$-limited direction β€” progress per step is capped at $1/\kappa$), and why standardising fixes it (it equalises the eigenvalues; the cost is that you must map back with $w = w_{\text{std}}/\text{std}$ to read the answer in original units).
    • Then break it: set the scale to $1000$ and predict the new iteration count before running. $\kappa(X^\top X)$ goes as the square, and the raw run's iterations go with it β€” this is why an un-normalised input can make a model look untrainable when it is merely un-scaled.
    Checkpoint

    Prove it, then start Module 08

    Answer first, then open the feedback

    Multiple choice β€” instant feedback

    1. Which statement about SGD versus full-batch gradient descent is true?

    2. What does momentum with $\beta = 0.9$ actually do?

    3. Which statement about L1 and L2 regularisation is correct?

    4. "Newton's method converges quadratically, so it should be the default for deep networks." Why not?

    5. You add a regulariser $\lambda\Omega(\theta)$ with a larger $\lambda$. What happens to training error and test error?

    6. Ridge regression is $\min_w \frac{1}{n}\|Xw-y\|^2$ subject to $\|w\|^2 \le t$. What does the multiplier $\mu$ mean, and how does it relate to the usual penalty $\lambda$?

    Open questions β€” answer aloud, without notes

    1. Write the training objective with empirical risk and regulariser. Which symbols are data, which are parameters, which are hyperparameters β€” and which of them can the gradient change?
    2. Why does convexity make every local minimum global? Give the one-line segment argument, and name one ML objective that is convex and one that is not.
    3. Derive the gradient descent update from $J(\theta+\Delta) \approx J(\theta) + \nabla J^\top\Delta$: why is $-\nabla J$ the best direction, and why must the step be small?
    4. For $J(\theta)=\frac{1}{2}\theta^2$, write the iteration, the closed form of $\theta_k$, and the exact convergence condition on $\eta$. What happens at $\eta = 2$?
    5. What does $\beta$ do in momentum? What is the look-back window for $\beta = 0.95$, and what is the side effect of that memory on the effective step size?
    6. Write the Lagrangian of the constrained ridge problem, take the stationarity condition, and derive $w(\mu) = (X^\top X + n\mu I)^{-1}X^\top y$. When is $\mu$ zero?
    7. Why does L1 produce exact zeros and L2 not? Give both the subgradient argument and the geometric one (corners of the $\ell_1$ ball).
    8. Your training loss prints nan at step 37. List the first three things you check, in order, and say what each fix tells you about the underlying mathematics.

    Re-answer these in 1 / 3 / 7 days β€” cold, no notes. If you cannot re-derive the ridge correspondence or the $0 \lt \eta \lt 2$ condition on the spot, they are not yours yet.

    Continue in your ML notebook

    This module is the "why"; the companion notebook is the "what it does". ML module 05 Β· Regression & gradient descent runs the update rule on a real dataset and shows the train/test trade that $\lambda$ is steering; ML module 12 Β· Tuning, validation & error analysis is the outer loop that picks $\eta$, $\lambda$ and batch size honestly, by cross-validation rather than by ritual. The optimizer you chose here is only as good as the evaluation you point it at.