Math · Week 1 · Note 3 of 14
Matrices, matrix multiplication and shapes
A matrix is a table of numbers, and multiplying one by a vector is doing a dot product with every row at once. That single idea, plus a rule about which shapes fit together, is nearly all the matrix algebra machine learning uses. This note builds it with small tables and ends with the shape bookkeeping that catches most bugs.
Why this note matters
Your data is a table: one row per house, one column per feature. Your model’s weights are a list, one per
feature. “Predict every house at once” is the table times the list, and that is a matrix-vector product. The
linear regression note already used this, with X @ w; this
note is where you learn exactly what that line does and when it fails. Later, every piece of a
neural network is a matrix multiplication followed by something simple, the
number everyone quotes for a model, its parameter count, is the sizes of its matrices added up, and the error you
will see most often while learning, some version of “shapes (32, 3) and (4, 3) not aligned”, is a matrix whose rows
and columns do not fit the thing next to it.
Interview questions that are really this note:
- “What is the shape of the result when a (32, 3) data matrix meets a (4, 3) weight matrix, and why does it fail as written?”
- “A weight matrix from 768 inputs to 3072 outputs: what shape is it, and how many parameters is that?”
- “How many arithmetic operations does one matrix multiply cost? Why do GPUs help?”
- “Why is different from , and when does the bracketing versus matter?”
- “Why is attention quadratic in the length of the text?” (Week 8; the shape half of the answer is here.)
In plain words
| Everyday idea | Technical term | Symbol or code |
|---|---|---|
| A table of numbers | matrix | , (capital letters) |
| How many rows by how many columns | shape (dimensions) | ; A.shape == (m, n) |
| The number in row , column | entry (element) | ; A[i, j] |
| Flip the table over its diagonal | transpose | ; A.T |
| Dot every row with a vector | matrix-vector product | ; A @ x |
| Dot every row of one with every column of another | matrix multiplication (matmul) | ; A @ B |
| Multiply matching cells, no summing | element-wise (Hadamard) product | ; A * B |
| Stretch a smaller array to fit a bigger one | broadcasting | Z + b |
| The table that changes nothing | identity matrix | ; np.eye(n) |
| Many examples processed together | batch | the first axis, (batch, …) |
What a matrix is
A matrix is a rectangular grid of numbers with rows and
columns; we say its shape is , read “m by n”,
rows first. Matrices get capital letters, or , and entries get two subscripts,
for row and column . Math counts from 1; NumPy and PyTorch
count from 0, so the 2 in Fig. 1 is on paper and A[0, 1] in code.
A vector fits into this picture as a matrix with one column: a column vector of shape
. Written flat, it is a row vector of shape
. Mathematicians default to columns, which is why formulas say
with the vector on the right. NumPy adds a third option, the plain 1-D array of shape (n,) with no
second number at all, and treats it as whichever one makes the multiplication work. Most shape bugs come from
mixing up these three; the common mistakes list has the specifics.
You will meet matrices in three roles, and it helps to name the role each time:
- A data matrix : one row per example, one column per feature. Four days of walk inputs is a matrix. A batch of 32 images flattened to 784 pixels is .
- A weight matrix : one row per output the model computes, one column per input feature. The two walk scores from the vectors note make a weight matrix.
- A lookup table: a language model keeps one embedding vector per token it knows. For GPT-2 that is a matrix, and “looking up a token” means reading its row.
Matrix times vector: a stack of dot products
The matrix-vector product is defined entry by entry: entry of the result is the dot product of row of with .
Two consequences fall straight out of the definition. First, the vector must have as many entries as each row has, so a matrix can only multiply a 3-vector. Second, the result has one entry per row, so it is a 2-vector. In shape language: . The 3s touch and cancel; the 2 and the 1 survive. That is the shape rule, and the next section says it in full.
There is a second way to read the same product, and it is worth seeing once because it is how 3Blue1Brown and many textbooks present it. Instead of “each row dotted with ”, read it as “the columns of , mixed in the amounts given by ”:
The result is a linear combination of the columns, with the input’s entries as the coefficients. Both readings give the same numbers; use the row reading for computing and the column reading for the “matrix as a function” picture in a moment.
Matrix times matrix and the shape rule
For of shape and of shape , the product has shape , and each entry is a dot product of a row of with a column of :
Work one fully. Say every step out loud the first time; it is the only way the row-column dance becomes automatic.
| Entry | Row of A | Column of B | Dot product |
|---|---|---|---|
| (1, 2, 3) | (7, 9, 11) | 7 + 18 + 33 = 58 | |
| (1, 2, 3) | (8, 10, 12) | 8 + 20 + 36 = 64 | |
| (4, 5, 6) | (7, 9, 11) | 28 + 45 + 66 = 139 | |
| (4, 5, 6) | (8, 10, 12) | 32 + 50 + 72 = 154 |
The habit that prevents bugs: write the shapes before you compute. “(2 × 3) times (3 × 2): inner 3s match, result is 2 × 2.” Do this for every multiplication in every formula until it is reflex. Interviewers watch for it, and it is how you will debug your own code: print the shapes, line them up, find the pair that does not touch.
Try it matrixmultiplication.xyz An animation of the row-by-column dance for any shapes you type in. Enter the A and B above and watch each entry of AB being formed. Five minutes makes the definition physical. Read Khan Academy: Matrices (Precalculus) Do the exercise sets “Multiplying matrices by matrices” and “Properties of matrix multiplication”. Skip determinants and inverses entirely; they are out of scope.Order matters, grouping does not
Not commutative. in general. With the and above, is but is : not even the same shape. Even for square matrices the entries differ:
(Multiplying by that 0-1 matrix on the right swaps the columns of the other matrix; on the left it swaps the rows. Which side you multiply on decides which axis you act on. This is why formulas are careful to write and not .)
Associative. , always. The answer is identical; the cost is not. Suppose a big matrix is written as the product of two thin ones, () and (), so the big matrix is . (Week 10 meets a technique called LoRA that does exactly this to shrink the cost of adapting a language model.) To apply it to one input vector you can compute:
- : first form , which is million multiplications, then multiply the vector.
- : first ( multiplications, giving an 8-vector), then times that (another 6,144). About 12 thousand in total.
Same result, four hundred times less work. Choosing the bracketing is a real engineering decision, and associativity is what makes it legal.
Distributive. . This is what lets you write and compute the two halves separately, which is how that week-10 technique adds its small update to a frozen big matrix.
The identity. The square matrix with 1s on the diagonal and 0s elsewhere, , leaves anything it multiplies unchanged: and . It is the matrix version of the number 1. It shows up in week 3, when a penalty on big weights adds a multiple of to a matrix, and again in several deep-learning tricks later.
A matrix is a function
A matrix defines a function , and it is a special kind of function called a linear transformation: it respects addition and scaling, and . One consequence matters a great deal in week 5: composing two linear functions gives a linear function, and in matrix language composing the functions is multiplying the matrices. Apply then and you have applied (note the order: the first applied is on the right, nearest the vector). So two matrix multiplications in a row, with nothing in between, are no more powerful than one. That is why neural networks put a simple bend between their layers.
Read off where the two basis arrows go and you know the whole matrix. In Fig. 4, “one step right” lands at and “one step up” lands at ; those are the two columns of . Any other point is a combination of those two steps, so it goes to the same combination of the two landing spots: is one right plus one up, so it goes to . This is the column reading from the matrix-vector section, now as a picture.
The picture is what lets you think about a weight matrix as something that reshapes the space the data lives in, so that the categories become easier to separate. The eigenvector note in week 4 asks the natural follow-up: are there directions a given matrix only stretches, without turning? That question is behind PCA .
Watch 3Blue1Brown: Linear transformations and matrices | Chapter 3 About 11 minutes. The animated version of Fig. 4. After it, matrix multiplication stops being a recipe and becomes a picture. Watch 3Blue1Brown: Matrix multiplication as composition | Chapter 4 About 10 minutes. Why multiplying matrices means composing transformations, and a visual reason the order matters.Batches: many inputs at once
Four walk days as a data matrix, the two scores as a weight matrix, the biases as a vector:
We want each day (a row of ) dotted with each score’s weights (a row of ). Row-with-row is not what matmul does; it does row-with-column. So flip on its side first, turning its rows into columns, and multiply:
Check the shapes: . Check one entry: day 4 is , the walk weights are , dot product , plus bias , gives , the bottom-left entry. Check the bias: it is a 2-vector, one per score, and it was added to every row. That silent stretching of a vector across a matrix is broadcasting, covered below.
This “weights times a batch, plus a bias” step is so common that deep-learning libraries give it a name, a
linear layer, and store with shape (outputs, inputs), one row per output,
computing exactly as above. (You will meet it as nn.Linear in
week 6.) Two facts to memorize now, because they answer a whole family of interview questions:
- The weight shape is (outputs, inputs). A layer from 768 inputs to 3072 outputs has a weight of shape
(3072, 768). Parameters: , the weights plus one bias per output. - It changes only the last axis. Input
(batch, in)becomes(batch, out). Input(batch, seq, in), which is how a language model carries a batch of sentences, becomes(batch, seq, out). Every leading axis is left alone.
Two of these steps in a row, with a bend between them, as a shape table. Nothing in it is new; it is the shape rule applied twice. (In week 5 this table is a small neural network.)
| Step | Operation | Shapes | Result |
|---|---|---|---|
| input | batch of 32 examples, 3 features | (32, 3) | |
| layer 1 | , is (4, 3) | (32, 3)(3, 4) | (32, 4) |
| bend | element-wise, e.g. replace negatives by 0 (ReLU ) | no change | (32, 4) |
| layer 2 | , is (2, 4) | (32, 4)(4, 2) | (32, 2) |
Transpose, and all pairwise dot products
The transpose of an matrix is the matrix with :
Three rules, each checkable by shapes alone:
- . Flip twice, back where you started.
- . The order reverses. Check: is , is , so is ; and is . The other order, , would be , a different shape entirely.
- is the dot product. A column () transposed is a row (), times a column () gives : one number. This is why papers write for .
Fig. 5 is the single most important shape in the later tracks, so make sure it is clear now. Put vectors of dimension as the rows of a matrix () and another as the rows of (also ). Then is , and by the definition of matmul its entry is row of dotted with column of , which is row of . So entry is : the similarity between vector of the first stack and vector of the second. All pairs in one multiplication.
The same trick with one matrix and itself, , gives every example’s dot product with every
other example, a Gram matrix. It is how you would compute all pairwise similarities
between a thousand embeddings in one line: E @ E.T. And in week 8, the
step inside a language model that decides how much each word should look at each other word is exactly Fig. 5,
with one stack called the queries and the other the keys.
How much work a multiplication is
Multiplying by computes entries, each a dot product of length : multiplications and about additions, so roughly FLOPs (floating-point operations). For one of GPT-2’s bigger weight matrices, 768 by 3072, applied to a text of 1,024 tokens , that is billion FLOPs for one matrix of one pass through the model. Multiply by the number of such matrices, the number of training tokens and a factor of three for the learning step, and you have the training cost estimates you see in papers.
Three facts that follow:
- Why GPUs. The dot products do not depend on each other, so they can all run at the same time. A GPU is thousands of small arithmetic units built to do exactly that, and modern ones have dedicated matrix-multiply hardware (tensor cores).
- Why memory, not arithmetic, is often the limit. Reading numbers and writing can take longer than the arithmetic when is small. Batching exists largely so that each weight read from memory is used for many rows at once.
- Why parameter count ≈ FLOPs per token. A weight matrix of entries costs FLOPs per input vector. So running a model with parameters once costs about FLOPs per token. That rule of thumb answers a whole family of interview questions about the cost of using a model.
Element-wise operations and broadcasting
Element-wise operations pair up matching entries and never mix rows with columns.
, (A * B in code), ,
are all element-wise, and all keep the shape. The trap: A * B in NumPy is
element-wise, not matrix multiplication. Matmul is A @ B. In the last note the same trap was
w * x versus w @ x; here it costs you a wrong answer with no error message, because two
matrices of the same shape can be either *-ed or, if square, @-ed.
Broadcasting is the rule for element-wise operations between arrays of different shapes. Line the shapes up from the right. Two dimensions are compatible if they are equal or one of them is 1 (or missing). The size-1 dimension is stretched to match. So:
| Shapes | Aligned from the right | Result | What happened |
|---|---|---|---|
(4, 2) + (2,) | (4, 2) and (–, 2) | (4, 2) | the bias vector is copied onto each of the 4 rows |
(4, 2) + (4, 1) | (4, 2) and (4, 1) | (4, 2) | the column is copied across the 2 columns |
(4, 2) + (1, 2) | (4, 2) and (1, 2) | (4, 2) | the row is copied down the 4 rows |
(4, 2) + (3,) | (4, 2) and (–, 3) | error | 2 and 3 are neither equal nor 1 |
(4, 2) + (4,) | (4, 2) and (–, 4) | error | alignment is from the right, so 2 is compared with 4, not 4 with 4 |
The last row is the one that bites. To subtract a per-row mean of shape (4,) from a (4, 2)
matrix you must first make it (4, 1) with means[:, None] or keepdims=True.
Standardising features in the feature scaling note does exactly
this kind of thing, and so does a normalisation step inside every language model.
Code
Type these out. Before running each line that prints a shape, write down the shape you expect.
Shapes, indexing, transpose and the error you will see
import numpy as np
A = np.array([[1, 2, 3],
[4, 5, 6]]) # 2 rows, 3 columns
B = np.array([[7, 8],
[9, 10],
[11, 12]]) # 3 rows, 2 columns
print(A.shape) # (2, 3)
print(B.shape) # (3, 2)
print(A @ B) # @ is matrix multiplication
# [[ 58 64]
# [139 154]] shape (2, 2): the inner 3s matched
# the other order gives a different, 3 x 3 answer
print(B @ A)
# [[ 39 54 69]
# [ 49 68 87]
# [ 59 82 105]]
# transpose: rows become columns, shape (3, 2)
print(A.T)
# [[1 4]
# [2 5]
# [3 6]]
print(A[0]) # row 0: [1 2 3]
# column 1: [2 5] (the colon means "every row")
print(A[:, 1])
print(A[1, 2]) # row 1, column 2, counting from 0: 6
# Shapes that do not fit raise an error. try / except catches it so we can
# print the message.
try:
# (2, 3) @ (2, 3): the inner sizes 3 and 2 do not match
A @ A
except ValueError as err:
print(err)
# matmul: Input operand 1 has a mismatch in its core dimension 0, ... (size
# 2 is different from 3)
A batch through a weight matrix: the walk days and the houses
import numpy as np
# Four days (rows) x three features (columns): sunny, cold, tired.
X = np.array([[1, 0, 0],
[1, 1, 0],
[0, 0, 1],
[1, 0, 1]], dtype=float) # shape (4, 3)
# Two scores, one row of weights each: shape (2, 3) = (outputs, inputs).
W = np.array([[3, -2, -4], # score 1: "should I walk?"
[1, -1, 0]], dtype=float) # score 2: "is the weather good?"
b = np.array([-1, 0], dtype=float) # one bias per score: shape (2,)
# (4, 3) @ (3, 2) -> (4, 2); b is added to every row
Z = X @ W.T + b
print(Z)
# [[ 2. 1.]
# [ 0. 0.]
# [-5. 0.]
# [-2. 1.]]
# : one row per day, one column per score
print(Z.shape)
# prints (4, 2)
# The house data the same way: three houses, two features (size, rooms).
houses = np.array([[1.0, 2],
[1.5, 3],
[2.0, 3]]) # shape (3, 2)
w = np.array([150.0, 15.0]) # one weight per feature
b0 = 10.0 # one bias
# @ (2,) -> (3,): all three predictions at once
print(houses @ w + b0)
# prints (3, 2)
# [190. 280. 355.]
The shape table of two steps with a bend between
import numpy as np
# a random-number generator; the 0 makes the numbers repeat every run
rng = np.random.default_rng(0)
# a batch: 32 examples, 3 features each (random stand-in data)
X = rng.normal(size=(32, 3))
# step 1 weights: 3 inputs -> 4 outputs, stored as (outputs, inputs)
W1 = rng.normal(size=(4, 3))
b1 = np.zeros(4) # one bias per output, starting at 0
W2 = rng.normal(size=(2, 4)) # step 2 weights: 4 inputs -> 2 outputs
b2 = np.zeros(2)
H = X @ W1.T + b1 # step 1
print(H.shape) # (32, 4) only the last axis changed
# the bend: every negative number becomes 0
H = np.maximum(H, 0)
# element by element, so the shape is unchanged
print(H.shape)
# prints (32, 4)
Y = H @ W2.T + b2 # step 2
print(Y.shape) # (32, 2)
# .size = how many numbers an array holds
n_params = W1.size + b1.size + W2.size + b2.size
print(n_params) # 26 = (4*3 + 4) + (2*4 + 2)
# Give the input an extra leading axis (8 groups of 128 examples): only the
# last axis changes.
X3 = rng.normal(size=(8, 128, 3))
print((X3 @ W1.T + b1).shape) # (8, 128, 4)
All pairwise dot products in one multiplication
import numpy as np
rng = np.random.default_rng(0)
A = rng.normal(size=(4, 8)) # 4 vectors of length 8, one per row
B = rng.normal(size=(4, 8)) # another 4
S = A @ B.T # (4, 8) @ (8, 4) -> (4, 4)
print(S.shape) # (4, 4)
# Entry (i, j) is the dot product of row i of A with row j of B. Check one
# of them:
by_hand = A[1] @ B[2] # row 1 of A dotted with row 2 of B
# (isclose allows for tiny rounding differences)
print(np.isclose(S[1, 2], by_hand))
# prints True
# One stack against itself: every pair of vectors compared in one line.
G = A @ A.T # (4, 4)
print(np.allclose(G, G.T)) # True: symmetric, because a·b = b·a
# square every number, then add along each row
self_dots = (A ** 2).sum(axis=1)
print(np.allclose(np.diag(G), self_dots))
# prints True: the diagonal holds each vector dotted with itself
Broadcasting, including the case that fails
import numpy as np
Z = np.zeros((4, 2)) # 4 rows, 2 columns of zeros
b = np.array([-1.0, 0.0]) # shape (2,)
# : b is stretched across the 4 rows
print((Z + b).shape)
# prints (4, 2)
col = np.array([[10.0], [20.0], [30.0], [40.0]]) # shape (4, 1)
# : the column is stretched across the 2 columns
print((Z + col).shape)
# prints (4, 2)
row_means = Z.mean(axis=1) # one number per row
print(row_means.shape) # (4,)
# : [:, None] adds an axis of size 1
print(row_means[:, None].shape)
# prints (4, 1)
# : now it lines up row by row
print((Z - row_means[:, None]).shape)
# prints (4, 2)
try:
# (4, 2) and (3,): the last sizes 2 and 3 do not match
Z + np.array([1.0, 2.0, 3.0])
except ValueError as err:
# operands could not be broadcast together with shapes (4,2) (3,)
print(err)
Read
NumPy documentation: numpy.matmul (the @ operator)
Read the first paragraph and the note on how 1-D arrays are promoted. That note explains why a (3,) vector works on either side of @.
Where you will meet this again
- Week 3, penalising big weights. The exact solution of regression with a penalty is : transposes, a product, the identity matrix.
-
Week 5, neural networks. A layer is
followed by a bend, and a network is the shape table above with more rows. PyTorch (week 6) calls the step
nn.Linearand stores the weight as (outputs, inputs), exactly as here. - Week 7, embeddings. A language model’s lookup table is (50,257 × 768 for GPT-2). Looking up a token is reading one row, which is the same as multiplying a one-hot row of length by the matrix.
- Week 8, attention. Attention makes three stacks of vectors with three weight matrices, forms the matrix of Fig. 5 (called ), turns each row into percentages, and multiplies by the third stack to get again. Three matmuls, and the shape rule tells you the shape at every step. Because the middle matrix is , attention costs grow with the square of the text length.
- Week 10, cost tricks. The KV cache is two of those stacks kept in memory so they are not recomputed; its size is a shape calculation. LoRA adds a product of two thin matrices to a frozen weight and uses associativity to apply it cheaply. And parameter counts are sums of over the weight matrices, priced at 2 FLOPs per parameter per token.
The full interview answer
The typical form is a shape question dressed up as something else: “walk me through the shapes when a batch goes through a weight matrix” or, after week 8, “what is the shape of the attention scores and why”. Here is a complete answer in about seventy seconds.
If they push further, be ready for:
- “Why (outputs, inputs) and not (inputs, outputs)?” Convention from writing with column vectors; one row per output. With examples as rows you apply the transpose.
- “How many parameters in a layer from 768 inputs to 3072 outputs?” .
- “Is matmul commutative?” No. Associative and distributive, yes. Give the thin-matrices bracketing example.
- “What is the difference between
*and@?” Element-wise product versus matrix product. - “Why is attention quadratic in sequence length?” (Week 8.) Its score matrix is : work and memory.
Practice questions
Answer each one out loud before opening it.
What is the shape of A B if A is 5 × 3 and B is 3 × 7? And B A?
is . is : inner 7 and 5 do not match, so it is not defined.
In one sentence, what is entry (i, j) of A B?
The dot product of row of with column of .
A step takes 20 input features to 5 outputs. What are the shapes of its weight matrix and bias, and how many parameters does it have?
Weight (5, 20), bias (5,). Parameters .
An array of shape (16, 50, 20) goes through that 20-to-5 step. What comes out?
(16, 50, 5). Only the last axis changes.
Why do we compute X Wᵀ rather than X W?
Because is stored as (outputs, inputs) with one row per output, and the batch is (batch, inputs). Row-with-row dot products need the second matrix flipped so its rows become columns: .
Is matrix multiplication commutative? Associative? Give one practical consequence of each answer.
Not commutative: swapping changes the result or the shape, so formulas must keep their order. Associative: you may choose the bracketing, so for two thin matrices you compute instead of and save hundreds of times the work.
What is (A B)ᵀ?
. The order reverses; check with shapes.
A and B are both (n, d). What is A Bᵀ, what is its entry (i, j), and why does the cost grow with the square of n?
; entry is ; there are entries, each costing operations, so time and memory grow with the square of . (This is why attention, in week 8, is “quadratic in sequence length”.)
How many FLOPs is a matmul of (m × n) by (n × p), and what rule of thumb follows for running a model once?
About . A weight matrix with parameters costs FLOPs per input vector, so running the model once is roughly 2 FLOPs per parameter per token.
What is the difference between A * B and A @ B in NumPy?
A * B multiplies matching entries (element-wise, same shape in or out). A @ B is matrix multiplication (dot products of rows with columns, shapes must chain).
Can you add an array of shape (4,) to one of shape (4, 2)? What about (2,)?
(4,) fails: aligned from the right, 4 is compared with 2. (2,) works: it is broadcast as one copy per row. To add per-row values reshape to (4, 1).
What do the columns of a matrix tell you about the function x ↦ M x?
They are where the basis vectors land. Column 1 is applied to (1, 0, …), column 2 to (0, 1, …), and every other input goes to the corresponding linear combination of the columns.
Practice problems
Paper first. Write shapes before numbers, every time. Solutions are hidden under each problem.
By hand
1. For each pair of shapes, say whether A B is defined and give its shape: (2,3)(3,4); (3,4)(2,3); (1,5)(5,1); (5,1)(1,5); (4,4)(4,4); (6,2)(2,2).
(2,4); not defined (4 ≠ 2); (1,1), a single number; (5,5); (4,4); (6,2).
2. Compute A B and B A for A = [[1, 2], [3, 4]] and B = [[2, 0], [1, 2]].
: row 1 of A with columns of B: ; row 2: . So . : row 1 of B: ; row 2: . So . Different, as expected.
3. Multiply A = [[1, 2, 3], [4, 5, 6]] by the column (1, 0, −1). Then read the answer as a combination of A’s columns.
Row 1: ; row 2: . Result . As columns: .
4. Write the transpose of [[1, 2, 3], [4, 5, 6]] and of the column vector (7, 8, 9). What are their shapes?
, shape 3 × 2. The column (3 × 1) becomes the row , shape 1 × 3.
5. Three walk days X = [[1,1,1], [0,1,0], [1,0,0]], weights W = [[3,−2,−4],[1,−1,0]], bias b = (−1, 0). Compute Z = X Wᵀ + b with its shape.
Shape (3 × 3)(3 × 2) = 3 × 2. Day 1 (1,1,1): score 1: ; score 2: . Day 2 (0,1,0): ; . Day 3 (1,0,0): ; . .
6. A model applies three weight matrices in a row: 784 inputs → 256 outputs, then 256 → 128, then 128 → 10, each with a bias. Give every weight shape and the total parameter count.
Weights (256, 784), (128, 256), (10, 128). Parameters: ; ; . Total 235,146.
7. Input (64, 784) through the three steps in problem 6. Write the shape after each step. Then do it for an input of shape (8, 20, 784).
(64, 256) → (64, 128) → (64, 10). With the extra axis: (8, 20, 256) → (8, 20, 128) → (8, 20, 10). Only the last axis changes.
8. A and B are both (128, 64). Give the shape of A Bᵀ, the number of entries, and the FLOPs to compute it. If 128 doubles to 256, by what factor does each grow?
(128, 128); 16,384 entries; about million FLOPs. Doubling to 256 quadruples the entries (65,536) and the FLOPs (about 8.4 million). Quadratic in .
9. A 1024 × 1024 matrix is written as B A with B of shape 1024 × 16 and A of shape 16 × 1024. How many numbers do B and A hold together, as a percentage of the full matrix? Compare the cost of (B A) x against B (A x) for one vector.
parameters against : about 3.1%. Forming first costs million multiplications; costs . About 500 times cheaper.
10. Which of these broadcasts, and to what shape? (3, 4) + (4,); (3, 4) + (3,); (3, 4) + (3, 1); (3, 1) + (1, 4); (2, 3, 4) + (3, 4); (2, 3, 4) + (2, 4).
(3, 4) yes; (3,) no (3 vs 4); (3, 1) yes → (3, 4); (3, 1)+(1, 4) yes → (3, 4), both stretch; (2, 3, 4)+(3, 4) yes → (2, 3, 4); (2, 4) no (4 vs 4 fine, then 2 vs 3 fails).
11. M = [[0, −1], [1, 0]]. Where do (1, 0) and (0, 1) go? Sketch the image of the unit square. What does this matrix do to the plane?
Columns: (1, 0) → (0, 1) and (0, 1) → (−1, 0). The square turns a quarter turn anticlockwise. is a 90° rotation. Apply it twice: , a half turn.
12. GPT-2 small: d = 768, vocabulary 50,257, 12 layers. Estimate the parameters in the embedding matrix and in the two feed-forward matrices of one layer (768 → 3072 → 768). Then estimate forward-pass FLOPs per token for the feed-forward parts of all 12 layers.
Embedding: million. Feed-forward per layer: million. All 12: about 56.7 million. FLOPs per token: 2 per parameter, about 113 million for the feed-forward blocks alone.
Code
13. Implement matmul with three nested loops. Check it against @ on random shapes, and make it raise a clear error when the inner dimensions differ.
import numpy as np
def matmul(A, B):
m, n = A.shape # rows and columns of A
n2, p = B.shape # rows and columns of B
if n != n2:
raise ValueError(f"inner dimensions differ: A is {m}x{n}, B is {n2}x{p}")
C = np.zeros((m, p)) # the answer, filled in below
for i in range(m): # each row of A
for j in range(p): # each column of B
# the dot product of that row and that column
for k in range(n):
C[i, j] += A[i, k] * B[k, j]
return C
rng = np.random.default_rng(0)
for m, n, p in [(2, 3, 2), (5, 1, 4), (3, 7, 3), (1, 6, 1)]:
A = rng.normal(size=(m, n))
B = rng.normal(size=(n, p))
# same answer as NumPy, up to rounding
assert np.allclose(matmul(A, B), A @ B)
print("all shapes agree with @") # all shapes agree with @
try:
matmul(np.zeros((2, 3)), np.zeros((2, 3)))
except ValueError as err:
# inner dimensions differ: A is 2x3, B is 2x3
print(err)
14. Time your loop matmul against NumPy’s @ for 200 × 200 matrices. Then time @ for 1000 × 1000. Compute the FLOPs per second NumPy achieves.
import time
import numpy as np
rng = np.random.default_rng(0)
A = rng.normal(size=(200, 200))
B = rng.normal(size=(200, 200))
start = time.perf_counter()
C_loops = matmul(A, B) # the function from problem 13
loop_seconds = time.perf_counter() - start
start = time.perf_counter()
C_numpy = A @ B
numpy_seconds = time.perf_counter() - start
# same answer, very different speed
assert np.allclose(C_loops, C_numpy)
numpy_ms = numpy_seconds * 1000
speedup = loop_seconds / numpy_seconds
print(f"loops: {loop_seconds:.2f} s numpy: {numpy_ms:.2f} ms speed-up: {speedup:,.0f}x")
n = 1000
A = rng.normal(size=(n, n))
B = rng.normal(size=(n, n))
start = time.perf_counter()
C = A @ B
seconds = time.perf_counter() - start
# n*n answers, each needing n multiplies and n adds
flops = 2 * n * n * n
# billions of operations per second
gflops_per_second = flops / seconds / 1e9
print(f"{gflops_per_second:.1f} GFLOP/s on this CPU")
# Your numbers will differ. One laptop printed:
# loops: 5.74 s numpy: 9.75 ms speed-up: 588x
# 25.4 GFLOP/s on this CPU
# The ratio is the point: NumPy is hundreds to thousands of times faster
# than Python loops,
# and a GPU does tens of thousands of GFLOP/s.
15. Write the shape table’s step → bend → step as a function for a batch, asserting the shape after every step. Then make it accept an input with an extra leading axis without changing any code.
import numpy as np
rng = np.random.default_rng(0)
# step 1: 3 inputs -> 4 outputs, stored as (outputs, inputs)
W1 = rng.normal(size=(4, 3)) * 0.5
b1 = np.zeros(4)
W2 = rng.normal(size=(2, 4)) * 0.5 # step 2: 4 inputs -> 2 outputs
b2 = np.zeros(2)
def forward(X):
# every axis except the last, e.g. (32,) or (8, 128)
leading = X.shape[:-1]
H = X @ W1.T + b1 # step 1
assert H.shape == leading + (4,)
H = np.maximum(H, 0) # the bend: negatives become 0
assert H.shape == leading + (4,)
Y = H @ W2.T + b2 # step 2
assert Y.shape == leading + (2,)
return Y
print(forward(rng.normal(size=(32, 3))).shape) # (32, 2)
# : same code, extra leading axis
print(forward(rng.normal(size=(8, 128, 3))).shape)
# prints (8, 128, 2)
16. Compute all pairwise dot products between 1,000 random vectors of length 64 in one matrix expression. Check one entry with a plain loop, and confirm the result is symmetric and its diagonal holds each vector dotted with itself.
import numpy as np
rng = np.random.default_rng(0)
# 1,000 vectors of length 64, one per row
E = rng.normal(size=(1000, 64))
# (1000, 64) @ (64, 1000) -> (1000, 1000): every pair at once
S = E @ E.T
print(S.shape) # (1000, 1000)
print(S.size) # 1000000 dot products from one line
i, j = 3, 17
by_hand = 0.0
# the dot product of rows 3 and 17, written out
for k in range(64):
by_hand += E[i, k] * E[j, k]
assert np.isclose(S[i, j], by_hand)
print(round(by_hand, 3)) # 5.803
# symmetric: row i with row j = row j with row i
assert np.allclose(S, S.T)
self_dots = (E ** 2).sum(axis=1) # each vector dotted with itself
assert np.allclose(np.diag(S), self_dots)
# In the next note you divide each row by its length first;
# then this same line gives the cosine similarity of every pair.
Common mistakes
All sources
Everything linked above, in the order to use it, plus one reference.
- 3Blue1Brown, “Linear transformations and matrices | Chapter 3” and “Matrix multiplication as composition | Chapter 4” (videos, about 10 minutes each).
- matrixmultiplication.xyz (interactive).
- Khan Academy, Matrices (Precalculus) (exercises). Multiplication and properties only.
- NumPy, Broadcasting and numpy.matmul (documentation), and the site’s own NumPy reference.
- Deisenroth, Faisal and Ong, Mathematics for Machine Learning, sections 2.2–2.3 and 2.7 (free PDF). The formal treatment of matrices and linear maps.