← All tutorials

On this page

1 The XOR problem 2 ReLU and piecewise linear functions 3 The one-hidden-layer MLP 4 The forward pass 5 Width, depth and capacity 6 L1 and L2 regularization 7 Activation functions 8 MLPs in practice 9 MLP training failures 10 References

Multilayer Perceptrons (MLP)

The XOR problemfour blobs, one straight cutReLUmax(0, x), one kink at zeroOne hidden layera sum of kinks at learned placesThe forward passone point through the networkWidth, depth and capacitytoo much capacity fits label noiseL1 and L2 regularizationpenalties on the weight sizesActivation functionsReLU, leaky ReLU, sigmoid, tanh, GELUMLP training failuresdead units, init scale, learning rateIn practiceone PyTorch training step, cost, gotchas123456789
Figure 1. Each box's marker is the number of the section that teaches it.

A multilayer perceptron (MLP) stacks linear layers with a nonlinear activation function between them, so it can draw boundaries that one straight line cannot; training tunes every weight to lower the loss.

An MLP takes one gradient step per batch over many passes through the data: a forward pass predicts, and a backward pass, about twice the forward work, finds each weight's gradient.

1 The XOR problem

Logistic regression, the straight-line classifier, fails on the XOR layout: four blobs at the corners of a square, each class on one diagonal.

blue +orangeorange+ blue11223
Figure 2. Blue 1 and orange 2 fill opposite corners, so any straight cut 3 leaves a side mixed.

Every model on this page trains on these blobs: four Gaussian clouds, 1,200 points, 8% of the labels flipped.

0%50%100%train51.8%test47.0%coin flip, 50%12
Figure 3. Logistic regression on XOR 1 scores about a coin flip 2 (an illustrative offline run).

2 ReLU and piecewise linear functions

The simplest nonlinear function is the ReLU, max(0, x), also called a hinge function: flat, then a straight ramp, with one kink. A sum of shifted hinges is a piecewise linear function that can follow a curve.

A sum of hinges is a piecewise line, and you get to choose where every kink goes.
ReLU(x) = max(0, x)
max(0, x) takes the larger of 0 and x: negative in, zero out, positive in, straight through. Flat on the left, a ramp on the right, one kink at zero.
f(x) = a + Σk wk·ReLU(x − tk)
The fitted curve f(x) starts at height a, then adds one hinge per k: each hinge is shifted so its kink sits at tk and scaled by wk, the change in slope at that kink. Each added hinge adds one kink.

The hinge fit in NumPy.

import numpy as np
K = 8
x = np.linspace(-2, 2, 200)
y = x ** 2
j = np.arange(K)
t = -2 + 4 * (j + 0.5) / K  # t_k
D = x[:, None] - t
ramps = np.maximum(0, D)
ones = np.ones((len(x), 1))
Phi = np.hstack([ones, ramps])
ls = np.linalg.lstsq
sol = ls(Phi, y, rcond=None)
coef = sol[0]  # [a, w_1..w_K]
fit = Phi @ coef
  1. 1Samples the parabola y = x².
  2. 2One ReLU column per kink tk, plus ones for a.
  3. 3Least squares for a and every wk (Section 2).
true curve y = x² (dashed)one hingekink1
Figure 4. One hinge has one kink 1; the parabola curves everywhere.

Figure 5. Each hinge adds one kink; with 8 the fit follows the parabola closely except at the left edge.

3 The one-hidden-layer MLP

