Ch. 00
Math & Python Prereqs
Linear algebra, calculus, probability, NumPy. The vocabulary every later chapter assumes.
The whole curriculum runs on five mathematical objects and one programming environment. The objects are vectors, matrices, derivatives, probabilities, and entropies. The environment is Python 3.10+ with NumPy, PyTorch, pandas, and a Jupyter kernel. If you can multiply two matrices in your head for shape correctness, write the chain rule for without looking it up, compute for a coin flip, and import numpy as np from muscle memory, this chapter is a 90-minute skim. If those words feel slippery, this is the chapter where they stop being slippery. The math is not deep. It is the same six tools used over and over for the next 26 chapters, and the goal here is to make those tools so cheap to reach for that you stop noticing them.
FIG 00.1 · Learning outcomes
By the end of this chapter you will be able to:
- Multiply two matrices in NumPy and predict the output shape before running the code.
- Take the gradient of $f(\mathbf{w}) = \frac{1}{2}\|X\mathbf{w} - \mathbf{y}\|^2$ on paper and verify it matches
torch.autograd.grad. - Compute the entropy of a discrete distribution, and explain why cross-entropy is the loss function for almost every classifier you will train.
- Write a vectorized NumPy function that beats its Python-for-loop equivalent by tens to hundreds of times on a length-10000 array (exact multiple varies by machine; the assessment only requires clearing a comfortable floor).
- Set up a reproducible Python environment with
venv,pip,torch, and a Jupyter kernel on either Colab or your own machine. - Read a corpus reference like
08-geron-notebooks/math_linear_algebra §determinantand know whianother chapter words inside that file are the load-bearing ones.
FIG 00.2 · What you need first
- High-school algebra (you remember what $\sum$ and $\prod$ mean).
- Basic Python. You have written
for i in range(10): print(i)at some point. No more is assumed. - A Google account, or a laptop with 8GB+ RAM. The chapter's code runs free on Colab.
This chapter has no inbound prereqs from inside obvix-learn. It is the root of the DAG. If you have already taken a linear algebra course and used NumPy for a week, skim sections 2–7 for vocabulary, then go to section 9 for the practical setup.
FIG 00.3.1
Why this chapter exists
The math you need for the first 14 chapters of obvix-learn is the math you need for one specific operation. That operation is The core routine of training: check which way is downhill, take a small step, and repeat until the guesses are good.Full glossary → on a parameterized function evaluated against data. Every classical ML algorithm, every neural network, every transformer, every diffusion model, every RLHF policy, every mech-interp probe, is some variant of:
- Define a function parameterized by .
- Define a loss that scores on data .
- Compute .
- Update .
That's it. The reason you need linear algebra is that is almost always a collection of matrices, and is a matrix-shaped object you compute by chain rule. The reason you need A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary → is that the loss usually has the form , a negative log-likelihood. The reason you need information theory is to know why A loss that measures how far a model's predicted chances are from the true answer.Full glossary → is the right loss to minimize when you don't know the true data distribution.
Everything in this chapter exists to make those four bullet points readable.
FIG 00.3.2
Python setup: the only environment you'll need
You have two reasonable options for running every code block in this curriculum: Google Colab (zero install, free GPU access, browser-based) or local Python (more control, faster iteration, no quota limits). Pick Colab for the first ten chapters. Pick local for the rest.
Colab path. Go to colab.research.google.com. Create a new notebook. Type the following in the first cell and run it:
import sys, torch, numpy as np, pandas as pd, sklearn
print(f"Python {sys.version.split()[0]}")
print(f"PyTorch {torch.__version__}, CUDA available: {torch.cuda.is_available()}")
print(f"NumPy {np.__version__}, pandas {pd.__version__}, sklearn {sklearn.__version__}")You should see Python 3.10+, PyToranother chapter.x, The system that lets code run on a graphics chip instead of the main processor.Full glossary → True if you selected the T4 runtime. Done.
Local path (Linux/macOS; Windows users add wsl or use Anaconda):
# 1. Create a project venv (use Python 3.10 or newer)
python3 -m venv ~/.venvs/obvix
source ~/.venvs/obvix/bin/activate
# 2. Upgrade pip
pip install --upgrade pip
# 3. Install the core stack
pip install numpy pandas matplotlib scikit-learn jupyter
# 4. PyTorch — pick the right index URL for your CUDA version
# For CPU only:
pip install torch --index-url https://download.pytorch.org/whl/cpu
# For CUDA 12.1:
# pip install torch --index-url https://download.pytorch.org/whl/cu121
# 5. Test
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"If you want notebooks locally: jupyter lab. If you want a tighter editor: VS Code with the Python extension opens .ipynb files natively. If you want package isolation per project: uv from Astral is the fastest tool and the one I use now.
FIG 00.3.3
NumPy in 30 lines
NumPy is the array library every other Python ML library is built on. The only object you need to know is np.ndarray. It is a multi-dimensional, homogeneous, typed buffer with a shape and a How many pixels a sliding filter jumps with each step across an image.Full glossary →.
import numpy as np
# Construction
a = np.array([1, 2, 3, 4]) # shape (4,), dtype int64
b = np.zeros((3, 4)) # shape (3, 4), dtype float64
c = np.ones((2, 3), dtype=np.float32) # shape (2, 3), dtype float32
d = np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
e = np.linspace(0, 1, 5) # [0., 0.25, 0.5, 0.75, 1.]
r = np.random.randn(3, 4) # standard normal, shape (3, 4)
# Shape and dtype
r.shape # (3, 4)
r.dtype # float64
r.ndim # 2
r.size # 12
# Indexing and slicing
r[0] # first row, shape (4,)
r[:, 0] # first column, shape (3,)
r[1:3, :2] # rows 1-2, cols 0-1, shape (2, 2)
r[r > 0] # boolean mask, returns 1D array of all positives
# Reshape (no copy, just stride change)
r.reshape(4, 3)
r.reshape(-1) # flatten to 1D
r.T # transpose, shape (4, 3)
# Arithmetic: element-wise by default
a + 1 # broadcast scalar
r + r # element-wise
r * 2 # element-wise
r ** 2 # element-wise
# Matrix multiplication: explicit
A = np.random.randn(3, 4)
B = np.random.randn(4, 5)
C = A @ B # shape (3, 5)
# Equivalent: np.matmul(A, B), np.dot(A, B) for 2D
# Reductions
r.sum() # scalar
r.sum(axis=0) # shape (4,) — sum each column
r.sum(axis=1) # shape (3,) — sum each row
r.mean(), r.std(), r.max(), r.argmax()The two ideas that trip up beginners: broadcasting and the axis One of the model's internal numbers that gets adjusted as it learns.Full glossary →.
Broadcasting. When you add a (3, 4) array to a (4,) array, NumPy stretches the second to (3, 4) by repeating along axis 0. The full rule: dimensions of size 1 (or absent) are stretched to match. (3, 1) + (1, 4) = (3, 4). This is the operation that makes vectorized code short. It is also the operation that produces silent shape bugs when you forget that (3,) and (3, 1) are different.
Axis. r.sum(axis=0) means "collapse axis 0", returning an array with one fewer dim along that axis. Get this wrong and your loss curve looks fine but the A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → is for the wrong quantity. The fastest debugging trick in machine learning: print(x.shape) everywhere.
FIG 00.3.4
Linear algebra: the four operations you actually use
The 800-page linear algebra textbook covers eight semesters of material. Machine learning uses about four operations from it. Here they are.
One piece of notation first, since it runs through everything below: means a vector of real numbers (a list of length ), and means a matrix with rows and columns. The shape is the whole point — reading "" as " is -by-" is most of what you need.
Vector addition and scalar multiplication
For and :
An operation done to each number on its own, not by combining them together.Full glossary →. This is The core routine of training: check which way is downhill, take a small step, and repeat until the guesses are good.Full glossary →'s update step: .
Inner (dot) product
For :
The most-used operation in ML. A neuron's pre-activation is . An A mechanism that lets a model look back over all the input and focus on the parts that matter right now.Full glossary → score is . A score for whether two sets of numbers are pointing the same direction, even if one is bigger overall.Full glossary → is .
x = np.array([1., 2., 3.])
y = np.array([4., 5., 6.])
x @ y # 32.0
np.dot(x, y) # same
(x * y).sum() # same, slower pathMatrix-vector and matrix-matrix product
For , :
For , :
The thing to remember: inner dimensions must match, outer dimensions become the output shape. (3, 4) @ (4, 5) = (3, 5). (3, 4) @ (5, 4) is an error.
When two shapes almost line up but the inner dimensions are swapped, the fix is the transpose: (written A.T in NumPy) flips a matrix's rows and columns, so an matrix becomes with . It is the standard way to rotate one operand so the inner dimensions meet.
A fully-connected layer is one matrix-matrix product plus a broadcasted vector add:
where , , , .
B, d_in, d_out = 32, 100, 50
X = np.random.randn(B, d_in)
W = np.random.randn(d_out, d_in)
b = np.random.randn(d_out)
Y = X @ W.T + b # (32, 100) @ (100, 50) + (50,) -> (32, 50)
assert Y.shape == (B, d_out)Norms
The (Euclidean) norm of a vector:
The norm (Manhattan):
The norm in general:
You use for distances and energies, for sparsity penalties, for worst-case bounds. The Frobenius norm of a matrix is . It is the norm of the flattened matrix.
np.linalg.norm(x) # L2
np.linalg.norm(x, ord=1) # L1
np.linalg.norm(x, ord=np.inf) # Linf
np.linalg.norm(A) # Frobenius for 2DTwo operations to know about but not memorize.
Inverse and pseudo-inverse. For square non-singular , the inverse satisfies . You almost never compute it explicitly in ML; you solve via np.linalg.solve(A, b) instead. For non-square , the Moore-Penrose pseudo-inverse generalizes this. It shows up in the closed-form solution to least squares: , which you basically never compute that way in practice (you use SGD or sklearn.linear_model.LinearRegression).
Eigendecomposition and SVD. when is symmetric. for any matrix. These power dimensionality reduction (PCA), and they show up in transformer training as the singular value spectrum of A number inside the model that gets multiplied by an input, deciding how much that input pushes on the final guess.Full glossary → matrices. another chapter is the chapter where they earn their keep.
FIG 00.3.5
Calculus: derivatives that scale to vectors
ML uses three calculus tools: scalar derivatives, the chain rule, and vector gradients. Everything else (Taylor series, integration by parts, the implicit function theorem) lives outside the curriculum.
Scalar derivatives
The rules you need:
| Function | Derivative |
|---|---|
| (constant) | |
The A function that takes any number and squeezes it into a single confidence between 0 and 1.Full glossary → is the squashing function that maps any real number into , which is why it turns a score into a A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary → for binary classification. Its derivative shows up so often it's worth memorizing in that factored form — and note that the factor goes to zero as the output saturates toward 0 or 1, the fact that section 7 leans on to explain why squared error stalls there but A loss that measures how far a model's predicted chances are from the true answer.Full glossary → does not.
Chain rule
If and , then
This is the rule. A calculation that works backward from the mistake to figure out how much each weight and bias was to blame for it.Full glossary → is the chain rule applied carefully. The careful part is that you have to remember what activations you saw on the way in.
Example. . Let . Then and , so .
Partial derivatives and gradients
If , the partial derivative with respect to holds all other coordinates fixed:
The A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → is the vector of all partials:
Three facts about the gradient that you need:
- Direction of steepest ascent. points in the direction increases fastest, locally.
- Zero at minima. If is a local minimum and is smooth, .
- Linear in . . .
Common gradients in ML
Each identity below is derived the same mechanical way: write the scalar function out as a sum over coordinates, take the ordinary partial holding the rest fixed, then stack those partials back into a vector — that stacked vector is the gradient. You never need a new rule; it is the scalar chain rule applied one coordinate at a time. The four results worth memorizing, so you don't redo the bookkeeping each time:
For , , :
The last one is the gradient of the least-squares loss. Setting it to zero gives the normal equations .
Verifying gradients with autograd
When in doubt, check your hand-derived gradient against PyTorch.
import torch
# Define a simple quadratic
def f(w, X, y):
return ((X @ w - y) ** 2).sum() / 2
# Random data
torch.manual_seed(0)
X = torch.randn(10, 3)
y = torch.randn(10)
w = torch.randn(3, requires_grad=True)
# Forward + backward
loss = f(w, X, y)
loss.backward()
# Hand-derived gradient: X^T (Xw - y)
manual = X.T @ (X @ w - y)
print("autograd:", w.grad)
print("manual: ", manual)
assert torch.allclose(w.grad, manual, atol=1e-5)If assert fails, your math is wrong. If it passes, you can trust the autograd.
FIG 00.3.6
Probability: distributions, expectations, conditioning
A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary → shows up in three places: defining loss functions, modeling stochastic optimizers, and reasoning about sampling.
Random variables and distributions
A random variable is a function from a sample space to a value space. We almost never write the sample space. What we work with is the distribution .
Discrete: and .
Continuous: (a density, can be ) and . Probabilities are integrals over intervals.
The distributions you'll meet in the first ten chapters:
| Name | Support | Parameters | Why it shows up |
|---|---|---|---|
| Bernoulli | Binary classification target; A training trick where the model randomly switches off some of its own pieces each pass, so it can't lean too hard on any one of them.Full glossary → mask (each unit kept/dropped by its own coin flip) | ||
| Categorical | Multi-class target | ||
| Gaussian | Choosing the starting values for a model's weights before any learning happens.Full glossary →, noise models | ||
| Uniform | No-opinion default; random init (no pull toward any value) |
Expectation
The expected value of under :
- Discrete:
- Continuous:
Linearity. always. . This is the one rule you'll use ten times per derivation.
Variance. .
Joint, conditional, marginal
Given a joint :
- Marginal: (or integrate for continuous)
- Conditional:
- Independence: iff
Bayes' rule
Reading: posterior = likelihood × prior / evidence. The evidence is just the normalizer.
Bayes' rule is the basis of every probabilistic classifier you'll meet: naive Bayes, Gaussian discriminant analysis, logistic regression's probabilistic interpretation, Bayesian neural networks. It also justifies why we train classifiers to output and pick as the prediction.
Maximum likelihood: the principle behind every loss
Given data and a parameterized model , the maximum likelihood estimate is:
Products of probabilities When a number gets so tiny that the computer rounds it down to zero and loses it.Full glossary →. We take logs:
That last expression is negative log-likelihood, the most common A rule that turns one bad guess into a single number measuring how far off it was, where lower is better.Full glossary → in deep learning. For Gaussian it becomes mean squared error. For categorical it becomes A loss that measures how far a model's predicted chances are from the true answer.Full glossary →. Same idea, different distributional assumption.
FIG 00.3.7
Information theory: entropy and cross-entropy
You need three quantities. All are measured in nats (natural log) or bits (log base 2). Use nats; PyTorch does.
Entropy
The entropy of a distribution :
Entropy measures uncertainty. A deterministic distribution ( is a one-hot) has . A uniform over outcomes has . Intuitively: how surprised you'd be, on average, by a sample.
Cross-entropy
The A loss that measures how far a model's predicted chances are from the true answer.Full glossary → between (truth) and (your model):
This is what you minimize when you train a classifier. If is the empirical distribution (a one-hot for each training example) and is your model's predictive distribution, then cross-entropy is the same loss as negative log-likelihood:
Read that again. Cross-entropy and NLL are two names for the same thing when the target distribution is a one-hot.
KL divergence
The Kullback-Leibler divergence from to :
KL is non-negative, zero iff , not symmetric (). It appears as the "distance" between distributions in VAEs, in RLHF, in mech-interp The raw score the model outputs before it's converted into a clean percentage with sigmoid or softmax.Full glossary →-difference analyses.
Why cross-entropy works for classification
Two reasons.
First, it has the right A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary →. For A function that turns a list of raw scores into confidences for several options that add up to exactly 1 (100%).Full glossary → output and one-hot target , the gradient of with respect to the logits — the raw, un-normalized scores the model emits just before the softmax turns them into probabilities — is exactly . That is the simplest possible gradient: model minus target. It makes training stable.
Second, it correctly penalizes overconfident wrong predictions. Mean squared error on probabilities saturates the gradient when the prediction is near 0 or 1. Cross-entropy does not. A confident wrong prediction (say when the truth is ) produces a large loss and a large gradient.
import torch
import torch.nn.functional as F
# logits: (batch, n_classes), targets: (batch,) integer labels
logits = torch.randn(4, 3)
targets = torch.tensor([0, 2, 1, 1])
loss = F.cross_entropy(logits, targets)
print(loss.item()) # one scalar
# What it does under the hood: log_softmax + nll_loss
log_probs = F.log_softmax(logits, dim=-1)
manual = -log_probs[torch.arange(4), targets].mean()
assert torch.allclose(loss, manual)FIG 00.3.8
Putting it together: linear regression in 30 lines
Every concept from sections 4–7 shows up in one place: deriving and implementing linear regression. This is the smallest end-to-end ML algorithm and it earns its place as the first thing you implement.
Problem. Given with and , find , that minimize:
Probabilistic interpretation. Assume with . The MLE of under this model is exactly the minimizer of the squared loss. That is why squared loss is the canonical regression loss: it is NLL under Gaussian noise.
A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary →. Stack the data as , . Absorb into by adding a column of ones to . Then and from section 4:
The same fit done two ways against the library. First, the closed-form solver: sklearn's LinearRegression solves the normal equations directly, where the from-scratch path descends to the same minimum by SGD.
sklearn LinearRegression (closed form) vs. gradient descent from scratch
Classical MLmodel = LinearRegression()
model.fit(X, y)
print(model.coef_, model.intercept_)def gradient_descent(X, y, lr=0.1, n_steps=500):
N, d = X.shape
w = np.zeros(d)
b = 0.0
losses = []
for step in range(n_steps):
losses.append(loss(w, b, X, y))
residual = X @ w + b - y
dw = X.T @ residual / N
db = float(residual.mean())
w = w - lr * dw
b = b - lr * db
return w, b, lossesfrom scratch: lab/solution.py: gradient_descent
- 1
model.fit(X, y)the entire for-loop that iteratively updates w and b for n_steps - 2
model.coef_the returned w (weight vector) - 3
model.intercept_the returned b, learned via the db = residual.mean() gradient - 4
implicit objective: minimize ||Xw + b - y||^2loss = 0.5 * (residual ** 2).mean(), the same least-squares bowl - 5
fit_intercept=True (default) centers X and y to recover bb is a separate parameter updated by its own gradient db
What the one call hides
- Solution mechanism: sklearn does NOT do gradient descent — it solves the normal equations via an SVD-based least-squares solver (scipy.linalg.lstsq), reaching the exact minimizer in one shot rather than iterating.
- No learning rate, no step count, no convergence loop: the closed form has no hyperparameters, so the iterative dynamics the scratch loop teaches are absent.
- Intercept handling: fit_intercept=True centers the data internally and recovers the bias after fitting, not as a gradient parameter.
- No regularization by default (plain OLS); for L2 you switch to Ridge, a different class.
- Numerical conditioning: the SVD solver is robust to near-singular X^T X, whereas a naive (X^T X)^-1 X^T y would blow up.
- Gotcha: fit() reaches the global optimum exactly and instantly, so it will NOT match a scratch run that used too few steps or too small a learning rate — equal answers only in the converged limit.
- Gotcha: model.intercept_ is the scalar bias and model.coef_ is the weight array; beginners mix up which is w and which is b.
- Gotcha: It silently fits unregularized OLS, so on collinear or wide data the coefficients can explode without warning.
Prefer sklearn (or the normal equations) for a plain linear model in production — it's exact and faster; the scratch gradient-descent version is to internalize that .fit() lands at the bottom of the same convex bowl, and that GD is the fallback when the data doesn't fit in memory or the loss has no closed form.
On the job: At work you call .fit(); the only hand-written code is feature prep and choosing OLS vs Ridge — you reach for GD by hand only when the problem outgrows the closed form (out-of-core, non-convex, custom loss).
And the PyTorch version of the same descent — nn.Linear trained with torch.optim.SGD — which mirrors the from-scratch loop step for step, just with autograd doing the gradient bookkeeping.
nn.Linear + SGD vs. gradient descent from scratch
DL primitivemodel = nn.Linear(in_features=1, out_features=1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
loss_fn = nn.MSELoss()
for step in range(500):
optimizer.zero_grad()
loss = loss_fn(model(X_t).squeeze(), y_t)
loss.backward()
optimizer.step()def gradient(w, b, X, y):
N = X.shape[0]
residual = X @ w + b - y
dw = X.T @ residual / N
db = float(residual.mean())
return dw, db
def gradient_descent(X, y, lr=0.1, n_steps=500):
N, d = X.shape
w = np.zeros(d)
b = 0.0
losses = []
for step in range(n_steps):
losses.append(loss(w, b, X, y))
dw, db = gradient(w, b, X, y)
w = w - lr * dw
b = b - lr * db
return w, b, lossesfrom scratch: lab/solution.py: gradient_descent
- 1
nn.Linear(1, 1) holds .weight and .bias as the trainable parametersw = np.zeros(d) and b = 0.0 - 2
model(X_t) forward passX @ w + b inside loss/gradient computes the prediction - 3
nn.MSELoss()(pred, y) = mean(residual**2)0.5 * (residual ** 2).mean() — same objective up to the 0.5 factor, which halves the effective gradient (hence lr/2 to match) - 4
loss.backward() builds .weight.grad and .bias.grad via reverse-mode autogradgradient() hand-derives dw = X.T @ residual / N and db = residual.mean() - 5
optimizer.step() with torch.optim.SGDthe loop body w = w - lr*dw; b = b - lr*db - 6
optimizer.zero_grad()no analog — the scratch loop recomputes the gradient fresh each step, so there is nothing to clear
What the one call hides
- Autograd: backward() derives the gradient by reverse-mode chain rule, so you never write X.T @ residual yourself and never see whether your hand-math is right.
- Parameter init: nn.Linear initializes weight/bias from Kaiming-uniform U(-1/sqrt(fan_in), 1/sqrt(fan_in)), NOT zeros like the scratch version — so without tying the weights the two start from different points (both still converge on this convex problem).
- Reduction: MSELoss defaults to 'mean' (divides by N and by output dim) and has no 0.5 factor, so the same lr is effectively 2x the scratch version's.
- Gradient accumulation: .grad accumulates across backward() calls, which is why zero_grad() is mandatory — a footgun the scratch loop sidesteps entirely.
- Shape handling: nn.Linear emits (N, 1); without .squeeze() it broadcasts against a (N,) target and silently computes the wrong loss.
- Gotcha: Forgetting optimizer.zero_grad() makes gradients accumulate across steps, so the effective learning rate grows each iteration and training diverges.
- Gotcha: MSELoss has no 0.5 factor, so a learning rate copied verbatim from the scratch loop is effectively doubled.
- Gotcha: nn.Linear default init is random, not zero, so 'reproduce my scratch run exactly' fails unless you also seed and overwrite the weights.
In most application code, avoid hand-rolling a linear model's training loop — you use nn.Linear+SGD (or sklearn); the from-scratch version exists so you can see that .backward()+.step() is literally w = w - lr * (X.T @ residual / N).
On the job: What you actually write by hand is the training loop glue around these primitives (zero_grad/backward/step ordering, the .squeeze() shape fix, the lr) — not the gradient math, which autograd owns.
Three implementations, three different APIs, same math, same answer. You now have all the pieces of every classical ML algorithm in this curriculum.
FIG 00.3.9
Vectorization: why loops are slow
NumPy and PyTorch operations dispatch to vectorized C and The system that lets code run on a graphics chip instead of the main processor.Full glossary → kernels. Python for loops do not. The performance gap is two to three orders of magnitude.
import numpy as np
import time
N = 1_000_000
x = np.random.randn(N)
# Slow: Python loop
t0 = time.time()
total_loop = 0.0
for xi in x:
total_loop += xi ** 2
t_loop = time.time() - t0
# Fast: NumPy vectorized
t0 = time.time()
total_np = (x ** 2).sum()
t_np = time.time() - t0
print(f"Python loop: {t_loop:.3f}s")
print(f"NumPy: {t_np:.6f}s")
print(f"Speedup: {t_loop / t_np:.0f}x")On a typical laptop, the speedup ranges from tens to a few hundred times, depending on machine and array size — it is not a fixed number. On GPU with PyTorch, the speedup over CPU NumPy is another 10–100x for large enough arrays.
The two habits this teaches:
- Never write a
forloop over A small group of examples the model looks at together before making one adjustment to its numbers.Full glossary → dimensions in your inner training loop. If you find yourself iterating over batches inside a batch, your code is wrong. Reshape, broadcast, einsum. np.einsumandtorch.einsumare your friends. When you can't see how to broadcast, write the index notation explicitly and let the library figure it out. The string'bnk,bkm->bnm'just names each axis with a letter: any letter that appears on the left but not after the->is summed over (herek, exactly the inner dimension of a matmul), and a shared leading letter likebis carried along untouched — so this does one(n,k) @ (k,m)matmul for each of thebbatch elements. You do not need to be fluent in this yet; it gets a proper treatment once batched tensors become routine. Reach for it only when broadcasting gets hard to read.
# Batched matmul without einsum: messy
A = np.random.randn(32, 10, 20) # (batch, n, k)
B = np.random.randn(32, 20, 15) # (batch, k, m)
C = A @ B # works, but you have to know matmul broadcasts the leading dims
# With einsum: explicit
C = np.einsum('bnk,bkm->bnm', A, B)FIG 00.3.10
pandas: data wrangling without tears
pandas is the library for reading CSVs, joining tables, filtering rows, and slicing columns. You use it before NumPy in every project, because real data lives in CSVs and TSVs and SQLite files, not in np.random.randn calls.
import pandas as pd
# Load
df = pd.read_csv("housing.csv")
# Inspect
df.head() # first 5 rows
df.shape # (n_rows, n_cols)
df.dtypes # column types
df.describe() # summary statistics per numeric column
df.info() # null counts and types
# Select
df["price"] # one column, returns Series
df[["price", "area"]] # multiple columns, returns DataFrame
df.iloc[0:5, :] # positional indexing
df.loc[df["price"] > 1e6, "area"] # boolean mask + column select
# Mutate
df["price_per_sqft"] = df["price"] / df["area"]
# Aggregate
df.groupby("ocean_proximity")["price"].mean()
# Missing data
df.isna().sum() # how many NaNs per column
df.dropna() # drop rows with any NaN
df.fillna(df.median(numeric_only=True)) # fill with median
# To NumPy
X = df.drop(columns=["target"]).values
y = df["target"].valuesThat covers ≈80% of what you'll do with pandas in this curriculum. The other 20% is pd.merge (SQL-style joins), pd.pivot_table (Excel-style pivots), and pd.to_datetime (time series parsing).
FIG 00.3.11
matplotlib: plots that don't fight you
Plot once. Look at the data. Adjust the model. Look at the data again. Matplotlib is the library for the looking-at part.
import matplotlib.pyplot as plt
import numpy as np
# Single plot
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.figure(figsize=(6, 4))
plt.plot(x, y, label="sin(x)")
plt.xlabel("x")
plt.ylabel("y")
plt.title("A sine wave")
plt.legend()
plt.grid(True)
plt.show()
# Multiple subplots
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].scatter(np.random.randn(100), np.random.randn(100))
axes[0].set_title("Scatter")
axes[1].hist(np.random.randn(1000), bins=30)
axes[1].set_title("Histogram")
plt.tight_layout()
plt.show()
# Common ML plots
# Training curve
plt.plot(losses, label="train")
plt.plot(val_losses, label="val")
plt.xlabel("step")
plt.ylabel("loss")
plt.yscale("log") # log scale for loss curves
plt.legend()
# Confusion matrix as a heatmap
import sklearn.metrics
cm = sklearn.metrics.confusion_matrix(y_true, y_pred)
plt.imshow(cm, cmap="Blues")
plt.colorbar()
plt.xlabel("predicted")
plt.ylabel("true")FIG 00.3.12
The single Colab cell that sets up every chapter
For convenience, here's the boilerplate I paste into the first cell of every notebook in this curriculum.
# Common imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
import sklearn
# Reproducibility
np.random.seed(42)
torch.manual_seed(42)
# Better plot defaults
plt.rcParams['figure.figsize'] = (8, 5)
plt.rcParams['figure.dpi'] = 100
plt.rcParams['axes.grid'] = True
plt.rcParams['grid.alpha'] = 0.3
# Device selection
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")Three habits this builds:
- A starting number that makes a program's 'random' choices come out the same every time.Full glossary → RNGs when Being able to run the same code again and get exactly the same result.Full glossary → matters. For learning and debugging, deterministic behavior helps you tell whether a code change caused a result. Production systems may also need reproducible tests and incident replay, while intentionally stochastic behavior should use an explicit, controlled source of randomness.
- Always check your Where a piece of data lives and gets worked on: the main processor or the faster graphics chip.Full glossary →. Putting tensors on the wrong device produces a runtime error. Putting them on CPU when you meant GPU produces a 50x slowdown silently.
- Always set plot defaults once. Tiny plots are unreadable. The default 6x4 dpi 72 plot from matplotlib is too small. Bump it.
FIG 00.4 · Safety lens · this chapter
The math itself is value-neutral, but the practice of math-in-ML has three failure modes that anyone serious about AI safety should internalize early. Two of them are about reasoning errors. One is about Being able to run the same code again and get exactly the same result.Full glossary → hygiene.
Numerical instability hides bugs that look like When a model memorizes the quirks and flukes of its study examples instead of the real pattern, so it flops on anything new.Full glossary →. If you exponentiate large logits without subtracting the max first, your A function that turns a list of raw scores into confidences for several options that add up to exactly 1 (100%).Full glossary → returns nan. If your variance estimate is computed as E[X^2] - E[X]^2 on near-equal scales, catastrophic cancellation makes it negative and your loss curve mysteriously diverges. Both produce model behavior that looks like "the training failed" when really the math was wrong. AI safety researchers spend a non-trivial fraction of their time on numerical stability, because evaluation pipelines that silently produce nan log a "model failed safely" when the truth is "we never tested anything". See 08-geron-notebooks/extra_autodiff §numerical-stability and 16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__information-theory §numerical-stability for the standard fixes: log-sum-exp tricks for softmax, two-pass variance estimators for statistics, and using PyTorch's F.cross_entropy instead of writing softmax-then-NLL by hand.
Reproducibility is a safety property. When you train a model and report a result, another researcher needs to be able to reproduce it. If your only documentation is "I ran the notebook", you have shipped a non-replicable artifact. The fixes are mechanical: A starting number that makes a program's 'random' choices come out the same every time.Full glossary → every RNG (np.random.seed, torch.manual_seed, torch.cuda.manual_seed_all, and random.seed), pin every package version (pip freeze > requirements.txt), and log the git hash of your code with every run. The reason this matters for safety specifically: alignment research is empirical research about claims like "method X reduces harmful behavior by Y%". Unreproducible claims are unfalsifiable claims. Nanda's mech-interp methodology essays make the same point about interp findings: unreproducible circuit claims are circuit claims about a particular initialization, not about the architecture. Anthropic's Core Views on AI Safety (25-alignment-canon/www-anthropic-com-news-core-views-on-ai-safety §empirical-research) is explicit that empirical alignment research depends on this discipline. See also 06-practice/madewithml-mlops-eda §reproducibility and the Yan et al. ML-systems posts in 06-practice/huyenchip-index.
The mental habit you want to build, starting now: when you derive a A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary →, verify it with autograd. When you compute a A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary →, check that it lies in [0, 1] and sums to 1. When you compute a loss, log the histogram of intermediate values once before Rewriting your numbers so that very large measurements and very small ones are put on a comparable footing.Full glossary → up. The cost is ten minutes. The savings are sometimes weeks of debugging. The deeper safety habit underneath: believe nothing until it has been measured. That habit is the precondition for every other safety practice in this curriculum.
FIG 00.5 · Chapter notebook
Build this chapter with your own hands
A single self-contained notebook. You implement the ideas, check yourself against assert cells as you go, then finish with a capstone. Hint ladders and folded solutions throughout, so it runs top-to-bottom even before you fill anything in.
What you'll build
- A finite-difference derivative checker that disagrees with the analytic gradient until you fix the step size, the way Karpathy opens micrograd.
- A fully-connected layer's forward pass Y = X @ W.T + b from raw shapes, plus the broadcast rules that make it one line.
- The least-squares gradient X.T @ (X w - y) derived on paper, then confirmed against torch.autograd to five decimals.
- softmax, cross_entropy, and entropy from scratch, checked against torch.nn.functional, including the overflow bug that makes a naive softmax return nan.
- A from-scratch gradient descent that recovers y = 3x + 5 from noisy data, the smallest end-to-end ML algorithm.
~1 min on CPU · 108 cells · 13 checked exercises · runs in Colab
FIG 00.6 · Going further
02-code-refs/learnx-pythonthe language reference. Skim it once if Python is new.
08-geron-notebooks/math_linear_algebraGéron's standalone linear algebra notebook. Worked examples, runnable in Colab.
08-geron-notebooks/math_differential_calculusthe calculus companion. Same approach.
16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__*D2L's appendix is the densest reference for the math you'll meet in deep learning specifically.
01-explorables/seeingtheory-basic-probabilitythe best interactive intro to probability on the web. 20 minutes well spent.
02-code-refs/amidi-cs229-algebra-calculusStanford CS229's two-page cheat sheet. Print it. Tape it next to your monitor.
23-textbooks/math4mlif you want a full textbook treatment, this is the one.
23-textbooks/mackay-itila §1-2the entropy intuitions chapter. Less computational, more philosophical. Worth it.
FIG 00.7 · What this enables
Chapters you can now read, with the connecting idea written out.
Now that you know what a gradient is, every algorithm in another chapter is "compute this loss, take this gradient, take this step".
You can run pandas on the California housing data without flinching.
Cross-entropy, softmax, and logistic regression are the entire chapter, and you have the math for all three.
FIG 00.8 · 40 sources
- 02-code-refs/amidi-cs229-algebra-calculus
- 02-code-refs/amidi-cs229-prob-stats
- 02-code-refs/amidi-cs229-ml-tips
- 02-code-refs/devhints-numpy
- 02-code-refs/devhints-pandas
- 02-code-refs/learnx-python
- 02-code-refs/ml-glossary-calculus
- 02-code-refs/ml-glossary-linear-algebra
- 02-code-refs/ml-glossary-loss-functions
- 02-code-refs/ml-glossary-math-notation
- 02-code-refs/ml-glossary-probability
- 02-code-refs/quickref-numpy
- 02-code-refs/quickref-python
- 02-code-refs/quickref-pytorch
- 03-curricula/google-mlcc-linear-regression
- 04-stanford/cs229-main-notes-pdf
- 06-practice/geron-install
- 06-practice/huyenchip-index
- 06-practice/kaggle-data-viz
- 06-practice/kaggle-pandas
- 06-practice/kaggle-python
- 06-practice/madewithml-mlops-eda
- 08-geron-notebooks/04_training_linear_models
- 08-geron-notebooks/extra_autodiff
- 08-geron-notebooks/extra_gradient_descent_comparison
- 08-geron-notebooks/math_differential_calculus
- 08-geron-notebooks/math_linear_algebra
- 08-geron-notebooks/tools_matplotlib
- 08-geron-notebooks/tools_numpy
- 08-geron-notebooks/tools_pandas
- 16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__geometry-linear-algebraic-ops
- 16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__information-theory
- 16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__maximum-likelihood
- 16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__multivariable-calculus
- 16-d2l-sections/chapter_appendix-mathematics-for-deep-learning__random-variables
- 16-d2l-sections/chapter_appendix-tools-for-deep-learning__colab
- 16-d2l-sections/chapter_appendix-tools-for-deep-learning__jupyter
- 01-explorables/distill-momentum
- 01-explorables/seeingtheory-basic-probability
- 01-explorables/seeingtheory-bayesian-inference