Neural Networks Explained

Neural Networks: Many Neurons, One Big Function

A note on tone

Like my other notes, this is my own understanding written in plain words. For the full picture, take a proper deep learning course. Think of this as a friendly map, not the textbook.

Series: Neurons, Weights and Bias Explained → Neural Networks (this note) → Activation Functions and GELU → Transformers Explained → Multi-Head Attention Explained.

In the last note we saw that one neuron can only draw a straight line. Real problems (faces, speech, language) are not separated by straight lines. The fix is beautifully simple: use lots of neurons, in layers.

Layers

A neural network is neurons arranged in columns called layers:

In a fully connected layer, every neuron takes input from every neuron in the previous layer.

This little network has 3 inputs, 4 hidden neurons and 2 outputs. Count the dials: the first layer has 3×4 weights plus 4 biases, and the second has 4×2 weights plus 2 biases. That is 26 learnable numbers. GPT-scale models have hundreds of billions, but they are counted the same way.


The forward pass: it is just matrix multiplication

Here is the part that connects to Matrix Multiplication Explained. All the neurons in a layer do their weighted sum at the same time. Stack the weights of each neuron into a row, and the entire layer becomes one matrix multiplication:

h=f(W1x+b1)y=W2h+b2

Look at the shapes in the picture. W1 is 4×3 because there are 4 neurons each looking at 3 inputs. Multiply by a 3×1 input and you get 4×1: one number per hidden neuron. Then W2 (2×4) turns that into the 2 outputs.

import numpy as np

def forward(x, W1, b1, W2, b2):
    h = np.maximum(0, W1 @ x + b1)   # layer 1: multiply, add, bend (ReLU)
    return W2 @ h + b2               # layer 2: multiply, add

W1, b1 = np.random.randn(4, 3), np.zeros(4)
W2, b2 = np.random.randn(2, 4), np.zeros(2)
print(forward(np.array([1.0, 0.5, -1.0]), W1, b1, W2, b2))

And if you feed a whole batch of 1000 examples, the vectors become a 3×1000 matrix and it is still a single multiplication. This is why GPUs are so good at this.


Why the bend matters

Look at the middle of each layer: the little f. That is the activation function, and it is not optional.

Without it, two layers give W2(W1x)=(W2W1)x. That is just one matrix in disguise. A hundred layers would still be equivalent to one layer, and one layer can only draw a straight line. The bend after every layer lets the next layer build something genuinely new on top. Depth only helps because of the bend. The next note goes deep on this: Activation Functions and GELU.

How powerful is it?

The universal approximation theorem (Cybenko 1989, Hornik 1991) says a network with even one hidden layer, if wide enough, can approximate any reasonable continuous function. "Can" is doing a lot of work: it says the network exists, not that training will find it easily. Deeper networks tend to get there with far fewer neurons.


How a network learns

At the start, the weights are random, so the output is garbage. Learning is a loop of four steps:

  1. Forward pass: run the input through and get a guess.
  2. Loss: compare the guess to the right answer, and turn "how wrong" into a single number. For predicting numbers, mean squared error is common. For classes, cross-entropy is.
  3. Backward pass (backpropagation): use the chain rule from calculus to work out how much each weight contributed to the error. This is the blame step. It was popularized by Rumelhart, Hinton and Williams in 1986.
  4. Update: nudge every weight a little in the direction that shrinks the loss:
w←w−η∂L∂w

The η is the learning rate, the size of the nudge. Too big and training jumps around and blows up. Too small and it takes forever. This method is called gradient descent: imagine standing on a hilly landscape in fog and always stepping downhill.

In practice you don't use all the data for each nudge. You use small random batches, and one pass over all the data is an epoch.

A complete tiny example

XOR is the classic problem one neuron cannot solve (no straight line separates it), but a tiny network can:

import torch, torch.nn as nn

X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
y = torch.tensor([[0.], [1.], [1.], [0.]])

model = nn.Sequential(nn.Linear(2, 4), nn.Tanh(), nn.Linear(4, 1), nn.Sigmoid())
opt = torch.optim.Adam(model.parameters(), lr=0.05)
loss_fn = nn.BCELoss()

for epoch in range(500):
    opt.zero_grad()
    loss = loss_fn(model(X), y)   # forward + loss
    loss.backward()               # backpropagation
    opt.step()                    # update the weights

print(model(X).round())           # usually [[0],[1],[1],[0]]
Being honest

Random starting weights mean this occasionally gets stuck. If it prints the wrong answer, run it again. That randomness is a real feature of training, not a bug in the code.


Things that go wrong

The big picture

Key takeaway

A neural network is layers of weighted sums with bends in between: one giant function with millions of adjustable numbers. Running it is matrix multiplication. Training it is repeating guess, measure, blame, nudge until the guesses get good.

Try it yourself

Next: Activation Functions and GELU

Related: Neurons, Weights and Bias Explained, Matrix Multiplication Explained, State Machines Explained