A one-hidden-layer MLP is a sum of hinges whose positions and weights are learned. Each unit (the name comes from Rosenblatt's perceptron) takes a weighted sum of its inputs, adds a bias, and applies the activation.

The one-hidden-layer MLP in two lines.
h = ReLU(W₁x + b₁)
The first layer's weights W₁ and bias b₁ make H weighted sums of the input x, and ReLU sets the negative ones to zero to give the hidden values h.
ŷ = σ(W₂h + b₂)
The second layer (W₂, b₂) adds the hidden values into one score, and the sigmoid σ turns it into the probability ŷ.

In PyTorch the first Linear makes H lines from the 2 inputs, ReLU kinks each, and the last Linear mixes them into one score.

import torch.nn as nn
H = 128  # hidden units
mlp = nn.Sequential(
    nn.Linear(2, H),
    nn.ReLU(),
    nn.Linear(H, 1),
)

Without the ReLU, the two layers collapse.

Two linear layers with no activation collapse into one linear layer.
ŷ = W₂(W₁x + b₁) + b₂
With no activation between them, the first layer (weights W₁, bias b₁) feeds the input x straight into the second (W₂, b₂), giving the output ŷ.
ŷ = (W₂W₁)x + (W₂b₁ + b₂)
It multiplies out to one matrix and one bias: one straight cut, still useless on the blobs. The activation is what makes the network more than a line.

4 The forward pass

A forward pass turns one input point into a probability: each hidden unit computes a weighted sum, ReLU sets negative sums to zero, and the output unit adds the results and applies the sigmoid.

Figure 6. With hand-picked weights h1 fires on the top-left blob and h2 on the bottom-right.

The hidden layer computes weighted sums and applies ReLU; the output layer sums again and applies the sigmoid.
h = ReLU(W₁x+b₁)
Each hidden unit takes a weighted sum of the inputs plus a bias, and ReLU sets negative sums to zero. Without the activation the two layers collapse into one matrix and one bias, still linear, still useless on the blobs.
ŷ = σ(W₂h+b₂)
The second layer adds the hidden values into one score, and the sigmoid σ maps any score to a probability between 0 and 1: a large positive score means almost surely blue, zero means a coin flip. Here ŷ is 0.68, a 0.68 chance of blue.
x the inputs; h the hidden valuesŷ the outputW1, b1 the first layer's weights and biasW2, b2 the second layer's weights and biasReLU sets negative sums to 0σ the sigmoid, maps the sum to a probability

The same forward pass in PyTorch; no_grad marks it as prediction only, with no gradients kept.

import torch
# x: (2,); W1: (2, 2); W2: (1, 2)
with torch.no_grad():
    h = torch.relu(W1 @ x + b1)
    s = W2 @ h + b2
    y_hat = torch.sigmoid(s)

4.1 The backward pass

Training needs every weight's gradient. Backpropagation applies the chain rule layer by layer, from the output back to the input, and reuses each layer's result for the layer before it. For one first-layer weight, with L the binary cross-entropy loss of Logistic Regression.

The gradient of one first-layer weight, by the chain rule.
∂L∂W₁jk = ∂L∂s·∂s∂hj·∂hj∂zj·∂zj∂W₁jk
W₁jk links input k to hidden unit j; zj is that unit's sum before ReLU, and s the output score before the sigmoid. The chain rule multiplies the slope of each step, from the loss back to the weight.
= (ŷ − y)·W₂j·1[zj > 0]·xk
With the sigmoid and cross-entropy, ∂L/∂s is the error ŷ − y; the output weight W₂j carries it back to unit j; ReLU passes it only if the unit fired (1[zj > 0] is 1 then, else 0); xk is the input the weight multiplies. A unit that never fires gets no gradient, the dead ReLU of Section 9.

In PyTorch loss.backward() computes all of these at once (autograd). The backprop_pytorch notebook in the Machine-Learning-Concepts repo writes the backward pass by hand.

5 Width, depth and capacity

Capacity is how complicated a boundary the model can draw; width (units per layer) and depth (number of layers) both add to it. A model with higher training and lower test accuracy is overfitting.

linearsmall MLPlargelarge + L21234
Figure 7. A linear model cuts straight 1; 128 then 64 units split smoothly 2; 512 down to 32 units after 3,000 epochs bend around mislabeled points 3; a weight penalty (L2, Section 6) removes the bends 4. Illustrative offline runs with Adam (Optimizers from SGD to Adam); no published script.

Here the large model overfits only when nothing penalizes its weights.

Figure 8. At capacity 3 the boundary bends around mislabeled points; at 4 the L2 penalty smooths them out.

6 L1 and L2 regularization

The large model's boundary bends around single points because its weights grew large to fit every flipped label. Regularization adds a penalty on the weights to the training loss, in two common forms.

The L1 and L2 penalties add the weights' sizes, or their squares, to the loss.
Ltotal = L + λ·Σi |wi|
L is the data loss, how wrong the model is; λ (lambda) is the penalty strength: raise it and weights shrink toward 0 and the boundary smooths, at 0 there is no penalty, and a very large λ pushes every weight to 0 so the model underfits; Σi adds up over every weight i; the bars around wi drop its sign. Under this L1 penalty every unit of weight costs the same, so a small weight that barely helps is pushed all the way to zero.
Ltotal = L + λ·Σi wi2
The L2 penalty squares each weight instead, so a weight twice as large costs four times as much; the biggest weights shrink hardest while small ones are left nearly alone, and few reach exactly zero.

L1 pushes many weights to exactly zero, giving a sparse model where only a few weights do the work.

0weightpenaltyL2 growswith the squareL1 growslinearly12
Figure 9. L1 1 costs the same per unit of weight; L2 2 costs little near zero and climbs fast.

Dropout sets a random share of units to zero at each training step, so the net cannot rely on any one unit; see Regularization.

7 Activation functions

ReLU is the default activation for hidden layers because it is cheap and trains well; other activations suit other jobs.

Four common activation functions and what each does to its input.
LeakyReLU(x) = max(0.1x, x)
Below zero 0.1x is the larger, so a little gets through (slope 0.1, as in the figure) and the unit keeps a small gradient, so it never gets stuck outputting zero (a dead ReLU, Section 9).
sigmoid(x) = 11 + e−x
Maps any input into 0 to 1, the standard choice for yes-or-no outputs: as x grows the e−x term (e is the exponential base) fades and the value nears 1.
tanh(x) = ex − e−xex + e−x
Maps any input into −1 to 1, centered at zero, so it often trains faster than sigmoid.
GELU(x) = x·Φ(x)
Φ(x) is the chance a standard bell-curve draw falls below x, so GELU is a smooth ReLU that lets small negatives through slightly; it is common in transformer blocks such as BERT and GPT-2.

Output layers use their own: sigmoid for yes-or-no, softmax (class scores into probabilities that add up to 1) for many classes.

Each activation is one PyTorch module.

import torch.nn as nn
hidden = nn.ReLU()
leaky = nn.LeakyReLU(0.1)
yes_no = nn.Sigmoid()
zero_centered = nn.Tanh()
many_classes = nn.Softmax(dim=-1)
gelu = nn.GELU()  # transformers

Figure 10. Each activation 1 to 4 has a kink or a curve, which lets a network bend its boundary.

8 MLPs in practice

One training step; the loss takes raw scores (logits) and applies the sigmoid inside.

import torch
from torch import nn
from torch.optim import AdamW

model = nn.Sequential(
    nn.Linear(2, 64),
    nn.ReLU(),
    nn.Dropout(0.1),
    nn.Linear(64, 1),
)
params = model.parameters()
opt = AdamW(params, lr=1e-3)
loss_fn = nn.BCEWithLogitsLoss()
x = torch.randn(128, 2)
y = torch.randint(0, 2, (128, 1))
y = y.float()
model.train()  # dropout on
loss = loss_fn(model(x), y)
opt.zero_grad()
loss.backward()
opt.step()
  1. 1The MLP with dropout, AdamW and the loss.
  2. 2A random batch of 128 points and labels.
  3. 3Dropout on, loss, gradients and one step.

The step runs once per batch, for as many epochs as the validation score improves; memory holds the weights, the activations kept for the backward pass, and the optimizer's two running averages per weight.

SettingWhere it entersRaise itLower itDefault
Hidden unitsWidth of W1More kinks; can overfitFewer; at 0 no hidden layer100 (MLPClassifier)
lrStep size of each updateFaster; too high divergesSlower1e-3 (AdamW)
weight_decayShrinks every weight each stepSmoother boundary; too high underfitsNo shrinking at 01e-2 (AdamW)
Dropout(p)Share of hidden units zeroed each training stepStronger regularization, slower fit; near 1 drops almost every unitWeaker; 0 turns it off0.5 (nn.Dropout)
alphaMLPClassifier's L2 λAs λ above1e-4

Call model.eval() and wrap prediction in torch.no_grad(), or dropout stays on and memory grows. Use BCEWithLogitsLoss on raw scores rather than a sigmoid then BCELoss, which PyTorch documents as less numerically stable. AdamW's weight decay shrinks weights directly instead of adding the L2 term to the loss; with Adam the two differ.

9 MLP training failures

CauseSymptomFix
Dead ReLUsunits output zero for every input, so they stop learning and the loss stallsLeaky ReLU, or a smaller learning rate lr (the step size of each update)
Bad init scaleactivations blow up or fade layer by layerHe init for ReLU layers, starting weights scaled to each layer's input count so signals keep their size (nn.init.kaiming_normal_)
Learning rate too highthe loss jumps around or turns NaNlower lr; see Optimizers from SGD to Adam

MLPs treat every input feature alike; CNN Basics: Convolution, Stride and Pooling shows how convolutions share weights across an image to see local patterns.

10 References

  1. Rosenblatt, F. (1958). The perceptron: a probabilistic model for information storage and organization in the brain. Psychological Review.
  2. Rumelhart, D. E., et al. (1986). Learning representations by back-propagating errors. Nature.
  3. Nair, V., et al. (2010). Rectified linear units improve restricted Boltzmann machines. ICML.
  4. Srivastava, N., et al. (2014). Dropout: a simple way to prevent neural networks from overfitting. Journal of Machine Learning Research.