Jacobian matrices
Fourteen lessons that connect high-school mathematics to AI. Start with the plain-language explanation and a small example. Then read the full formulas, derivations, and Python code at your own pace.
Read the opening example. Try the question before revealing its answer. Then work through the detailed notes below it. You do not need to understand every proof on the first reading.
Symbols: a subscript such as xi identifies one entry. Σ means add a collection of terms. ∈ means “belongs to”. The symbol ≈ means “approximately equal”, not exactly equal. A parameter is an adjustable number in a model.
Why This Matters
Every neural network is a differentiable function of millions (sometimes billions) of parameters. Training means using gradients to find parameters that minimize loss. This guide builds the mathematical foundations you need to understand, implement, and research modern AI systems — from basic calculus and linear algebra to information theory and optimization.
01.01 · Functions
Introduction
A model is a rule that turns an input into an output. Start with the tiny rule that doubles 4 and adds 1, producing 9, then see how the same idea describes layers and whole networks.
- Learning goal
Evaluate, name, and combine simple functions, then distinguish a linear rule from an affine rule.
- Before you start
Basic arithmetic, Python function calls, and reading a simple coordinate graph.
Lesson plan
- Trace a concrete input through a rule and learn function notation.
- Connect inputs, parameters, and outputs to a small neural-network layer.
- Compare linear, affine, and composed functions using several worked examples.
Start with one prediction. For f(x)=2x+1, write f(4) before reading on. The input is 4, the fixed rule doubles it and adds 1, and the output is 9. A parameter would be a number inside the rule that training may change.
What is a function?
A function is a rule that maps each allowed input to exactly one output. The notation $f(x)$ means "the output of $f$ when the input is $x$." It does not mean $f$ multiplied by $x$.
A function $f: A \to B$ assigns to each element $x \in A$ (the domain) exactly one element $f(x) \in B$ (the codomain). The set of all outputs $\{f(x) : x \in A\}$ is the range.
Example: A simple rule
Consider $f(x) = 2x + 1$. This rule doubles the input and adds 1:
- $f(2) = 2(2) + 1 = 5$
- $f(3) = 2(3) + 1 = 7$
- $f(-1) = 2(-1) + 1 = -1$
For $g(x) = x^2$, both $g(2) = 4$ and $g(-2) = 4$. This is still a function: each input has one output, but different inputs can share an output.
Functions in neural networks
A neural network is a parameterised function $f_\theta: \mathcal{X} \to \mathcal{Y}$, where $\theta$ are the trainable parameters (weights and biases). A simple linear layer computes:
$$f_\theta(x) = wx + b$$where $w$ (weight) and $b$ (bias) are parameters we adjust during training. In Python:
def predict(x, w=2, b=1):
return w * x + b
print(predict(3)) # 7
print(predict(3, w=4)) # 13
Linear vs. affine
A linear map satisfies $f(x+y) = f(x) + f(y)$ and $f(cx) = cf(x)$. Example: $f(x) = 2x$.
An affine function is a linear map plus an offset: $f(x) = wx + b$. Neural network layers are typically affine transformations followed by nonlinear activations.
Composition
Neural networks compose many functions. If $f(x) = 2x$ and $g(x) = x^2$, then $(g \circ f)(x) = g(f(x)) = (2x)^2 = 4x^2$. Composition is associative but not commutative: $g \circ f \neq f \circ g$ in general.
Every neural network, no matter how complex, is ultimately a composition of many simple functions. Understanding how to differentiate compositions (the chain rule) is the foundation of backpropagation.
01.02 · Derivatives
Introduction
Suppose a square has side length 3 and the side grows slightly. We want the local rate at which its area changes, not only the area itself; shrinking difference quotients reveals that rate.
- Learning goal
Estimate and calculate a derivative, then use it to predict a small change in a function.
- Before you start
Functions, powers, subtraction, division, and the idea of slope on a graph.
Lesson plan
- Measure change over a small interval and interpret its slope.
- Shrink the interval for the square function and derive the exact rate.
- Use derivative notation to predict nearby output changes and identify limits.
Predict a nearby value. If f(x)=x², then f(3)=9. The slope at 3 is 6, so moving to 3.01 predicts an increase near 6×0.01=0.06. The exact value is 9.0601, which shows both the usefulness and the local limit of the derivative.
What is a derivative?
The derivative measures the instantaneous rate of change of a function at a point. For $f(x)$, the derivative at $x$ is:
$$f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}$$Geometrically, $f'(x)$ is the slope of the tangent line to the curve $y = f(x)$ at the point $(x, f(x))$.
The derivative $f'(x)$ is the unique number (if it exists) such that:
$$f(x+h) \approx f(x) + f'(x) \cdot h$$for small $h$. The approximation becomes exact in the limit $h \to 0$.
Example: Derivative of $x^2$
Let $g(x) = x^2$. At $x = 3$, we compute:
| Input change | Output change | Ratio |
|---|---|---|
| 3 → 3.1 | 9 → 9.61; change 0.61 | 6.1 |
| 3 → 3.01 | 9 → 9.0601; change 0.0601 | 6.01 |
| 3 → 3.001 | 9 → 9.006001; change 0.006001 | 6.001 |
The ratios approach 6, so $g'(3) = 6$.
Derivation
Using the limit definition:
$$g'(x) = \lim_{h \to 0} \frac{(x+h)^2 - x^2}{h} = \lim_{h \to 0} \frac{2xh + h^2}{h} = \lim_{h \to 0} (2x + h) = 2x$$So $g'(x) = 2x$, giving $g'(3) = 6$ and $g'(-2) = -4$.
Using the derivative to predict changes
If $x$ moves from 3 to 3.02 (change $h = 0.02$), the derivative predicts:
$$\Delta g \approx g'(3) \cdot h = 6 \times 0.02 = 0.12$$The exact change is $3.02^2 - 3^2 = 0.1204$. Close, but not exact — the approximation improves as $h$ shrinks.
At $x = -2$, $g'(-2) = -4$. If $x$ increases to $-1.99$ ($h = +0.01$):
$$\Delta g \approx -4 \times 0.01 = -0.04$$The output decreases. A negative derivative means increasing the input locally decreases the output.
Notation
| Notation | Read as | Context |
|---|---|---|
| $f'(x)$ | "f prime of x" | Lagrange notation, common in calculus |
| $\frac{df}{dx}$ | "derivative of f with respect to x" | Leibniz notation, emphasizes the variable |
| $Df(x)$ | "D f at x" | Operator notation, used in higher dimensions |
| $\dot{f}$ | "f dot" | Time derivative in physics |
The derivative is a local linear approximation. It tells you the rate of change at a specific point, not globally. The approximation $f(x+h) \approx f(x) + f'(x)h$ is accurate only for small $h$.
01.03 · Vectors
Introduction
A model may receive several features at once, such as values 2 and 3. A vector keeps those values together, while a dot product combines them with weights 4 and 1 to produce one score, 11.
- Learning goal
Represent feature lists as vectors and calculate, code, and interpret their dot product in small examples.
- Before you start
Basic algebra, ordered lists, multiplication, addition, and simple Python or NumPy arrays.
Lesson plan
- Name vector dimensions, components, length, direction, and basic arithmetic operations.
- Calculate a weighted score by multiplying matching entries and adding them.
- Implement vector operations and connect dot products to angles and similarity.
Name the coordinates first. Let x=[2,3] mean two measured features. With weights w=[4,1], the dot product is 4×2+1×3=11. Swapping the vector entries changes the meaning and usually changes the answer.
What is a vector?
A vector is an ordered list of numbers. In AI, vectors represent features, parameters, gradients, and more:
$$\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix} \in \mathbb{R}^n$$Example: a study session might be represented as $\mathbf{x} = [2, 3]$, where $x_1 = 2$ hours reading and $x_2 = 3$ exercises attempted. Order matters: $[3, 2]$ is a different vector.
For vectors $\mathbf{u}, \mathbf{v} \in \mathbb{R}^n$ and scalar $c \in \mathbb{R}$:
- Addition: $\mathbf{u} + \mathbf{v} = [u_1+v_1, \ldots, u_n+v_n]$
- Scalar multiplication: $c\mathbf{u} = [cu_1, \ldots, cu_n]$
- Dot product: $\mathbf{u} \cdot \mathbf{v} = \sum_{i=1}^n u_i v_i$
- Norm (length): $\|\mathbf{u}\| = \sqrt{\mathbf{u} \cdot \mathbf{u}}$
The dot product
The dot product combines two vectors into a scalar. It measures how much one vector "aligns" with another:
$$\mathbf{w} \cdot \mathbf{x} = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n$$Example: with $\mathbf{w} = [4, 1]$ and $\mathbf{x} = [2, 3]$:
$$\mathbf{w} \cdot \mathbf{x} = 4(2) + 1(3) = 8 + 3 = 11$$If $\mathbf{w}$ represents weights (importance) and $\mathbf{x}$ represents features, the dot product gives a weighted score. Increasing $x_1$ by 1 adds $w_1 = 4$ to the score; increasing $x_2$ by 1 adds $w_2 = 1$.
Vector operations in code
import numpy as np
x = np.array([2, 3])
w = np.array([4, 1])
# Dot product
score = np.dot(w, x) # or w @ x
print(score) # 11
# Addition
y = x + np.array([1, 2]) # [3, 5]
# Scalar multiplication
z = 2 * x # [4, 6]
# Norm
length = np.linalg.norm(x) # sqrt(13) ≈ 3.606
Geometric interpretation
A vector $\mathbf{x} = [x_1, x_2]$ can be drawn as an arrow from the origin $(0,0)$ to the point $(x_1, x_2)$. The dot product has a geometric formula:
$$\mathbf{u} \cdot \mathbf{v} = \|\mathbf{u}\| \|\mathbf{v}\| \cos\theta$$where $\theta$ is the angle between the vectors. This shows that:
- $\mathbf{u} \cdot \mathbf{v} > 0$ when $\theta < 90°$ (vectors point in similar directions)
- $\mathbf{u} \cdot \mathbf{v} = 0$ when $\theta = 90°$ (vectors are orthogonal/perpendicular)
- $\mathbf{u} \cdot \mathbf{v} < 0$ when $\theta > 90°$ (vectors point in opposite directions)
The dot product is the fundamental operation in neural networks. A linear layer computes $\mathbf{y} = W\mathbf{x} + \mathbf{b}$, where each output $y_i$ is the dot product of the $i$-th row of $W$ with $\mathbf{x}$.
01.04 · Gradients
Introduction
A loss depends on more than one parameter, so one slope is not enough. At the point 3, negative 1, the gradient gives one sensitivity per direction and guides a step that lowers loss from 5 to 1.25.
- Learning goal
Calculate a two-variable gradient and use it to perform and interpret one complete descent update.
- Before you start
Single-variable derivatives, vectors, coordinate points, and substitution into simple algebraic expressions.
Lesson plan
- Hold other inputs fixed to calculate one partial derivative at a time.
- Collect partial derivatives into a gradient vector and read its direction.
- Apply a learning-rate-scaled descent step and explore the resulting vector field.
Use a loss instead of a hill. For L(w,b)=(w−1)²+(b+2)², the gradient at (3,−1) is [4,2]. Predict the descent direction: both coordinates should decrease, so we step opposite the gradient.
The central concept
The gradient is the workhorse of machine learning. It tells you the direction and rate of steepest increase of a scalar function. Gradient descent — stepping opposite the gradient — is how neural networks learn.
Partial derivatives
When a function depends on multiple variables, we can ask how it changes as one variable moves while the others stay fixed. The partial derivative with respect to $x_i$ is:
$$\frac{\partial f}{\partial x_i}(x) = \lim_{h \to 0} \frac{f(x_1, \ldots, x_i+h, \ldots, x_n) - f(x)}{h}$$Let $L(w, b) = (w-1)^2 + (b+2)^2$. Then:
- $\frac{\partial L}{\partial w} = 2(w-1)$ (treat $b$ as constant)
- $\frac{\partial L}{\partial b} = 2(b+2)$ (treat $w$ as constant)
At $(w,b) = (3, -1)$: $\partial L/\partial w = 4$ and $\partial L/\partial b = 2$.
The gradient
The gradient collects all partial derivatives into a single vector:
$$\nabla f(x_1, \ldots, x_n) = \begin{bmatrix} \frac{\partial f}{\partial x_1} \\ \vdots \\ \frac{\partial f}{\partial x_n} \end{bmatrix}$$For our example: $\nabla L(w,b) = [2(w-1),\; 2(b+2)]$. At $(3,-1)$: $\nabla L = [4, 2]$.
The gradient $\nabla f(x)$ points in the direction of steepest increase of $f$ at $x$. Its magnitude $\|\nabla f(x)\|$ equals the rate of increase in that direction.
Proof sketch: The directional derivative in direction $\mathbf{u}$ (unit vector) is $D_\mathbf{u} f = \nabla f \cdot \mathbf{u} = \|\nabla f\| \cos\theta$. This is maximized when $\theta = 0$, i.e., $\mathbf{u}$ points along $\nabla f$.
Gradient descent
To minimize a loss $L$, step opposite the gradient:
$$\theta_{k+1} = \theta_k - \eta \nabla L(\theta_k)$$where $\eta > 0$ is the learning rate (step size).
Starting at $(w,b) = (3, -1)$ with $\eta = 0.25$:
$$\begin{bmatrix} w \\ b \end{bmatrix}_{\text{new}} = \begin{bmatrix} 3 \\ -1 \end{bmatrix} - 0.25 \begin{bmatrix} 4 \\ 2 \end{bmatrix} = \begin{bmatrix} 2 \\ -1.5 \end{bmatrix}$$The new loss is $(2-1)^2 + (-1.5+2)^2 = 1 + 0.25 = 1.25$, down from 5.
Learning rate matters
For a quadratic $L(w) = (w-1)^2$ starting at $w_0 = 3$:
- $\eta = 0.1$: slow, steady convergence ✓
- $\eta = 0.5$: converges in one step ✓
- $\eta = 1.2$: overshoots, loss increases ✗
For a convex quadratic with Hessian eigenvalues $\lambda_i$, gradient descent converges iff $0 < \eta < 2/\lambda_{\max}$. Larger $\eta$ causes divergence.
The gradient as a vector field
The gradient $\nabla f$ is a vector field: at each point in the input space, it assigns a vector. Plotting these vectors over level curves of $f$ reveals the geometry:
- The gradient is orthogonal to level curves (contour lines)
- It points uphill (steepest ascent)
- Its magnitude $\|\nabla f\|$ tells you how steep the slope is
01.05 · Matrices
Introduction
Two output neurons can reuse the same input vector while applying different weights. A two-row matrix turns input 2, 3 into outputs 11 and 6 by performing one dot product per row.
- Learning goal
Read matrix shapes and calculate matrix-vector products, transposes, and basic matrix operations in runnable code.
- Before you start
Vectors, dot products, rows and columns, and basic Python indexing.
Lesson plan
- Read a matrix as a rectangular table with named row and column sizes.
- Compute each output from one matrix row and check shape compatibility.
- Use transpose and NumPy operations while tracking every input and output shape.
Check the shape before multiplying. A matrix with shape (3,2) maps a two-value input to a three-value output. Each output row forms one weighted sum from the same two inputs. A common wrong answer reverses these dimensions.
What is a matrix?
A matrix is a rectangular array of numbers. It can represent a linear transformation, a dataset, or a set of parameters:
$$W = \begin{bmatrix} 4 & 1 \\ 0 & 2 \end{bmatrix}$$This $2 \times 2$ matrix has 2 rows and 2 columns. We describe its shape as (rows, columns).
For $A \in \mathbb{R}^{m \times n}$ and $B \in \mathbb{R}^{n \times p}$, the product $C = AB \in \mathbb{R}^{m \times p}$ has entries:
$$C_{ij} = \sum_{k=1}^n A_{ik} B_{kj}$$Each output entry is the dot product of row $i$ of $A$ with column $j$ of $B$.
Matrix-vector product
A matrix acts on a vector to produce a new vector. With $W$ as above and $\mathbf{x} = [2, 3]^\top$:
$$W\mathbf{x} = \begin{bmatrix} 4 & 1 \\ 0 & 2 \end{bmatrix} \begin{bmatrix} 2 \\ 3 \end{bmatrix} = \begin{bmatrix} 4(2) + 1(3) \\ 0(2) + 2(3) \end{bmatrix} = \begin{bmatrix} 11 \\ 6 \end{bmatrix}$$Each row of $W$ produces one output via a dot product with $\mathbf{x}$.
A linear layer in a neural network computes $\mathbf{y} = W\mathbf{x} + \mathbf{b}$, where:
- $\mathbf{x} \in \mathbb{R}^n$ is the input vector
- $W \in \mathbb{R}^{m \times n}$ is the weight matrix
- $\mathbf{b} \in \mathbb{R}^m$ is the bias vector
- $\mathbf{y} \in \mathbb{R}^m$ is the output vector
Shape compatibility
For $A \in \mathbb{R}^{m \times n}$ and $B \in \mathbb{R}^{p \times q}$, the product $AB$ is defined only if $n = p$ (inner dimensions match). The result has shape $(m, q)$:
$$(m \times n) \cdot (n \times q) \to (m \times q)$$A $2 \times 3$ matrix cannot multiply a $2 \times 1$ vector directly — the inner dimensions (3 and 2) don't match. Always check shapes before multiplying.
Transpose
The transpose $W^\top$ swaps rows and columns:
$$W = \begin{bmatrix} 4 & 1 \\ 0 & 2 \end{bmatrix} \implies W^\top = \begin{bmatrix} 4 & 0 \\ 1 & 2 \end{bmatrix}$$Properties:
- $(A^\top)^\top = A$
- $(AB)^\top = B^\top A^\top$
- $(A + B)^\top = A^\top + B^\top$
Matrix operations in code
import numpy as np
W = np.array([[4, 1], [0, 2]])
x = np.array([2, 3])
# Matrix-vector product
y = W @ x # or W.dot(x)
print(y) # [11, 6]
# Transpose
Wt = W.T
print(Wt) # [[4, 0], [1, 2]]
# Matrix-matrix product
X = np.array([[2, 5], [3, 3]])
Y = W @ X
print(Y) # [[11, 23], [6, 6]]
01.06 · Derivation Rules & Examples
Introduction
Repeatedly rebuilding a derivative from limits is slow. For a rule containing three times a squared input plus five times the input, a small toolbox differentiates each term and gives a slope of 17 at input 2.
- Learning goal
Choose and apply standard derivative rules correctly to polynomial, exponential, logarithmic, trigonometric, and activation functions.
- Before you start
Functions, derivatives as local slopes, exponents, logarithms, and basic algebra.
Lesson plan
- Build a toolbox from constant, power, sum, product, and quotient rules.
- Differentiate exponential, logarithmic, trigonometric, and common neural-network activation functions carefully.
- Combine the rules in worked expressions and check the result at a value.
Choose the rule from the expression. A sum needs the sum rule. A multiplication of two changing functions needs the product rule. A function inside another function needs the chain rule. First mark the outermost operation; then work inward.
The toolbox of differentiation
Rather than returning to the limit definition every time, we use a small set of rules that compose to differentiate almost any function encountered in machine learning.
| Rule | Formula | Example |
|---|---|---|
| Constant | $\frac{d}{dx}[c] = 0$ | $\frac{d}{dx}[5] = 0$ |
| Power | $\frac{d}{dx}[x^n] = nx^{n-1}$ | $\frac{d}{dx}[x^3] = 3x^2$ |
| Sum | $(f+g)' = f' + g'$ | $\frac{d}{dx}[x^2+3x] = 2x+3$ |
| Scalar multiple | $(cf)' = cf'$ | $\frac{d}{dx}[5x^2] = 10x$ |
| Product | $(fg)' = f'g + fg'$ | $\frac{d}{dx}[x^2\sin x] = 2x\sin x + x^2\cos x$ |
| Quotient | $\left(\frac{f}{g}\right)' = \frac{f'g - fg'}{g^2}$ | $\frac{d}{dx}\!\left[\frac{x}{x+1}\right] = \frac{1}{(x+1)^2}$ |
| Chain | $(f(g(x)))' = f'(g(x)) \cdot g'(x)$ | $\frac{d}{dx}[\sin(x^2)] = \cos(x^2)\cdot 2x$ |
Exponentials and logarithms
These two functions are central to information theory and probability in ML:
$$\frac{d}{dx}[e^x] = e^x, \qquad \frac{d}{dx}[\ln x] = \frac{1}{x}, \qquad \frac{d}{dx}[a^x] = a^x \ln a$$The softmax function $p_i = \frac{e^{z_i}}{\sum_j e^{z_j}}$ appears in classification. Its derivative with respect to $z_i$ is:
$$\frac{\partial p_i}{\partial z_i} = p_i(1 - p_i)$$This follows from the quotient rule and the fact that $\frac{d}{dz_i}[e^{z_i}] = e^{z_i}$.
Trigonometric functions
$$\frac{d}{dx}[\sin x] = \cos x, \qquad \frac{d}{dx}[\cos x] = -\sin x, \qquad \frac{d}{dx}[\tan x] = \sec^2 x$$Activation functions in neural networks
Every activation function must be differentiable (almost everywhere) so gradients can flow backward:
| Function | Formula | Derivative |
|---|---|---|
| Sigmoid | $\sigma(z) = \frac{1}{1+e^{-z}}$ | $\sigma(z)(1-\sigma(z))$ |
| Tanh | $\tanh(z)$ | $1 - \tanh^2(z)$ |
| ReLU | $\max(0, z)$ | $\begin{cases}1 & z>0\\0 & z<0\end{cases}$ |
| Leaky ReLU | $\max(\alpha z, z)$ | $\begin{cases}1 & z>0\\\alpha & z<0\end{cases}$ |
| GELU | $z\,\Phi(z)$ | $\Phi(z) + z\,\phi(z)$ |
The product rule is not $(fg)' = f'g'$. That's the chain rule applied to something else. The product rule always has two terms: $f'g + fg'$.
Worked examples
Example 1: Mean squared error
$L(\theta) = \frac{1}{N}\sum_{i=1}^N (f_\theta(x_i) - y_i)^2$. Using the chain rule and power rule:
$$\frac{\partial L}{\partial \theta} = \frac{2}{N}\sum_{i=1}^N (f_\theta(x_i) - y_i) \cdot \frac{\partial f_\theta(x_i)}{\partial \theta}$$Example 2: Cross-entropy loss
$L = -\sum_i y_i \log p_i$. With respect to $p_j$:
$$\frac{\partial L}{\partial p_j} = -\frac{y_j}{p_j}$$Example 3: L2 regularisation
$R(\theta) = \frac{\lambda}{2}\|\theta\|^2 = \frac{\lambda}{2}\sum_i \theta_i^2$. Then:
$$\frac{\partial R}{\partial \theta_j} = \lambda\theta_j \implies \nabla_\theta R = \lambda\theta$$01.07 · The Chain Rule
Introduction
A tiny network first doubles input 3 and then squares the result. The final output changes through both stages, so the local changes, 2 and 12, must be multiplied to obtain 24.
- Learning goal
Differentiate nested single-variable and multivariable functions step by step by multiplying the correct local derivatives.
- Before you start
Function composition, basic derivative rules, multiplication of expressions, and introductory partial derivatives.
Lesson plan
- Trace an input through inner and outer functions before differentiating.
- Multiply local derivatives across two stages, then extend to longer chains.
- Apply the multivariable rule to a tiny network and verify each dependency.
Trace one chain. Let u=2x and y=u². At x=3, dy/du=12 and du/dx=2. Their product gives dy/dx=24. Each factor measures one local link.
The most important rule in ML
The chain rule lets you differentiate compositions of functions. Since neural networks are compositions of many layers, the chain rule is the mathematical engine of backpropagation.
Single-variable chain rule
If $y = f(g(x))$, then:
$$\frac{dy}{dx} = \frac{df}{dg}\cdot\frac{dg}{dx} = f'(g(x))\cdot g'(x)$$Let $y = \sin(x^2)$. Here $f(u) = \sin u$ and $g(x) = x^2$:
$$\frac{dy}{dx} = \cos(x^2) \cdot 2x = 2x\cos(x^2)$$Multi-step compositions
For a chain $y = f_3(f_2(f_1(x)))$:
$$\frac{dy}{dx} = \frac{df_3}{df_2}\cdot\frac{df_2}{df_1}\cdot\frac{df_1}{dx}$$Each factor is the derivative of one layer with respect to its input. This product structure is what makes backpropagation efficient.
Multivariable chain rule
When intermediate values are vectors, the derivatives become Jacobian matrices, and the chain rule becomes matrix multiplication:
$$\frac{\partial L}{\partial \mathbf{x}} = J_1^\top J_2^\top \cdots J_K^\top \frac{\partial L}{\partial \mathbf{y}}$$where $J_i$ is the Jacobian of the $i$-th layer.
A neural network computes $\mathbf{y} = f_K(f_{K-1}(\cdots f_1(\mathbf{x})))$. The gradient of the loss with respect to the input is:
$$\nabla_\mathbf{x} L = J_1^\top J_2^\top \cdots J_K^\top \nabla_\mathbf{y} L$$This product of transposed Jacobians is computed right-to-left in backpropagation.
Leibniz notation makes the chain rule intuitive
The notation $\frac{dy}{dx} = \frac{dy}{du}\cdot\frac{du}{dx}$ looks like fractions cancelling. While this is technically an abuse of notation, it's a powerful mnemonic. For a chain $x \to a \to b \to y$:
$$\frac{dy}{dx} = \frac{dy}{db}\cdot\frac{db}{da}\cdot\frac{da}{dx}$$Worked example: a tiny network
Consider a two-layer network with one neuron each:
$$a = W_1 x + b_1, \quad h = \sigma(a), \quad y = W_2 h + b_2, \quad L = (y - t)^2$$To find $\frac{\partial L}{\partial W_1}$, apply the chain rule:
$$\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial y}\cdot\frac{\partial y}{\partial h}\cdot\frac{\partial h}{\partial a}\cdot\frac{\partial a}{\partial W_1}$$Computing each factor:
- $\frac{\partial L}{\partial y} = 2(y - t)$
- $\frac{\partial y}{\partial h} = W_2$
- $\frac{\partial h}{\partial a} = \sigma'(a) = h(1-h)$
- $\frac{\partial a}{\partial W_1} = x$
Multiplying: $\frac{\partial L}{\partial W_1} = 2(y-t) \cdot W_2 \cdot h(1-h) \cdot x$
If each Jacobian has spectral radius $< 1$, the product shrinks exponentially with depth. This is the vanishing gradient problem that killed deep sigmoid networks. Fixes: ReLU activations, residual connections, careful initialisation.
01.08 · Backpropagation in Python
Introduction
A network may contain many chained calculations, but the loss still needs one gradient for every parameter. Backpropagation caches the forward values, then walks from loss to input while reusing local derivatives.
- Learning goal
Implement reverse-mode differentiation for a tiny network and explain each cached value and gradient update.
- Before you start
Chain rule, derivatives, Python functions, and following values through a calculation graph.
Lesson plan
- Run the forward calculation and save the intermediate values needed later.
- Implement the reverse pass by multiplying and accumulating local derivatives.
- Trace a computation graph and explain the practical memory-versus-recomputation trade-off clearly.
Predict signs before code. If a larger weight makes an already-too-large prediction even larger, its loss gradient should be positive. Gradient descent will subtract that value. If the program reports the opposite sign, inspect the error definition and local derivative.
From chain rule to algorithm
Backpropagation is the efficient algorithm for computing gradients in neural networks. It applies the chain rule right-to-left, reusing intermediate values from the forward pass.
For a feedforward network with $z^{(\ell)} = W^{(\ell)}a^{(\ell-1)} + b^{(\ell)}$ and $a^{(\ell)} = \sigma(z^{(\ell)})$, define the error signal $\delta^{(\ell)} = \nabla_{z^{(\ell)}} L$:
- Output layer: $\delta^{(L)} = \nabla_{a^{(L)}} L \odot \sigma'(z^{(L)})$
- Backpropagate error: $\delta^{(\ell)} = (W^{(\ell+1)})^\top \delta^{(\ell+1)} \odot \sigma'(z^{(\ell)})$
- Weight gradient: $\nabla_{W^{(\ell)}} L = \delta^{(\ell)} (a^{(\ell-1)})^\top$
- Bias gradient: $\nabla_{b^{(\ell)}} L = \delta^{(\ell)}$
Implementation from scratch
Here's a minimal backprop implementation for a two-layer network:
import numpy as np
def sigmoid(z): return 1 / (1 + np.exp(-z))
def sigmoid_grad(a): return a * (1 - a)
# Forward pass
def forward(x, W1, b1, W2, b2):
z1 = W1 @ x + b1
a1 = sigmoid(z1)
z2 = W2 @ a1 + b2
a2 = sigmoid(z2)
cache = (x, z1, a1, z2, a2)
return a2, cache
# Backward pass
def backward(y_true, cache, W2):
x, z1, a1, z2, a2 = cache
m = x.shape[1] # batch size
# Output layer error
# Squared error, summed over outputs and averaged over the batch.
dz2 = 2 * (a2 - y_true) * sigmoid_grad(a2) # shape: (n_out, m)
dW2 = (1/m) * dz2 @ a1.T # shape: (n_out, n_hid)
db2 = (1/m) * np.sum(dz2, axis=1, keepdims=True)
# Hidden layer error
da1 = W2.T @ dz2 # shape: (n_hid, m)
dz1 = da1 * sigmoid_grad(a1) # element-wise
dW1 = (1/m) * dz1 @ x.T # shape: (n_hid, n_in)
db1 = (1/m) * np.sum(dz1, axis=1, keepdims=True)
return dW1, db1, dW2, db2
Step-by-step walkthrough
Given input $\mathbf{x} = [1, 0]^\top$, target $y = 1$, and random initial weights:
- Forward: compute $z_1, a_1, z_2, a_2$ layer by layer
- Loss: $L = (a_2 - y)^2$
- Backward: compute $\delta^{(2)}, \delta^{(1)}$, then all gradients
- Update: $W \leftarrow W - \eta \nabla_W L$ for each parameter
Computational graphs
Modern frameworks (PyTorch, JAX) build a computational graph during the forward pass, recording every operation. Backward traversal of this graph applies the chain rule automatically:
import torch
x = torch.tensor([1.0, 0.0], requires_grad=True)
W1 = torch.randn(4, 2, requires_grad=True)
b1 = torch.randn(4, requires_grad=True)
W2 = torch.randn(1, 4, requires_grad=True)
b2 = torch.randn(1, requires_grad=True)
# Forward — PyTorch records the graph
h = torch.sigmoid(W1 @ x + b1)
y = torch.sigmoid(W2 @ h + b2)
loss = (y - 1.0)**2
# Backward — gradients computed automatically
loss.backward()
print(W1.grad) # ∂L/∂W1
print(W2.grad) # ∂L/∂W2
Always verify your backprop implementation with numerical gradients:
$$\frac{\partial L}{\partial \theta_i} \approx \frac{L(\theta+\varepsilon e_i) - L(\theta-\varepsilon e_i)}{2\varepsilon}$$Try several small values of $\varepsilon$ and use double precision. Differences depend on rounding, the step size, and whether the function is smooth at the test point. Large discrepancies need investigation, but one universal tolerance cannot identify every bug.
Memory and efficiency
Backprop requires storing all intermediate activations $a^{(\ell)}$ from the forward pass. This is why deep networks are memory-intensive. Techniques to reduce memory:
- Gradient checkpointing: recompute some activations during backward pass
- Mixed precision: use FP16 for activations, FP32 for gradients
- Activation recomputation: trade compute for memory
01.09 · The Jacobian Matrix
Introduction
One gradient describes one output, but a function can return several outputs. For a two-input, two-output linear map, the Jacobian arranges four sensitivities so each row belongs to an output and each column to an input.
- Learning goal
Construct and interpret a Jacobian matrix, then use its products in multivariable chain-rule calculations efficiently.
- Before you start
Partial derivatives, gradient vectors, matrices, matrix multiplication, and multivariable function composition.
Lesson plan
- Arrange output-by-input partial derivatives into a matrix with clear dimensions.
- Calculate Jacobians for a linear map and an element-wise nonlinear function.
- Connect Jacobian products and vector products to efficient automatic differentiation.
Count rows and columns. If a function takes two inputs and returns three outputs, its Jacobian has shape (3,2). Row i describes output i. Column j describes sensitivity to input j.
When outputs are vectors
The gradient applies to scalar-valued functions. When $f: \mathbb{R}^n \to \mathbb{R}^m$ produces a vector output, we need the Jacobian matrix:
For $f: \mathbb{R}^n \to \mathbb{R}^m$ with components $f_1, \ldots, f_m$, the Jacobian is the $m \times n$ matrix:
$$J_f = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix}$$Row $i$ contains the gradient of the $i$-th output component.
Example: a linear map
For $f(\mathbf{x}) = A\mathbf{x}$ where $A \in \mathbb{R}^{m \times n}$, the Jacobian is simply $A$ itself:
$$J_f = A$$This makes sense: the linear map is its own best linear approximation everywhere.
Example: element-wise nonlinearity
For $f(\mathbf{x}) = \sigma(\mathbf{x})$ applied element-wise (e.g., sigmoid, ReLU), the Jacobian is diagonal:
$$J_f = \text{diag}(\sigma'(x_1), \sigma'(x_2), \ldots, \sigma'(x_n))$$Each output $f_i$ depends only on input $x_i$, so all off-diagonal entries are zero.
For $p_i = \frac{e^{z_i}}{\sum_j e^{z_j}}$, the Jacobian is:
$$J_{ij} = \frac{\partial p_i}{\partial z_j} = p_i(\delta_{ij} - p_j) = \begin{cases} p_i(1-p_i) & i=j \\ -p_i p_j & i \neq j \end{cases}$$In matrix form: $J = \text{diag}(\mathbf{p}) - \mathbf{p}\mathbf{p}^\top$.
The Jacobian in the chain rule
For a composition $\mathbf{y} = f(g(\mathbf{x}))$, the chain rule becomes:
$$J_{f \circ g}(\mathbf{x}) = J_f(g(\mathbf{x})) \cdot J_g(\mathbf{x})$$This is just matrix multiplication. For a neural network with $K$ layers:
$$J_{\text{network}} = J_K \cdot J_{K-1} \cdots J_1$$Jacobian-vector products (JVPs)
In practice, we rarely compute the full Jacobian (which is $m \times n$ and can be huge). Instead, we compute:
- JVP (forward-mode): $J\mathbf{v}$ — how the output changes when the input moves in direction $\mathbf{v}$
- VJP (reverse-mode): $\mathbf{u}^\top J$ — the gradient of $\mathbf{u}^\top f(\mathbf{x})$ with respect to $\mathbf{x}$
Backpropagation computes VJPs. PyTorch's torch.autograd.grad computes VJPs; torch.autograd.functional.jvp computes JVPs.
Backpropagation multiplies gradients by layer Jacobians. Their singular values describe how much they can stretch or shrink vectors. Many contractions can make gradients small; repeated expansion can make them large. The directions and the full product matter, not just one number for each layer.
Eigenvalues apply only to square matrices, and the largest eigenvalue magnitude alone is not a general test for gradient stability. Initialization methods help control activation and gradient scales, but do not guarantee stable training.
01.10 · Hadamard Product (Element-wise Operations)
Introduction
Sometimes two matrices should interact cell by cell rather than row by column. Multiplying matching entries of two two-by-two matrices produces values 5, 12, 21, and 32 without mixing positions.
- Learning goal
Calculate element-wise matrix products and distinguish them from matrix multiplication, masking, gating, and broadcasting behavior.
- Before you start
Vectors, matrices, shapes, ordinary multiplication, and basic NumPy array operations.
Lesson plan
- Multiply matching matrix entries and confirm that the output shape stays unchanged.
- Contrast element-wise multiplication with matrix multiplication using the same inputs.
- Apply the operation to masks and gates, then inspect broadcasting behavior in code.
Keep two products separate. The Hadamard product of [2,3] and [4,5] is [8,15]. Their dot product is the single number 23. Same inputs, different output shape, different purpose.
Element-wise multiplication
When two arrays have the same shape, you can multiply them position-by-position. This is the Hadamard product, written $\odot$:
$$\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \odot \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 5 & 12 \\ 21 & 32 \end{bmatrix}$$The top-right entry is $2 \times 6 = 12$. It does not mix numbers from other positions.
For $A, B \in \mathbb{R}^{m \times n}$, the Hadamard product $A \odot B$ has entries $(A \odot B)_{ij} = A_{ij} B_{ij}$. It requires $A$ and $B$ to have the same shape.
Hadamard vs. matrix multiplication
Using the same two matrices:
$$\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 19 & 22 \\ 43 & 50 \end{bmatrix}$$The top-right entry here is $1 \times 6 + 2 \times 8 = 22$ — a dot product of row 1 and column 2. Very different from the Hadamard result of 12.
Both products produce a $2 \times 2$ result here, so checking shape alone won't catch confusing them. Always be clear about whether you want element-wise ($\odot$) or matrix ($\cdot$ or $@$) multiplication.
Masks and gating
The Hadamard product is how you apply masks — zeroing out some entries while keeping others:
$$\begin{bmatrix} 3 \\ 7 \\ 2 \end{bmatrix} \odot \begin{bmatrix} 1 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 3 \\ 0 \\ 2 \end{bmatrix}$$The mask $[1, 0, 1]$ hides the middle entry. This pattern appears everywhere in ML:
- Dropout: multiply activations by a random binary mask
- Attention: mask out padding tokens before softmax
- Gating (LSTM, GRU): element-wise multiply gates with hidden states
In code
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# Element-wise (Hadamard)
print((a * b).tolist()) # [[5, 12], [21, 32]]
# Matrix multiplication
print((a @ b).tolist()) # [[19, 22], [43, 50]]
If $c = a \odot b$, then $\frac{\partial L}{\partial a} = \frac{\partial L}{\partial c} \odot b$ and $\frac{\partial L}{\partial b} = \frac{\partial L}{\partial c} \odot a$. The gradient flows through element-wise, scaled by the other operand.
Broadcasting
NumPy extends element-wise operations to compatible but unequal shapes via broadcasting. For example, adding a vector to every row of a matrix:
X = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
print(X + b) # [[11, 22, 33], [14, 25, 36]]
01.11 · Entropy & Information Theory
Introduction
A certain event needs no surprise, while a fair coin needs one bit and four equally likely outcomes need two. Entropy turns this uncertainty into a number that also supports classification loss and language-model perplexity.
- Learning goal
Calculate entropy for small distributions and connect uncertainty, cross-entropy loss, and perplexity without confusing their meanings.
- Before you start
Probabilities, logarithms, weighted sums, and the idea of a class prediction.
Lesson plan
- Compare certain and uncertain distributions using exact small probability examples.
- Calculate entropy and examine its minimum, maximum, and symmetry properties.
- Extend the same calculation to cross-entropy classification loss and language-model perplexity.
Predict the uncertain case. A fair coin gives two equally likely outcomes, so one observation carries 1 bit of uncertainty. A coin that always lands heads has 0 bits: the outcome adds no surprise under that distribution.
Measuring uncertainty
Entropy quantifies the uncertainty or "surprise" in a probability distribution. It's the foundation of cross-entropy loss, the standard loss for classification.
For a discrete probability distribution $p = (p_1, \ldots, p_n)$:
$$H(p) = -\sum_{i=1}^n p_i \log_2 p_i$$Measured in bits (with $\log_2$) or nats (with $\ln$). Convention: $0 \log 0 = 0$.
Intuition
Entropy is the expected number of bits needed to encode an outcome drawn from $p$. Equivalently, it's the expected "surprise" — where surprise of event $i$ is $-\log_2 p_i$:
$$H(p) = \mathbb{E}_p[-\log_2 p_i] = \sum_i p_i (-\log_2 p_i)$$- Fair coin: $p = (0.5, 0.5) \implies H = 1$ bit. Maximum uncertainty.
- Biased coin: $p = (0.9, 0.1) \implies H \approx 0.47$ bits. Less uncertainty.
- Deterministic: $p = (1, 0) \implies H = 0$ bits. No uncertainty.
Properties of entropy
- $H(p) \geq 0$, with equality iff $p$ is a point mass (deterministic)
- $H(p) \leq \log_2 n$, with equality iff $p$ is uniform
- $H$ is concave: mixing distributions increases entropy
Cross-entropy
When we use a model $q$ to encode data actually drawn from the true distribution $p$, the expected code length is the cross-entropy:
$$H(p, q) = -\sum_i p_i \log q_i$$By Gibbs' inequality, $H(p, q) \geq H(p)$, with equality iff $q = p$. The excess $H(p,q) - H(p)$ is the KL divergence.
Cross-entropy loss in classification
In classification, the true label is a one-hot vector $y$ (the true distribution $p$), and the model outputs probabilities $\hat{y}$ (the model distribution $q$). The cross-entropy loss is:
$$L = -\sum_i y_i \log \hat{y}_i = -\log \hat{y}_{y^*}$$where $y^*$ is the true class. Minimising cross-entropy pushes the model to assign high probability to the correct class.
Cross-entropy loss has well-behaved gradients: $\frac{\partial L}{\partial z_i} = \hat{y}_i - y_i$ (for softmax output). MSE with sigmoid produces vanishing gradients when predictions are wrong, making learning slow. Cross-entropy avoids this.
Perplexity
In language modelling, perplexity is the exponentiated cross-entropy:
$$\text{PPL} = 2^{H(p, q)} \quad \text{or} \quad e^{H(p, q)}$$It measures the effective branching factor of the model. Lower perplexity = better model. A perplexity of 10 means the model is as uncertain as choosing uniformly among 10 options.
01.12 · KL Divergence
Introduction
Suppose the true distribution gives two outcomes equal weight, but a model predicts 90 percent and 10 percent. KL divergence measures the extra mismatch, and reversing the two distributions changes the answer.
- Learning goal
Calculate KL divergence for small distributions, explain its asymmetry, and relate it to entropy and cross-entropy.
- Before you start
Discrete probability distributions, logarithms, entropy, cross-entropy, and summing weighted terms over possible outcomes.
Lesson plan
- Compare two small distributions and calculate their weighted log ratios.
- Test non-negativity and asymmetry directly by reversing the two distribution arguments.
- Connect KL to cross-entropy and examine forward, reverse, and machine-learning uses.
Fix the direction. D_KL(p||q) asks how costly it is to use model distribution q when data follows p. Reversing the arguments asks a different question and can produce a different number.
Distance between distributions
The Kullback-Leibler (KL) divergence measures how one probability distribution differs from another. It's not a true distance (it's asymmetric), but it's the fundamental measure of dissimilarity in information theory.
For discrete distributions $p$ and $q$:
$$D_{KL}(p \| q) = \sum_i p_i \log \frac{p_i}{q_i} = \mathbb{E}_p\!\left[\log \frac{p_i}{q_i}\right]$$For continuous distributions: $D_{KL}(p \| q) = \int p(x) \log \frac{p(x)}{q(x)}\,dx$.
Properties
- $D_{KL}(p \| q) \geq 0$ (Gibbs' inequality), with equality iff $p = q$
- $D_{KL}(p \| q) \neq D_{KL}(q \| p)$ in general (asymmetric)
- Does not satisfy the triangle inequality (not a metric)
Relationship to entropy and cross-entropy
KL divergence decomposes neatly:
$$D_{KL}(p \| q) = H(p, q) - H(p)$$It's the extra bits needed when using $q$ to encode data from $p$, beyond the optimal code length $H(p)$.
Let $p = (0.5, 0.5)$ and $q = (0.9, 0.1)$:
$$D_{KL}(p \| q) = 0.5\ln\frac{0.5}{0.9} + 0.5\ln\frac{0.5}{0.1} \approx 0.5(-0.588) + 0.5(1.609) \approx 0.511 \text{ nats}$$Reversing: $D_{KL}(q \| p) = 0.9\ln\frac{0.9}{0.5} + 0.1\ln\frac{0.1}{0.5} \approx 0.368$ nats. The order changes the result. Natural logarithms give nats; base-2 logarithms give bits.
Forward vs. reverse KL
The asymmetry matters in practice:
- Forward KL $D_{KL}(p \| q)$: $q$ must cover all modes of $p$ (zero-avoiding). Used in maximum likelihood.
- Reverse KL $D_{KL}(q \| p)$: $q$ can focus on one mode of $p$ (zero-forcing). Used in variational inference.
Applications in ML
Variational inference (VAEs)
In a VAE, we minimise $D_{KL}(q_\phi(z|x) \| p(z))$ to regularise the encoder's posterior toward the prior.
Knowledge distillation
Distill a large teacher model into a small student by minimising $D_{KL}(p_{\text{teacher}} \| p_{\text{student}})$ on soft predictions.
Policy gradient (RL)
Trust-region methods (TRPO, PPO) constrain $D_{KL}(\pi_{\text{old}} \| \pi_{\text{new}})$ to prevent destructive policy updates.
KL divergence is infinite when $q_i = 0$ but $p_i > 0$. For positive model probabilities, stable log-probability calculations help avoid numerical underflow. Adding a small constant changes the distribution and needs renormalization; it is an approximation, not a way to make a genuinely infinite divergence finite without changing the problem.
01.13 · Singular Value Decomposition (SVD)
Introduction
A large matrix may contain a simpler structure than its number of entries suggests. Singular value decomposition separates its action into a rotation or reflection, axis scaling, and another rotation or reflection.
- Learning goal
Interpret the three SVD matrix factors and use singular values to reason about useful low-rank approximation.
- Before you start
Matrices, matrix multiplication, transpose operations, vectors, dimensions, and basic geometric transformations.
Lesson plan
- Read the factor shapes and apply the three transformations from right to left.
- Interpret singular values as strengths along special input and output directions.
- Keep leading components for approximation, then inspect applications and NumPy output.
Predict what compression removes. Keeping the largest singular values preserves the strongest matrix directions. Dropping a small singular value removes its direction's contribution. This is a controlled approximation, not automatic removal of “unimportant meaning.”
The most useful matrix factorisation
The SVD decomposes any matrix into three interpretable pieces. It reveals the fundamental structure of a linear map and is used throughout ML for dimensionality reduction, compression, and analysis.
Any matrix $A \in \mathbb{R}^{m \times n}$ can be factored as:
$$A = U \Sigma V^\top$$where:
- $U \in \mathbb{R}^{m \times m}$ is orthogonal (left singular vectors)
- $\Sigma \in \mathbb{R}^{m \times n}$ is diagonal with non-negative entries $\sigma_1 \geq \sigma_2 \geq \cdots \geq 0$ (singular values)
- $V \in \mathbb{R}^{n \times n}$ is orthogonal (right singular vectors)
Geometric interpretation
Any linear map $A$ can be decomposed into three steps:
- Rotate or reflect the input space ($V^\top$)
- Scale each axis independently ($\Sigma$)
- Rotate or reflect into the output space ($U$)
The singular values $\sigma_i$ tell you how much $A$ stretches space along each principal direction.
Truncated SVD and low-rank approximation
Keeping only the top $k$ singular values gives the best rank-$k$ approximation:
$$A_k = \sum_{i=1}^k \sigma_i u_i v_i^\top$$The Eckart-Young theorem guarantees this minimises $\|A - B\|_F$ over all rank-$k$ matrices $B$.
A $512 \times 512$ grayscale image has 262,144 entries. Its SVD has 512 singular values. Keeping only the top 50 gives a rank-50 approximation that often looks nearly identical to the original, using only $512 \times 50 + 50 + 512 \times 50 = 51,250$ numbers — an 80% reduction.
Applications in ML
Principal Component Analysis (PCA)
PCA on a centred data matrix $X$ is the SVD of $X/\sqrt{n-1}$. The right singular vectors are the principal components; the singular values give the standard deviations along each component.
LoRA (Low-Rank Adaptation)
LoRA fine-tunes large language models by decomposing weight updates as low-rank matrices:
$$W_{\text{new}} = W_{\text{frozen}} + BA$$where $B \in \mathbb{R}^{d \times r}$ and $A \in \mathbb{R}^{r \times k}$ with $r \ll \min(d,k)$. This is an implicit SVD-like decomposition of the update $\Delta W = BA$.
Condition number
The condition number of $A$ is $\kappa(A) = \sigma_{\max}/\sigma_{\min}$. Large $\kappa$ means the matrix is ill-conditioned — small input changes produce large output changes. This affects numerical stability of linear solvers and gradient flow.
Computing SVD
import numpy as np
A = np.array([[1, 2], [3, 4], [5, 6]])
U, s, Vt = np.linalg.svd(A, full_matrices=False)
print("Singular values:", s) # [9.525, 0.514]
print("Rank-1 approx:", s[0] * np.outer(U[:,0], Vt[0,:]))
# Truncated to top-k
k = 1
A_k = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
full_matrices=False gives the reduced SVD: $U$ is $m \times \min(m,n)$ and $V$ is $n \times \min(m,n)$. The full version extends the orthonormal bases; it does not pad those bases with zero columns. Use the reduced form when the extra basis vectors are unnecessary.
01.14 · Moving Averages (EMA)
Introduction
A noisy measurement sequence can hide its direction. For values 8, 4, 6, and 2, exponential averages with decay 0.5 and 0.9 react at different speeds, making the smoothing trade-off visible.
- Learning goal
Calculate simple and exponential moving averages by hand, including effective windows and early-step bias correction.
- Before you start
Weighted averages, sequences, multiplication, subtraction, and a simple Python loop.
Lesson plan
- Average a fixed window and identify the storage and delay it introduces.
- Update an exponential average recursively and compare two decay values.
- Explain effective window, bias correction, optimizer statistics, and model-weight averaging.
Calculate one update. With old average 10, new value 14, and decay 0.75, the exponential moving average becomes 0.75×10 + 0.25×14 = 11. Larger decay keeps more history and reacts more slowly.
Smoothing noisy sequences
During training, loss values, gradients, and metrics fluctuate. Moving averages smooth these sequences to reveal underlying trends.
Simple moving average (SMA)
Average the last $k$ values:
$$\text{SMA}_t = \frac{1}{k}\sum_{i=t-k+1}^{t} x_i$$When a new value arrives, drop the oldest and add the newest.
Exponential moving average (EMA)
The EMA keeps a single running estimate, blending the old estimate with the new observation:
$\beta$ controls the smoothing: higher $\beta$ = more smoothing, slower reaction to changes.
Starting with $v_0 = 8$, observations $[4, 6, 2]$:
| Observation | $\beta=0.5$ | $\beta=0.9$ |
|---|---|---|
| 8 (init) | 8.000 | 8.000 |
| 4 | 6.000 | 7.600 |
| 6 | 6.000 | 7.440 |
| 2 | 4.000 | 6.896 |
$\beta=0.9$ reacts more slowly — it retains more of the past.
Effective window size
An EMA with decay $\beta$ behaves roughly like a simple average over the last $\frac{1}{1-\beta}$ observations:
- $\beta = 0.9 \implies$ effective window $\approx 10$
- $\beta = 0.99 \implies$ effective window $\approx 100$
- $\beta = 0.999 \implies$ effective window $\approx 1000$
Bias correction
When initialised at $v_0 = 0$, the first few estimates are biased toward zero. The correction divides by $1 - \beta^t$:
$$\hat{v}_t = \frac{v_t}{1 - \beta^t}$$At $t=1$ with $\beta=0.9$: $\hat{v}_1 = v_1 / 0.1 = 10 v_1$. As $t \to \infty$, $\beta^t \to 0$ and the correction vanishes.
If you initialise with the first observation ($v_0 = x_0$), do not apply bias correction — it's only needed for zero-initialisation.
EMA in optimisers
EMAs are the core mechanism of modern optimisers:
| Optimiser | What it averages |
|---|---|
| SGD + Momentum | EMA of gradients ($m_t$) |
| RMSProp | EMA of squared gradients ($v_t$) |
| Adam | EMA of gradients ($m_t$) and squared gradients ($v_t$), both bias-corrected |
| AdamW | Adam + decoupled weight decay |
EMA for model weights
Averaging the model weights themselves over training often improves generalisation:
$$\bar{\theta}_t = \beta \bar{\theta}_{t-1} + (1-\beta)\theta_t$$EMA gives recent weights more influence. It differs from standard Stochastic Weight Averaging (SWA), which equally averages selected training checkpoints. Evaluate the averaged weights before choosing them for inference; averaging is not a guarantee of better accuracy.
Implementation
class EMA:
def __init__(self, beta=0.999):
self.beta = beta
self.v = None
self.t = 0
def update(self, x):
self.t += 1
if self.v is None:
self.v = x # initialise with first observation
else:
self.v = self.beta * self.v + (1 - self.beta) * x
# Bias correction (only if zero-initialised)
return self.v # or self.v / (1 - self.beta**self.t)
# Usage: smoothing training loss
ema = EMA(beta=0.99)
for loss in training_losses:
smoothed = ema.update(loss)