The Math Behind AI

Vectors, matrices, gradients, and probability, built one picture at a time

Sjoerd Vink

Modern AI papers are written in three languages at once: linear algebra, calculus and probability. Most of what you need from each fits in a handful of ideas.

This essay builds those ideas one at a time. Each one appears three ways: as numbers you can check, as a picture you can see, and as code you can run.

  1. 01 Vectors
  2. 02 Dot product
  3. 03 Matrices
  4. 04 Derivatives
  5. 05 Chain rule
  6. 06 Probability
v = np.array([3, 2])w = np.array([-1, 1])np.linalg.norm(v)1.5 * vv + wE["king"] - E["man"] + E["woman"]
v32king−0.350.351.270.590.64−0.13⋮768 numbers32‖v‖ = 3.61v−0.5vwv + wmanwomankingqueen

01 Vectors

A vector is a list of numbers.

A neural network can only work with numbers. A pixel, a word and a sound all get turned into lists of numbers called vectors. This one has two entries.

v=[32]v = \begin{bmatrix} 3 \\ 2 \end{bmatrix}

Papers write v∈R2v \in \mathbb{R}^2, which means vv is a list of two real numbers. Its entries are v1=3v_1 = 3 and v2=2v_2 = 2.

The same two numbers are also directions: 3 steps to the right, then 2 up. An arrow from the origin to where you end up shows the vector as a point in space, with a direction.

Every vector is both a list of numbers to compute with and an arrow to think with.

The arrow has a length, written ∥v∥\lVert v \rVert. The two entries are the sides of a right triangle, so Pythagoras gives it.

∥v∥=32+22=3.61\lVert v \rVert = \sqrt{3^2 + 2^2} = 3.61

With more entries the rule stays the same: square each entry, add them up, take the square root. This length is called the norm.

Multiplying a vector by a number scales every entry. The arrow keeps its direction and changes length. A negative number also flips it to point the other way.

1.5 v=[4.53.0]−0.5 v=[−1.5−1.0]1.5\,v = \begin{bmatrix} 4.5 \\ 3.0 \end{bmatrix} \qquad -0.5\,v = \begin{bmatrix} -1.5 \\ -1.0 \end{bmatrix}

Adding two vectors adds their entries. In the picture, put ww at the tip of vv. The sum is the arrow from the origin to the end of ww.

v+w=[32]+[−11]=[23]v + w = \begin{bmatrix} 3 \\ 2 \end{bmatrix} + \begin{bmatrix} -1 \\ 1 \end{bmatrix} = \begin{bmatrix} 2 \\ 3 \end{bmatrix}

Scaling and adding are all you need for most of what follows. A weighted sum such as 0.7 a+0.3 b0.7\,a + 0.3\,b is the two operations together.

Language models give every token a vector called its embedding. Directions in that space can carry meaning. In a famous result from early word embeddings, the step from “man” to “woman” is roughly the same arrow as the step from “king” to “queen”.

king−man+woman≈queen\text{king} - \text{man} + \text{woman} \approx \text{queen}

Real embeddings have hundreds or thousands of entries, not two. Nobody can draw them, but length, scaling and addition work exactly the same way.

a = np.array([3, 1])b = np.array([1, 2])a @ bnorm(a) * norm(b) * np.cos(theta)a @ b / norm(a)
θab−505a · b5.00a · b = 3×1 + 1×2 = 5‖a‖ ‖b‖ cos θ = √10 × √5 × cos 45° = 5shadow = a · b / ‖a‖ = 5 / 3.16 = 1.58θ = 90° cos θ = 0.00 a · b = 0.00θ = 150° cos θ = −0.87 a · b = −6.12θ = 45° cos θ = 0.71 a · b = 5.00

02 Dot product

The dot product multiplies and adds.

Take two vectors with the same number of entries. Multiply the entries that line up, then add the products. One number comes out.

a⋅b=∑iaibi=3×1+1×2=5a \cdot b = \sum_i a_i b_i = 3 \times 1 + 1 \times 2 = 5

Papers also write it as a⊤ba^{\top} b or ⟨a,b⟩\langle a, b \rangle. In NumPy it is a @ b.

That number also has a meaning in the picture. It is the product of the two lengths and the cosine of the angle between the arrows.

