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 ABAB different from BABA, and when does the bracketing (AB)C(AB)C versus A(BC)A(BC) 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 ideaTechnical termSymbol or code
A table of numbersmatrixAA, WW (capital letters)
How many rows by how many columnsshape (dimensions)m×nm \times n; A.shape == (m, n)
The number in row ii, column jjentry (element)AijA_{ij}; A[i, j]
Flip the table over its diagonaltransposeA⊤A^\top; A.T
Dot every row with a vectormatrix-vector productAxA\mathbf{x}; A @ x
Dot every row of one with every column of anothermatrix multiplication (matmul)ABAB; A @ B
Multiply matching cells, no summingelement-wise (Hadamard) productA⊙BA \odot B; A * B
Stretch a smaller array to fit a bigger onebroadcastingZ + b
The table that changes nothingidentity matrixII; np.eye(n)
Many examples processed togetherbatchthe first axis, (batch, …)

What a matrix is

123456 row 1: the vector (1, 2, 3) column 2: (2, 5) A₁₂ = 2 (row 1, column 2) shape: 2 × 3 (rows × columns) in code A[0, 1], counting from 0 A =
Fig. 1. Anatomy of a 2 × 3 matrix. Shape is rows × columns. The entry A₁₂ sits in row 1, column 2. Each row is a 3-vector; each column is a 2-vector.

A matrix is a rectangular grid of numbers with mm rows and nn columns; we say its shape is m×nm \times n, read “m by n”, rows first. Matrices get capital letters, AA or WW, and entries get two subscripts, AijA_{ij} for row ii and column jj. Math counts from 1; NumPy and PyTorch count from 0, so the 2 in Fig. 1 is A12A_{12} 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 n×1n \times 1. Written flat, it is a row vector of shape 1×n1 \times n. Mathematicians default to columns, which is why formulas say WxW\mathbf{x} 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 XX: one row per example, one column per feature. Four days of walk inputs is a 4×34 \times 3 matrix. A batch of 32 images flattened to 784 pixels is 32×78432 \times 784.
  • A weight matrix WW: one row per output the model computes, one column per input feature. The two walk scores from the vectors note make a 2×32 \times 3 weight matrix.
  • A lookup table: a language model keeps one embedding vector per token it knows. For GPT-2 that is a 50,257×76850{,}257 \times 768 matrix, and “looking up a token” means reading its row.

Matrix times vector: a stack of dot products

3-2-41-10 101 -11 W · x = 2 × 3 3 × 1 2 × 1 (3)(1) + (−2)(0) + (−4)(1) = −1 (1)(1) + (−1)(0) + (0)(1) = 1
Fig. 2. W x: each row of W is dotted with x to give one entry of the result. Row 1 gives −1, row 2 gives 1. The inner 3s must match; the result has as many entries as W has rows.

The matrix-vector product WxW\mathbf{x} is defined entry by entry: entry ii of the result is the dot product of row ii of WW with x\mathbf{x}.

(Wx)i=∑jWij xj=wi⋅x(W\mathbf{x})_i = \sum_j W_{ij}\, x_j = \mathbf{w}_i \cdot \mathbf{x}

Two consequences fall straight out of the definition. First, the vector must have as many entries as each row has, so a 2×32 \times 3 matrix can only multiply a 3-vector. Second, the result has one entry per row, so it is a 2-vector. In shape language: (2×3)(3×1)=(2×1)(2 \times 3)(3 \times 1) = (2 \times 1). 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 x\mathbf{x}”, read it as “the columns of WW, mixed in the amounts given by x\mathbf{x}”:

Wx=x1(31)+x2(−2−1)+x3(−40)=1(31)+0(−2−1)+1(−40)=(−11)W\mathbf{x} = x_1 \begin{pmatrix} 3 \\ 1 \end{pmatrix} + x_2 \begin{pmatrix} -2 \\ -1 \end{pmatrix} + x_3 \begin{pmatrix} -4 \\ 0 \end{pmatrix} = 1\begin{pmatrix} 3 \\ 1 \end{pmatrix} + 0\begin{pmatrix} -2 \\ -1 \end{pmatrix} + 1\begin{pmatrix} -4 \\ 0 \end{pmatrix} = \begin{pmatrix} -1 \\ 1 \end{pmatrix}

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 AA of shape m×nm \times n and BB of shape n×pn \times p, the product C=ABC = AB has shape m×pm \times p, and each entry is a dot product of a row of AA with a column of BB:

