08 Β· Algorithms as math assemblies
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.
Remember β retrieve before you read
Six questions. Write your answers before opening the cards- Write the normal equation for least squares β the full chain from $\nabla_w\|Xw-y\|^2 = 0$ to $w = \dots$.
- Logistic regression's loss is called binary cross-entropy. Where does it come from β which distribution, which estimation principle, and why the logarithm?
- k-means repeats two steps until nothing changes. What are they?
- A decision tree picks splits by an impurity criterion. Name one impurity measure and say what it equals at a pure node.
- In an SVM, what exactly is the margin, and which quantity does maximising it shrink?
- 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
2. Why sigmoid + Bernoulli likelihood gives cross-entropy
3. The two steps k-means alternates
4. What impurity a tree splits on
5. What the margin is in an SVM
6. What makes a problem convex
Understand β take the machines apart
Same four-part shape for every algorithm: assume β math β win/break β code1 Β· 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:
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:
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:
| Failure | The precise reason | The fix |
|---|---|---|
| Collinear features | Dependent columns β $\operatorname{rank}(X) \lt d$ β $X^\top X$ singular β no inverse | Ridge, 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 solutions | Regularise, 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 error | QR / SVD solvers (what sklearn uses) β never form the inverse yourself |
| Bias where the truth is curved | Nothing in the algebra fails; the model class is just wrong | Add features, or change model |
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):
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:
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$:
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:
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.
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:
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:
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:
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:
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.
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:
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:
| Variant | Feature type | The per-feature model | Where you meet it |
|---|---|---|---|
| Gaussian NB | Continuous | $\mathcal{N}(\mu_{jc}, \sigma_{jc}^2)$, one mean and variance per feature per class | Default for numeric tables; sensor data, quick baselines |
| Multinomial NB | Counts | Bag-of-words frequencies; works in log space with term counts | Text classification β spam, topic, sentiment |
| Bernoulli NB | Binary | Per-feature present/absent probability | Short 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
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:
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.
- 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.
- 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)$.
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.
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.
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):
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:
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.
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:
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):
Eliminating the slack variables gives the form libraries actually optimise β the hinge loss plus the same regulariser, unconstrained:
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:
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 type | Algorithm | Mathematical backbone | The knob that matters | Characteristic failure |
|---|---|---|---|---|
| Regression | Linear regression (OLS) | Projection onto column space Β· Module 02 | Feature set / basis | Singular $X^\top X$; extrapolates linearly forever |
| Regression, high-dim | Ridge / Lasso | Positive-definite quadratic + Lagrangian Β· Modules 02, 07 | $\lambda$ | Ridge keeps useless features; lasso drops correlated twins arbitrarily |
| Binary / multiclass | Logistic regression | Bernoulli likelihood + cross-entropy Β· Modules 04, 06 | Regularisation ($C$) | Cannot fit a non-linear boundary; features must do the work |
| Text / fast baseline | Naive Bayes | Bayes' rule + independence Β· Module 04 | Smoothing $\alpha$ | Overconfident posteriors; correlated features double-counted |
| Clustering | k-means | Alternating minimisation; Voronoi geometry Β· Module 07 | $k$, initialisation | Spherical-cluster assumption; local optima; scale sensitivity |
| Compression / denoise | PCA | Eigendecomposition + SVD (EckartβYoung) Β· Module 02 | Number of components $k$ | Components are unit-driven if unscaled; blind to the target |
| Interpretable rules | Decision tree | Information gain (entropy / Gini) Β· Module 06 | max_depth / leaf size | High variance when deep; axis-aligned steps waste diagonal boundaries |
| Robust tabular | Random forest | Bagging = variance averaging Β· Module 05 | Feature randomness (max_features) | Extrapolates poorly; importance diluted among correlated features |
| Best-in-class tabular | Gradient boosting | Gradient descent on function space, any loss Β· Module 07 | Learning rate Γ number of trees | Tunes easily into overfitting; needs careful regularisation |
| Max-margin / kernels | SVM | Quadratic program, Lagrange/KKT, kernels Β· Modules 07, 02 | $C$ and $\gamma$ | $O(n^2)$β$O(n^3)$ training; probabilities need extra calibration |
| Small data, no training | k-NN | Distance metric Β· Module 02 norms | $k$, feature scaling | Curse of dimensionality: with many features, all points become equidistant and "nearest" stops meaning anything |
If you remember only one thing, per algorithm
Implement β run the assemblies
Five blocks: two from scratch, three to read carefullyBlock 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.
- 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.
Prove it, then start Module 09
Answer first, then open the feedbackMultiple 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?
n_init) exist. (a) is the trap: convergence is guaranteed, global optimality is not. (b) confuses training randomness with prediction randomness β once fitted, assignment is deterministic. (c) states the truth and the falsehood in one breath: non-convexity is the reason global optimality fails.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
- 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.
- 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)$.
- Why does $+\lambda I$ make $X^\top X + \lambda I$ invertible even when $X$ is rank-deficient? Give the two-line argument.
- 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.
- Write the Naive Bayes formula, then explain in two sentences why a false independence assumption still yields a good classifier but bad probabilities.
- 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.
- State PCA's three equivalent characterisations, and say why "max variance" and "min reconstruction error" are the same optimisation.
- 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.
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.