a⋅b=∥a∥ ∥b∥cos⁡θ=10×5×cos⁡45∘=5a \cdot b = \lVert a \rVert\, \lVert b \rVert \cos\theta = \sqrt{10} \times \sqrt{5} \times \cos 45^\circ = 5

The lengths set the scale. The cosine measures agreement: 1 when the arrows point the same way, 0 at a right angle, −1 when they point in opposite directions.

From the tip of bb, drop a line onto the line through aa, meeting it at a right angle. The stretch from the origin to where it lands, shown in amber, is the projection of bb onto aa. Think of it as the shadow bb casts on aa. It is 1.58 long.

The dot product equals the length of that shadow times the length of aa.

The dot product measures how far one vector goes in the direction of the other.

Turn bb until it makes a right angle with aa. The shadow disappears and the dot product is 0.

Vectors at right angles are called orthogonal. Neither has any part that points along the other.

Turn further and the shadow falls behind the origin. The dot product becomes negative: -6.12 at 150°.

a⋅b{>0pointing the same way=0at right angles<0pointing aparta \cdot b \begin{cases} > 0 & \text{pointing the same way} \\ = 0 & \text{at right angles} \\ < 0 & \text{pointing apart} \end{cases}

Now turn bb yourself. The value follows cos⁡θ\cos\theta: largest when the arrows line up, zero at 90°, most negative when they point opposite ways.

The arithmetic does not care how many entries there are. In 4,096 dimensions the dot product still means agreement in direction, even though nobody can draw the angle.

A single dot product measures how much two vectors agree. Used on embeddings, it measures how similar two things are.

scores = E @ qE = E / np.linalg.norm(E, axis=1, keepdims=True)scores = E @ (q / np.linalg.norm(q))best = np.argsort(-scores)[:k]
dogpuppytruckstockdot product with qdog2.83puppy1.19truck1.62stock−0.86

02 Dot product/ Similarity

Only the direction should count.

Search engines, recommendation systems and retrieval for chatbots all rank items by the dot product between their embeddings and a query vector qq.

Here the rose arrow qq is a query about puppies. The raw dot product ranks “dog” first, with 2.83 against 1.19, only because its arrow is longer. In real embeddings, length often reflects how common a word is, not what it means.

The fix is to divide every vector by its length first. All arrows then end on the unit circle, and the dot product is exactly the cosine of the angle between them.

cos⁡(a,q)=a⋅q∥a∥ ∥q∥\cos(a, q) = \frac{a \cdot q}{\lVert a \rVert\, \lVert q \rVert}

Now “puppy” wins with 1.00 against 0.93. This is cosine similarity.

Stack all item embeddings as the rows of a matrix EE. One product, E @ q, then scores every item at once, and sorting the scores gives the best matches.

Attention in a Transformer uses the same step. Every token has a query, and it scores every token, itself included, by a dot product. The Transformer essay builds that up from here.

A dot product compares a vector with a fixed direction. Make that direction learnable, and you have a neuron.

score = w @ xscore = w @ x + bside = score > 0scores = W @ x + b
ww = [1, 1]score = w · xw · x = 0on a line ⟂ wx = [2.5, 2.0]w · x = 4.5 > 0b = −3w · x + b = 1.5shift = 3 / ‖w‖ = 2.123 neurons3 linesW x + b

02 Dot product/ Neuron

A neuron is a dot product.

An artificial neuron holds a weight vector ww and a number bb called the bias. Given an input xx, it computes one score.

score=w⋅x+b\text{score} = w \cdot x + b

Here w=[1,1]w = [1, 1] and there are two kinds of points. We want the score to tell them apart.

Ignore the bias for now. Which points have w⋅x=0w \cdot x = 0? Exactly the ones at a right angle to ww. Together they form a line through the origin, perpendicular to ww.

In three dimensions this is a plane. In more, it is called a hyperplane, and the idea is the same.

On the side ww points to, the score is positive. On the other side it is negative. The marked point scores 4.5.

The sign says which side of the line a point is on, and the size grows with its distance from the line. This line is in the wrong place, though: most rose points are on the positive side too.

The bias moves the line without turning it. With b=−3b = -3, the boundary moves 2.12 along ww, and the sign of the score now separates the two classes.

Training a neuron means finding the ww and bb that put the line in the right place.

A layer has many neurons, each with its own weights and bias, and so each with its own line. Stack the weight vectors as the rows of a matrix WW, and the whole layer becomes one operation.

Wx+b=[w1⋅x+b1w2⋅x+b2w3⋅x+b3]Wx + b = \begin{bmatrix} w_1 \cdot x + b_1 \\ w_2 \cdot x + b_2 \\ w_3 \cdot x + b_3 \end{bmatrix}

A layer is many dot products computed at once. That is what a matrix does, which is the next chapter.

A = np.array([[2, -1],              [1,  1]])A @ xx[0] * A[:, 0] + x[1] * A[:, 1]np.linalg.det(A)(R @ A) @ x
e1e2xA2−11112=032×1 + (−1)×2 = 01×1 + 1×2 = 31×[2, 1] + 2×[−1, 1]= [0, 3]e₁ → [2, 1]e₂ → [−1, 1]det A = 2×1 − (−1)×1= 3RA = [−1, −1] [2, −1]RAx = [−3, 0]

03 Matrices

A matrix moves space.

A matrix is a grid of numbers. Multiplying it by a vector gives a new vector. Read row by row, each output entry is the dot product of one row with the input, exactly like a layer of neurons.

Ax=[2−111][12]=[03]Ax = \begin{bmatrix} 2 & -1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 2 \end{bmatrix} = \begin{bmatrix} 0 \\ 3 \end{bmatrix}

The same product can also be read column by column. AxAx takes 1 of the first column and 2 of the second, and adds them.

Ax=1[21]+2[−11]Ax = 1 \begin{bmatrix} 2 \\ 1 \end{bmatrix} + 2 \begin{bmatrix} -1 \\ 1 \end{bmatrix}

In the picture, xx is 1 step along e1=[1,0]e_1 = [1, 0] followed by 2 steps along e2=[0,1]e_2 = [0, 1].

Now apply AA to every point of the plane at once. The grid tilts and stretches, but its lines stay straight, parallel and evenly spaced, and the origin does not move. That is what linear means.

The columns of a matrix are where e1e_1 and e2e_2 land.

Every other point follows the same recipe as before: 1 step along the new e1e_1, 2 along the new e2e_2.

The unit square becomes a parallelogram. Its area is the determinant, and every area in the plane is scaled by the same factor.

det⁡A=2⋅1−(−1)⋅1=3\det A = 2 \cdot 1 - (-1) \cdot 1 = 3

A determinant of 0 means the plane is squashed onto a line or a point, and some information is lost for good. A negative determinant means the plane is flipped over.

Follow AA with a quarter turn RR. The two steps together are a single matrix, the product RARA.

RA=[0−110][2−111]=[−1−12−1]RA = \begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix} \begin{bmatrix} 2 & -1 \\ 1 & 1 \end{bmatrix} = \begin{bmatrix} -1 & -1 \\ 2 & -1 \end{bmatrix}

