Lecture 14: From the Perceptron to the Multilayer Perceptron (MLP)

Babafemi Sorinolu

Learning objectives

By the end of this lesson, you should be able to:

  • Explain why a single perceptron cannot learn XOR.
  • Build an MLP by connecting perceptrons into layers.
  • Carry out a forward pass through an MLP by hand.
  • Explain what the hidden layer does to the data.
  • Explain why MLPs use smooth activations such as the sigmoid.
  • Do one step of backpropagation on a tiny network.

Part 1: Where the perceptron gets stuck

Quick recap: the perceptron

\[ z = w_1x_1 + w_2x_2 + b \qquad\qquad \hat y = \text{step}(z) = \begin{cases} 1, & z \ge 0\\ 0, & z < 0 \end{cases} \]

The perceptron predicts 1 on one side of the line

\[ w_1x_1 + w_2x_2 + b = 0 \]

and 0 on the other side.

Tip

A perceptron draws one straight line. That is both its strength and its limit.

A perceptron can do AND and OR

Gate \(w_1\) \(w_2\) \(b\) Rule
AND 1 1 −1.5 fires only if \(x_1+x_2 \ge 1.5\)
OR 1 1 −0.5 fires if \(x_1+x_2 \ge 0.5\)

Check OR on \((0,1)\): \(\;z = 1(0) + 1(1) - 0.5 = 0.5 \ge 0 \;\Rightarrow\; \hat y = 1\) ✓

Check AND on \((0,1)\): \(\;z = 1(0) + 1(1) - 1.5 = -0.5 < 0 \;\Rightarrow\; \hat y = 0\) ✓

Now try XOR

XOR (“exclusive or”) is 1 when exactly one input is 1.

\(x_1\) \(x_2\) XOR
0 0 0
0 1 1
1 0 1
1 1 0

Your turn (1 minute): find \(w_1, w_2, b\) so that a single perceptron computes XOR.

Spoiler: you can’t. Let’s see why.

Picture it

The two blue points sit on opposite corners. Any straight line that separates them from one orange X puts the other orange X on the wrong side.

Proof: one perceptron cannot do XOR

Suppose some \(w_1, w_2, b\) worked. Then all four rows must hold:

Input Need Condition
\((0,0)\) 0 \(b < 0\)
\((0,1)\) 1 \(w_2 + b \ge 0\)
\((1,0)\) 1 \(w_1 + b \ge 0\)
\((1,1)\) 0 \(w_1 + w_2 + b < 0\)

Add rows 2 and 3: \(\;w_1 + w_2 + 2b \ge 0 \;\Rightarrow\; w_1 + w_2 + b \ge -b\)

Row 1 says \(b<0\), so \(-b > 0\), so \(\;w_1 + w_2 + b > 0\). That contradicts row 4. No solution exists. ∎

History: Minsky & Papert (1969) highlighted limits like this one. The fix is more than one layer.

Part 2: Stacking perceptrons

The idea: break XOR into easy pieces

XOR means “at least one is on, but not both”:

\[ \text{XOR}(x_1,x_2) = \text{OR}(x_1,x_2)\ \text{ AND NOT }\ \text{AND}(x_1,x_2) \]

\(x_1\) \(x_2\) OR AND OR and not AND XOR
0 0 0 0 0 0
0 1 1 0 1 1
1 0 1 0 1 1
1 1 1 1 0 0

OR and AND are each one perceptron. Can the last step (“\(h_1\) and not \(h_2\)”) be a perceptron too?

Yes: “\(h_1\) and not \(h_2\)” is linearly separable

Use \(\;\hat y = \text{step}(h_1 - h_2 - 0.5)\)

\(h_1\) \(h_2\) \(z = h_1 - h_2 - 0.5\) \(\hat y\)
0 0 −0.5 0
1 0 0.5 1
1 1 −0.5 0

The pair \((h_1,h_2) = (0,1)\) never happens: AND can’t be on while OR is off.

So: three perceptrons, wired together, compute XOR.

Our first multilayer perceptron

\[ h_1 = \text{step}(x_1 + x_2 - 0.5) \qquad h_2 = \text{step}(x_1 + x_2 - 1.5) \qquad \hat y = \text{step}(h_1 - h_2 - 0.5) \]

Vocabulary

  • Input layer: just the features. No computation happens here.
  • Hidden layer(s): perceptrons whose outputs we never see directly.
  • Output layer: produces the prediction \(\hat y\).
  • Fully connected: every node feeds every node in the next layer.
  • Feed-forward: information flows one way, left → right.

