Index Ai Academy
Phase 4 · Deep learning · Neural networks

Multi-layer networks

Stack layers to learn complex functions.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

One neuron can only split your data with a single straight line. Real problems need curves, corners and islands. A layer is many neurons working side by side, each drawing its own line. The next layer combines those lines into shapes. Stack layers like this and you get a multi-layer perceptron (MLP), the basic neural network. It isn't only history. Every block of a transformer, which is the design behind every large language model (LLM) in use today, contains an MLP. Those MLP parts hold most of the model's parameters, the numbers it learns.

What you'll be able to do

  • Describe a layer as many neurons computed at once with a matrix multiplication
  • Solve XOR with a two-layer network and explain how the hidden layer does it
  • Count a network's parameters from its layer sizes
  • Explain what depth and width add, informally (universal approximation)

What it covers

  1. A layer is many neurons at once
  2. Worked example: one layer by hand
  3. Many layers: the multi-layer perceptron
  4. Learning the bends from data
  5. How big real networks get

A layer is many neurons at once

You've met the neuron. It multiplies each input by a weight and adds them up. It adds a bias. Then it passes the total through an activation function, such as ReLU. One neuron answers one question about its inputs.

Now picture a panel of judges at a baking contest. Every judge tastes the same cake. Each one cares about something different: sweetness, texture or looks. Each writes down one score.

A layer works like that panel. Several neurons read the same inputs. Each has its own weights, so each asks its own question. The layer's output is the list of their answers.

You could work out each neuron one at a time. Matrix multiplication, from Phase 2, does them all in one step.

The formula

Here are the pieces, one at a time:

  • x\mathbf{x} is the list of inputs. Bold type means a list of numbers (a vector).
  • WW is the weight matrix. Each column holds one neuron's weights.
  • b\mathbf{b} is the list of biases, one per neuron.
  • ff is the activation function, such as ReLU.
  • h\mathbf{h} is the layer's output, one number per neuron.
h=f(xW+b)\mathbf{h} = f(\mathbf{x} W + \mathbf{b})

In plain words: multiply the inputs by the weight matrix. That gives every neuron's weighted sum at once. Add each neuron's bias. Then pass every result through the activation.

Now the shapes. Say the layer gets 3 inputs and has 4 neurons. Then WW has 3 rows and 4 columns, written 3 × 4. There's one row per input and one column per neuron. The bias list has 4 entries, and so does the output.

Go deeper Which way around is W?

This course stores WW as inputs × outputs. That way a whole batch of inputs, one per row, can be multiplied by WW directly.

PyTorch's nn.Linear stores the flipped version, called the transpose. It's written W⊤W^\top. PyTorch then computes xW⊤\mathbf{x} W^\top. The math is the same. Only the way the numbers are stored differs.

Quick check
A layer gets 5 inputs and has 2 neurons. What shape is its weight matrix W, stored as inputs × outputs?
  1. 2 × 5
  2. 7 × 1
  3. 5 × 2
  4. 5 × 5
Show the answer

5 × 2

One row per input (5) and one column per neuron (2). Each column holds one neuron's 5 weights.

Worked example

Worked example: one layer by hand

Two inputs, 2 and 1, go into a layer of three ReLU neurons. Work out the layer's output. Each column of W is one neuron's weights.

import numpy as np

x = np.array([2.0, 1.0])              # 2 inputs
W = np.array([[1.0, -1.0, 0.5],
              [1.0,  2.0, -1.0]])     # 2 inputs × 3 neurons
b = np.array([0.0, -1.0, 1.0])        # one bias per neuron
z = x @ W + b                         # every weighted sum at once
h = np.maximum(0, z)                  # ReLU on each one
print("weighted sums:", z)
print("layer output: ", h)
It runs right here in your browser, with nothing to install.

Take one neuron at a time. Neuron 1 has weights 1 and 1: 2 × 1 + 1 × 1 = 3. Its bias is 0, so its sum is 3.

Neuron 2 has weights −1 and 2: 2 × (−1) + 1 × 2 = 0. Its bias is −1, so its sum is −1.

Neuron 3 has weights 0.5 and −1: 2 × 0.5 + 1 × (−1) = 0. Its bias is 1, so its sum is 1.

ReLU turns negative numbers into 0 and leaves the rest alone. So the sums 3, −1 and 1 become the outputs 3, 0 and 1. The @ sign is matrix multiplication. It did all three weighted sums in one line.

Many layers: the multi-layer perceptron