Multiplying matrices composes transformations, and (RA)x=R(Ax)(RA)x = R(Ax). The order matters: ARAR turns first and gives a different result.

C = A @ BC[i, j] = A[i, :] @ B[:, j]Y = X @ W + bY = X @ layer.weight.T + layer.bias
AinputsBneuronsCoutputs1200132101102331210110@=+ blayer.weight(2, 3)(2, 3)(3, 2)(2, 2)(2, 3) @ (3, 2) → (2, 2)row 0 · col 0 = 1×2 + 2×0 + 0×1 = 2row 1 · col 1 = 0×1 + 1×1 + 3×0 = 1Y = X @ W + bY = X @ layer.weight.T + layer.bias

03 Matrices/ Multiplication

Matrix multiplication is a grid of dot products.

Matrices do not have to be square. A matrix with 2 rows and 3 columns has shape (2, 3) and lives in R2×3\mathbb{R}^{2 \times 3}.

Two matrices can be multiplied when the inner sizes match. Those sizes are summed over and disappear.

(m×n) (n×p)→(m×p)(m \times n)\,(n \times p) \to (m \times p)

Each entry of the result is a row of AA dotted with a column of BB. Entry (i,j)(i, j) uses row ii and column jj.

Cij=∑kAikBkjC_{ij} = \sum_k A_{ik} B_{kj}

