Brian WilcoxZero → HeroNeural Networks: 0 → Hero
Neural Networks · Backprop · From Scratch

Build a brain
from scratch.

A neural network is not magic. It is a big differentiable function with thousands of tunable knobs, plus an algorithm that nudges every knob in the direction that makes the function less wrong.

This guide builds the idea one rung at a time (intuition first, then the math, then code you could actually run) and lets you watch the two core mechanisms move: the forward pass that makes predictions, and backpropagation that teaches.

FORWARD  data → prediction BACKWARD  error → every knob
12
rungs
0 → hero
10
live demos
real code, in-browser
~40
lines
autograd from scratch
0
dependencies
vanilla JS
00

Orientation: what a network really is

the mental model everything hangs on

Strip away the biology metaphors. As an engineer, hold these three facts and you can derive the rest.

1. It's a function. A network takes an input vector x and returns an output y_hat. Internally it's just multiplications, additions, and a sprinkle of simple nonlinear functions. Nothing you couldn't write in an afternoon.

2. It has parameters. Scattered through that function are numbers: the weights and biases. These are the knobs. "Training" means finding knob settings that make y_hat match reality on examples we've seen.

3. It's differentiable. Because every operation inside is smooth, we can ask: "if I nudge this one knob up a hair, does the error go up or down, and how fast?" That answer is a gradient. Collect the gradient for every knob and you know exactly which way to step. Repeat a few thousand times and the function fits the data.

The one-sentence version

Learning = repeatedly measuring how wrong you are, then adjusting every parameter slightly in the direction that reduces the wrongness. Forward pass measures. Backward pass assigns blame. Update steps.

The rest of this guide is just zooming into each of those words (neuron, activation, loss, gradient, backprop) until you've built the machine yourself and watched it learn.

01

The neuron: the atom

weighted sum, a bias, a squish

A single neuron does three tiny things, in order: it weights its inputs, sums them with a bias, and passes the result through a nonlinear "activation."

one neuron, n inputs z = w₁·x₁ + w₂·x₂ + … + wₙ·xₙ + b
a = σ(z)  # σ is the activation (tanh, ReLU, sigmoid…)

Think of each weight w as a volume knob on one input: how much should this feature matter, and with what sign? The bias b shifts the whole sum: it lets the neuron fire even when inputs are zero, or stay quiet until inputs are large. The activation bends the straight line into a curve (rung 02 explains why that matters).

Intuition: what the weighted sum measures

The sum w·x is a dot product: it measures how much the input x points in the same direction as the weight vector w. Big and positive when x looks like w's preferred pattern, negative when it's the opposite. So a neuron is really asking one question: "how much does this input resemble the template I've learned?" The bias just moves the threshold for answering "a lot."

The two views below are the same neuron. Left: the activation curve, where your current z lands on σ. Right: the input plane, shaded by output, with the neuron's decision line (z = 0) and the weight vector drawn as an arrow. Move the weights and watch the line rotate to face the w direction; move the bias and watch it slide.

Live neuron

Two inputs, one neuron, shown two ways. The line on the right is the neuron's decision boundary; the arrow is the weight vector it points along.

z lands on the σ curve → output a
high a low aline: z=0 · arrow: w
02

Why nonlinearity is the whole point

without a squish, depth buys you nothing

Stack two linear layers and you get one linear layer. The activation function is what makes depth meaningful.

two stacked linear layers, no activation h = W₁x + b₁
y = W₂h + b₂ = W₂(W₁x + b₁) + b₂ = (W₂W₁)x + (W₂b₁ + b₂)
# that's just one linear layer with weights W₂W₁ and bias W₂b₁+b₂

No matter how many linear layers you chain, the composition is still a single straight-line transform. It can only ever draw a flat decision boundary. Real data (spirals, clusters, the curve of a sine) is not separable by a line.

Insert a nonlinearity between layers and the spell breaks. Each layer can now bend space, and bends stacked on bends produce arbitrarily complex shapes. This is the informal content of the universal approximation theorem: a network with one hidden layer and a nonlinearity can approximate essentially any continuous function, given enough neurons.

Intuition

Weights and biases move and stretch space (linear). The activation folds it (nonlinear). Learning is finding the right sequence of stretches and folds so the two classes end up on opposite sides of a simple cut.

The usual activations

nameformulafeel
ReLUmax(0, z)cheap, fast, the modern default for hidden layers
tanh(eᶻ−e⁻ᶻ)/(eᶻ+e⁻ᶻ)smooth, zero-centered, output in (−1, 1)
sigmoid1/(1+e⁻ᶻ)output in (0, 1), good for a final probability

You already saw all three live in rung 01: flip the dropdown there and notice ReLU's hard hinge versus tanh's gentle S. That shape is the only "intelligence" a single neuron has; everything else is composition.

Here's the theorem made visible. Each neuron in a hidden layer produces one bump or hinge; adding them up builds a custom curve. Below, a one-hidden-layer network fits a wiggly target. Watch the faint individual neuron outputs (the building blocks) combine into the bold curve that chases the target. Add neurons and the fit gets sharper: that's "given enough neurons, approximate anything."

Universal approximation: building a curve from bumps

Faint cyan = individual neuron outputs aᵢ·σ(wᵢx+bᵢ). Bold cyan = their sum (the network). Dashed amber = the target. Press Fit to train it by gradient descent.

03

Layers & the forward pass

neurons in parallel, written as one matrix multiply

A layer is just many neurons looking at the same inputs in parallel. Stack the weights into a matrix and the whole layer becomes a single matrix multiply, which is exactly why GPUs make this fast.

a layer with m neurons, n inputs z = W·x + b  # W is (m×n), x is (n,), b is (m,)
a = σ(z)      # applied element-wise, output is (m,)

A multilayer perceptron (MLP) chains these: the activations of one layer are the inputs to the next. Running data left-to-right through every layer to produce a prediction is the forward pass.

forward.pynumpy
import numpy as np

def relu(z): return np.maximum(0, z)

def forward(x, params):
    # params is a list of (W, b) tuples, one per layer
    a = x
    for i, (W, b) in enumerate(params):
        z = W @ a + b              # weighted sum (the matrix multiply)
        a = relu(z) if i < len(params)-1 else z  # last layer: no squish
    return a                          # the prediction y_hat
Why it's a matrix multiply

Row i of W holds the weights of neuron i. The dot product of that row with x is that neuron's weighted sum. Doing all rows at once is the matrix–vector product. Batch many examples together and it becomes matrix–matrix, perfectly suited to parallel hardware.

