Math Relearn/ Learning/ Module 08

Algorithms as math assemblies

The classic algorithms are not eight different ideas β€” they are the same five mathematical parts bolted together in different orders. Take each one apart, name the parts, and you can predict how it fails before it does.

On this page
    Track 3 Β· Learning

    08 Β· Algorithms as math assemblies

    ⏱ ~2 h 30πŸ“Ž NumPy / scikit-learn🎯 Point at any classic algorithm and name the math holding it up.

    Modules 01–07 handed you the parts: matrices, derivatives, probability, information, optimisation. This module is the assembly line. Eight algorithms, each stripped down to the objective it minimises, the assumption it makes, and the exact piece of math that holds it up β€” because once you can name the parts, you can predict how each one breaks.

    By the end you can

    • Derive the normal equation from $\|Xw-y\|^2$ and read it geometrically as a projection.
    • Explain why ridge has a closed form and lasso does not, and what $\lambda$ does to each coefficient path.
    • Write logistic regression's gradient from memory and see why it is OLS's gradient with new residuals.
    • State what Naive Bayes assumes, why the assumption is wrong, and why the argmax survives anyway.
    • Run k-means as an alternating optimisation and say why it converges to a local optimum.
    • Name the backbone, the one knob that matters, and the characteristic failure of eleven algorithms.

    Why this module exists

    You have met algorithms as APIs: call fit, read score. That view survives until the day a model behaves strangely in production β€” ridge shrinks a coefficient you needed, a Naive Bayes classifier says 0.9999 when it is wrong, k-means gives different clusters on a different Tuesday. None of those failures is mysterious if you know which assumption produced them, and none is diagnosable if you do not.

    So this module reassembles the classics from the math of Modules 01–07. Every algorithm gets the same four-part shape β€” what it assumes, the math (objective, update, closed form), when it wins / when it breaks, code β€” so that by the fifth algorithm the shape itself becomes the lesson: these things are all the same object, a minimisation, wearing different clothes.

    This is also the last stop before the algorithms you will actually use at scale. Logistic regression's gradient is the shape of a neural network's layer update; gradient boosting is gradient descent moved to function space. Learn the assemblies here and the deep learning course stops being new material.

    A machine learning algorithm is not a black box: it is a small assembly of linear algebra, calculus, probability and optimisation β€” and once you can name the parts, you can predict how each one fails.
    Pass 1

    Remember β€” retrieve before you read

    Six questions. Write your answers before opening the cards
    1. Write the normal equation for least squares β€” the full chain from $\nabla_w\|Xw-y\|^2 = 0$ to $w = \dots$.
    2. Logistic regression's loss is called binary cross-entropy. Where does it come from β€” which distribution, which estimation principle, and why the logarithm?
    3. k-means repeats two steps until nothing changes. What are they?
    4. A decision tree picks splits by an impurity criterion. Name one impurity measure and say what it equals at a pure node.
    5. In an SVM, what exactly is the margin, and which quantity does maximising it shrink?
    6. What makes an optimisation problem convex β€” and why should you care about that word before you call fit?

    Model answers

    1. The normal equation, from memory
    $w = (X^\top X)^{-1}X^\top y$. Chain: $\nabla_w\|Xw-y\|^2 = 2X^\top(Xw-y) = 0 \Rightarrow X^\top Xw = X^\top y \Rightarrow w = (X^\top X)^{-1}X^\top y$. It exists only when $X^\top X$ is invertible β€” that is, when the columns of $X$ are linearly independent (Module 02, rank).
    2. Why sigmoid + Bernoulli likelihood gives cross-entropy
    Bernoulli likelihood, maximised on the log scale. A binary label has $p(y=1\mid x)=p$, so one sample contributes $p^y(1-p)^{1-y}$; over $n$ independent samples the likelihood is $\prod_i p_i^{y_i}(1-p_i)^{1-y_i}$. Taking $-\log$ turns the product into a sum: $-\sum_i [y_i\log p_i + (1-y_i)\log(1-p_i)]$ β€” that is binary cross-entropy (Module 04 for the likelihood, Module 06 for what the logarithm measures).
    3. The two steps k-means alternates
    Assign, then update. (1) Every point joins the cluster of its nearest centroid; (2) each centroid moves to the mean of its members. Neither step increases the within-cluster squared error, so the objective falls monotonically β€” to a local optimum that depends on where the centroids started.
    4. What impurity a tree splits on
    Gini impurity $1-\sum_c p_c^2$ or entropy $-\sum_c p_c\log_2 p_c$. Both are $0$ at a pure node (all one class) and maximal at a uniform mix (Gini $=1-1/K$, entropy $=\log_2 K$). The split chosen is the one that reduces the weighted child impurity the most β€” the decision-tree face of information gain (Module 06).
    5. What the margin is in an SVM
    The gap between the separating hyperplane and the nearest data points on either side. With $y_i(w^\top x_i+b)\ge 1$, the geometric margin is $1/\|w\|$, so the whole street is $2/\|w\|$. Maximising the margin is therefore minimising $\tfrac12\|w\|^2$ β€” a smaller $\|w\|$ means a flatter, more stable boundary.
    6. What makes a problem convex
    A bowl shape: any local minimum is a global one. Formally, the feasible set is convex and the objective's line segments lie above the graph ($f(\theta x + (1-\theta)y) \le \theta f(x) + (1-\theta)f(y)$); for a smooth $f$, a positive semidefinite Hessian suffices. You care because gradient descent on a convex problem is guaranteed to reach the best answer β€” every roadblock then comes from the data or the model, never from the optimiser (Module 07).
    Pass 2

    Understand β€” take the machines apart

    Same four-part shape for every algorithm: assume β†’ math β†’ win/break β†’ code

    1 Β· Ordinary least squares β€” the one with a closed form

    What it assumes. The target is a linear function of the features, $f(x) = x^\top w$, plus noise that is independent, mean-zero and constant-variance. That noise story is not decoration: Module 04 showed that Gaussian noise + maximum likelihood is squared-error loss, so minimising $\|Xw-y\|^2$ is not a convention β€” it is the maximum-likelihood estimate of $w$ under that noise model.

    The math. Stack the data in $X \in \mathbb{R}^{n\times d}$ and minimise the squared error. Expand the derivative, using the fact that $X^\top X$ is symmetric:

    $$\nabla_w \|Xw - y\|_2^2 = 2X^\top(Xw - y) = 0 \;\Longrightarrow\; X^\top X w = X^\top y \;\Longrightarrow\; w = (X^\top X)^{-1} X^\top y$$ the normal equation β€” one linear solve, no iterations, no learning rate

    The same result has a picture, and the picture is the part worth keeping. $Xw$ is a combination of the columns of $X$, so the predictions live in the column space of $X$; least squares asks for the point of that space closest to $y$, which is its orthogonal projection (Module 02). Closest means the error vector $e = y - Xw$ is perpendicular to every column of $X$ β€” and "perpendicular to every column" written with dot products is exactly the normal equation again:

    $$y = Xw + e \quad\text{with}\quad X^\top e = 0 \qquad\Longleftrightarrow\qquad X^\top(y - Xw) = 0$$ residual βŠ₯ column space β€” the geometric statement and the algebraic one are the same sentence

    When it wins. Small and medium $d$ (up to a few thousand), features that are genuinely independent, and any time you need the answer in one pass with no hyperparameters to tune. When it breaks β€” four distinct ways, each with its own fix:

    FailureThe precise reasonThe fix
    Collinear featuresDependent columns β‡’ $\operatorname{rank}(X) \lt d$ β‡’ $X^\top X$ singular β‡’ no inverseRidge, or drop a column (the dummy-variable trap in regression is this exact case)
    $d \gt n$ (more features than samples)$X^\top X$ is $d\times d$ but has rank at most $n \lt d$ β€” always singular; infinitely many perfect-fit solutionsRegularise, or use the dual form $w = X^\top(XX^\top)^{-1}y$, which inverts an $n\times n$ matrix instead
    Ill-conditioning$\kappa(X^\top X) = \kappa(X)^2$: nearly-dependent columns square the condition number and drown the answer in rounding errorQR / SVD solvers (what sklearn uses) β€” never form the inverse yourself
    Bias where the truth is curvedNothing in the algebra fails; the model class is just wrongAdd features, or change model
    OLS's failure is never "the features are not Gaussian"

    Gaussian noise is the likelihood story β€” it explains why the loss is squared error and it licenses confidence intervals (Module 05). The formula $w=(X^\top X)^{-1}X^\top y$ is pure algebra and works for any $y$ at all. What breaks the formula is rank deficiency and conditioning, not the shape of the noise. When residuals are badly non-Gaussian or heavy-tailed, the estimate is still computable but no longer efficient and the intervals are no longer honest β€” a different problem with a different fix (robust losses, bootstrap).

    The gradient-descent version, as a preview. When $n$ and $d$ are in the millions, $X^\top X$ is too big to form, so you minimise the same objective by stepping downhill (Module 07):

    $$w \leftarrow w - \eta\,\frac{2}{n}X^\top(Xw - y)$$ the loss is a convex quadratic, so this converges for $\eta \lt 2/\lambda_{\max}(\tfrac{2}{n}X^\top X)$ β€” you will implement it below

    2 Β· Ridge and Lasso β€” the closed form gets a constraint

    What they assume. The same linear model, plus a belief that no single feature should carry the whole prediction: the weights should be small. That belief has a name β€” it is the bias you add to trade a little training error for a lot less variance (Module 05's bias–variance lever).

    Ridge: the math. Add an $\ell_2$ penalty and differentiate as before β€” the only change is one extra term in the gradient:

    $$\min_w\ \|Xw - y\|_2^2 + \lambda\|w\|_2^2 \qquad\Longrightarrow\qquad w = (X^\top X + \lambda I)^{-1} X^\top y$$ same normal equation, with $\lambda I$ added before inverting

    Why this survives a singular $X^\top X$ is a two-line argument you already own from Module 02 (positive definiteness) β€” for any $\lambda \gt 0$ and any $x \ne 0$:

    $$x^\top(X^\top X + \lambda I)x = \|Xx\|_2^2 + \lambda\|x\|_2^2 \ge \lambda\|x\|_2^2 \gt 0$$ every direction costs at least $\lambda$, so nothing is flat, so the matrix is invertible β€” even when $X$ is rank deficient

    Module 07's Lagrangian view says the same thing from the other side: the penalised problem is the constrained one, $\min \|Xw-y\|^2$ subject to $\|w\|_2^2 \le t$, with $\lambda$ playing the role of the multiplier that enforces the budget. You choose $\lambda$; it tells you how big a weight ball the solution must live in.

    Lasso: the $\ell_1$ version. Same model, different penalty β€” and the closed form disappears:

    $$\min_w\ \|Xw - y\|_2^2 + \lambda\|w\|_1 \qquad\text{with } \|w\|_1 = \sum_j |w_j|$$ no closed form: $|w_j|$ has a corner at zero, so the gradient does not exist there and the stationarity equation has no algebraic solution

    What you get instead is sparsity. The geometric reason is worth one picture: at the optimum, the ellipse of the loss surface touches the constraint region. For ridge that region is a sphere, and an ellipse touches a sphere at a generic point β€” every coordinate nonzero. For lasso it is a diamond ($\ell_1$ ball) with sharp corners on the axes, and ellipses like to catch corners: at a corner, some $w_j$ is exactly zero. Those zeros are why lasso doubles as feature selection, and why it is the default weapon for genomics-style data where $d \gg n$ β€” thousands of features, a handful of samples, and you want the three genes that matter.

    The coefficient path, as $\lambda$ grows. Ridge shrinks every weight smoothly toward zero (all of them, none ever exactly zero); lasso zeroes features out one at a time, in order of usefulness. Elastic net splits the difference, $\lambda_1\|w\|_1 + \lambda_2\|w\|_2^2$, and fixes a real lasso weakness: when features are correlated, lasso picks one arbitrarily and discards its twins, while the $\ell_2$ part keeps correlated groups together.

    Reading $\lambda$ as a signal-to-noise dial

    Module 04 showed ridge as MAP estimation with a Gaussian prior on $w$: $\lambda$ is the ratio of the noise variance to the weight prior variance. Small $\lambda$ β€” you trust the data; large $\lambda$ β€” you trusted the prior. In cross-validation terms (Module 05), $\lambda$ is the knob you sweep because it moves you along the bias–variance curve; almost all "tuning" of linear models is tuning this one number.

    3 Β· Logistic regression β€” the closed form disappears, the gradient does the work

    What it assumes. A binary label generated from a probability that is a linear function of the features, pushed into $(0,1)$ by a link function. The link is the sigmoid:

    $$p(y=1\mid x) = \sigma(w^\top x) = \frac{1}{1 + e^{-w^\top x}}$$ a real number on $(-\infty,\infty)$ in, a probability in $(0,1)$ out

    Invert it and the model reads in log-odds β€” the linear model is not on the probability, it is on the log-odds $\log\frac{p}{1-p} = w^\top x$. That gives every coefficient a unit you can quote in a meeting: one extra unit of feature $j$ multiplies the odds of class 1 by $e^{w_j}$. A coefficient of 0.7 means the odds roughly double ($e^{0.7} \approx 2.0$); a coefficient of $-1$ divides them by $e$.

    The math. Labels are Bernoulli given $x$, so the likelihood is the product of $p_i^{y_i}(1-p_i)^{1-y_i}$ over samples (Module 04). Maximise its logarithm, negate, and you have the loss:

    $$L(w) = -\frac{1}{n}\sum_{i=1}^{n}\Big[y_i \log p_i + (1-y_i)\log(1-p_i)\Big], \qquad p_i = \sigma(w^\top x_i)$$ binary cross-entropy β€” Module 06 derives it from surprise; here it is the Bernoulli negative log-likelihood

    Differentiate it, and here is the lovely structural fact of this whole module. Using $\sigma'(z) = \sigma(z)(1-\sigma(z))$, the $n$ per-sample terms collapse into one matrix expression β€” identical in shape to the least-squares gradient, with the residuals swapped:

    $$\nabla_w L = \frac{1}{n} X^\top(\sigma(Xw) - y)$$ compare OLS: $\tfrac{1}{n}X^\top(Xw-y)$. Same assembly β€” predictions minus targets, weighted by the features

    Why is there no closed form then? Set that gradient to zero and you get $X^\top(\sigma(Xw)-y) = 0$, a system that is non-linear in $w$ β€” the sigmoid has no inverse that isolates $w$ from a sum. There is nothing to solve for. What remains is the good news from Module 07: the loss is convex, so plain gradient descent converges to the global optimum, and you will run it by hand below.

    Multiclass: softmax regression. One weight vector per class, and the link becomes the softmax, which is the sigmoid's many-class sibling:

    $$p(y=c\mid x) = \frac{e^{w_c^\top x}}{\sum_{c'} e^{w_{c'}^\top x}}$$ exponentiate each class score, normalise so they sum to 1 β€” the same cross-entropy loss, now over classes

    When it wins. This is the everyday production classifier for a reason: convex (no seed lottery), one row per feature of interpretation, cheap to train, and β€” its genuine strength over trees and boosting β€” its output probabilities are usually calibrated, meaning "0.8" really means about 80%. When it breaks: the boundary is a hyperplane. Anything a straight line cannot separate β€” XOR, concentric circles β€” it cannot learn, however much data you give it. Fix: add features (kernels, splines, interactions) or change model.

    Calibrated is not the same as accurate

    Logistic regression will tell you 0.62/0.38 on genuinely 62/38 data β€” but only if the features and the regularisation are honest. Heavy regularisation shrinks $w$, which pulls probabilities toward the base rate (over-shrunk models look confident-but-flat); heavy separation (a feature that perfectly splits the classes) pushes coefficients to $\pm\infty$ and probabilities to 0 or 1. If you need calibrated scores, check them (reliability plots, Module 05's honest evaluation) rather than assuming them.

    4 Β· Naive Bayes β€” Bayes plus an independence assumption

    What it assumes. A generative story: first draw the class from its prior, then draw each feature independently from the class's distribution. Logistic regression (previous section) is discriminative β€” it models $p(y\mid x)$ directly and says nothing about how $x$ was produced. Naive Bayes models $p(x,y)$ and gets $p(y\mid x)$ by Bayes' rule (Module 04), paying one assumption for everything:

    $$p(y=c\mid x) \propto p(y=c)\prod_{j=1}^{d} p(x_j\mid y=c)$$ prior Γ— a product of per-feature likelihoods β€” the "naive" is the product

    Why the assumption is wrong but the classifier still works. Features are never independent (height and weight, word counts in a sentence). But the product only has to get the ranking right, and correlated features mostly double-count the same evidence in both classes β€” the inflation cancels when you compare class 1 to class 2. Module 04's own block demonstrated it: Gaussian NB scored 0.832 against logistic regression's 0.844 on data whose features were correlated. The ranking survives; the numbers do not (see the overconfidence pitfall below).

    The variants differ only in what "$p(x_j\mid y)$" means:

    VariantFeature typeThe per-feature modelWhere you meet it
    Gaussian NBContinuous$\mathcal{N}(\mu_{jc}, \sigma_{jc}^2)$, one mean and variance per feature per classDefault for numeric tables; sensor data, quick baselines
    Multinomial NBCountsBag-of-words frequencies; works in log space with term countsText classification β€” spam, topic, sentiment
    Bernoulli NBBinaryPer-feature present/absent probabilityShort texts, hashed features, "any of these signals fired"

    Laplace smoothing, and the one line of math that forces it. The product form has a knife edge: if the training data contains a single class $c$ in which feature $j$ never appeared, then $\hat p(x_j\mid c) = 0$, and

    $$\prod_{j} p(x_j\mid c) = \dots \times \underbrace{0}_{\text{one missing feature}} \times \dots = 0$$ one zero factor kills the whole product β€” that class gets posterior 0 forever, no matter the evidence

    The fix is to refuse exact zeros: add a pseudo-count $\alpha$ before normalising, $\hat p(x_j\mid c) = \frac{n_{jc} + \alpha}{N_c + \alpha V}$ (usually $\alpha=1$, "add-one"). It is the counting version of the prior from Module 04 β€” a small belief inserted where the data was silent.

    When it wins: tiny data, many features, embarrassingly parallel counting (training is one pass over the data β€” no optimiser at all), $O(d)$ prediction. When it breaks: anywhere you need the probabilities themselves. Because correlated evidence is counted repeatedly, the product of many terms pushes posteriors to 0 or 1 β€” Naive Bayes is notoriously overconfident. Rank with it; do not quote its 0.9999.

    5 Β· k-means β€” alternating optimisation

    What it assumes. Nothing probabilistic at all: the data is $K$ clumps, each well described by a centre, and "a good clustering" means small within-cluster squared distance:

    $$\min_{\{C_k,\,\mu_k\}}\ \sum_{k=1}^{K}\sum_{i \in C_k} \|x_i - \mu_k\|_2^2$$ the inertia β€” the quantity every elbow plot is drawn from

    The math is two alternating half-solutions. You cannot minimise over assignments and centres at once β€” but each half alone is trivial, so k-means alternates them.

    1. Assignment step. Fix the centres, move each point to its nearest one. The set of points assigned to centre $k$ is exactly the Voronoi cell of $\mu_k$ β€” the region of space closer to $\mu_k$ than to any other centre β€” so the optimal assignment is a geometric fact, not a search.
    2. Update step. Fix the assignments, move each centre to its cluster mean. The mean is not a convention either: it is the minimiser, one gradient away. For one cluster, $\nabla_\mu \sum_{i\in C}\|x_i-\mu\|^2 = -2\sum_{i\in C}(x_i-\mu) = 0 \Rightarrow \mu = \bar{x}_C$.

    Each step leaves the objective unchanged or smaller, and the objective cannot go below zero β€” so the process terminates. What it terminates at is a local optimum: a partition no single point move or centre move can improve, possibly far from the best one. That is why k-means needs restarts, and why k-means++ (spread the initial centres by sampling proportionally to squared distance) is the default initialisation in every library β€” better starts, better local optima, same iterations.

    Choosing $k$, and how it fails

    Neither number falls out of the objective β€” inertia always prefers larger $k$. Two standard detectors: the elbow plots inertia against $k$ and looks for the knee where gains collapse (it measures only within-cluster compactness); the silhouette scores each point $s = (b-a)/\max(a,b)$, where $a$ is its distance to its own cluster and $b$ to the nearest other one β€” so it measures compactness against separation, in $[-1,1]$. Elbows lie when clusters have unequal variance; silhouettes are honest but $O(n^2)$.

    Four failure modes, all inherited from the geometry

    Voronoi cells are convex and roughly spherical, so non-spherical clusters (crescents, rings) get cut in half regardless of $k$ β€” use density-based methods there. Different scales mean the largest-variance feature owns the distance, so standardise your columns first or your clusters measure your units. Imbalanced sizes make big clusters swallow small ones, since every point votes by distance alone. And an empty cluster after an update deletes a centre and silently changes $k$ β€” libraries handle it by re-seeding, but you should know it happened.

    k-means is the hard-assignment special case of EM

    Expectation-maximisation for a Gaussian mixture replaces each hard assignment with a soft "responsibility" $r_{ik} = P(\text{point } i \text{ belongs to } k)$, then updates means as responsibility- weighted averages. Set the responsibilities to 0 or 1 β€” one hot cluster, equal spherical covariance β€” and EM becomes k-means. That is why Gaussian mixtures are the drop-in fix when k-means' single number per point is too blunt, and why EM's "each step does not decrease the likelihood" guarantee is the same guarantee you just used for k-means.

    6 Β· PCA β€” the algorithm you can now fully derive

    Module 02 section 11 derived PCA's engine (eigenvectors of the covariance matrix); section 12 gave you the Eckart–Young theorem. This section is the assembly: one algorithm, three statements that turn out to be the same statement.

    $$\max_{\|u\|_2 = 1}\ \operatorname{Var}(X_c u) \qquad\Longleftrightarrow\qquad \min_{\operatorname{rank}(B) \le k}\ \|X_c - B\|_F \qquad\Longleftrightarrow\qquad \text{top-}k\ \text{singular vectors}$$ most-variance direction = least-reconstruction-error subspace = best rank-$k$ approximation

    Why equivalent? The total variance of the data is fixed, and it splits exactly: variance kept along the $k$ directions + energy lost in the reconstruction error = total (Parseval's identity, applied to the SVD expansion). Maximise one term of a fixed sum and you minimise the other β€” so "directions that see the most" and "directions you can least afford to drop" are the same directions, and Eckart–Young says those directions are the top singular vectors.

    The algorithm, in five steps (each one you have already run in Module 02's code):

    1 Β· Standardisez-scores, if units differ β†’ 2 Β· Centre$X_c = X - \bar{x}$ β†’ 3 Β· Eigendecompose$\Sigma = V\Lambda V^\top$, or SVD of $X_c$ β†’ 4 Β· Project$Z = X_c V_k$ β†’ 5 Β· Keep $k$explained $=\lambda_i/\sum_j\lambda_j$

    Two details decide whether your PCA means anything. Standardise before you centre whenever features carry different units β€” a feature in euros varies 10⁴ times more than one in radians, so its variance, and therefore PC1, reflects your unit choice, not your data (Module 02's pitfall box). And select $k$ by explained variance: keep the components holding 90–95% of $\sum_i\lambda_i$ β€” the fraction is printed by PCA(n_components=...).explained_variance_ratio_.

    One real use: feed the 30 retained components to a classifier instead of the original 2 000 features β€” you cut training time, remove directions that were pure noise, and often improve test accuracy (the denoising effect: the low-variance tail is where noise hides). One misuse: reading the components as features. PC1 is a weighted mix of all input features, its sign is arbitrary, and it is blind to the target β€” "PC7 predicts churn" is not a statement you can act on. For interpretable predictors, use the model; for compression and visualisation, use PCA.

    7 Β· Decision trees, forests, boosting β€” information theory as a splitting rule

    What it assumes. Almost nothing β€” which is the point. A tree is recursive binary splitting: at each node, pick the feature $j$ and threshold $t$ that make the two children as pure as possible, and repeat. No linear model, no distance metric, no distribution. The search is greedy, because the exact answer (the best tree over all subsets of features and thresholds) is NP-hard β€” one sentence, and it explains every overfitting symptom you will meet.

    The math is Module 06's, wearing a decision hat. Impurity measures how mixed a node is:

    $$\text{Gini}(p) = 1 - \sum_c p_c^2, \qquad H(p) = -\sum_c p_c \log_2 p_c, \qquad \text{IG} = H(\text{parent}) - \sum_{\text{child}} \frac{n_{\text{child}}}{n}\,H(\text{child})$$ information gain = bits of surprise removed by the split β€” Module 06 derives why entropy is the right measure; trees just consume it

    Concrete numbers, so it is not a symbol: a 50/50 node has entropy 1 bit and Gini 0.5. Split it into two children that are each 90/10, and the weighted child entropy is 0.469 bits β€” the split removed $1 - 0.469 = 0.531$ bits, and that is the split a tree takes.

    Overfitting and its three levers. A deep tree can memorise: limit it with max_depth, min_samples_leaf, or prune back after growing. The bias–variance story (Module 05 section 9) is textbook here: a shallow tree has near-zero variance and high bias (it cannot express much); a deep tree has near-zero bias on training data and enormous variance (one flipped label near a threshold reshapes a whole branch).

    Forests fix the variance. Random forests average many deep trees, each fitted to a bootstrap resample and each seeing only a random subset of features at every split. Averaging reduces variance (Module 05) as long as the trees are not identical β€” hence the feature randomness, which decorrelates them. Bias barely moves; variance collapses.

    Boosting fixes the bias. Trees are grown sequentially, each new one fitted to the mistakes of its predecessors. In one line each: AdaBoost reweights misclassified points and is exactly coordinate descent on the exponential loss; gradient boosting takes the loss $L(y, \hat y)$, computes the negative gradient $-\partial L / \partial \hat y$ per training point (the "pseudo-residuals"), and fits a regression tree to it. That second sentence is bigger than it looks: gradient descent from Module 07, generalised from weight space to function space, for any differentiable loss β€” square it and you get least-squares boosting, take logs and you get log-loss boosting, and XGBoost/LightGBM are this loop with cleverer trees and second-order terms.

    The one-question diagnostic for ensembles

    Ask "does averaging happen at the end (bagging) or is it sequential accumulation (boosting)?" Averaging independent-ish errors cuts variance β€” forests tolerate deep, overfit trees. Accumulating corrections cuts bias β€” boosting wants shallow trees with low bias themselves. Swap the answer and every tuning decision inverts (this is the module's favourite MCQ trap for a reason).

    8 Β· Support vector machines β€” geometry as the whole story

    What it assumes. That the classes are separable by some boundary, and that among all separating hyperplanes the fattest one is the best. Maximise the margin; the constraints say every point sits at least one margin-width on the right side:

    $$\min_{w,b}\ \tfrac12\|w\|_2^2 \qquad \text{s.t.}\qquad y_i(w^\top x_i + b) \ge 1 \quad \forall i$$ minimising $\|w\|^2$ = maximising the street width $2/\|w\|$ β€” the whole geometry in one line

    The "wider street is better" claim is not aesthetic: it is Module 05's variance argument. A boundary that threads between two close points is fitted to those points (high variance); a wide-street boundary barely moves when any single point moves (low variance), so it generalises. Small $\|w\|$, smooth boundary, low variance β€” three phrasings of one idea.

    Soft margin. Real data is not separable, so slack variables $\xi_i$ let point $i$ violate its constraint by a price $C$ β€” written out with the Lagrangian (Module 07's KKT machinery):

    $$\min_{w,b}\ \tfrac12\|w\|^2 + C\sum_i \xi_i \quad\text{s.t.}\quad y_i(w^\top x_i+b) \ge 1 - \xi_i,\ \ \xi_i \ge 0$$ $$\mathcal{L}(w,b,\xi,\alpha) = \tfrac12\|w\|^2 + C\sum_i \xi_i - \sum_i \alpha_i\big(y_i(w^\top x_i+b) - 1 + \xi_i\big)$$ $C$ trades street width against violations β€” large $C$ penalises every mistake (narrow, obsequious street); small $C$ tolerates them

    Eliminating the slack variables gives the form libraries actually optimise β€” the hinge loss plus the same regulariser, unconstrained:

    $$\min_w\ \tfrac12\|w\|^2 + C\sum_i \max(0,\ 1 - y_i\,\hat y_i)$$ hinge loss: charge only points inside the margin (or on the wrong side); everything beyond the street is free

    That zero-gradient region is why SVMs are robust to outliers that lie far on the correct side: they are already paying nothing. The KKT conditions mark exactly which points do pay: support vectors are the points with $\alpha_i \gt 0$ β€” those on or inside the margin. Delete every other point and the solution does not change: the entire model is defined by a handful of borderline cases.

    The dual, and why it opens the door to kernels. Solving the Lagrangian for $w$ and substituting back gives the dual problem, where $w$ has vanished and the data survives only inside dot products:

    $$\max_{\alpha}\ \sum_i \alpha_i - \tfrac12\sum_i\sum_j \alpha_i\alpha_j\,y_i y_j\, K(x_i, x_j), \qquad 0 \le \alpha_i \le C,\ \ \sum_i \alpha_i y_i = 0$$ the kernel trick: wherever the dual has $x_i^\top x_j$, write $K(x_i,x_j)$ instead β€” no feature map ever computed

    This is the reason the dual matters: the algorithm never sees the features, only their pairwise $K$. Choose $K(x,z) = \phi(x)^\top\phi(z)$ for some enormous $\phi$ and you have implicitly moved to a space where the data might be linearly separable β€” without ever writing $\phi$ down. The standard choice is the RBF kernel $K(x,z) = \exp(-\gamma\|x-z\|_2^2)$, which corresponds to an infinite-dimensional feature space: nearby points get weight near 1, distant ones near 0, and the decision boundary becomes a smooth contour hugging the data. $\gamma$ and $C$ are the two knobs: $\gamma$ controls how far the influence of one point reaches.

    Why SVMs faded for big data, and what survived. The dual's quadratic program scales $O(n^2)$ to $O(n^3)$ in the number of samples β€” with a million rows it simply stops, while a forest or a logistic model trains on a laptop. But the ideas did not fade: margins are the vocabulary of generalisation in deep learning (max-margin classifiers, large-margin softmax), kernels live on inside PCA (kernel PCA) and Gaussian processes, and dual methods with $\alpha$ weights are the backbone of support vector and attention-style reweightings you will meet in Module 09.

    9 Β· The selection map

    Everything above, compressed into the reference you will actually use: which backbone, which knob, which failure. When you meet a new algorithm, find its nearest row and ask what the row's backbone does to it.

    Problem typeAlgorithmMathematical backboneThe knob that mattersCharacteristic failure
    RegressionLinear regression (OLS)Projection onto column space Β· Module 02Feature set / basisSingular $X^\top X$; extrapolates linearly forever
    Regression, high-dimRidge / LassoPositive-definite quadratic + Lagrangian Β· Modules 02, 07$\lambda$Ridge keeps useless features; lasso drops correlated twins arbitrarily
    Binary / multiclassLogistic regressionBernoulli likelihood + cross-entropy Β· Modules 04, 06Regularisation ($C$)Cannot fit a non-linear boundary; features must do the work
    Text / fast baselineNaive BayesBayes' rule + independence Β· Module 04Smoothing $\alpha$Overconfident posteriors; correlated features double-counted
    Clusteringk-meansAlternating minimisation; Voronoi geometry Β· Module 07$k$, initialisationSpherical-cluster assumption; local optima; scale sensitivity
    Compression / denoisePCAEigendecomposition + SVD (Eckart–Young) Β· Module 02Number of components $k$Components are unit-driven if unscaled; blind to the target
    Interpretable rulesDecision treeInformation gain (entropy / Gini) Β· Module 06max_depth / leaf sizeHigh variance when deep; axis-aligned steps waste diagonal boundaries
    Robust tabularRandom forestBagging = variance averaging Β· Module 05Feature randomness (max_features)Extrapolates poorly; importance diluted among correlated features
    Best-in-class tabularGradient boostingGradient descent on function space, any loss Β· Module 07Learning rate Γ— number of treesTunes easily into overfitting; needs careful regularisation
    Max-margin / kernelsSVMQuadratic program, Lagrange/KKT, kernels Β· Modules 07, 02$C$ and $\gamma$$O(n^2)$–$O(n^3)$ training; probabilities need extra calibration
    Small data, no trainingk-NNDistance metric Β· Module 02 norms$k$, feature scalingCurse of dimensionality: with many features, all points become equidistant and "nearest" stops meaning anything

    If you remember only one thing, per algorithm

    least squaresprojection β†’ ridgeλ·‖wβ€–Β², always invertible β†’ lassoβ€–w‖₁ β‡’ sparsity β†’ logisticsigmoid + cross-entropy β†’ naive BayesBayes + independence
    k-meansalternating means β†’ PCAeigen of covariance β†’ treesentropy splits β†’ forestbagging β‡’ less variance β†’ boostingfit the βˆ’gradient
    SVMmargin + Lagrange β†’ kernelsdot products, hidden Ο† β†’ k-NNdistance + curse of d
    Every row of that table is the same sentence with different blanks filled in: assume a model, pick a loss, minimise it with the tool your algebra allows β€” closed form if you are lucky, gradient descent if you are not.
    Pass 3

    Implement β€” run the assemblies

    Five blocks: two from scratch, three to read carefully

    Block 1 Β· OLS from scratch β€” closed form vs gradient descent vs sklearn

    Three routes to the same answer. If they disagree by more than machine precision, something upstream is broken β€” this is the cheapest sanity check in the whole notebook.

    import numpy as np
    from sklearn.linear_model import LinearRegression
    
    rng = np.random.default_rng(0)
    n, d = 200, 5
    X = rng.normal(size=(n, d))
    w_true = np.array([1.5, -2.0, 0.0, 3.0, 0.5])   # note the third coefficient is exactly zero
    y = X @ w_true + rng.normal(scale=0.5, size=n)   # Gaussian noise: OLS is the MLE (Module 04)
    
    # 1) the normal equation, one line
    w_norm = np.linalg.solve(X.T @ X, X.T @ y)
    
    # 2) the same objective by gradient descent (Module 07's engine)
    w_gd = np.zeros(d)
    for _ in range(2000):
        w_gd -= 0.1 * 2 * X.T @ (X @ w_gd - y) / n   # gradient of ||Xw - y||^2 / n
    
    # 3) sklearn solves by SVD and never forms an inverse;
    #    fit_intercept=False because we generated data with no intercept
    w_sk = LinearRegression(fit_intercept=False).fit(X, y).coef_
    
    print("normal eq :", np.round(w_norm, 4))   # [ 1.5386 -2.0429 -0.0157  3.0203  0.5539]
    print("grad desc :", np.round(w_gd, 4))     # same to 4 decimals
    print("sklearn   :", np.round(w_sk, 4))     # same to 4 decimals
    print("max |norm - gd|      : %.2e" % np.abs(w_norm - w_gd).max())      # 4.00e-15
    print("max |norm - sklearn| : %.2e" % np.abs(w_norm - w_sk).max())      # 3.55e-15

    Two observations. The gradient descent answer agrees to $4\times10^{-15}$ β€” not because GD is magic, but because the loss is a convex quadratic, so 2 000 steps of a modest learning rate land in the same bowl. And the third coefficient came back as $-0.0157$ against a true $0$: least squares has no reason to produce an exact zero, it only shrinks noise. That gap is precisely the hole lasso fills.

    Block 2 Β· Logistic regression from scratch β€” the gradient, literally

    import numpy as np
    from sklearn.datasets import make_classification
    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import train_test_split
    
    X, y = make_classification(n_samples=500, n_features=2, n_redundant=0,
                               n_informative=2, class_sep=2.0, random_state=7)
    X = (X - X.mean(0)) / X.std(0)                   # GD needs comparable axes
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=0)
    
    def sigmoid(z): return 1 / (1 + np.exp(-z))
    
    w = np.zeros(3)                                  # 2 weights + bias
    for _ in range(10000):
        p = sigmoid(Xtr @ w[:2] + w[2])
        err = p - ytr                                # "residuals" -- exactly OLS's shape
        w[:2] -= 1.0 * (Xtr.T @ err) / len(ytr)      # grad_w = X^T (sigmoid(Xw) - y) / n
        w[2]  -= 1.0 * err.mean()                    # the bias is a column of ones
    
    acc_tr = ((sigmoid(Xtr @ w[:2] + w[2]) > 0.5) == ytr).mean()
    acc_te = ((sigmoid(Xte @ w[:2] + w[2]) > 0.5) == yte).mean()
    print("scratch  train %.4f  test %.4f  w=%s" % (acc_tr, acc_te, np.round(w, 3)))
    # scratch  train 0.9787  test 0.9600  w=[0.502 6.863 1.06 ]
    
    clf = LogisticRegression(C=1e6, max_iter=5000).fit(Xtr, ytr)   # C huge ~ unpenalised
    w_sk = np.r_[clf.coef_[0], clf.intercept_[0]]
    print("sklearn  train %.4f  test %.4f  w=%s" % (clf.score(Xtr, ytr), clf.score(Xte, yte),
                                                    np.round(w_sk, 3)))
    # sklearn  train 0.9787  test 0.9600  w=[0.501 6.863 1.061]
    print("max |w_scratch - w_sklearn|:", np.abs(w - w_sk).max())   # 8.2e-4

    The gradient is the two-line formula from section 3, implemented literally: predictions minus targets, times the features, averaged. Note the honest touch β€” C=1e6 makes sklearn's default L2 regularisation negligible, so both are solving the same unpenalised problem; with sklearn's default $C=1$ the weights shrink noticeably (its $w_2$ lands near 4.6 instead of 6.9) while the accuracy barely moves. That gap between "coefficients" and "predictions" is regularisation in one screenshot.

    Block 3 Β· k-means from scratch β€” watch the objective fall (and watch it get stuck)

    import numpy as np
    from itertools import permutations
    from sklearn.datasets import make_blobs
    from sklearn.cluster import KMeans
    
    X, y = make_blobs(n_samples=400, centers=4, cluster_std=1.1, random_state=42)
    
    def sq_dists(X, mu):                             # (k, n) squared distances
        return np.stack([((X - m) ** 2).sum(1) for m in mu])
    
    def run(X, mu, iters=25):
        mu, hist = mu.copy(), []
        for _ in range(iters):
            sq = sq_dists(X, mu)
            assign = sq.argmin(0)                    # assignment step: nearest centre
            hist.append(float(sq.min(0).sum()))      # objective, recorded before the update
            mu = np.stack([X[assign == k].mean(0) for k in range(len(mu))])   # update: the mean
        return hist, assign
    
    rng = np.random.default_rng(0)
    runs = [run(X, X[rng.choice(len(X), 4, replace=False)]) for _ in range(10)]
    finals = [h[-1] for h, _ in runs]
    best_h, best_a = runs[int(np.argmin(finals))]
    print("final inertia of 10 random starts:", sorted(round(v, 1) for v in finals))
    # [927.5, 927.5, 927.5, 927.5, 927.5, 927.5, 927.5, 2960.2, 2960.4, 2961.4]
    print("best run, inertia per iteration  :", [round(v, 1) for v in best_h[:6]], "...", round(best_h[-1], 1))
    # [20790.8, 12187.4, 11678.6, 6027.0, 932.8, 927.5] ... 927.5
    print("monotone decreasing:", bool(np.all(np.diff(best_h) <= 1e-9)))   # True
    
    km = KMeans(n_clusters=4, n_init=10, random_state=0).fit(X)   # k-means++ initialisation
    print("my best-of-10 : %.1f   sklearn (++): %.1f" % (best_h[-1], km.inertia_))
    best = max(np.mean(best_a == np.array(p)[km.labels_]) for p in permutations(range(4)))
    print("same partition up to relabelling: %.3f" % best)         # 1.000

    Read the two printed lines together. The objective fell every single iteration β€” the alternating-minimisation guarantee, observed. But three of the ten random starts froze at β‰ˆ2960 instead of 927: same guarantee, worse local optimum, no warning. That is the entire argument for k-means++ and for n_init restarts β€” and the reason clustering answers deserve a second look before you believe them.

    Block 4 Β· Trees and ensembles β€” the bias–variance pattern, on the same data

    import numpy as np
    from sklearn.datasets import make_classification
    from sklearn.model_selection import train_test_split
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
    
    # features 0..5 informative, 6..7 linear copies of them, 8..9 pure noise
    X, y = make_classification(n_samples=2000, n_features=10, n_informative=6, n_redundant=2,
                               n_clusters_per_class=1, class_sep=1.2, flip_y=0.01, random_state=3)
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=3)
    
    models = {
        "tree, depth 2 (high bias)":       DecisionTreeClassifier(max_depth=2, random_state=0),
        "tree, depth 12 (high variance)":  DecisionTreeClassifier(max_depth=12, random_state=0),
        "random forest, 300 trees":        RandomForestClassifier(n_estimators=300, random_state=0),
        "gradient boosting, 200 trees":    GradientBoostingClassifier(n_estimators=200, random_state=0),
    }
    for name, m in models.items():
        m.fit(Xtr, ytr)
        print("%-32s train %.3f   test %.3f" % (name, m.score(Xtr, ytr), m.score(Xte, yte)))
    # tree, depth 2 (high bias)      train 0.929   test 0.937
    # tree, depth 12 (high variance) train 1.000   test 0.973
    # random forest, 300 trees       train 1.000   test 0.993
    # gradient boosting, 200 trees   train 1.000   test 0.992
    
    imp = models["random forest, 300 trees"].feature_importances_
    print("importance on informative features 0-5 : %.2f" % imp[:6].sum())   # 0.94
    print("importance on junk features 6-9        : %.2f" % imp[6:].sum())   # 0.06

    The four rows are the bias–variance curve of Module 05, walked left to right: the depth-2 tree cannot fit the data even on training samples (0.929 β€” bias-limited); the depth-12 tree fits training perfectly and loses 2.7 points on test (variance); averaging 300 decorrelated trees recovers almost all of it (variance down, bias untouched); boosting chases the remaining bias and also lands at 0.992. The importance line is information gain's fingerprint: 94% of the forest's split gains sit on the six features that actually generate the labels.

    Block 5 Β· SVM β€” the margin, and the points that define it

    import numpy as np
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from sklearn.datasets import make_moons
    from sklearn.svm import SVC
    from sklearn.model_selection import train_test_split
    
    X, y = make_moons(n_samples=400, noise=0.25, random_state=0)
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0)
    
    for C in (0.1, 1.0, 100.0):
        clf = SVC(kernel="rbf", C=C).fit(Xtr, ytr)
        print("C=%-6g test acc %.3f   support vectors %d of %d training points"
              % (C, clf.score(Xte, yte), clf.support_.size, len(Xtr)))
    # C=0.1    test acc 0.925   support vectors 152 of 280
    # C=1      test acc 0.942   support vectors 82 of 280
    # C=100    test acc 0.942   support vectors 55 of 280
    
    clf = SVC(kernel="rbf", C=1.0).fit(Xtr, ytr)
    xx, yy = np.meshgrid(np.linspace(X[:, 0].min() - .5, X[:, 0].max() + .5, 300),
                         np.linspace(X[:, 1].min() - .5, X[:, 1].max() + .5, 300))
    Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
    plt.figure(figsize=(6, 4.5))
    plt.contour(xx, yy, Z, levels=[-1, 0, 1], linestyles=["--", "-", "--"], alpha=.7)
    plt.scatter(Xtr[:, 0], Xtr[:, 1], c=ytr, s=18, cmap="coolwarm")
    sv = clf.support_vectors_
    plt.scatter(sv[:, 0], sv[:, 1], s=90, facecolors="none", edgecolors="k",
                linewidths=1.6, label="%d support vectors" % len(sv))
    plt.legend(); plt.title("SVM: boundary, margin lines, support vectors")
    plt.savefig("svm_moons.png", dpi=110, bbox_inches="tight")

    The moons are not linearly separable, and a linear boundary is not even close β€” the RBF kernel smuggles in the infinite-dimensional feature map and draws a curved boundary. The count line is the concept becoming concrete: at $C=1$, 82 of the 280 training points are the model; the other 198 could be deleted with no change to the boundary. And as $C$ grows, the margin tightens to obey more points, so the support vector count falls β€” exactly the slack-variable story from section 8.

    Exercise Β· the four-row honesty table Β· 25 minutes
    • Build the table. Take one dataset you know (or make_classification(n_samples=800, n_features=20, n_informative=8, random_state=1)), split it, and fit: LogisticRegression, DecisionTreeClassifier(max_depth=None), RandomForestClassifier(), KNeighborsClassifier(). Record for each: train accuracy, test accuracy, and β€” in your own words β€” the single mathematical concept you now suspect is responsible for the gap (e.g. "deep tree: greedy splitting on noise β‡’ variance").
    • Rank the gaps. Which model has the biggest train–test gap? Which has the smallest? Does the ranking match the bias–variance story of Modules 05 and 07, or does something surprise you?
    • Predict the shift. Write two sentences: which of the four will degrade most on shifted data (e.g. a later time window, different feature scaling), and why β€” name the assumption it makes that the shift violates.
    • Check yourself. Actually re-run on shifted data (shuffle one feature's scale by Γ—10, or filter to one class-heavy slice) and see whether your prediction held. Being wrong here is more useful than being right.
    Checkpoint

    Prove it, then start Module 09

    Answer first, then open the feedback

    Multiple choice β€” instant feedback

    1. You fit ordinary least squares and the coefficients come out absurd β€” some enormous, some wildly negative, sign-flipping between near-identical runs. What is the precise reason?

    2. What is the real difference between ridge and lasso at $\lambda \gt 0$?

    3. Why does the kernel trick live in the SVM dual?

    4. You run k-means five times with different random seeds and get five different clusterings. Which statement is true?

    5. Random forests average many deep trees; gradient boosting grows trees sequentially on the previous trees' errors. Which pairing of "what it fixes" is correct?

    6. A Naive Bayes classifier outputs $p(\text{spam}\mid x) = 0.9999$ and is wrong. Why is this its characteristic failure rather than an accident?

    Open questions β€” answer aloud, without notes

    1. Derive from memory: the normal equation. Start at $\min_w\|Xw-y\|^2$, differentiate, solve. Then state in one geometric sentence what $Xw$ and $y-Xw$ are.
    2. Derive from memory: logistic regression's gradient. Start from $L = -\frac1n\sum[y_i\log\sigma(z_i) + (1-y_i)\log(1-\sigma(z_i))]$ with $z_i = w^\top x_i$, use $\sigma'(z)=\sigma(z)(1-\sigma(z))$, and arrive at $\frac1n X^\top(\sigma(Xw)-y)$.
    3. Why does $+\lambda I$ make $X^\top X + \lambda I$ invertible even when $X$ is rank-deficient? Give the two-line argument.
    4. Ridge's coefficient path as $\lambda\to\infty$: what happens, and why never an exact zero? Contrast with lasso, and give the geometric reason for the difference.
    5. Write the Naive Bayes formula, then explain in two sentences why a false independence assumption still yields a good classifier but bad probabilities.
    6. k-means: prove in one line that the update step $\mu_k=\bar{x}_{C_k}$ minimises the within-cluster error, and say what the monotone objective does not guarantee.
    7. State PCA's three equivalent characterisations, and say why "max variance" and "min reconstruction error" are the same optimisation.
    8. A colleague says "we use boosting because variance is the problem with our model, and forests because we need lower bias." Correct both halves, one sentence each.

    Re-answer these in 1 day, 3 days, and 7 days from memory. Questions 1 and 2 are derivations β€” do them on paper with no peeking; a derivation you can rebuild is the only kind that survives. Questions 3 and 6 should eventually come out in under a minute each.

    Continue in your ML notebook

    This module was the math; the modelling side of each algorithm β€” tuning, diagnostics, failure analysis β€” lives in the companion notebook: ML module 05 Β· Regression & gradient descent, ML module 06 Β· Logistic regression, ML module 07 Β· Trees, forests & boosting, ML module 08 Β· Distance & probabilistic models (Naive Bayes, k-NN, k-means and mixtures, built from section 4 and 5 here), ML module 09 Β· Unsupervised learning (PCA and clustering as workflows). Module 09 of this notebook then shows the same parts β€” gradients, cross-entropy, alternating minimisation β€” reappearing at scale inside deep learning and agents.