Every row meets every column. An m×nm \times n by n×pn \times p product is m⋅pm \cdot p dot products, each of length nn.

[120013][210110]=[2331]\begin{bmatrix} 1 & 2 & 0 \\ 0 & 1 & 3 \end{bmatrix} \begin{bmatrix} 2 & 1 \\ 0 & 1 \\ 1 & 0 \end{bmatrix} = \begin{bmatrix} 2 & 3 \\ 3 & 1 \end{bmatrix}

All of those dot products are independent of each other, so they can run in parallel. GPUs are built to do exactly that.

This is how a layer runs on a batch. Each row of XX is an input, each column of WWis one neuron's weights, and one product scores every input against every neuron.

Y=XW+bY = XW + b

Papers often write a single input as a column and use WxWx, with one row of WW per neuron, as in the previous chapter. Code puts inputs in rows and writes X @ W, with one column per neuron. It is the same computation with everything transposed, because (Wx)⊤=x⊤W⊤(Wx)^{\top} = x^{\top} W^{\top}.

The transpose A⊤A^{\top} swaps rows and columns: (A⊤)ij=Aji(A^{\top})_{ij} = A_{ji}. PyTorch's nn.Linear stores its weight with shape (out, in), so it computes x @ W.T + b.

When shapes do not line up, a missing transpose is the first thing to check.

Multiplying matrices only ever gives another matrix. Something else has to bend space.

mlp = nn.Sequential(    nn.Linear(2, 2),    nn.ReLU(),    nn.Linear(2, 2),)
W₁x + b₁ReLUW₂h + b₂max(0, z)W₂(W₁x + b₁) + b₂is still one linear layer

03 Matrices/ Nonlinearity

Depth needs a bend.

Here is the plane again, with a circle drawn on it. We will push it through a tiny network with two layers and watch what happens to the shape.

The first layer, W1x+b1W_1 x + b_1, turns and shrinks the plane, and the bias shifts it. The circle shrinks and moves, and the grid lines stay straight.

Apply the second layer straight away and the grid is still straight. Two linear layers multiply into one:

W2(W1x+b1)+b2=(W2W1) x+(W2b1+b2)W_2(W_1 x + b_1) + b_2 = (W_2 W_1)\,x + (W_2 b_1 + b_2)

A hundred linear layers in a row can do no more than a single one. Depth alone adds nothing.

Put a ReLU between them. ReLU keeps positive numbers and replaces negative ones with zero, entry by entry.

ReLU⁡(z)=max⁡(0,z)\operatorname{ReLU}(z) = \max(0, z)

Every point with a negative coordinate is pushed onto an axis. The plane folds, and everything outside the top-right quarter is pressed flat against its edges.

The second layer then moves the folded sheet. The result is no longer one straight grid. It is made of flat pieces joined at creases, and each ReLU neuron adds a crease.

f(x)=W2 ReLU⁡(W1x+b1)+b2f(x) = W_2\, \operatorname{ReLU}(W_1 x + b_1) + b_2

This two-layer network is an MLP. Every Transformer block contains one, with thousands of dimensions instead of two and often a smoother cousin of ReLU such as GELU. With enough creases it can bend space into almost any shape.

Matrices move space. Nonlinearities bend it. Training chooses the matrices.

f = lambda x: (x - 1) ** 2 / 3 + 0.3slope = (f(x + h) - f(x)) / hdf = lambda x: 2 * (x - 1) / 3x = x - lr * df(x)
hriseslope = 1.50

04 Derivatives

A derivative is a slope.

Training means turning a model's weights until a loss, a number that measures its mistakes, is as small as possible. Start with a single weight xx and a loss f(x)f(x).

We are at x=2.5x = 2.5, where the loss is 1.05. Should xx go up or down? From the picture it is obvious. A computer has no picture. It can only evaluate ff at points.

So it tries a nearby point. Move xx by h=1.5h = 1.5 and compare the two values of ff. Rise over run gives the slope of the line through both points.

f(x+h)−f(x)h=1.50\frac{f(x + h) - f(x)}{h} = 1.50