Step through it below. Each highlighted row of W is one neuron; its dot product with the input x (plus that neuron's bias) produces one entry of the output z. Four neurons, four rows, four outputs: all computed in one multiply.

One matrix multiply = a whole layer of neurons

W · x + b = z.   The lit row is the current neuron; its dot product with x lands in the matching slot of z.

04

Measuring wrong: the loss

collapse all the error into one number

To improve, the network needs a single scalar that says "how bad was that prediction?" Smaller is better. That scalar is the loss. Training is literally the search for parameters that minimize it.

Regression → mean squared error

predicting a number L = (1/N) · Σ (ŷᵢ − yᵢ)²  # average squared gap, punishes big misses hard

Why squared? The error ŷ − y is a length; squaring it is literally the area of a square with that side. Double the miss and the penalty quadruples, so MSE cares far more about fixing big mistakes than small ones. Drag the prediction and watch the square (the loss) grow.

MSE: the loss is the area of the error square

Move the prediction ŷ toward or away from the target y. Left: the error as a literal square. Right: the loss curve (ŷ−y)², a parabola; your current spot is the dot.

Classification → cross-entropy

predicting a probability (binary) L = −(1/N) · Σ [ yᵢ·log(ŷᵢ) + (1−yᵢ)·log(1−ŷᵢ) ]
# near 0 when confident & right; explodes when confident & wrong

For one example the formula collapses to a single term: the loss is just −log(p), where p is the probability the model assigned to the correct answer. Get the truth confidently right (p→1) and loss vanishes. Be confidently wrong (p→0) and −log(p) rockets toward infinity. That asymmetry is the whole point: it makes overconfident mistakes very expensive.

Cross-entropy: the cost of being wrong, by confidence

Slide the probability your model assigned to the true class. The curve is −log(p): gentle near 1, a cliff near 0.

The loss is the only thing the network is trying to reduce. Choosing it is a design decision with real consequences: it defines what "good" means. MSE says "be close on average." Cross-entropy says "be calibrated and don't be confidently wrong."

Key shift in perspective

Once a loss is fixed, the network's parameters are the only variables left. The loss becomes a function of the weights: L(W, b). Picture a vast hilly landscape where every point is one setting of all the knobs, and altitude is the loss. Training = finding a valley.

05

Going downhill: gradient descent

the gradient points uphill, so step the other way

We have a loss landscape and we want the lowest valley. We can't see the whole map, but at our current spot we can compute the slope. The gradient is the vector of slopes, one per parameter, pointing in the direction of steepest increase. So we step the opposite way.

the update rule: this is gradient descent, in full w  ←  w  −  η · ∂L/∂w
# η (eta) = learning rate, the step size. ∂L/∂w = slope of loss w.r.t. this weight

Read it as: "to update a weight, subtract a small multiple of its gradient." If nudging w up makes the loss go up (positive gradient), we move w down. If up makes loss go down (negative gradient), the minus sign moves w up. Either way, toward lower loss.

From one slope to a million

The picture below is 1-D, but a real network has thousands of weights. The gradient is just all those individual slopes bundled into one vector: one partial derivative ∂L/∂wᵢ per weight. Geometrically it's an arrow on the loss surface pointing straight uphill; −gradient is the single steepest-downhill direction. The update rule fires on every weight at once, each nudged by its own slope.

The learning rate η is the single most important dial you'll touch. Too small and learning crawls. Too large and you overshoot the valley and bounce, or diverge entirely. Drag the slider below and watch the ball.

Gradient descent on a loss curve

The ball reads the local slope and steps downhill by −η · slope. Try a tiny rate, then crank it past the point of overshoot.

What you just saw

On a convex bowl, descent always finds the bottom. On a bumpy surface it can settle into a local minimum: a valley that isn't the deepest. Real loss landscapes have millions of dimensions, where (happily) good-enough valleys are abundant. This is why a humble step-downhill rule scales to billion-parameter models.

One question remains, and it's the crux of the whole field: the network has thousands of weights buried across many layers. How do we get ∂L/∂w for every single one, efficiently? That's backpropagation.

06

The chain rule: backpropagation

the idea that makes deep learning possible

Backprop is calculus's chain rule, applied with bookkeeping. The forward pass builds an expression out of small operations. Backprop walks that expression in reverse, handing each operation its share of the blame for the final loss.

Here is the only calculus you need:

the chain rule if  L depends on a,  and a depends on z,  then
∂L/∂z = ∂L/∂a · ∂a/∂z
# "blame flowing into z" = "blame at a" × "how z locally affects a"

Every operation knows its own local derivative (a multiply's, an add's, a tanh's, each is a one-liner). Backprop just multiplies these local derivatives along every path from the loss back to each weight. Each node receives gradient from its output, multiplies by its local derivative, and passes the result to its inputs. A single backward sweep computes ∂L/∂w for all weights at once.

The local rules (memorize these four)

+  splits evenly: passes gradient through unchanged to both inputs. (Each input affected the sum one-for-one.)
×  swaps & scales: sends each input the gradient times the other input. (If a·b, then nudging a moves the product by b.)
tanh  throttles: multiplies incoming gradient by (1 − tanh²), near zero when the neuron is saturated, so a maxed-out neuron barely learns.
 amplifies with size: multiplies incoming gradient by 2x: the further off you are, the stronger the correction.

Below is a real computational graph for one neuron feeding a squared-error loss. Press Forward to compute every value left-to-right (cyan). Then press Backward to flow gradients right-to-left (amber), one step at a time, with the chain rule spelled out. Tweak the inputs and run it again.

The backprop scope: one neuron + loss

Expression:  a = tanh(w₁x₁ + w₂x₂ + b),   loss L = (a − y)².   Top number in each node = value, bottom = gradient ∂L/∂node.

Press Forward to compute values, then Backward to flow gradients. Or jump to the end with Full pass.
value (forward) gradient (backward) not yet computed

Notice the payoff: the gradient on w₁ tells you exactly how to change w₁ to reduce the loss, and you got it for free, just by reversing the same graph you already built going forward. That's the trick that scales to GPT.

07

Autograd from scratch

a tiny engine that does backprop for any expression

You don't want to derive gradients by hand for every new model. Instead, build an engine that records operations as they happen and knows how to reverse each one. This is "automatic differentiation": the beating heart of PyTorch and JAX, and you can write a usable version in ~40 lines.

The trick: wrap every number in a Value object that remembers (a) its data, (b) which values produced it, and (c) a closure that knows how to push gradient to those parents. Calling .backward() on the final loss walks the graph in reverse topological order and fills in .grad everywhere.

micrograd.py: the enginepure python
import math

class Value:
    def __init__(self, data, _children=()):
        self.data = data
        self.grad = 0.0            # ∂L/∂self, filled in by backward()
        self._backward = lambda: None  # local rule: push grad to parents
        self._prev = set(_children)

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data + other.data, (self, other))
        def _backward():
            self.grad  += 1.0 * out.grad   # + passes grad through
            other.grad += 1.0 * out.grad
        out._backward = _backward
        return out

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data * other.data, (self, other))
        def _backward():
            self.grad  += other.data * out.grad  # × sends the OTHER input
            other.grad += self.data  * out.grad
        out._backward = _backward
        return out

    def tanh(self):
        t = math.tanh(self.data)
        out = Value(t, (self,))
        def _backward():
            self.grad += (1 - t*t) * out.grad   # d/dz tanh = 1 - tanh²
        out._backward = _backward
        return out

    def backward(self):
        # build reverse topological order, then apply each local rule
        topo, seen = [], set()
        def build(v):
            if v not in seen:
                seen.add(v)
                for p in v._prev: build(p)
                topo.append(v)
        build(self)
        self.grad = 1.0              # ∂L/∂L = 1, the seed
        for v in reversed(topo):
            v._backward()

