Skip to content
Gen AI: Zero to One

Vectors & Matrices: The Operations

Phase 0TypeBuildLanguagePythonTime~40 minutesPrereqLinear Algebra, Intuitively
Vectors & Matrices: 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 = 0 breaks 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:

python
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

For matrix multiplication (m × n) @ (n × p), what has to be true?

The identity matrix is…

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:

OperationWhat it doesWhere it shows up
Add / subtractcombine matching positionsadding the bias; residual connections
Scalar multiplyscale every elementlearning_rate * gradient
Element-wise (Hadamard) *multiply matching positionsgates (LSTM/GRU), masks, dropout
Matrix multiply @dot every row into every columnevery layer’s forward pass
Transpose flip rows and columnsQ @ Kᵀ in attention; backprop
Determinantone number: how much it scales area/volumechecking invertibility; change of variables
Inverseundo a transformationsolving 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.

aba + b
Vector addition is tip-to-tail: a + b lands at the far corner of the parallelogram the two vectors make.
python
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 number

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

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

python
| 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 @
Ruleshapes must match exactlyinner dims must match: (m × k)(k × n)
Each output cellone producta dot product (multiply and add)
Output shapesame as inputs(m × n)
Used forgates, masks, dropoutlayer transforms, attention

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:

python
(m × k) @ (k × n) = (m × n)
         ↑     ↑
      these must matchand they cancel

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

python
(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
Input: a 784-vector (a flattened 28×28 image)
Weights: a (128 × 784) matrix
(128 × 784) @ (784 × 1) — the 784s cancel
Output: a 128-vector of features
Stack another layer and repeat

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. B maps a vector into some space, and A has 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 B then A generally lands somewhere different from A then B, so AB ≠ BA. It isn’t a quirk of the arithmetic — it’s the plain fact that the order you apply operations in changes the outcome.
vrot→scalescale→rot
Same input v, two familiar moves (rotate 90°, stretch along x). Rotate-then-scale and scale-then-rotate send v to different places — that's AB ≠ BA. Order of transforms is order of operations.

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

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

python
(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.

col 1col 2
det = 3·2.5 − 1·1 = 6.5. Non-zero area → the transform keeps 2-D → invertible.
col 1col 2 = ½·col 1
det = 3·0.5 − 1·1.5 = 0. The parallelogram collapses to a line → singular, no inverse.

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:

DeterminantGeometryConsequence
largespace is stretched a lotinvertible; columns are strongly independent
= 1area/volume preserveda pure rotation or shear (rigid-ish)
= 0collapsed to a line/pointsingular — not invertible, rank-deficient
negativespace 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.

python
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

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.

python
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.])  -> unchanged

Broadcasting: 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.

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

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:

python
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 output

Then the same thing in PyTorch — identical math, orders of magnitude faster:

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

Write down every tensor’s shape
For @: do the inner dims match? For * or +: do the shapes match or broadcast?
Name the culprit (here: 784 vs 128 don’t cancel)
Fix with the minimal move — usually a .T or a reshape
Re-check the OUTPUT shape is what you expected

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

OperationWhere you’ll meet it in real systems
Matrix × vector / matrixEvery dense layer’s forward pass; batching many inputs at once
Element-wise productLSTM/GRU gates, attention masks, dropout, LayerNorm scaling
TransposeQ @ Kᵀ attention scores; Wᵀ in the backward pass; x @ W.T in a layer
DeterminantInvertibility checks; the change-of-variables term in normalizing flows
Inverse / solveLeast-squares and linear regression; covariance whitening
IdentityResidual/skip connections (x + F(x)); sensible weight initialization
BroadcastingAdding bias across a batch; adding positional encodings; normalization

Use it

Run the from-scratch Matrix and the PyTorch mirror, then watch the diagrams regenerate:

bash
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, broadcasting

Read 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

  1. Verify the inverse. Multiply A @ inv(A) for three different 2×2 matrices and confirm you get the identity. Then try one with det = 0 — what happens, and why?
  2. Prove the transpose rule. Pick any non-square A and B that can multiply, and check numerically that (A @ B).T == B.T @ A.T. Then explain, from shapes alone, why A.T @ B.T usually isn’t even legal.
  3. Element-wise vs matmul. Take two 2×2 matrices and compute both A * B and A @ B. Write one sentence on why a forget gate wants the first and a layer wants the second.
  4. Build a two-layer network. Using only your Matrix class (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.
  5. 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

The key difference between element-wise (*) and matrix multiplication (@) is…

X is (32 × 784), W is (128 × 784). Which expression is valid and gives (32 × 128)?

A matrix has determinant 0. That means…

(A @ B).T equals…

How does the identity matrix relate to a residual (skip) connection?

A layer output is (64 × 256). To add one learned bias per feature, the bias should be…

You need to solve A x = b for a large A. Best practice is…

Key terms

TermWhat people sayWhat 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
series progress0%
Code & notebooks for this series