A multi-layer perceptron chains layers together. "Perceptron" is an early name for an artificial neuron. The inputs go into a first layer. Its outputs become the inputs of the next layer, and so on. The last layer gives the answer.

The layers in the middle are called hidden layers. "Hidden" only means their values are neither the inputs nor the final answer. There's nothing secret about them.

The activation between layers matters. Without it, two layers collapse into a single layer. You saw that in the activation functions lesson.

XOR with one hidden layer

Many hallways have a light with a switch at each end. With both switches down, the light is off. Flip either one and the light comes on. Flip the other too and it goes off again. The light is on when exactly one switch is up. That rule is called XOR ("exclusive or").

In the neuron lesson, one neuron failed at XOR. No straight line can split its four points correctly. Two hidden ReLU neurons are enough:

  • The first neuron computes ReLU of (input 1 + input 2). It's on when at least one input is on.
  • The second computes ReLU of (input 1 + input 2 − 1). It's on only when both are on.
  • The output takes the first neuron and subtracts twice the second.

Try the input (1, 1) by hand. The first neuron gives 1 + 1 = 2. The second gives 1 + 1 − 1 = 1. The output is 2 − 2 × 1 = 0. Both switches are up, so the light is off. That's correct.

import numpy as np

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
W1 = np.array([[1.0, 1.0],
               [1.0, 1.0]])           # 2 inputs → 2 hidden neurons
b1 = np.array([0.0, -1.0])
W2 = np.array([[1.0], [-2.0]])       # 2 hidden → 1 output
b2 = np.array([0.0])

H = np.maximum(0, X @ W1 + b1)       # hidden layer
out = H @ W2 + b2                    # output layer
for x, h, o in zip(X, H, out):
    print(f"input {x}  hidden {h}  output {o[0]:.0f}")

The outputs are 0, 1, 1 and 0. That's XOR.

Now look at the hidden column. The hidden layer gives each point a new description. The inputs (0, 1) and (1, 0) both become (1, 0). The input (1, 1) becomes (2, 1). In this new description, one straight line can split the two answers. The output neuron draws that line.

This is what hidden layers do in general. They redescribe the data until the answer becomes simple.

Try it now

Flip the inputs and follow the values through the hidden layer. This is the network from this section.

Quick check
In the XOR network, the first hidden neuron is ReLU(x1 + x2) and the second is ReLU(x1 + x2 − 1). What does the hidden layer turn the input (1, 0) into?
  1. (0, 0)
  2. (1, 0)
  3. (2, 1)
  4. (1, 1)
Show the answer

(1, 0)

The first neuron gives 1 + 0 = 1. The second gives 1 + 0 − 1 = 0. So (1, 0) becomes (1, 0), the same point as (0, 1).

Learning the bends from data

You picked the XOR weights by hand. Real networks learn their weights from data, with the gradient descent you met in Phase 3.

The code below makes 300 points in two curved shapes that hook into each other, like two half moons. The noise setting scatters the points a little. Two models then learn to tell the moons apart:

  • Logistic regression, which is one neuron. It can only cut with a straight line.
  • An MLP with 2 inputs, two hidden layers of 16 neurons each, and 1 output.

The scikit-learn library does the training. max_iter=3000 lets the MLP take up to 3,000 passes over the data. random_state=0 fixes the random choices, such as where the points land and the network's starting weights. So you get the same result every run.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier

X, y = make_moons(n_samples=300, noise=0.2, random_state=0)
models = {"one neuron (logistic regression)": LogisticRegression(),
          "MLP: 2 → 16 → 16 → 1": MLPClassifier(hidden_layer_sizes=(16, 16), max_iter=3000, random_state=0)}

gx, gy = np.meshgrid(np.linspace(-1.6, 2.6, 150), np.linspace(-1.1, 1.6, 150))
fig, axes = plt.subplots(1, 2, figsize=(10, 3.6))
for ax, (name, m) in zip(axes, models.items()):
    m.fit(X, y)
    zz = m.predict_proba(np.c_[gx.ravel(), gy.ravel()])[:, 1].reshape(gx.shape)
    ax.contourf(gx, gy, zz, levels=[0, 0.5, 1], alpha=0.25, cmap="coolwarm")
    ax.scatter(X[:, 0], X[:, 1], c=y, cmap="coolwarm", s=10)
    ax.set_title(name)
    print(f"{name:<34} training accuracy {m.score(X, y):.2f}")

The shaded areas show which class each model would pick at every spot. The single neuron's border is a straight line, so it gets points near the tips of the moons wrong. The MLP's border bends to follow the moons, and its training accuracy is higher. Training accuracy is measured on the same points the model learned from. Later lessons test on new data.