Cij=∑k=1nAik Bkj=(row i of A)⋅(column j of B)C_{ij} = \sum_{k=1}^{n} A_{ik}\, B_{kj} = (\text{row } i \text{ of } A) \cdot (\text{column } j \text{ of } B)
A 2 × 3 B 3 × 2 = AB 2 × 2 inner 3 = 3: match, they cancel outer 2 survives as rows outer 2 survives as columns A 2 × 3 A 2 × 3 inner 3 ≠ 2: not defined
Fig. 3. The shape rule. Inner dimensions must match and disappear; outer dimensions survive. (2 × 3)(3 × 2) works and gives 2 × 2. (2 × 3)(2 × 3) fails because 3 ≠ 2.

Work one fully. Say every step out loud the first time; it is the only way the row-column dance becomes automatic.

A=(123456)2×3B=(789101112)3×2A = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}_{2\times3} \qquad B = \begin{pmatrix} 7 & 8 \\ 9 & 10 \\ 11 & 12 \end{pmatrix}_{3\times2}
EntryRow of AColumn of BDot product
C11C_{11}(1, 2, 3)(7, 9, 11)7 + 18 + 33 = 58
C12C_{12}(1, 2, 3)(8, 10, 12)8 + 20 + 36 = 64
C21C_{21}(4, 5, 6)(7, 9, 11)28 + 45 + 66 = 139
C22C_{22}(4, 5, 6)(8, 10, 12)32 + 50 + 72 = 154
AB=(5864139154)2×2AB = \begin{pmatrix} 58 & 64 \\ 139 & 154 \end{pmatrix}_{2\times2}

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. AB≠BAAB \ne BA in general. With the AA and BB above, ABAB is 2×22 \times 2 but BABA is 3×33 \times 3: not even the same shape. Even for square matrices the entries differ:

(1234)(0110)=(2143)(0110)(1234)=(3412)\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 4 & 3 \end{pmatrix} \qquad \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} = \begin{pmatrix} 3 & 4 \\ 1 & 2 \end{pmatrix}

(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 XW⊤XW^\top and not W⊤XW^\top X.)

