Gen AI · Course · 03 of 7
Vectors & Matrices: The Operations
Vector ops
- ·add / subtract = tip-to-tail
- ·scalar × = stretch
- ·element-wise (Hadamard)
Element-wise vs matmul
- ·* → same shape, positions
- ·@ → rows · columns
- ·@ composes transforms → order matters
- ·the #1 tensor bug
Shapes
- ·(m×k)@(k×n)=(m×n)
- ·inner dims cancel
- ·debug shapes first
Transpose
- ·flip rows/cols
- ·Q @ Kᵀ, Wᵀ in backprop
- ·(AB)ᵀ = Bᵀ Aᵀ
Determinant & inverse
- ·det = area/volume scale
- ·det=0 → singular
- ·inverse undoes; solve, don’t invert
Identity & broadcasting
- ·I = multiply by 1
- ·I + F = residual
- ·bias added by broadcast
The payoff
- ·relu(W @ x + b)
- ·one dense layer
- ·built from scratch
Last lesson was the intuition — what vectors, dot products, and rank mean. This one is the toolkit: the actual operations you type, the shapes that have to line up, and the two multiplications everyone confuses. By the end you’ll build a full neural-network layer from scratch — and read tensor-shape errors like plain English, which is half of every ML interview and most of the debugging you’ll ever do.
Learning objectives
- Do the core operations by hand — add, scale, transpose, multiply — and know which rule each one follows
- Tell element-wise (
*) apart from matrix multiply (@) instantly, and know when each applies - Read a shape mismatch and fix it — the single most useful debugging reflex in ML
- Explain determinant (area/volume scaling) and inverse (undo), and why
det = 0breaks both - See why the identity matrix is a residual connection and how broadcasting adds bias for free
- Build one dense layer —
relu(W @ x + b)— with no libraries, then in PyTorch - Walk into an interview able to reason out loud about shapes, commutativity, transposes, and invertibility
The problem
You open a neural network and the whole forward pass of a layer is one line:
output = activation(W @ x + b)That @ is matrix multiplication. W is a matrix of weights, x is the input vector, b is a bias, and activation bends the result. If those operations are fuzzy, this line is magic. If they’re not, it’s just transform, shift, bend — three moves. Every image is a grid of numbers, every embedding is a vector, every layer is a matrix transformation. Fluency here is like knowing variables before you write code — non-negotiable.
Pre-lesson check
0/3 answered// question
For matrix multiplication (m × n) @ (n × p), what has to be true?
// question
The identity matrix is…
// question
In PyTorch, `A * B` (for two same-shape tensors) computes…
The operations map
Seven operations carry almost all of deep learning. Here’s the whole territory before we walk it:
| Operation | What it does | Where it shows up |
|---|---|---|
| Add / subtract | combine matching positions | adding the bias; residual connections |
| Scalar multiply | scale every element | learning_rate * gradient |
Element-wise (Hadamard) * | multiply matching positions | gates (LSTM/GRU), masks, dropout |
Matrix multiply @ | dot every row into every column | every layer’s forward pass |
Transpose ᵀ | flip rows and columns | Q @ Kᵀ in attention; backprop |
| Determinant | one number: how much it scales area/volume | checking invertibility; change of variables |
| Inverse | undo a transformation | solving linear systems (least squares) |
Warm up: vector operations
Adding two vectors is tip-to-tail: walk along the first, then along the second; where you land is the sum. Scaling multiplies the length (and flips direction if the scalar is negative). No shape rules to worry about yet — same-length in, same-length out.
import torch
a = torch.tensor([3., 1.])
b = torch.tensor([1., 2.5])
print(a + b) # tensor([4.0000, 3.5000]) -> tip-to-tail
print(2 * a) # tensor([6., 2.]) -> scaled (twice as long)
print(a * b) # tensor([3.0000, 2.5000]) -> ELEMENT-WISE: positions multiplied
print(a @ b) # tensor(5.5000) -> DOT PRODUCT: one numberLook at those last two lines. Same two vectors, one * away from each other, completely different objects: a * b is a vector (positions multiplied), a @ b is a single number (multiply and add). That split is the whole ballgame once we get to matrices.
The distinction that trips everyone: * vs @
There are two ways to “multiply” matrices and they are not variations on a theme — they’re different operations with different rules and different shapes out.
Element-wise (Hadamard): matching positions
Line the two grids up and multiply cell by cell. Both matrices must be the same shape, and the result is that same shape.
| 1 2 | | 5 6 | | 1·5 2·6 | | 5 12 |
| 3 4 | * | 7 8 | = | 3·7 4·8 | = | 21 32 |Matrix multiply: rows into columns
Each output cell is a dot product: row i of the left with column j of the right. This is the operation from last lesson — a whole grid of dot products at once.
| 1 2 | | 5 6 | | 1·5+2·7 1·6+2·8 | | 19 22 |
| 3 4 | @ | 7 8 | = | 3·5+4·7 3·6+4·8 | = | 43 50 |Element-wise * | Matrix multiply @ | |
|---|---|---|
| Rule | shapes must match exactly | inner dims must match: (m × k)(k × n) |
| Each output cell | one product | a dot product (multiply and add) |
| Output shape | same as inputs | (m × n) |
| Used for | gates, masks, dropout | layer transforms, attention |
// question
Checkpoint: an LSTM ‘forget gate’ multiplies a vector of values in [0,1] against the cell state to decide what to keep or erase, position by position. Which operation is that?
Shapes are the grammar
Matrix multiply has one hard rule, and internalizing it will save you more time than any other single thing in this course:
(m × k) @ (k × n) = (m × n)
↑ ↑
these must match — and they cancelWrite the shapes side by side; the inner pair has to be equal and it disappears, and you keep the outer pair. It’s dimensional cancellation, like units in physics.
(128 × 784) @ (784 × 1) = (128 × 1)
weights input output
inner: 784 = 784 ✓ valid -> 784-dim input becomes a 128-dim output›The layer that turns 784 pixels into 128 features
// question
Checkpoint: you have a batch X of shape (32 × 784) and weights W of shape (128 × 784). PyTorch throws a shape error on X @ W. What’s the fix?
Matrix multiply is composition: do B, then A
Here’s the why under the mechanics. From last lesson, a matrix is a transformation — vector in, moved vector out. So what is A @ B? It’s a single matrix that does both transforms back to back: (A @ B) x = A (B x) — first B transforms x, then A transforms the result. You read it right to left: the matrix nearest the vector goes first.
That one idea explains two rules you just took on faith:
- Why the inner dims must match.
Bmaps a vector into some space, andAhas to accept that space as its input. So B’s output size = A’s input size — the inner numbers meet in the middle because that’s exactly where one transform hands off to the next. - Why order matters. Doing
BthenAgenerally lands somewhere different fromAthenB, soAB ≠ BA. It isn’t a quirk of the arithmetic — it’s the plain fact that the order you apply operations in changes the outcome.
// question
Checkpoint: you stack three linear layers — W3 @ (W2 @ (W1 @ x)) — with no activation between them. Why is that no more powerful than a single layer?
Transpose: flip it
Transpose swaps rows and columns — cell (i, j) becomes (j, i). An (m × n) matrix becomes (n × m). That’s it mechanically, but it’s everywhere: it’s the Kᵀ in attention’s Q @ Kᵀ, and it’s how gradients travel backward through a layer (the backward pass multiplies by Wᵀ).
A = torch.tensor([[1, 2, 3],
[4, 5, 6]]) # (2 × 3)
print(A.T) # (3 × 2)
# tensor([[1, 4],
# [2, 5],
# [3, 6]])One property is a genuine interview favorite because you can reason it out instead of memorizing it: transposing a product reverses the order.
(A @ B).T == B.T @ A.T›Why (AB)ᵀ = Bᵀ Aᵀ — argue it from shapes
You don’t need the algebra; the shapes force it. Say A is (m × k) and B is (k × n), so A @ B is (m × n) and (A @ B)ᵀ is (n × m).
Now try to rebuild (n × m) from the transposed pieces. Aᵀ is (k × m) and Bᵀ is (n × k). The only order whose inner dims cancel is Bᵀ @ Aᵀ: (n × k) @ (k × m) = (n × m). Aᵀ @ Bᵀ wouldn’t even be legal. The reversal isn’t a rule to memorize — it’s the only thing that type-checks.
Determinant: how much space gets stretched
Feed the two columns of a 2×2 matrix in as vectors; they span a parallelogram. The determinant is the (signed) area of that parallelogram — how much the transformation blows up or shrinks space. In 3-D it’s a volume; in general it’s the volume-scaling factor of the transform.
For a 2×2 the formula is short: det([[a, b], [c, d]]) = ad − bc. What the number says matters more than how you compute it:
| Determinant | Geometry | Consequence |
|---|---|---|
| large | space is stretched a lot | invertible; columns are strongly independent |
| = 1 | area/volume preserved | a pure rotation or shear (rigid-ish) |
| = 0 | collapsed to a line/point | singular — not invertible, rank-deficient |
| negative | space is flipped (mirrored) | orientation reversed, still invertible |
Inverse: undo the transformation
If a matrix A maps x to A @ x, its inverse A⁻¹ maps it straight back: A⁻¹ @ (A @ x) = x. Equivalently A @ A⁻¹ = I. It exists only when det ≠ 0 — you can’t undo a transform that already threw a dimension away.
A = torch.tensor([[4., 7.],
[2., 6.]])
Ainv = torch.linalg.inv(A)
print(torch.round(A @ Ainv)) # the identity -> confirmed inverse
# tensor([[1., 0.],
# [0., 1.]])
singular = torch.tensor([[1., 2.],
[2., 4.]]) # row 2 = 2·row 1 -> det = 0
# torch.linalg.inv(singular) -> LinAlgError: singular matrix// question
Checkpoint: what condition guarantees a square matrix has an inverse?
Identity: the do-nothing matrix (and residual connections)
The identity I has 1s down the diagonal and 0s everywhere else. A @ I = A, I @ x = x — it’s multiplication by 1. Sounds boring until you notice what I + something does.
I = torch.eye(3)
print(I)
# tensor([[1., 0., 0.],
# [0., 1., 0.],
# [0., 0., 1.]])
x = torch.tensor([5., 7., 9.])
print(I @ x) # tensor([5., 7., 9.]) -> unchangedBroadcasting: free shape-stretching
When you add a bias vector to a whole matrix of outputs, the shapes don’t match — yet it just works. Broadcasting stretches the smaller array along the missing dimension so the two line up, without ever copying the data in memory.
outputs = torch.tensor([[1, 2, 3],
[4, 5, 6]]) # (2 × 3) — two examples
bias = torch.tensor([10, 20, 30]) # (3,) — one per feature
print(outputs + bias)
# bias is stretched across both rows:
# tensor([[11, 22, 33],
# [14, 25, 36]])The rule: line the shapes up from the right; each dimension must be equal, or one of them must be 1 (a size-1 or missing dim gets stretched). That’s exactly how W @ x + b adds one bias per output unit across an entire batch, for free.
// question
Checkpoint: layer output is shape (64 × 256) — 64 examples, 256 features. You add a bias. What bias shape gets added correctly, one value per feature?
The payoff: build a dense layer from scratch
Every piece is now on the table. A dense (fully-connected) layer is exactly relu(W @ x + b): transform the input with a weight matrix, shift it with a bias (broadcast), bend it with an activation. Here’s the whole thing with a hand-built Matrix — no libraries — so nothing is hidden:
class Matrix:
def __init__(self, data):
self.data = [list(r) for r in data]
self.rows, self.cols = len(self.data), len(self.data[0])
self.shape = (self.rows, self.cols)
def __add__(self, other): # broadcasts a column bias
return Matrix([[self.data[i][j] + other.data[i][0]
for j in range(self.cols)] for i in range(self.rows)])
def matmul(self, other):
return Matrix([[sum(self.data[i][k] * other.data[k][j]
for k in range(self.cols))
for j in range(other.cols)] for i in range(self.rows)])
def relu(m):
return Matrix([[max(0.0, v) for v in row] for row in m.data])
x = Matrix([[0.5], [0.8], [0.2]]) # (3 × 1) input
W = Matrix([[0.2, -0.4, 0.1], [-0.3, 0.9, 0.5]]) # (2 × 3) weights
b = Matrix([[0.1], [0.1]]) # (2 × 1) bias
out = relu(W.matmul(x) + b) # (2 × 3)@(3 × 1) + (2 × 1)
print(out.shape, out.data) # (2, 1) -> the layer's outputThen the same thing in PyTorch — identical math, orders of magnitude faster:
x = torch.tensor([[0.5], [0.8], [0.2]]) # (3, 1)
W = torch.randn(2, 3) # (2, 3)
b = torch.tensor([[0.1], [0.1]]) # (2, 1)
out = torch.relu(W @ x + b) # relu(W @ x + b)
print(out.shape) # torch.Size([2, 1])Think like an interviewer
This section is here on purpose: interviews don’t test whether you memorized ad − bc — they test whether you can reason about shapes and operations out loud. Here’s the thought process that makes you sound fluent, plus the traps that catch people.
The shape-debugging reflex
Ninety percent of real ML bugs are shape bugs, and interviewers know it. When you hit mat1 and mat2 shapes cannot be multiplied (32×784 and 128×784), don’t guess — run the checklist out loud:
Saying that sequence while you debug signals more competence than any clever one-liner. The fix above is X @ W.T: (32 × 784) @ (784 × 128) = (32 × 128).
Questions you can reason out (not memorize)
›“Is matrix multiplication commutative? Is AB the same as BA?”
No. Usually AB ≠ BA — and often BA isn’t even a legal shape. Concrete counterexample: with A = [[1,1],[0,1]] and B = [[1,0],[1,1]], AB = [[2,1],[1,1]] but BA = [[1,1],[1,2]]. The intuition is composition (see “Matrix multiply is composition” above): AB means “do B, then A,” and the order you apply transforms in changes where you land.
›“What’s the transpose of a product, (AB)ᵀ?”
Bᵀ Aᵀ — the order reverses. Don’t recite it; derive it from shapes (see the box above): it’s the only ordering whose inner dimensions cancel. Same reversal holds for the inverse: (AB)⁻¹ = B⁻¹ A⁻¹ (to undo “socks then shoes,” take off shoes then socks).
›“When can’t you invert a matrix, and what would you do instead?”
When it’s not square, or its det = 0 (singular / rank-deficient — a redundant direction). Even when an inverse exists, you rarely form it: to solve Ax = b you call solve(A, b), which is faster and more stable. For non-square or noisy systems you reach for the pseudo-inverse / least-squares — which, from last lesson, is a projection onto the column space.
›“What’s the cost of multiplying an (m × k) by a (k × n) matrix?”
O(m · k · n) multiply-adds — one dot product of length k for each of the m × n output cells. For square n × n that’s the famous O(n³). It’s why big matmuls dominate training cost and why GPUs (which do thousands of these in parallel) matter. Clever algorithms like Strassen shave the exponent to ~2.81, but the practical speedups come from hardware and BLAS, not asymptotics.
›“Why store weights as (out × in) and compute x @ W.T?”
A batch of inputs is (batch × in). To get (batch × out) you need a (in × out) on the right — that’s W.T when W is stored (out × in). Frameworks store it (out × in) because it reads naturally (“this layer has out neurons, each seeing in inputs”), then transpose at multiply-time. Knowing this makes shape errors in a training loop a five-second fix.
Where each idea shows up
| Operation | Where you’ll meet it in real systems |
|---|---|
| Matrix × vector / matrix | Every dense layer’s forward pass; batching many inputs at once |
| Element-wise product | LSTM/GRU gates, attention masks, dropout, LayerNorm scaling |
| Transpose | Q @ Kᵀ attention scores; Wᵀ in the backward pass; x @ W.T in a layer |
| Determinant | Invertibility checks; the change-of-variables term in normalizing flows |
| Inverse / solve | Least-squares and linear regression; covariance whitening |
| Identity | Residual/skip connections (x + F(x)); sensible weight initialization |
| Broadcasting | Adding bias across a batch; adding positional encodings; normalization |
Use it
Run the from-scratch Matrix and the PyTorch mirror, then watch the diagrams regenerate:
python foundations/matrix-operations/matrices.py # from-scratch Matrix + a dense layer, then PyTorch
python foundations/matrix-operations/visualize.py # element-wise vs matmul, determinant-as-area, broadcastingRead them inline without leaving the page: · .
Ship it
This lesson produces:
- — a from-scratch
Matrix(add, scalar, element-wise, matmul, transpose, determinant, inverse, identity), a dense layer, and the PyTorch equivalents side by side - — the walkthrough, cell by cell
- / — verify the inverse, extend to 3×3, and build a two-layer network with no libraries
- — regenerates the operation diagrams as PNGs
Exercises
- Verify the inverse. Multiply
A @ inv(A)for three different 2×2 matrices and confirm you get the identity. Then try one withdet = 0— what happens, and why? - Prove the transpose rule. Pick any non-square
AandBthat can multiply, and check numerically that(A @ B).T == B.T @ A.T. Then explain, from shapes alone, whyA.T @ B.Tusually isn’t even legal. - Element-wise vs matmul. Take two 2×2 matrices and compute both
A * BandA @ B. Write one sentence on why a forget gate wants the first and a layer wants the second. - Build a two-layer network. Using only your
Matrixclass (no libraries), wire input (3) → hidden (4) → output (2) with random weights, run a forward pass, and assert every intermediate shape is what you predicted. - Break it on purpose. Add a shape
(3,)tensor to a shape(3, 1)tensor in PyTorch. Predict the output shape before you run it, then explain the broadcasting that produced it.
Post-lesson quiz
0/7 answered// question
The key difference between element-wise (*) and matrix multiplication (@) is…
// question
X is (32 × 784), W is (128 × 784). Which expression is valid and gives (32 × 128)?
// question
A matrix has determinant 0. That means…
// question
(A @ B).T equals…
// question
How does the identity matrix relate to a residual (skip) connection?
// question
A layer output is (64 × 256). To add one learned bias per feature, the bias should be…
// question
You need to solve A x = b for a large A. Best practice is…
Key terms
| Term | What people say | What it actually means |
|---|---|---|
| Element-wise (Hadamard) | "regular multiplication" | Multiply matching positions; shapes must match (or broadcast). PyTorch * |
| Matrix multiply | "rows times columns" | A dot product per output cell; (m × k) @ (k × n) = (m × n). PyTorch @ |
| Shape | "the dimensions" | The (rows × cols) of a tensor; the thing to check first when anything breaks |
| Transpose | "flip it" | Swap rows and columns; (m × n) → (n × m). Reverses in a product: (AB)ᵀ = Bᵀ Aᵀ |
| Determinant | "some number from the matrix" | The signed area/volume scaling factor. 0 ⇒ a dimension is crushed (singular) |
| Inverse | "undo the matrix" | The transform that reverses A; A @ A⁻¹ = I. Exists only when det ≠ 0 |
| Singular | "a broken matrix" | det = 0 / rank-deficient: no inverse, no unique solution to A x = b |
| Identity matrix | "the boring one" | 1s on the diagonal; multiply-by-1. I + F is a residual connection |
| Broadcasting | "magic shape-fixing" | Stretching a smaller array along a size-1/missing dim to match a larger one |
| Dense / linear layer | "a fully-connected layer" | relu(W @ x + b): transform, shift (broadcast), bend |