d2l::interactive

Chapter 5 // Foundations

Multilayer Perceptrons

Softmax regression could only draw straight boundaries — and one stubborn point proved it. Stack a hidden layer with a nonlinearity and the boundary learns to bend. This is your first truly deep network.

Adapted from the original chapter of Dive into Deep Learning · CC BY-SA 4.0 · the Python shown is the book's; the figures run a faithful TypeScript port in your browser

Every model so far has been linear: the output moves monotonically with each input. That is a strong assumption, and often a wrong one — no straight line separates a checkerboard, tells a cat from its photographic negative, or reads a pixel in the context of its neighbors. The fix is almost embarrassingly simple: add a layer of hidden units, and a nonlinearity between the layers.

Act 1 · A line learns to bend

01/05

accuracy 50.5%loss 0.695

Chapter 4 hit a wall like this. These four blobs are XOR: class by diagonal, the classic set no straight line can split. Softmax regression gives up — weights collapse toward zero, accuracy stuck near 50%, a coin flip.

One hidden layer, one nonlinearity — that is the whole recipe. In symbols, the hidden representation and the output are:

H=σ(XW(1)+b(1)),O=HW(2)+b(2)\mathbf{H} = \sigma(\mathbf{X}\mathbf{W}^{(1)} + \mathbf{b}^{(1)}), \qquad \mathbf{O} = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}

The nonlinearity σ\sigma is load-bearing: drop it and the two layers collapse back into one. With it, a single hidden layer is already a universal approximator — given enough units, it can fit any function. Learning the right weights is the hard part, and for that we need to know how gradients flow.

Act 2 · Backprop, one gate at a time

01/05

forward — the computation ►x10.60x2-0.40h10.64h20.00h30.40o1·o2·L·one hidden unit fired 0 — ReLU clamped it

One example through a tiny net. The forward passfills each hidden unit: sum the inputs, then ReLU. The middle unit's sum came out negative, so ReLU clamps it to zero— it's dark.

Backprop is just the chain rule run in reverse — but that reversed product of matrices is also where deep networks go wrong, long before they get a chance to learn anything at all.

Act 3 · Signals that die or blow up

01/05

10⁵10³10¹10⁻¹10⁻³10⁻⁵‖grad‖layer depth →

Backprop multiplies one matrix per layer. In a deep net the gradient is a product of many factors — and products are volatile. Stay in the shaded band and learning works; leave it and it breaks.

Careful initialization keeps a deep network trainable. The next problem is the opposite one: a network with millions of parameters can memorize its training set outright. One of the most effective cures is to inject noise on purpose.

Act 4 · Dropout, on purpose

01/05

hidden layer h12345678all units activep = 0.5 · E[h′] = h, so the expected output never changes

A model overfits when its units co-adapt: they form brittle partnerships that memorize the training set. Here is one hidden layer, all eight units firing.

The lab

Three claims from the acts, yours to poke at: that hidden width is what buys a bent boundary, that the choice of activation decides whether gradients survive depth, and that dropout is unbiased noise that leaves the expected output alone.

▸ lab

Grow the hidden layer

Zero units is a straight line. Add hidden units and watch the boundary earn its folds.

hidden units

accuracy 100.0%

loss 0.001

4 folds are enough to wrap both diagonals. XOR solved.

▸ lab

Activations and the gradients they pass

Compare ReLU, sigmoid, and tanh — then stack layers and watch which gradients survive.

sigmoid(x)derivative — slope ≤ 0.25

Slope peaks at just 0.25 and vanishes at the tails — deep stacks starve.

best-case gradient after 10 layers

0.25^10 = 9.5e-7

Vanished. The early layers get essentially no signal.

▸ lab

Dropout, and why it stays fair

Zero units at random, rescale the survivors — then average many draws and watch E[h′] land back on h.

origh′mean0.81.61.600.500.001.02.02.000.30.60.600.91.81.800.600.000.40.80.800.71.41.40

kept 6/8 this draw · averaged over 1 draw

survivors scale by ×2.00

Any single draw is lopsided — some units gone, survivors doubled. Keep sampling.

How deep networks generalize

An over-parametrized network can fit anything — including randomly shuffled labels. By the classical story, that should doom it to overfit. Yet the same networks, trained on real labels, generalize beautifully. Deep learning breaks the tidy picture from chapter 3, and the theory is still catching up.

A real dataset: house prices

The book closes the chapter by leaving the lab and entering a Kaggle competition — predicting house prices in Ames, Iowa from 80 mixed features. The model is almost beside the point; the workflow is the lesson.

The real code

Every act above carries a </> chip with its section's exact PyTorch. The from-scratch MLP is just softmax regression with one extra layer and a ReLU:

mlp-implementation.md (PyTorch)
class MLPScratch(d2l.Classifier):
    def __init__(self, num_inputs, num_outputs, num_hiddens, lr, sigma=0.01):
        self.W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens) * sigma)
        self.b1 = nn.Parameter(torch.zeros(num_hiddens))
        self.W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs) * sigma)
        self.b2 = nn.Parameter(torch.zeros(num_outputs))
 
def relu(X):
    return torch.max(X, torch.zeros_like(X))
 
@d2l.add_to_class(MLPScratch)
def forward(self, X):
    X = X.reshape((-1, self.num_inputs))
    H = relu(torch.matmul(X, self.W1) + self.b1)   # hidden layer
    return torch.matmul(H, self.W2) + self.b2       # output layer
The concise version, with dropout (the full tour)
mlp-implementation.md · dropout.md (PyTorch)
# concise: stack Linear + ReLU + Dropout with nn.Sequential
class DropoutMLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nn.Flatten(), nn.LazyLinear(num_hiddens_1), nn.ReLU(),
            nn.Dropout(dropout_1), nn.LazyLinear(num_hiddens_2), nn.ReLU(),
            nn.Dropout(dropout_2), nn.LazyLinear(num_outputs))
 
hparams = {'num_outputs': 10, 'num_hiddens_1': 256, 'num_hiddens_2': 256,
           'dropout_1': 0.5, 'dropout_2': 0.5, 'lr': 0.1}
model = DropoutMLP(**hparams)
data = d2l.FashionMNIST(batch_size=256)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

Three things to remember

  1. Depth plus a nonlinearity buys expressiveness. Two linear layers collapse into one; a ReLU between them lets the network fold space into piecewise-linear regions and bend a boundary a straight line could never reach.
  2. Backprop is the chain rule in reverse, and it can misfire. The gradient is a product of one matrix per layer — too small and it vanishes, too large and it explodes. Xavier initialization, random and scaled, keeps it alive.
  3. Capacity is cheap; generalization is the work. A deep net can fit anything, so the real tools — weight decay, early stopping, dropout — all fight overfitting, and dropout does it by training a fresh thinned network every step.

Adapted from Chapter 5 of Dive into Deep Learning by Zhang, Lipton, Li, and Smola (CC BY-SA 4.0). The figures run this site's verified TypeScript port; all computation happens live in your browser.