Associative. (AB)C=A(BC)(AB)C = A(BC), always. The answer is identical; the cost is not. Suppose a big 768×768768 \times 768 matrix is written as the product of two thin ones, BB (768×8768 \times 8) and AA (8×7688 \times 768), so the big matrix is BABA. (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 x\mathbf{x} you can compute:

  • (BA)x(BA)\mathbf{x}: first form BABA, which is 768⋅8⋅768≈4.7768 \cdot 8 \cdot 768 \approx 4.7 million multiplications, then multiply the vector.
  • B(Ax)B(A\mathbf{x}): first AxA\mathbf{x} (8×768=6,1448 \times 768 = 6{,}144 multiplications, giving an 8-vector), then BB 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. A(B+C)=AB+ACA(B + C) = AB + AC. This is what lets you write (W+BA)x=Wx+BAx(W + BA)\mathbf{x} = W\mathbf{x} + BA\mathbf{x} 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, II, leaves anything it multiplies unchanged: Ix=xI\mathbf{x} = \mathbf{x} and IA=AI=AIA = AI = A. 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 II to a matrix, and again in several deep-learning tricks later.

A matrix is a function

(1, 0) (0, 1) (1, 1) (2, 0) = column 1 (1, 1) = column 2 (3, 1) M before after: M x for every x
Fig. 4. The matrix M = [[2, 1], [0, 1]] as a function of the plane. It sends (1, 0) to (2, 0) and (0, 1) to (1, 1), which are its columns. The unit square becomes a parallelogram; the point (1, 1) lands at (3, 1).

A matrix defines a function x↦Mx\mathbf{x} \mapsto M\mathbf{x}, and it is a special kind of function called a linear transformation: it respects addition and scaling, M(u+v)=Mu+MvM(\mathbf{u} + \mathbf{v}) = M\mathbf{u} + M\mathbf{v} and M(cv)=cMvM(c\mathbf{v}) = cM\mathbf{v}. 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 AA then BB and you have applied BABA (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 (2,0)(2, 0) and “one step up” lands at (1,1)(1, 1); those are the two columns of MM. Any other point is a combination of those two steps, so it goes to the same combination of the two landing spots: (1,1)(1, 1) is one right plus one up, so it goes to (2,0)+(1,1)=(3,1)(2, 0) + (1, 1) = (3, 1). 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:

X=(100110001101)4×3W=(3−2−41−10)2×3b=(−1, 0)X = \begin{pmatrix} 1 & 0 & 0 \\ 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 1 \end{pmatrix}_{4\times3} \qquad W = \begin{pmatrix} 3 & -2 & -4 \\ 1 & -1 & 0 \end{pmatrix}_{2\times3} \qquad \mathbf{b} = (-1,\ 0)

We want each day (a row of XX) dotted with each score’s weights (a row of WW). Row-with-row is not what matmul does; it does row-with-column. So flip WW on its side first, turning its rows into columns, and multiply:

Z=XW⊤+b=(100110001101)(31−2−1−40)+(−10)=(2100−50−21)4×2Z = XW^\top + \mathbf{b} = \begin{pmatrix} 1 & 0 & 0 \\ 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 1 \end{pmatrix}\begin{pmatrix} 3 & 1 \\ -2 & -1 \\ -4 & 0 \end{pmatrix} + \begin{pmatrix} -1 & 0 \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 0 & 0 \\ -5 & 0 \\ -2 & 1 \end{pmatrix}_{4\times2}

Check the shapes: (4×3)(3×2)=4×2(4 \times 3)(3 \times 2) = 4 \times 2. Check one entry: day 4 is (1,0,1)(1, 0, 1), the walk weights are (3,−2,−4)(3, -2, -4), dot product −1-1, plus bias −1-1, gives −2-2, 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 (2,)(2,) vector across a (4,2)(4, 2) 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 WW with shape (outputs, inputs), one row per output, computing XW⊤+bXW^\top + \mathbf{b} 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: 3072×768+3072=2,362,3683072 \times 768 + 3072 = 2{,}362{,}368, 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.)

StepOperationShapesResult
inputbatch of 32 examples, 3 features(32, 3)
layer 1XW1⊤+b1XW_1^\top + \mathbf{b}_1, W1W_1 is (4, 3)(32, 3)(3, 4)(32, 4)
bendelement-wise, e.g. replace negatives by 0 (ReLU )no change(32, 4)
layer 2HW2⊤+b2HW_2^\top + \mathbf{b}_2, W2W_2 is (2, 4)(32, 4)(4, 2)(32, 2)

Transpose, and all pairwise dot products

The transpose A⊤A^\top of an m×nm \times n matrix is the n×mn \times m matrix with (A⊤)ij=Aji(A^\top)_{ij} = A_{ji}:

(123456) ⁣⊤=(142536)\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}^{\!\top} = \begin{pmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{pmatrix}

Three rules, each checkable by shapes alone:

  • (A⊤)⊤=A(A^\top)^\top = A. Flip twice, back where you started.
  • (AB)⊤=B⊤A⊤(AB)^\top = B^\top A^\top. The order reverses. Check: AA is 2×32 \times 3, BB is 3×23 \times 2, so (AB)⊤(AB)^\top is 2×22 \times 2; and B⊤A⊤B^\top A^\top is (2×3)(3×2)=2×2(2 \times 3)(3 \times 2) = 2 \times 2. The other order, A⊤B⊤A^\top B^\top, would be (3×2)(2×3)(3 \times 2)(2 \times 3), a different shape entirely.
  • x⊤y\mathbf{x}^\top \mathbf{y} is the dot product. A column x\mathbf{x} (n×1n \times 1) transposed is a row (1×n1 \times n), times a column (n×1n \times 1) gives 1×11 \times 1: one number. This is why papers write w⊤x\mathbf{w}^\top\mathbf{x} for w⋅x\mathbf{w}\cdot\mathbf{x}.
A n × d = 4 × 8 row i = vector aᵢ Bᵀ d × n = 8 × 4, column j = vector bⱼ = A Bᵀ n × n = 4 × 4 entry (2, 3) = a₂ · b₃ = how similar vector 2 is to vector 3 ·
Fig. 5. All pairwise dot products at once. A holds 4 vectors as rows, B holds another 4 as rows. A Bᵀ is 4 × 4 and its entry (i, j) is aᵢ · bⱼ. With a stack of embeddings this is every similarity at once; in week 8 it is the score matrix of attention.

Fig. 5 is the single most important shape in the later tracks, so make sure it is clear now. Put nn vectors of dimension dd as the rows of a matrix AA (n×dn \times d) and another nn as the rows of BB (also n×dn \times d). Then AB⊤AB^\top is (n×d)(d×n)=n×n(n \times d)(d \times n) = n \times n, and by the definition of matmul its entry (i,j)(i, j) is row ii of AA dotted with column jj of B⊤B^\top, which is row jj of BB. So entry (i,j)(i, j) is ai⋅bj\mathbf{a}_i \cdot \mathbf{b}_j: the similarity between vector ii of the first stack and vector jj of the second. All n2n^2 pairs in one multiplication.

The same trick with one matrix and itself, XX⊤XX^\top, 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 m×nm \times n by n×pn \times p computes mpmp entries, each a dot product of length nn: mnpmnp multiplications and about mnpmnp additions, so roughly 2mnp2mnp 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 2×1024×768×3072≈4.82 \times 1024 \times 768 \times 3072 \approx 4.8 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 mpmp 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 mn+npmn + np numbers and writing mpmp can take longer than the arithmetic when nn 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 mnmn entries costs 2mn2mn FLOPs per input vector. So running a model with NN parameters once costs about 2N2N 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+BA + B, A⊙BA \odot B (A * B in code), max⁡(0,A)\max(0, A), A/2A / 2 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:

ShapesAligned from the rightResultWhat 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)error2 and 3 are neither equal nor 1
(4, 2) + (4,)(4, 2) and (–, 4)erroralignment 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.

Read NumPy documentation: Broadcasting Read “General Broadcasting Rules” and look at the pictures. PyTorch follows the same rules.

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 (X⊤X+λI)−1X⊤y(X^\top X + \lambda I)^{-1} X^\top \mathbf{y}: transposes, a product, the identity matrix.
  • Week 5, neural networks. A layer is XW⊤+bXW^\top + \mathbf{b} followed by a bend, and a network is the shape table above with more rows. PyTorch (week 6) calls the step nn.Linear and stores the weight as (outputs, inputs), exactly as here.
  • Week 7, embeddings. A language model’s lookup table is V×dV \times d (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 VV by the matrix.
  • Week 8, attention. Attention makes three stacks of vectors with three weight matrices, forms the n×nn \times n matrix of Fig. 5 (called QK⊤QK^\top), turns each row into percentages, and multiplies by the third stack to get n×dn \times d again. Three matmuls, and the shape rule tells you the shape at every step. Because the middle matrix is n×nn \times n, 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 BABA to a frozen weight and uses associativity to apply it cheaply. And parameter counts are sums of mn+mmn + m 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 WxW\mathbf{x} 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?” 768×3072+3072=2,362,368768 \times 3072 + 3072 = 2{,}362{,}368.
  • “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 n×nn \times n: n2dn^2 d work and n2n^2 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?

ABAB is 5×75 \times 7. BABA is (3×7)(5×3)(3 \times 7)(5 \times 3): 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 ii of AA with column jj of BB.

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 5×20+5=1055 \times 20 + 5 = 105.

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 WW is stored as (outputs, inputs) with one row per output, and the batch XX is (batch, inputs). Row-with-row dot products need the second matrix flipped so its rows become columns: (batch,in)(in,out)(\text{batch}, \text{in})(\text{in}, \text{out}).

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 B(Ax)B(A\mathbf{x}) instead of (BA)x(BA)\mathbf{x} and save hundreds of times the work.

What is (A B)ᵀ?

B⊤A⊤B^\top A^\top. 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?

n×nn \times n; entry (i,j)(i, j) is ai⋅bj\mathbf{a}_i \cdot \mathbf{b}_j; there are n2n^2 entries, each costing dd operations, so time and memory grow with the square of nn. (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 2mnp2mnp. A weight matrix with mnmn parameters costs 2mn2mn 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 MM 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]].

ABAB: row 1 of A with columns of B: (2+2,0+4)=(4,4)(2 + 2, 0 + 4) = (4, 4); row 2: (6+4,0+8)=(10,8)(6 + 4, 0 + 8) = (10, 8). So AB=[[4,4],[10,8]]AB = [[4, 4], [10, 8]]. BABA: row 1 of B: (2+0,4+0)=(2,4)(2 + 0, 4 + 0) = (2, 4); row 2: (1+6,2+8)=(7,10)(1 + 6, 2 + 8) = (7, 10). So BA=[[2,4],[7,10]]BA = [[2, 4], [7, 10]]. 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: 1+0−3=−21 + 0 - 3 = -2; row 2: 4+0−6=−24 + 0 - 6 = -2. Result (−2,−2)(-2, -2). As columns: 1⋅(1,4)+0⋅(2,5)−1⋅(3,6)=(−2,−2)1 \cdot (1, 4) + 0 \cdot (2, 5) - 1 \cdot (3, 6) = (-2, -2).

4. Write the transpose of [[1, 2, 3], [4, 5, 6]] and of the column vector (7, 8, 9). What are their shapes?

[[1,4],[2,5],[3,6]][[1, 4], [2, 5], [3, 6]], shape 3 × 2. The column (3 × 1) becomes the row (7,8,9)(7, 8, 9), 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: 3−2−4−1=−43 - 2 - 4 - 1 = -4; score 2: 1−1+0=01 - 1 + 0 = 0. Day 2 (0,1,0): −2−1=−3-2 - 1 = -3; −1-1. Day 3 (1,0,0): 3−1=23 - 1 = 2; 11. Z=[[−4,0],[−3,−1],[2,1]]Z = [[-4, 0], [-3, -1], [2, 1]].

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: 784⋅256+256=200,960784 \cdot 256 + 256 = 200{,}960; 256⋅128+128=32,896256 \cdot 128 + 128 = 32{,}896; 128⋅10+10=1,290128 \cdot 10 + 10 = 1{,}290. 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 2⋅128⋅64⋅128≈2.12 \cdot 128 \cdot 64 \cdot 128 \approx 2.1 million FLOPs. Doubling to 256 quadruples the entries (65,536) and the FLOPs (about 8.4 million). Quadratic in nn.

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.

2⋅1024⋅16=32,7682 \cdot 1024 \cdot 16 = 32{,}768 parameters against 10242=1,048,5761024^2 = 1{,}048{,}576: about 3.1%. Forming BABA first costs 1024⋅16⋅1024≈16.81024 \cdot 16 \cdot 1024 \approx 16.8 million multiplications; B(Ax)B(A\mathbf{x}) costs 16⋅1024+1024⋅16=32,76816 \cdot 1024 + 1024 \cdot 16 = 32{,}768. 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. MM is a 90° rotation. Apply it twice: M2=[[−1,0],[0,−1]]M^2 = [[-1, 0], [0, -1]], 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: 50,257×768≈38.650{,}257 \times 768 \approx 38.6 million. Feed-forward per layer: 768⋅3072+3072+3072⋅768+768≈4.72768 \cdot 3072 + 3072 + 3072 \cdot 768 + 768 \approx 4.72 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.

  1. 3Blue1Brown, “Linear transformations and matrices | Chapter 3” and “Matrix multiplication as composition | Chapter 4” (videos, about 10 minutes each).
  2. matrixmultiplication.xyz (interactive).
  3. Khan Academy, Matrices (Precalculus) (exercises). Multiplication and properties only.
  4. NumPy, Broadcasting and numpy.matmul (documentation), and the site’s own NumPy reference.
  5. 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.

One-minute revision