That's the entire idea. With __add__, __mul__, and tanh, you can build the neuron from rung 06: a = (w1*x1 + w2*x2 + b).tanh(), call loss.backward(), and read w1.grad directly. The same values the scope visualized, computed by code.

Watch the engine run. The expression L = tanh(w·x + b) uses exactly the three ops above. Record plays the forward pass: each operation builds a new Value, computes its .data, and pushes itself onto the tape (the topological order). Replay runs backward(): it seeds ∂L/∂L = 1 and walks the tape in reverse, firing each node's _backward closure to hand gradient to its parents. Each step echoes the exact closure from the code above as it fires.

The engine's two phases: record the tape, then replay it backward

Record appends each Value to the tape as it is created. Replay walks the tape in reverse, firing each _backward(). This is the code on the left, animated.

Press Record ▸ to run the forward pass and build the tape, then Replay ◂ to run backward().
tape (topological order):
forward: value recorded backward: grad filled not yet reached
Engineer's note

Real frameworks add more ops (exp, log, matmul), run on tensors instead of scalars, and execute on GPUs, but the architecture is exactly this: record a graph on the forward pass, replay it backward applying local derivatives. If you understand these 40 lines, you understand autograd.

08

The training loop

four lines, repeated until it learns

Everything assembled. The training loop is shorter than its explanation, and once you've seen it, you'll recognize it inside every deep-learning codebase on earth.

train.py: the canonical loopthe whole job
for step in range(epochs):
    y_hat = model(x)              # 1. FORWARD: predict
    loss  = loss_fn(y_hat, y)     # 2. SCORE: how wrong?

    for p in model.parameters():
        p.grad = 0.0             # 3a. reset grads (they accumulate!)
    loss.backward()               # 3b. BACKWARD: fill every .grad

    for p in model.parameters():
        p.data -= lr * p.grad     # 4. UPDATE: step every knob downhill
stepwhat happenswhich rung
forwardrun inputs through the layers to get predictions03
scorecollapse error into one loss number04
backwardchain rule fills ∂L/∂w for every weight06–07
updatenudge each weight: w −= lr · grad05
The one bug everyone hits once

Forgetting step 3a. Gradients accumulate by default (the += in the autograd engine), so if you don't zero them each step, you're summing the slopes of every past iteration and your model will thrash. optimizer.zero_grad() in PyTorch is this exact line.

Here is that loop turning. Each pass lights the four stages in order while a genuine tiny network (1 → 6 → 1, fitting a curve) takes one real gradient step. The loss on the right ticks down as the cycles accumulate.

The loop, turning: four stages driving a real network

Each cycle runs one full training step on a live 1 → 6 → 1 net. The stage that is lit is executing now; the loss curve is that net getting less wrong.

1FORWARDpredict ŷ
2SCOREloss = wrongness
3BACKWARDfill every ∂L/∂w
4UPDATEw −= lr·grad
target network
loss over cycles

That loop is what's running, thousands of times a second, in the live demo on the next rung. Go watch it actually learn.

09

A real MLP, learning live

everything above, running in front of you

This is the hero moment. Below is a genuine multilayer perceptron (forward pass, cross-entropy loss, backprop, gradient-descent update, exactly the loop from rung 08) training in your browser on a 2D classification problem. The background shading is the network's current decision surface; watch it bend itself around the data as the weights learn.

Pick a dataset that isn't linearly separable (spiral is the cruel one). Hit Train. Then experiment: drop the hidden layers to a tiny width and watch it fail to find the shape; crank the learning rate too high and watch the loss explode; switch ReLU vs tanh and feel the difference. Every mechanism in this guide is running here at once.

Live classifier: a 2-layer MLP trained by gradient descent

Shaded background = predicted class probability (cyan vs amber). Dots = training data. The boundary is where the network is unsure (50/50).

class 0 class 1 50% boundary
loss over time
What to take away

Architecture (width, depth, activation) sets the shapes the network can express; the learning rate sets whether it can find them without falling over. There's no separate "AI" ingredient; it is stretches, folds, a loss, and downhill steps. You just watched it solve a problem no straight line can.

10

Practitioner's reality

the things that decide whether it actually works

The math is simple; getting it to train well is craft. The knobs below are where most of your real time will go.

Weight initialization

Start all weights at zero and every neuron in a layer computes the same thing and receives the same gradient: they never differentiate. Start them too large and activations saturate (tanh flatlines, gradients vanish). The fix is principled random init scaled to layer size: He initialization for ReLU, Xavier/Glorot for tanh. The live demo uses these; that's why it trains instead of stalling.

Learning rate & schedules

Still the highest-leverage dial. In practice you don't keep it fixed: you warm up from small, then decay over training. Adaptive optimizers like Adam give each parameter its own effective rate using running averages of its gradients, usually the sane default over plain gradient descent.

Batches & stochasticity

Computing the loss over the entire dataset every step is expensive and, surprisingly, worse. Mini-batches (32–512 examples) give a noisy gradient estimate, and that noise helps escape bad valleys. One pass over all the data is an epoch.

Both paths below are real gradient descent on the same real dataset (a small linear-regression problem, so the loss over the two weights is an exact bowl you can see). Full-batch averages the gradient over every point: a smooth, direct glide to the minimum. A small mini-batch estimates the gradient from a handful of points, so its path jitters. Shrink the batch and watch the noise grow.

Full-batch vs mini-batch: two paths down the same bowl

Contours = the true loss surface L(w₁, w₂) over the whole dataset. Cyan = full-batch descent (smooth). Amber = mini-batch descent (noisy). Same start, same learning rate.

Overfitting & regularization

A big network can memorize the training set and fail on anything new. You catch this by watching a held-out validation set diverge from training loss. Defenses: more data, dropout (randomly silence neurons during training), weight decay (penalize large weights), and early stopping.

See it happen. The widget trains a real MLP (1 → H → 1) on a handful of noisy points sampled from a smooth curve, and tracks its loss on both the training points and a held-out validation set. Early on both fall together. Then the training loss keeps dropping toward zero while the validation loss bottoms out and climbs: the network is memorizing noise. Widen the capacity or crank the noise to make the gap yawn; narrow it and the gap closes.

Overfitting, live: training loss dives while validation loss climbs

Left: the learned curve against train (filled) and validation (hollow) points. Right: train loss vs validation loss. The moment amber turns up is where you should have stopped.

target● train○ validation
train loss validation loss

Vanishing & exploding gradients

In deep stacks, repeated multiplication of small local derivatives can drive gradients to zero (nothing learns) or blow them up (everything diverges). The historical answers, ReLU, careful init, normalization layers (BatchNorm, LayerNorm), and residual connections (let signal skip layers), are precisely what made very deep networks trainable.

You can see it. Below, a signal flows forward through 12 layers and a gradient flows backward through them. The bars are the magnitude (RMS) at each layer. Slide the weight scale, or press Sweep to watch it pass through every regime: too small and both the signal and the gradient decay to nothing (vanishing); too large and they blow up (exploding). The narrow healthy band in the middle is exactly what He/Xavier init computes for you automatically.

Why initialization decides whether a deep net learns

Cyan = forward signal magnitude per layer. Amber = backward gradient magnitude per layer. Find the scale where both stay flat across all 12 layers.

Debugging heuristic

When training won't converge, check in this order: (1) can the model overfit a tiny batch to ~zero loss? If not, there's a bug in forward/backward. (2) Is the learning rate sane? (3) Is the data normalized? Most "the model won't learn" bugs are one of these three, not the architecture.

11

To hero: the road ahead

where the same four steps take you next

Everything from here is the same skeleton (forward, loss, backward, update) with smarter structure for the forward pass. Each advance is essentially a clever answer to "what should the layers compute?"

The progression from MLP to frontier models

  • CNNs
    Convolutional networks

    Share weights across space so the model learns position-independent features. The breakthrough for images.

  • RNNs
    Recurrent networks & LSTMs

    Add a loop and a memory cell to process sequences: the first serious attack on language and time series.

  • ATTN
    Attention & Transformers

    Let every token look at every other token directly. Drop recurrence, parallelize massively. This is what powers modern LLMs.

  • SCALE
    Pretraining & scaling laws

    Same loop, vastly more parameters and data, self-supervised objectives. Capability emerges from scale.

  • ALIGN
    Fine-tuning & RLHF

    Take a pretrained base and shape its behavior with supervised examples and preference learning.

How to actually become a hero

Build the scalar autograd from rung 07 yourself, by hand, no copy-paste. Then make it train the MLP from rung 09 on real data. Re-derive backprop for a 2-layer net on paper once; it cements the chain rule permanently. Then port it to PyTorch and feel how loss.backward() is the exact engine you built. Finally, implement attention from scratch; it's smaller than you'd guess and it's the gateway to everything current.

Remember the whole guide in one breath

A network is a differentiable function. The forward pass turns inputs into a prediction and a loss. Backprop reverses that graph to find every weight's gradient. Gradient descent steps each weight downhill. Repeat. That is deep learning. The rest is engineering.

More from Zero → Hero

Browse all 15 guides →