A smaller step, h=0.5h = 0.5, gives a slope of 1.17. The line turns and hugs the curve more closely around xx.

Let hh shrink towards zero. The line ends up touching the curve at a single point. This is the tangent, and its slope is the derivative.

f′(x)=lim⁡h→0f(x+h)−f(x)h=1.00f'(x) = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h} = 1.00

Nobody takes these limits by hand. A few rules, such as xn→nxn−1x^n \to n x^{n-1}, ex→exe^x \to e^x and ln⁡x→1/x\ln x \to 1/x, together with the chain rule from the next chapter, cover nearly everything in deep learning.

A positive slope means the loss goes up to the right, so we step left, against the slope. This is gradient descent.

x←x−η f′(x)x \leftarrow x - \eta\, f'(x)

The step size η\eta is the learning rate. With η=0.75\eta = 0.75, each step here halves the distance to the minimum: 2.50, 1.75, 1.38, 1.19, 1.092.50,\ 1.75,\ 1.38,\ 1.19,\ 1.09.

Make the learning rate too large, η=3.3\eta = 3.3, and every step overshoots to the other side and lands further out than it started: 2.50, −0.80, 3.16, −1.59, 4.112.50,\ -0.80,\ 3.16,\ -1.59,\ 4.11. The loss gets worse and worse.

The derivative gives the direction. The learning rate decides how far to trust it.

Real networks do not have one weight. They have millions or billions. Start with two.

loss = w[0] ** 2 / 2 + 2 * w[1] ** 2loss.backward()with torch.no_grad():    w -= lr * w.grad    w.grad.zero_()
w₁w₂∂L/∂w₁ = −2.5∂L/∂w₂ = 3.2∇L−∇L

04 Derivatives/ Gradient

The gradient points uphill.

With two weights the loss becomes a landscape over the plane. Here it is drawn as a contour map: each ellipse joins points with the same loss, like height lines on a hiking map. The minimum sits in the middle.

L(w1,w2)=12w12+2w22L(w_1, w_2) = \tfrac{1}{2} w_1^2 + 2 w_2^2

Freeze w2w_2 and move only along w1w_1. That is an ordinary one-dimensional slope, called the partial derivative ∂L/∂w1\partial L / \partial w_1. Then do the same in the other direction.

∂L∂w1=w1=−2.5∂L∂w2=4w2=3.2\frac{\partial L}{\partial w_1} = w_1 = -2.5 \qquad \frac{\partial L}{\partial w_2} = 4 w_2 = 3.2

The curly ∂\partial means: change one input and hold all the others still.

Put the partial derivatives in a vector and you get the gradient, ∇L=[−2.5, 3.2]\nabla L = [-2.5,\ 3.2]. It points in the direction where the loss rises fastest, which is always at right angles to the contour line through the point.

The gradient is the slope in many dimensions: one number per weight, pointing uphill.

Gradient descent takes a small step against the gradient, computes a new gradient, and repeats. It is the same update as before, now with vectors.

w←w−η ∇L(w)w \leftarrow w - \eta\, \nabla L(w)

With η=0.2\eta = 0.2 the path curves down into the valley and settles at the minimum.

With η=0.45\eta = 0.45 the path zigzags. The valley is steep across w2w_2 and shallow along w1w_1, so a step that suits one direction overshoots in the other.

Real loss landscapes are full of valleys like this, and better optimizers reduce the zigzag. Momentum keeps a running average of past gradients, so the back-and-forth cancels out. Adam does the same and also gives each weight its own step size.

In PyTorch you never write the gradient yourself. loss.backward() fills w.grad with ∇L\nabla L, for millions of weights at once. It can do that because of the chain rule.

u = x ** 2y = 3 * u + 1dy_dx = 3 * (2 * x)
010xΔx = 0.21.50010uΔu ≈ 0.62.25010yΔy ≈ 1.87.75u = x²du/dx = 2x = 3y = 3u + 1dy/du = 3dy/dx = 3 × 3 = 9

05 Chain rule

Rates multiply.

A neural network is a chain of functions: each layer feeds the next. Take a short chain, u=x2u = x^2 followed by y=3u+1y = 3u + 1.

