Goal: Vectorise the MLP forward pass over a mini-batch of B examples and get every dimension right cold.
1. Why this session exists
In code, MLPs process a mini-batch of B examples at a time, not one example at a time — that is what makes GPU computation efficient. The math is identical, but the shapes change. Getting layer shapes right cold (no derivation, you see "B×d2" and just know what's there) is a skill we want before we start computing gradients in 2.5–2.7.
2. Single example: refresher
From 2.1, per-example forward at layer ℓ:
z(ℓ)=W(ℓ)a(ℓ−1)+b(ℓ),a(ℓ)=g(ℓ)(z(ℓ)),
with a(ℓ)∈Rdℓ, W(ℓ)∈Rdℓ×dℓ−1, b(ℓ)∈Rdℓ.
3. Batch: stacking examples as rows
Let A(ℓ−1)∈RB×dℓ−1 be the batch of activations at layer ℓ−1, one example per row. Then the batched pre-activation is
Z(ℓ)=A(ℓ−1)(W(ℓ))⊤+1B(b(ℓ))⊤∈RB×dℓ,
with 1B the all-ones column. In NumPy/PyTorch, the bias addition is implemented by broadcasting: you just write Z = A @ W.T + b.
The batched activation is the pointwise nonlinearity applied entry-by-entry:
A(ℓ)=g(ℓ)(Z(ℓ))∈RB×dℓ.
Why the transpose? Because we chose to put examples as rows of A. The matrix-vector product W(ℓ)a from §2 has a as a column on the right; to apply the same map to every row of A, the convenient form is AW⊤.
4. Dimension table
Every shape you'll ever need at the forward pass.
Input batch: A(0)=X∈RB×d0.
Weights: W(ℓ)∈Rdℓ×dℓ−1 (output dim × input dim).
Bias: b(ℓ)∈Rdℓ (output dim).
Pre-activation: Z(ℓ)=A(ℓ−1)(W(ℓ))⊤+b(ℓ)∈RB×dℓ.
Activation: A(ℓ)=g(ℓ)(Z(ℓ))∈RB×dℓ.
Output logits: Z(L)∈RB×dL.
Output probabilities: Y^=softmax(Z(L),axis=1)∈RB×dL — softmax over the class axis, separately for each row.
A worked instance — MNIST with (d0,d1,d2,d3)=(784,256,128,10) and batch B=32:
X is 32×784.
W(1) is 256×784, b(1) is 256.
Z(1) is 32×256, A(1) is 32×256.
W(2) is 128×256, b(2) is 128.
Z(2) is 32×128, A(2) is 32×128.
W(3) is 10×128, b(3) is 10.
Z(3) is 32×10, Y^ is 32×10.
If you can write this table cold, the rest of the chapter follows mechanically.
5. The training loss in batch form
For multi-class classification with K=dL classes and one-hot targets Y∈{0,1}B×K,
L(θ)=−B1i=1∑Bk=1∑KYi,klogY^i,k,
equivalently −B1tr(Y⊤logY^) with log elementwise. Using the log-sum-exp form from 0.10 / 1.7,
L(θ)=B1i=1∑B[−zi,yi(L)+logj=1∑Kezi,j(L)],
i.e. per-row stable log-sum-exp plus per-row true-class logit subtraction, then average. In PyTorch this is F.cross_entropy(logits, y) with logits of shape B×K and y of shape B (class indices).
6. Why this matters for what's next
Backprop in 2.5–2.7 will be derived in the same batched matrix form. Knowing the shapes ahead of time prevents silly transposition bugs: each gradient should be the same shape as its parameter, every δ(ℓ) should be the same shape as Z(ℓ), and any product that isn't dℓ×dℓ−1 where we wanted weights is a sign we got something wrong.
The forward pass is L matrix-multiplies and L pointwise activations.
Loss is a scalar summary; we'll backprop from it through this graph in 2.5.
Self-test
(a) For an MLP with widths (d0,d1,d2,d3)=(10,64,64,3) and batch B=8, list the shapes of X, W(ℓ), b(ℓ), Z(ℓ), A(ℓ) for ℓ=1,2,3.
(b) Verify that A(ℓ−1)(W(ℓ))⊤+b(ℓ) produces B independent vector additions (one per row), not a single matrix add.
(c) Count the number of scalar multiplications in the forward pass of the MNIST network of §4 for a batch B=32. Compare with the parameter count.
(d) Suppose you swapped to the column-major convention A(ℓ)∈Rdℓ×B. Rewrite the forward formula. Which layout is more cache-friendly when B is large? (Hint: think about how matmul libraries lay out memory and dispatch BLAS.)