Part 3: Worked example, forward pass

Forward pass for \(x = (0, 1)\)

Hidden unit 1 (OR):

\[ z_1 = (1)(0) + (1)(1) - 0.5 = 0.5 \quad\Rightarrow\quad h_1 = \text{step}(0.5) = 1 \]

Hidden unit 2 (AND):

\[ z_2 = (1)(0) + (1)(1) - 1.5 = -0.5 \quad\Rightarrow\quad h_2 = \text{step}(-0.5) = 0 \]

Output unit: its inputs are now \(h_1\) and \(h_2\), not \(x_1\) and \(x_2\).

\[ z_{\text{out}} = (1)(1) + (-1)(0) - 0.5 = 0.5 \quad\Rightarrow\quad \hat y = 1 \]

Target XOR\((0,1) = 1\). Correct ✓

Forward pass for \(x = (1, 1)\)

This is the case a single perceptron could never get right.

\[ z_1 = (1)(1) + (1)(1) - 0.5 = 1.5 \quad\Rightarrow\quad h_1 = 1 \]

\[ z_2 = (1)(1) + (1)(1) - 1.5 = 0.5 \quad\Rightarrow\quad h_2 = 1 \]

\[ z_{\text{out}} = (1)(1) + (-1)(1) - 0.5 = -0.5 \quad\Rightarrow\quad \hat y = 0 \]

Target XOR\((1,1) = 0\). Correct ✓. The AND unit “vetoed” the OR unit.

All four inputs

\(x_1\) \(x_2\) \(z_1\) \(h_1\) \(z_2\) \(h_2\) \(z_{\text{out}}\) \(\hat y\) XOR
0 0 −0.5 0 −1.5 0 −0.5 0 0 ✓
0 1 0.5 1 −0.5 0 0.5 1 1 ✓
1 0 0.5 1 −0.5 0 0.5 1 1 ✓
1 1 1.5 1 0.5 1 −0.5 0 0 ✓

Tip

Forward pass recipe: compute every node in a layer, then use those outputs as the inputs to the next layer. Repeat until you reach the output.

Why it works: the hidden layer re-draws the data

The hidden layer moves the points into a new space \((h_1, h_2)\). There, both XOR = 1 inputs land on the same spot, and one line separates the classes.

Why it works: two lines → a band


  • \(h_1\) draws the lower line.
  • \(h_2\) draws the upper line.
  • The output unit says “above line 1 and below line 2”.

More hidden units → more lines → more complicated regions.

Big idea: hidden layers let the network build non-linear decision boundaries out of simple linear pieces.

Try it: MLP playground

Drag the sliders to solve XOR, then switch the target to XNOR, NAND, …

The same network in matrix form

Stack the hidden weights in a matrix, one row per hidden unit:

\[ W = \begin{bmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 1 \end{bmatrix}, \quad \mathbf b = \begin{bmatrix} -0.5 \\ -1.5 \end{bmatrix}, \quad \mathbf v = \begin{bmatrix} 1 \\ -1 \end{bmatrix}, \quad c = -0.5 \]

\[ \mathbf h = f(W\mathbf x + \mathbf b) \qquad\qquad \hat y = f(\mathbf v^\top \mathbf h + c) \]

Example \(\mathbf x = (1,0)\): \(\;W\mathbf x + \mathbf b = \begin{bmatrix} 1 \\ 1 \end{bmatrix} + \begin{bmatrix} -0.5 \\ -1.5 \end{bmatrix} = \begin{bmatrix} 0.5 \\ -0.5 \end{bmatrix} \Rightarrow \mathbf h = \begin{bmatrix} 1 \\ 0 \end{bmatrix}\)

This is exactly how libraries compute it: one matrix multiply per layer.

Counting parameters

Each layer has (inputs × outputs) weights + (outputs) biases.

Our XOR network (2 → 2 → 1):

\[ \underbrace{2\times 2 + 2}_{\text{hidden}} \;+\; \underbrace{2 \times 1 + 1}_{\text{output}} \;=\; 6 + 3 = 9 \text{ parameters} \]

A digit classifier (784 → 128 → 10):

\[ (784 \times 128 + 128) + (128 \times 10 + 10) = 100{,}480 + 1{,}290 = 101{,}770 \]

We chose the 9 XOR weights by hand. Nobody can hand-pick 101,770, so the network has to learn them.

Part 4: How does an MLP learn?

Problem 1: hidden units have no target

The perceptron rule uses the error at that neuron:

\[ w_i \leftarrow w_i + \eta\,(y - \hat y)\,x_i \]

  • The output unit has a target \(y\). ✓
  • A hidden unit has no target. Was \(h_1\) “wrong”? We don’t know! ✗

We need a way to share the blame for an output error among the hidden units. This is the credit assignment problem.

Problem 2: the step function is flat

Nudging a weight a little almost never changes a step output, so the slope is 0 and calculus gives no signal. MLPs use smooth activations instead:

\[ \sigma(z) = \frac{1}{1 + e^{-z}} \qquad\qquad \sigma'(z) = \sigma(z)\,\bigl(1 - \sigma(z)\bigr) \]

Why not just drop the activation?

Without an activation, each layer is linear:

\[ \mathbf h = W_1 \mathbf x + \mathbf b_1, \qquad \hat y = W_2 \mathbf h + \mathbf b_2 \]

Substitute:

\[ \hat y = W_2(W_1 \mathbf x + \mathbf b_1) + \mathbf b_2 = \underbrace{(W_2 W_1)}_{W'}\mathbf x + \underbrace{(W_2 \mathbf b_1 + \mathbf b_2)}_{\mathbf b'} \]

That is still one straight line, no matter how many layers we stack!

Important

The non-linear activation between layers is what gives an MLP its power.

Worked example 2: the same net with sigmoids

Input \(\mathbf x = (1, 0)\), target \(y = 1\), loss \(L = \tfrac12 (y - \hat y)^2\), learning rate \(\eta = 1\).

Step 1: forward pass, hidden layer

\[ z_1 = (1)(1) + (1)(0) - 0.5 = 0.5 \qquad z_2 = (1)(1) + (1)(0) - 1.5 = -0.5 \]

\[ h_1 = \sigma(0.5) = \frac{1}{1 + e^{-0.5}} = 0.6225 \qquad h_2 = \sigma(-0.5) = 0.3775 \]

Note

Compared with the step version (\(h_1 = 1,\ h_2 = 0\)), the sigmoid gives soft answers between 0 and 1.

Step 2: forward pass, output and loss

\[ z_{\text{out}} = (1)(0.6225) + (-1)(0.3775) - 0.5 = -0.255 \]

\[ \hat y = \sigma(-0.255) = 0.437 \]

\[ L = \tfrac12 (1 - 0.437)^2 = 0.159 \]

The target is 1, but the network only says 0.437. With sigmoids, our hand-picked weights are too weak:

\(\mathbf x\) (0,0) (0,1) (1,0) (1,1)
\(\hat y\) 0.424 0.437 0.437 0.424

Every output is near 0.43. So we train the network.

Step 3: backprop, output layer

Gradient descent: move every parameter downhill, \(\;\theta \leftarrow \theta - \eta \dfrac{\partial L}{\partial \theta}\).

Error signal at the output (chain rule: loss → sigmoid → \(z_{\text{out}}\)):

\[ \delta_{\text{out}} = (\hat y - y)\,\hat y\,(1 - \hat y) = (0.437 - 1)(0.437)(0.563) = -0.139 \]

Each output-layer gradient is \(\delta_{\text{out}}\) × (the input on that wire):

Parameter Gradient New value \((\eta = 1)\)
\(v_1\) \(\delta_{\text{out}} \cdot h_1 = -0.139 \times 0.6225 = -0.086\) \(1 - (-0.086) = 1.086\)
\(v_2\) \(\delta_{\text{out}} \cdot h_2 = -0.139 \times 0.3775 = -0.052\) \(-1 - (-0.052) = -0.948\)
\(c\) \(\delta_{\text{out}} \cdot 1 = -0.139\) \(-0.5 - (-0.139) = -0.361\)

Step 4: backprop, hidden layer

The error flows backwards through the weight \(v_j\) and through hidden unit \(j\)’s sigmoid:

\[ \delta_j = \delta_{\text{out}} \cdot v_j \cdot h_j(1 - h_j) \qquad h_1(1-h_1) = h_2(1-h_2) = 0.235 \]

\[ \delta_1 = (-0.139)(1)(0.235) = -0.033 \qquad \delta_2 = (-0.139)(-1)(0.235) = +0.033 \]

Gradient = \(\delta_j\) × input. Since \(x_2 = 0\), the \(w_{12}\) and \(w_{22}\) gradients are 0 and those weights don’t change.

Parameter Gradient New value
\(w_{11}\) \(\delta_1 \cdot x_1 = -0.033\) \(1.033\)
\(b_1\) \(\delta_1 = -0.033\) \(-0.467\)
\(w_{21}\) \(\delta_2 \cdot x_1 = +0.033\) \(0.967\)
\(b_2\) \(\delta_2 = +0.033\) \(-1.533\)

Step 5: did it help?

Run the forward pass again on \(\mathbf x = (1,0)\) with the new parameters:

Before After 1 update
\(\hat y\) 0.437 0.497
Loss \(L\) 0.159 0.127
  • The output moved toward the target \(y = 1\).
  • The hidden units changed too: \(h_1\) (OR) got a bit stronger and \(h_2\) (AND) a bit weaker. Backprop assigned blame to the hidden layer.
  • Repeat over all four inputs, many times, and the network learns XOR.

The training loop: perceptron vs MLP

Perceptron MLP
Activation step sigmoid / ReLU (smooth)
Forward pass one weighted sum one weighted sum per layer
Error signal \(e = y - \hat y\) loss \(L\), e.g. \(\tfrac12(y - \hat y)^2\)
Update \(w_i \leftarrow w_i + \eta\, e\, x_i\) \(w \leftarrow w - \eta\, \partial L/\partial w\) (via backprop)
Can learn XOR? ✗ ✓

\[ \boxed{\text{Forward pass} \rightarrow \text{Loss} \rightarrow \text{Backward pass} \rightarrow \text{Update} \rightarrow \text{Repeat}} \]

In code: scikit-learn

import numpy as np
from sklearn.linear_model import Perceptron
from sklearn.neural_network import MLPClassifier

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])                      # XOR

p = Perceptron(random_state=0).fit(X, y)
print("Perceptron:", p.predict(X), "accuracy", p.score(X, y))

mlp = MLPClassifier(hidden_layer_sizes=(4,), activation="tanh",
                    solver="lbfgs", max_iter=1000, random_state=0).fit(X, y)
print("MLP:       ", mlp.predict(X), "accuracy", mlp.score(X, y))
Perceptron: [0 0 0 0] accuracy 0.5
MLP:        [0 1 1 0] accuracy 1.0

hidden_layer_sizes=(4,) means one hidden layer with 4 units. (8, 8) would mean two hidden layers.

Part 5: Practice

Practice 1: forward pass

\[ h_1 = \text{step}(2x_1 - x_2 - 1.5) \qquad h_2 = \text{step}(-x_1 + 2x_2 - 1.5) \qquad \hat y = \text{step}(h_1 + h_2 - 0.5) \]

Fill in the table. Which logic function is this?

\(x_1\) \(x_2\) \(h_1\) \(h_2\) \(\hat y\)
0 0 ? ? ?
0 1 ? ? ?
1 0 ? ? ?
1 1 ? ? ?

Practice 1: answer

\(x_1\) \(x_2\) \(z_1\) \(h_1\) \(z_2\) \(h_2\) \(\hat y\)
0 0 −1.5 0 −1.5 0 0
0 1 −2.5 0 0.5 1 1
1 0 0.5 1 −2.5 0 1
1 1 −0.5 0 −0.5 0 0

It is XOR again, built a different way: \(h_1\) = “\(x_1\) and not \(x_2\)”, \(h_2\) = “\(x_2\) and not \(x_1\)”, and the output is OR\((h_1, h_2)\). The same function can have many different weight settings.

Practice 2: the hidden space

Use the network from Practice 1.

  1. Where does each input land in the \((h_1, h_2)\) plane?
  2. Give a line in that plane that separates the classes.
  1. \((0,0) \to (0,0)\), \(\;(0,1) \to (0,1)\), \(\;(1,0) \to (1,0)\), \(\;(1,1) \to (0,0)\)
  2. \(h_1 + h_2 - 0.5 = 0\), which is exactly the output unit. The class-0 points both land at \((0,0)\), and the class-1 points lie on the other side of the line.

Practice 3: design XNOR

XNOR is the opposite of XOR: 1 when the inputs are equal.

Keep the lecture’s hidden layer (\(h_1\) = OR, \(h_2\) = AND). Choose new output weights \(v_1, v_2, c\).

Flip the output unit: \(\;\hat y = \text{step}(-h_1 + h_2 + 0.5)\)

\((h_1,h_2)\) \(z\) \(\hat y\)
(0,0) from (0,0) 0.5 1
(1,0) from (0,1) and (1,0) −0.5 0
(1,1) from (1,1) 0.5 1

Negating every output weight and the bias flips every decision (no input lands exactly on \(z = 0\)).

Practice 4: count the parameters

How many weights and biases does each fully connected network have?

  1. 4 → 5 → 3
  2. 784 → 128 → 10
  3. 10 → 20 → 20 → 1
  1. \((4 \cdot 5 + 5) + (5 \cdot 3 + 3) = 25 + 18 = \mathbf{43}\)

  2. \((784 \cdot 128 + 128) + (128 \cdot 10 + 10) = \mathbf{101{,}770}\)

  3. \((10 \cdot 20 + 20) + (20 \cdot 20 + 20) + (20 \cdot 1 + 1) = 220 + 420 + 21 = \mathbf{661}\)

Practice 5: why do we need activations?

A “network” has no activation functions:

\[ h = 2x + 1, \qquad \hat y = 3h - 4 \]

Write \(\hat y\) directly in terms of \(x\). What does this tell you?

\(\hat y = 3(2x + 1) - 4 = 6x - 1\).

Two linear layers collapse into one linear function. Without a non-linear activation, extra layers add nothing, so the model still can’t learn XOR.

Practice 6: sigmoid forward pass

Use the sigmoid network from worked example 2 (lecture weights, \(c = -0.5\)) with input \(\mathbf x = (0, 0)\), target \(y = 0\).

Find \(h_1\), \(h_2\), \(\hat y\) and \(L = \frac12 (y - \hat y)^2\). Use \(\sigma(-0.5) = 0.378\), \(\sigma(-1.5) = 0.182\), \(\sigma(-0.304) = 0.424\).

\(z_1 = -0.5 \Rightarrow h_1 = 0.378\); \(\quad z_2 = -1.5 \Rightarrow h_2 = 0.182\)

\(z_{\text{out}} = 0.378 - 0.182 - 0.5 = -0.304 \Rightarrow \hat y = 0.424\)

\(L = \frac12 (0 - 0.424)^2 = 0.090\)

Practice 7: one backprop update

Continue Practice 6 (\(\hat y = 0.424\), \(y = 0\), \(\eta = 1\)).

  1. Compute \(\delta_{\text{out}} = (\hat y - y)\,\hat y\,(1-\hat y)\).
  2. Update \(c\) and \(v_1\) (recall \(h_1 = 0.378\)).
  3. Will the new \(\hat y\) be bigger or smaller? Is that good?
  1. \(\delta_{\text{out}} = (0.424)(0.424)(0.576) = 0.104\)
  2. \(c \leftarrow -0.5 - 0.104 = -0.604\); \(\quad v_1 \leftarrow 1 - (0.104)(0.378) = 1 - 0.039 = 0.961\)
  3. Both changes lower \(z_{\text{out}}\), so \(\hat y\) gets smaller. That is good, because the target is 0.

Practice 8 (challenge): “exactly one”

Design an MLP with three inputs \(x_1, x_2, x_3 \in \{0,1\}\) that outputs 1 when exactly one input is 1.

Hint: reuse the “OR and not AND” idea with \(s = x_1 + x_2 + x_3\).

\(h_1 = \text{step}(x_1 + x_2 + x_3 - 0.5)\): “at least one is on”

\(h_2 = \text{step}(x_1 + x_2 + x_3 - 1.5)\): “at least two are on”

\(\hat y = \text{step}(h_1 - h_2 - 0.5)\): “at least one, but not two or more”

Same shape as our XOR net, with one more input!

Key takeaways

  • One perceptron = one straight line, so it cannot learn XOR.
  • An MLP chains perceptrons into layers: input → hidden → output.
  • The hidden layer transforms the data into a space where a line does work.
  • Forward pass: compute each layer from the previous one, \(\mathbf h = f(W\mathbf x + \mathbf b)\).
  • Non-linear activations are essential. Without them, layers collapse into one.
  • Training uses smooth activations + backpropagation (the chain rule) + gradient descent.

\[ \boxed{\text{Forward pass} \rightarrow \text{Loss} \rightarrow \text{Backward pass} \rightarrow \text{Update} \rightarrow \text{Repeat}} \]