At x=1.5x = 1.5 we get u=2.25u = 2.25 and y=7.75y = 7.75. How strongly does yy react when xx changes?

Nudge xx by a small amount. Near x=1.5x = 1.5, the square changes about 2x=32x = 3 times as fast as xx, so the nudge is stretched 3 times on its way to uu.

Then y=3u+1y = 3u + 1 stretches it 3 times more. Every function in the chain stretches the nudge by its own derivative.

So yy moves about 3 × 3 = 9 times as much as xx. That is the chain rule: the rate of a chain is the product of the rates of its links.

dydx=dydu⋅dudx\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}

The notation makes it look as though the dudu's cancel like in a fraction. They are not really fractions, but it is a good way to remember the rule.

p = w * xz = p + ba = torch.sigmoid(z)loss = (a - y) ** 2loss.backward()w.grad, b.grad
1.5−0.080.8−0.161.2−0.11−0.2−0.111.0−0.110.73−0.5410.071×+σ(a − y)²xwbyLp = w·x z = p + b a = σ(z) L = (a − y)²∂L/∂a = 2(a − y) = −0.54∂L/∂z = ∂L/∂a · σ′(z) = −0.54 × 0.20 = −0.11∂L/∂b = ∂L/∂p = ∂L/∂z = −0.11∂L/∂w = ∂L/∂p · x = −0.16 ∂L/∂x = ∂L/∂p · w = −0.08w.grad = −0.16 b.grad = −0.11

05 Chain rule/ Backpropagation

Backpropagation runs the chain rule backwards.

Here is one neuron with a sigmoid, scored by a squared error against a target yy, drawn as a computation graph. Each circle is one simple operation.

The forward pass computes left to right. The values are written in blue above the edges, ending in a loss of 0.072.

Now ask how the loss depends on every value, starting from the end. The last operation is a square, whose derivative is simple.

∂L∂a=2(a−y)=−0.54\frac{\partial L}{\partial a} = 2(a - y) = -0.54

It is negative: raising aa would lower the loss.

Next comes the sigmoid. Its local derivative is σ′(z)=σ(z)(1−σ(z))=0.20\sigma'(z) = \sigma(z)(1 - \sigma(z)) = 0.20. By the chain rule, multiply it with what arrived from the right.

∂L∂z=∂L∂a σ′(z)=−0.11\frac{\partial L}{\partial z} = \frac{\partial L}{\partial a}\,\sigma'(z) = -0.11

An addition passes the gradient on unchanged to both of its inputs, because nudging either input nudges the sum by the same amount. So ∂L/∂b=−0.11\partial L / \partial b = -0.11.

For a multiplication p=wxp = wx, the local derivative with respect to each input is the other input. So the gradient for ww is scaled by xx, and the gradient for xx by ww.

∂L∂w=∂L∂p x=−0.16\frac{\partial L}{\partial w} = \frac{\partial L}{\partial p}\, x = -0.16

Each operation only needs its own local derivative. Multiply them together on the way back.

That is backpropagation. During the forward pass, autograd records the graph. loss.backward() then walks it in reverse and fills in the gradient of every weight. One backward pass costs only a small multiple of a forward pass, typically about two, however many weights there are.

Deep networks multiply many local derivatives together, and that can go wrong. The sigmoid's derivative is never more than 0.25, so a long chain of sigmoids drives gradients towards zero. This vanishing gradient problem is one reason modern networks use ReLU and residual connections.

x = np.array([1, 3, 4, 5, 6, 7, 9])mu = x.mean()var = ((x - mu) ** 2).mean()sigma = np.sqrt(var)z = (x - mu) / sigma
σ = 2.45μ012345678910−3−2−101237 valuesμ = (1 + 3 + 4 + 5 + 6 + 7 + 9) / 7 = 5σ² = (16 + 4 + 1 + 0 + 1 + 4 + 16) / 7 = 6σ = √6 = 2.45N(μ, σ²) = N(5, 6)z = (x − μ) / σ

06 Probability

Data has a center and a spread.

Here are seven measurements. Networks constantly summarize and rescale numbers like these: their inputs, their activations inside every layer, and their weights when they are first set up.

The mean is the balance point. If every value were a weight on a ruler, the ruler would balance there. To compute it, add the values and divide by how many there are.

μ=1n∑ixi=5\mu = \frac{1}{n} \sum_i x_i = 5

Papers write the mean as an expectation, E[x]\mathbb{E}[x]. Read E\mathbb{E}as “the average of”.

How far are the values from the mean? Draw each distance as a square. The variance is the average area of those squares.

σ2=1n∑i(xi−μ)2=6\sigma^2 = \frac{1}{n} \sum_i (x_i - \mu)^2 = 6

Squaring makes every distance positive, and it makes values far from the mean count much more than values close to it.

Variance is measured in squared units. Its square root, the standard deviation σ=2.45\sigma = 2.45, is the side of the average square, back in the original units. It is the typical distance from the mean.

Many quantities bunch up around their mean in a bell shape. This is the normal distribution N(μ,σ2)\mathcal{N}(\mu, \sigma^2), and the mean and variance are all it needs.

p(x)=12πσ2exp⁡ ⁣(−(x−μ)22σ2)p(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\!\left(-\frac{(x - \mu)^2}{2\sigma^2}\right)

It is everywhere in AI. Weights often start out as random draws from it, and variational autoencoders and diffusion models generate new samples by starting from it.

Subtract the mean and divide by the standard deviation. The data now has mean 0 and standard deviation 1, and its shape is unchanged.

z=x−μσz = \frac{x - \mu}{\sigma}

LayerNorm does exactly this to every token vector inside a Transformer, and then applies a learned scale and shift. It keeps numbers in a steady range as they pass through dozens of layers.

A model that predicts the next word has to say how likely each word is. Raw scores are not enough for that.

logits = np.array([3.0, 2.0, 0.5, -1.0])e = np.exp(logits / T)p = e / e.sum()
the cat sat on the …logits3.0mat2.0floor0.5bed−1.0moon

06 Probability/ Softmax

Softmax turns scores into probabilities.

A language model ends with one score per word in its vocabulary. These raw scores are called logits. Here are four candidates for the next word in “the cat sat on the”.

Logits can be any number, negative ones included. They are not probabilities yet.

First, exponentiate. eze^z is always positive and keeps the order of the scores. It also stretches the gaps: a logit that is 1 higher becomes e≈2.7e \approx 2.7 times larger.

Then divide by the total. The results are positive and sum to 1, so they form a probability distribution. “mat” gets 0.68.

softmax⁡(z)i=ezi∑jezj\operatorname{softmax}(z)_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

Dividing the logits by a temperature TT first changes the shape. A low temperature sharpens the distribution towards the top word. A high temperature flattens it towards uniform.

This is the temperature setting of a chatbot. Drag it and watch the probabilities move.

loss = -np.log(p[target])loss = F.cross_entropy(logits, target)grad = p - np.eye(len(p))[target]
02400.51 pp = 0.68 · −log p = 0.38p0.68mat0.25floor0.06bed0.01moon

06 Probability/ Cross-entropy

Cross-entropy measures surprise.

The right next word is “mat”, and the model gave it a probability of 0.68. The loss is the negative log of that probability.

−log⁡0.68=0.38-\log 0.68 = 0.38

The curve shows this loss for every possible probability. It is 0 when the model gives the right answer probability 1, and grows as that probability shrinks.

A confident mistake is expensive. If the model gives “mat” only 0.05, the loss jumps to 3.00. The curve rises steeply near zero, so wrong answers given with confidence are punished hard.

A confident correct answer costs almost nothing: 0.03. Averaged over every token in the training data, this is the cross-entropy loss.

L=−1N∑i=1Nlog⁡pθ(yi∣xi)L = -\frac{1}{N} \sum_{i=1}^{N} \log p_\theta(y_i \mid x_i)

Read pθ(y∣x)p_\theta(y \mid x)as “the probability the model with weights θ\theta gives to yy when it sees xx”. Minimizing this loss is the same as maximizing the likelihood of the data. Language model papers often report eLe^{L} instead, called perplexity.

With softmax and cross-entropy combined, the gradient of one example's loss with respect to the logits is remarkably simple. With yy the one-hot vector of the right answer:

∂L∂z=p−y\frac{\partial L}{\partial z} = p - y

The right word's logit gets a gradient of -0.32, so a descent step raises it. Every other logit gets its own probability as gradient, so it is lowered in proportion to how likely the model thought it was.

Prediction minus truth. That is the error signal backpropagation carries into the network.

for step in range(60):    logits = X @ W + b    P = softmax(logits)    loss = -np.log(P[np.arange(n), y]).mean()    g = (P - Y) / n    dW, db = X.T @ g, g.sum(axis=0)    W -= lr * dW    b -= lr * db
  • 01 Vectors
  • 02 Dot product
  • 03 Matrices
  • 04 Derivatives
  • 05 Chain rule
  • 06 Probability
0.00.51.003060lossstepln 3 = 1.100.04
Putting it together

One training loop uses every chapter.

Here is a complete classifier trained from scratch in NumPy. The inputs are 45 points in the plane, each belonging to one of three classes. XX holds one point per row, and WW has one column of weights per class.

The forward pass is a single matrix product. Every point is dotted with every class's weight vector.

Softmax turns each row of logits into probabilities over the three classes.

The loss is the average cross-entropy: the negative log probability of the correct class. Before training, every class gets a third, so the loss starts at ln⁡3=1.10\ln 3 = 1.10.

YY holds the correct classes as one-hot rows. At the logits, the gradient of the average loss is (P−Y)/n(P - Y)/n: prediction minus truth, averaged. The chain rule carries it back through the matrix product.

∂L∂W=1nX⊤(P−Y)\frac{\partial L}{\partial W} = \frac{1}{n} X^{\top} (P - Y)

The transpose is there because of the shapes: (2, n) @ (n, 3) gives (2, 3), the shape of WW.

When you are unsure about a gradient formula, check that the shapes match.

Then take one step of gradient descent against each gradient, with learning rate η=1\eta = 1.

Repeat. After 60 steps the loss has dropped from 1.10 to 0.04.

Large language models are trained with this same loop. The forward pass is a far bigger function, gradients come from autograd, the optimizer is Adam instead of plain descent, and the data is trillions of tokens. The math is the same.

Field guide

Reading the notation

Papers pack these ideas into dense symbols. Here are the ones that appear most often, with what they mean and how they look in code.

x∈Rdx \in \mathbb{R}^d
a vector of d real numbers
x.shape == (d,)
W∈Rm×nW \in \mathbb{R}^{m \times n}
a matrix with m rows and n columns
W.shape == (m, n)
xi, Wijx_i,\ W_{ij}
a single entry
x[i], W[i, j]
x⊤y, ⟨x,y⟩x^{\top} y,\ \langle x, y \rangle
dot product
x @ y
∥x∥\lVert x \rVert
length, or norm
np.linalg.norm(x)
W⊤W^{\top}
transpose: rows become columns
W.T
WxWx
matrix times vector
W @ x
x⊙yx \odot y
entry-by-entry product
x * y
∂L∂w\frac{\partial L}{\partial w}
how L changes when only w changes
w.grad
∇θL\nabla_{\theta} L
gradient of L for all weights θ
loss.backward()
θ←θ−η∇θL\theta \leftarrow \theta - \eta \nabla_{\theta} L
one step of gradient descent
optimizer.step()
E[x]\mathbb{E}[x]
expected value, the average
x.mean()
σ2, σ\sigma^2,\ \sigma
variance and standard deviation
np.var(x), np.std(x)
σ(z)\sigma(z)
the sigmoid; σ means two things, context decides
torch.sigmoid(z)
N(μ,σ2)\mathcal{N}(\mu, \sigma^2)
normal distribution
mu + sigma * torch.randn(n)
x∼px \sim p
x is drawn at random from p
x = p.sample()
pθ(y∣x)p_{\theta}(y \mid x)
probability of y given x, under weights θ
softmax(model(x))[y]
−log⁡pθ(y∣x)-\log p_{\theta}(y \mid x)
cross-entropy loss for one example
F.cross_entropy(logits, y)

Vectors hold the data. Dot products compare it. Matrices transform it, and nonlinearities bend it. Derivatives say which way to change the weights, the chain rule gets that answer through every layer, and probability turns outputs into predictions and losses.

That is the toolkit. Put it to work in the Transformer and the autoencoder, where every step uses something from this page.

← Back to blog