Multi-layer perceptron
Stack affine maps and nonlinearities into an MLP, and trace one forward pass through its parameters.
A multi-layer perceptron (MLP) alternates two operations, an affine map and a pointwise nonlinearity, times. Each affine map is a linear algebra problem; each nonlinearity is a scalar function. The composition turns the linear-classifier story from pillar C into a function class that can fit any continuous target[1]. This lesson sets up the forward pass so the next lesson can derive backprop on it.
The single layer
A linear layer with input and output applies an affine map plus a nonlinearity:
The matrix rotates and scales; the bias shifts; the elementwise bends. Without the layer is just a linear map, and the whole network would collapse to one large linear map regardless of depth. The nonlinearity is what makes stacking useful[2].
Stacking layers
An -layer MLP composes such blocks:
The width of layer is the number of rows of ; the depth is . The final activation is task-specific: identity for regression, softmax for classification.
The parameter count is : matrices dominate, biases are a small surcharge.
Batched forward pass
In practice examples flow through at once. Stack inputs into and rewrite:
The bias broadcasts across the batch axis. Modern frameworks store weights
in (in, out) order and apply X @ W + b, which is the same operation
written with the transpose absorbed. Either convention is fine; pick one
and stick with it.
Output activations
For regression the output is the raw (identity activation), trained with squared error.
For binary classification the output is a single logit; apply sigmoid at prediction time and train with binary cross-entropy on the logit directly.
For multi-class classification with classes, the output is a length- logit vector; apply softmax at prediction time and train with cross-entropy. Skip the explicit softmax in training and use the fused log-softmax + NLL form from the log-sum-exp lesson.
What the forward pass costs
For a batch of examples and a layer mapping , the cost is multiply-adds. The total forward cost scales linearly in batch size and quadratically (roughly) in width. Depth multiplies cost by a constant; the doubling of layers does not square the cost.
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
def init_layer(in_dim, out_dim, rng):
# Simple Gaussian init; the initialization lesson refines this.
W = rng.normal(scale=0.1, size=(in_dim, out_dim))
b = np.zeros(out_dim)
return W, b
def relu(x): return np.maximum(0.0, x)
def softmax(z):
z = z - z.max(axis=1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=1, keepdims=True)
# 4-layer MLP for 3-class classification on 8-dim inputs
N, d_in, d_hid, K = 16, 8, 32, 3
X = rng.normal(size=(N, d_in))
W1, b1 = init_layer(d_in, d_hid, rng)
W2, b2 = init_layer(d_hid, d_hid, rng)
W3, b3 = init_layer(d_hid, d_hid, rng)
W4, b4 = init_layer(d_hid, K, rng)
# Forward pass
h1 = relu(X @ W1 + b1)
h2 = relu(h1 @ W2 + b2)
h3 = relu(h2 @ W3 + b3)
logits = h3 @ W4 + b4
probs = softmax(logits)
print(f"shapes: X {X.shape}, h1 {h1.shape}, logits {logits.shape}, probs {probs.shape}")
print(f"row sums of probs (should be 1): {probs.sum(axis=1)[:3]}")
print(f"argmax classes for the first 5 rows: {probs.argmax(axis=1)[:5]}")
# Parameter count
n_params = sum(p.size for p in [W1, b1, W2, b2, W3, b3, W4, b4])
print(f"total parameters: {n_params}")
# Forward cost (multiply-adds) for one batch
flops = N * (d_in * d_hid + d_hid * d_hid * 2 + d_hid * K)
print(f"~{flops:,} multiply-adds for one forward pass")The shapes flow consistently through the layers, the softmax rows sum to one, and the parameter count matches the matrix-plus-bias formula.
Exercises
Exercise 1. A 3-layer MLP maps inputs of dimension through hidden widths and to an output of size . Each layer is an affine map with a weight matrix and a bias. Count the total number of parameters by hand.
Solution
Step 1. Layer has weight matrix and bias , so it contributes parameters.
Step 2. Sum over the three layers:
The matrices () dominate; the biases () are a small surcharge, as the parameter-count formula predicts.
Exercise 2. Show that without a nonlinearity an -layer network collapses to a single affine map. Take with no , and find the single such that for .
Solution
Step 1. Write the first layer: .
Step 2. Feed it into the second:
Step 3. Distribute and group the terms by whether they multiply :
The composition is one affine map with and . Induction extends this to any , so depth buys nothing without a nonlinearity between the maps.
Exercise 3 (code). Build the same 2-layer linear network from Exercise 2 in numpy with random weights, then construct the single collapsed affine map and verify it produces identical outputs on a batch of inputs.
Solution
import numpy as np
rng = np.random.default_rng(0)
d0, d1, d2, N = 6, 8, 3, 16
W1 = rng.normal(size=(d1, d0)); b1 = rng.normal(size=d1)
W2 = rng.normal(size=(d2, d1)); b2 = rng.normal(size=d2)
X = rng.normal(size=(N, d0))
# Two-layer linear network (no nonlinearity), row-vector convention
H1 = X @ W1.T + b1
Y_deep = H1 @ W2.T + b2
# Collapsed single affine map
W_eff = W2 @ W1
b_eff = W2 @ b1 + b2
Y_flat = X @ W_eff.T + b_eff
assert np.allclose(Y_deep, Y_flat)
print("max abs difference:", np.abs(Y_deep - Y_flat).max())The two outputs agree to floating-point precision: stacking linear layers without a nonlinearity is a single linear layer in disguise, which is why has to sit between them.
The next lesson computes derivatives of this same forward pass: backpropagation.
References
- [1]Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, §6.1-6.4 (Deep Feedforward Networks). MIT Press. The feedforward network as composed affine maps and nonlinearities.↩
- [2]Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, §6.3 (Hidden Units). MIT Press. Why a nonlinearity between affine maps is what gives depth its expressive power.↩