Counting parameters

A network's parameters are all the numbers it learns: every weight and every bias. You can count them layer by layer. Each output neuron has one weight per input, plus its own bias.

parameters in a layer=(inputs+1)×outputs\text{parameters in a layer} = (\text{inputs} + 1) \times \text{outputs}

In plain words: each neuron in the layer needs one number per input, plus one for its bias. Multiply that by how many neurons the layer has.

Try a small network: 2 inputs, 3 hidden neurons, 1 output. The first layer has (2 + 1) × 3 = 9 parameters. The second has (3 + 1) × 1 = 4. The total is 13.

The code does the same count for a classic digit reader. Its images are 28 × 28 pixels, which makes 784 inputs. Then come 128 hidden neurons and 10 outputs, one per digit. Change sizes to [2, 3, 1] and it prints 13.

sizes = [784, 128, 10]          # a classic digit classifier: 28×28 pixels → 128 hidden → 10 digits
total = 0
for n_in, n_out in zip(sizes, sizes[1:]):
    layer = (n_in + 1) * n_out
    total += layer
    print(f"{n_in:>4} → {n_out:<4} weights {n_in * n_out:>7,} + biases {n_out:>4} = {layer:>7,}")
print(f"total: {total:,} parameters")

The first layer has 100,480 parameters and the second has 1,290. That's 101,770 in all. zip(sizes, sizes[1:]) pairs each layer's input count with its output count.

Quick check
What does this print?
inputs, outputs = 3, 2
print(inputs * outputs + outputs)
  1. 6
  2. 9
  3. 5
  4. 8
Show the answer

8

A layer has one weight per input for each neuron, plus one bias per neuron: 6 + 2 = 8.

Quick check
How many parameters does a layer with 50 inputs and 20 outputs have?
  1. 1,020
  2. 1,000
  3. 70
  4. 2,500
Show the answer

1,020

50 × 20 = 1,000 weights, plus 20 biases. Or use the rule: (50 + 1) × 20 = 1,020.

How big real networks get

For scale, GPT-2 small, which is a 2019 language model from OpenAI, has about 124 million parameters. The biggest LLMs today have hundreds of billions.

Each transformer block in an LLM contains an MLP. It typically widens the data to 4 times the model's width, then narrows it back. In that setup, those two weight matrices hold about two-thirds of the block's parameters.

Go deeper Where the two-thirds comes from

Take GPT-2 small, whose width is 768. Its MLP goes from 768 to 3,072 (4 times wider) and back. Each of its two weight matrices has 768 × 3,072 weights, about 2.36 million. Together that's about 4.72 million.

The rest of the block is mostly attention, which you'll meet in Phase 5. Attention has four matrices of 768 × 768, about 2.36 million weights in all.

So the MLP holds about 4.72 out of 7.08 million weights, which is two-thirds. This ignores the biases and a few small extras.

Width, depth and what MLPs can do

A network's width is how many neurons a layer has. Its depth is how many layers it has.

The universal approximation theorem is a famous result about width. It says a network with one hidden layer can get as close as you like to any continuous function, given enough neurons. A continuous function is a rule whose output has no sudden jumps.

Here's a simplified picture of why. Each ReLU neuron adds one bend to the output. With enough bends, you can trace any smooth curve, like drawing a circle with many short straight strokes.

"Enough" can be far too many to be practical, though. Depth helps. Each layer builds on the features of the one before, the way letters make words and words make sentences. A deep network can often represent a complicated function with far fewer neurons than a wide, shallow one.

The theorem promises that a good network exists. It doesn't promise that training will find it. The rest of this phase is about finding it.

Quick check
A network with one hidden layer of 3 neurons can't fit your data well. What does the universal approximation theorem tell you?
  1. Any network with one hidden layer will fit any data after enough training
  2. A one-hidden-layer network with enough neurons could fit it, but it may need very many, and training still has to find the weights
  3. The data must contain errors, because 3 neurons can fit any function
  4. You must add more layers, because one hidden layer can never fit curved data
Show the answer

A one-hidden-layer network with enough neurons could fit it, but it may need very many, and training still has to find the weights

The theorem says a wide enough network exists. It says nothing about how many neurons that takes or whether training will find the right weights.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

What does a hidden layer do, in one sentence?

  1. It transforms the inputs into new features that make the final decision easier
  2. It hides the training data from the model, so the model can't memorize it
  3. It stores the labels, so later layers can compare their output against them
  4. It removes the need for activations, since extra layers add the bends instead

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson