← Tensor by Tensor
§ Math Fundamentals for AI
Tensor by Tensor · Complete Guide

Math Fundamentals for AI

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.

How to study a lesson

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

  1. Trace a concrete input through a rule and learn function notation.
  2. Connect inputs, parameters, and outputs to a small neural-network layer.
  3. 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$.

Definition

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$
Example · Different inputs, same output

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.

Key insight

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

  1. Measure change over a small interval and interpret its slope.
  2. Shrink the interval for the square function and derive the exact rate.
  3. 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))$.

Definition · Derivative

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 changeOutput changeRatio
3 → 3.19 → 9.61; change 0.616.1
3 → 3.019 → 9.0601; change 0.06016.01
3 → 3.0019 → 9.006001; change 0.0060016.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.

Example · Negative derivative

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

NotationRead asContext
$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
Key insight

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

  1. Name vector dimensions, components, length, direction, and basic arithmetic operations.
  2. Calculate a weighted score by multiplying matching entries and adding them.
  3. 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.

Definition · Vector operations

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$$
Example · Interpretation

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)
Key insight

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

  1. Hold other inputs fixed to calculate one partial derivative at a time.
  2. Collect partial derivatives into a gradient vector and read its direction.
  3. 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}$$
Example

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]$.

Theorem · Steepest ascent

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).

Example · One step

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
Stability bound

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

  1. Read a matrix as a rectangular table with named row and column sizes.
  2. Compute each output from one matrix row and check shape compatibility.
  3. 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).

Definition · Matrix multiplication

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}$.

Example · Neural network layer

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)$$
Common error

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

  1. Build a toolbox from constant, power, sum, product, and quotient rules.
  2. Differentiate exponential, logarithmic, trigonometric, and common neural-network activation functions carefully.
  3. 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.

Core differentiation rules
RuleFormulaExample
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$$
Example · Softmax gradient

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:

FunctionFormulaDerivative
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)$
Common mistake

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

  1. Trace an input through inner and outer functions before differentiating.
  2. Multiply local derivatives across two stages, then extend to longer chains.
  3. 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)$$
Example

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.

Chain rule for neural networks

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$

Vanishing gradients

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

  1. Run the forward calculation and save the intermediate values needed later.
  2. Implement the reverse pass by multiplying and accumulating local derivatives.
  3. 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.

The four backprop equations

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$:

  1. Output layer: $\delta^{(L)} = \nabla_{a^{(L)}} L \odot \sigma'(z^{(L)})$
  2. Backpropagate error: $\delta^{(\ell)} = (W^{(\ell+1)})^\top \delta^{(\ell+1)} \odot \sigma'(z^{(\ell)})$
  3. Weight gradient: $\nabla_{W^{(\ell)}} L = \delta^{(\ell)} (a^{(\ell-1)})^\top$
  4. 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

Example · One training step

Given input $\mathbf{x} = [1, 0]^\top$, target $y = 1$, and random initial weights:

  1. Forward: compute $z_1, a_1, z_2, a_2$ layer by layer
  2. Loss: $L = (a_2 - y)^2$
  3. Backward: compute $\delta^{(2)}, \delta^{(1)}$, then all gradients
  4. 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
Gradient checking

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

  1. Arrange output-by-input partial derivatives into a matrix with clear dimensions.
  2. Calculate Jacobians for a linear map and an element-wise nonlinear function.
  3. 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:

Definition · Jacobian

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.

Example · Softmax Jacobian

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.

Gradient size and stability

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

  1. Multiply matching matrix entries and confirm that the output shape stays unchanged.
  2. Contrast element-wise multiplication with matrix multiplication using the same inputs.
  3. 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.

Definition

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.

Shape trap

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]]
Example · Backprop through element-wise ops

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

  1. Compare certain and uncertain distributions using exact small probability examples.
  2. Calculate entropy and examine its minimum, maximum, and symmetry properties.
  3. 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.

Definition · Shannon entropy

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)$$
Example · Fair vs. biased coin
  • 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.

Why not MSE for classification?

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

  1. Compare two small distributions and calculate their weighted log ratios.
  2. Test non-negativity and asymmetry directly by reversing the two distribution arguments.
  3. 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.

Definition · KL divergence

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)$.

Example

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.

Numerical stability

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

  1. Read the factor shapes and apply the three transformations from right to left.
  2. Interpret singular values as strengths along special input and output directions.
  3. 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.

Definition · SVD

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:

  1. Rotate or reflect the input space ($V^\top$)
  2. Scale each axis independently ($\Sigma$)
  3. 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$.

Example · Image compression

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 vs. reduced SVD

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

  1. Average a fixed window and identify the storage and delay it introduces.
  2. Update an exponential average recursively and compare two decay values.
  3. 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:

Definition · EMA
$$v_t = \beta v_{t-1} + (1-\beta) x_t, \qquad 0 \leq \beta < 1$$

$\beta$ controls the smoothing: higher $\beta$ = more smoothing, slower reaction to changes.

Example · $\beta = 0.9$ vs $\beta = 0.5$

Starting with $v_0 = 8$, observations $[4, 6, 2]$:

Observation$\beta=0.5$$\beta=0.9$
8 (init)8.0008.000
46.0007.600
66.0007.440
24.0006.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.

Initialisation matters

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:

OptimiserWhat it averages
SGD + MomentumEMA of gradients ($m_t$)
RMSPropEMA of squared gradients ($v_t$)
AdamEMA of gradients ($m_t$) and squared gradients ($v_t$), both bias-corrected
AdamWAdam + 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)