Deep Learning

How neural networks learn

A network learns by guessing, measuring how wrong the guess was, and nudging every weight in the direction that would have made it less wrong.

Read these first

On this page 7
  1. Why this had to be invented
  2. How it works
  3. The three words you will keep meeting
  4. Where you have already seen this
  5. What is honestly hard here
  6. Remember this
  7. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

A neural network learns by guessing, checking how wrong it was, and correcting itself a tiny bit — over and over.

You have done this with a bucket bath. You open the hot tap and the cold tap, put your hand in, and the water is too cold. So you open the hot tap a little more. Hand in again — now slightly too hot, so you ease it back.

Nobody calculates tap angles. You feel the gap between what you have and what you want, and you move in that direction. Then you check again.

That loop — guess, measure the gap, adjust, repeat — is the whole of how a neural network learns.

Why this had to be invented

In the previous lesson, every importance knob was typed in by hand. That works for a network with thirteen knobs and a made-up example.

Real networks have millions of knobs. Nobody can hand-tune a million numbers, and nobody knows what the right values would even look like.

So the network needs to find them itself. It needs to answer one question: "which way should I turn each knob?"

How it works

The network needs a number that says how bad its guess was. That number is called the loss: a score where a bigger value means a worse guess. Zero means a perfect guess.

   examples  →  network guesses  →  compare with the real answer
                      ↑                          │
                      │                          ↓
              nudge every knob  ←──────  "you were wrong, this much,
                 a tiny bit               and in this direction"

Follow that loop clockwise and it never ends. One trip around is one correction.

The important part is the arrow coming back. It does not carry only "you were wrong". It carries which direction each knob should move, and how strongly that knob was to blame.

The three words you will keep meeting

  • Loss — how wrong the guess was, as one number. Lower is better.
  • Learning rate — how big a correction to make. Nudge too gently and you are there all week; nudge too hard and you overshoot, like slamming the hot tap fully open.
  • Epoch — one complete pass through every training example. Networks usually need many.

Where you have already seen this

Your phone keyboard learns how you type. The first week it corrects "thoda" to "toda", and you fix it back. Every fix is a correction signal, and after a few weeks it stops fighting you.

Spam filters work the same way. Every time somebody marks a mail as spam, that is a labelled example, and the filter moves a little.

What is honestly hard here

The loop above is easy to picture. Working out how much each individual knob was to blame is genuinely difficult. A knob three layers back — three rows of stages away from the answer — only touches the answer through twenty other knobs.

That blame calculation is called backpropagation, and it has its own lesson. Almost nobody understands it on the first read. If it takes you three attempts, you are having the normal experience, not a slow one.

Remember this

  • Learning is a loop: guess, measure the gap, nudge, repeat.
  • The loss turns "how wrong were you" into a single number to push downward.
  • The network is never told the right weights — only the direction to move them.

What to learn next

Developer — Code and libraries.

Here is the loop with nothing hidden: no framework, no autograd, hand-derived gradients. Once you have watched a loss fall in code you wrote yourself, PyTorch stops feeling like magic.

Setup

bash
pip install numpy

Runs on CPU in under a second. The starting weights are typed by hand, so your numbers will match this page exactly rather than depending on a random seed.

The full training loop

train_from_scratch.py
import numpy as np

# Eight mangoes we already know the answer for. [softness, yellowness], both 0 to 1.
X = np.array([
    [0.9, 0.8], [0.8, 0.9], [0.7, 0.6], [0.6, 0.7],
    [0.2, 0.1], [0.1, 0.3], [0.3, 0.2], [0.2, 0.4],
])
y = np.array([[1.], [1.], [1.], [1.], [0.], [0.], [0.], [0.]])   # 1 = ripe, 0 = unripe

# Starting weights, written by hand so your run prints exactly these numbers.
W1 = np.array([[0.30, -0.20, 0.50],
               [-0.40, 0.10, 0.20]])
b1 = np.array([[0.00, 0.00, 0.00]])
W2 = np.array([[0.20], [-0.30], [0.40]])
b2 = np.array([[0.00]])


def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))


def forward(X):
    h_raw = X @ W1 + b1
    h = np.tanh(h_raw)              # the bend, so the two layers cannot collapse into one
    out = sigmoid(h @ W2 + b2)
    return h, out


def loss_of(pred, y):
    p = np.clip(pred, 1e-9, 1 - 1e-9)   # keeps log() away from zero, which is undefined
    return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))


_, first_guess = forward(X)
print("before training, guesses:", np.round(first_guess.ravel(), 3))
print("before training, loss:   ", round(loss_of(first_guess, y), 4))

lr = 0.5
n = len(X)
for epoch in range(1, 2001):
    h, out = forward(X)

    d_out = (out - y) / n                 # gradient of the loss wrt the pre-sigmoid score
    gW2 = h.T @ d_out
    gb2 = d_out.sum(axis=0, keepdims=True)

    d_h = (d_out @ W2.T) * (1 - h ** 2)   # (1 - tanh^2) is the slope of tanh
    gW1 = X.T @ d_h
    gb1 = d_h.sum(axis=0, keepdims=True)

    W2 -= lr * gW2
    b2 -= lr * gb2
    W1 -= lr * gW1
    b1 -= lr * gb1

    if epoch in (1, 50, 200, 500, 1000, 2000):
        print(f"epoch {epoch:4d}  loss {loss_of(out, y):.4f}")

_, final = forward(X)
print("after training, guesses: ", np.round(final.ravel(), 3))
print("after training, loss:    ", round(loss_of(final, y), 4))

new_mango = np.array([[0.75, 0.65]])
print("unseen mango [0.75, 0.65] ->", round(float(forward(new_mango)[1][0, 0]), 4))
Output
before training, guesses: [0.559 0.551 0.548 0.54  0.515 0.506 0.522 0.513]
before training, loss:    0.6601
epoch    1  loss 0.6601
epoch   50  loss 0.1017
epoch  200  loss 0.0110
epoch  500  loss 0.0034
epoch 1000  loss 0.0015
epoch 2000  loss 0.0007
after training, guesses:  [1.    1.    0.999 0.999 0.    0.    0.001 0.001]
after training, loss:     0.0007
unseen mango [0.75, 0.65] -> 0.9996

Before training every guess sits near 0.5 — the network is shrugging. After two thousand corrections it separates the two groups cleanly, and it also handles a mango it has never seen.

Line by line, the parts that are not obvious

loss_of — this is binary cross-entropy, a loss built for yes/no answers. It punishes confident wrong answers far harder than hesitant ones. Predicting 0.99 when the truth is 0 costs about 4.6; predicting 0.6 costs about 0.92.

np.clip(pred, 1e-9, 1 - 1e-9) — log(0) is undefined and returns -inf in NumPy, which poisons every later number into nan. Clipping keeps the input away from the edge. Every serious loss implementation does some version of this.

d_out = (out - y) / n — this is the whole reason sigmoid and cross-entropy are paired. Their derivatives cancel into prediction - truth, one subtraction. Dividing by n averages the correction over the batch, so the step size does not change when you feed more examples.

(1 - h ** 2) — the derivative of tanh written in terms of its own output. Since h is already computed, the slope costs one multiply. Activation functions are chosen partly for cheap derivatives like this.

W2 -= lr * gW2 — the gradient points uphill, toward a worse loss. Subtracting walks downhill. A += here is a classic bug: the loss climbs steadily and the model gets worse every epoch.

Common mistakes

Loss goes up, not down. Almost always a sign flip — either += instead of -=, or a gradient derived with the wrong sign. Check by running one step with a very small lr and confirming the loss drops a little.

Loss becomes nan after a few epochs. Usually log(0), or a learning rate large enough to send weights to infinity. Print the loss every epoch and find the first nan; the step before it holds the cause.

Forgetting to divide by n. Without it, your effective learning rate scales with batch size. Move from 8 examples to 256 and a working lr suddenly diverges, which is a confusing bug to chase.

Reading this loss as success. A loss of 0.0007 on eight clean, hand-picked examples means almost nothing. There is no held-out test set here, and the two groups are far apart on purpose. On real data, a training loss this low is a warning sign of memorisation — see overfitting-and-underfitting.

Try it yourself

Set lr = 5.0 and rerun. Watch the loss bounce around instead of settling. Then set lr = 0.001 and see it barely move in 2000 epochs. Somewhere between those two lies the useful range, and finding it is a real part of the job.

What to learn next

Researcher — Mathematics and papers.

Learning as optimisation

Training minimises an objective over a parameter vector. Given a dataset $\mathcal{D} = {(x_i, y_i)}_{i=1}^{n}$ drawn i.i.d. from an unknown distribution $P(x, y)$, we want to minimise the true risk:

$$ R(\theta) = \mathbb{E}{(x,y) \sim P}\left[ \ell!\left(f\theta(x),\, y\right) \right] $$

Where:

  • $\theta$ — all trainable parameters.
  • $f_\theta$ — the network as a function.
  • $\ell$ — the per-example loss function.
  • $P$ — the data-generating distribution, which we never observe.

$P$ is unknown, so we minimise the empirical risk instead:

$$ \hat{R}(\theta) = \frac{1}{n} \sum_{i=1}^{n} \ell!\left(f_\theta(x_i),\, y_i\right) $$

This substitution is called empirical risk minimisation (ERM), formalised by Vapnik (1991). The gap $R(\theta) - \hat{R}(\theta)$ is the generalisation gap, and controlling it is what regularisation, early stopping and held-out validation are all aimed at.

The update rule

Parameters move by stochastic gradient descent:

$$ \theta_{t+1} = \theta_t - \eta_t \nabla_\theta \hat{R}_{\mathcal{B}_t}(\theta_t) $$

Where:

  • $t$ — the iteration index.
  • $\eta_t > 0$ — the learning rate at step $t$.
  • $\mathcal{B}_t$ — a mini-batch sampled from $\mathcal{D}$.
  • $\nabla_\theta \hat{R}_{\mathcal{B}_t}$ — the gradient of the loss on that mini-batch.

The mini-batch gradient is an unbiased estimator of the full-batch gradient: $\mathbb{E}{\mathcal{B}}[\nabla \hat{R}{\mathcal{B}}] = \nabla \hat{R}$. Its variance falls as $O(1/|\mathcal{B}|)$, which is the entire trade-off behind batch-size selection.

Cross-entropy, and why it pairs with sigmoid

For binary targets $y \in {0, 1}$ and prediction $\hat{y} = \sigma(z)$ where $\sigma(z) = (1 + e^{-z})^{-1}$:

$$ \ell(\hat{y}, y) = -\left[ y \log \hat{y} + (1 - y) \log(1 - \hat{y}) \right] $$

Differentiating with respect to the pre-activation $z$, and using $\sigma'(z) = \sigma(z)(1 - \sigma(z))$:

$$ \frac{\partial \ell}{\partial z} = \hat{y} - y $$

The $\sigma'(z)$ factor cancels exactly. This matters practically: a squared-error loss on a sigmoid output retains that factor, and $\sigma'(z) \to 0$ when $z$ is large, so a confidently wrong prediction produces a vanishing gradient and the model stalls. Cross-entropy has no such failure mode, which is why it is the default for classification.

In production, compute this from logits with a numerically stable fused implementation — torch.nn.BCEWithLogitsLoss or torch.nn.CrossEntropyLoss — never by applying a softmax or sigmoid and then taking a log.

Convergence, stated honestly

The loss surface of a deep network is non-convex, so there is no guarantee of reaching a global minimum. Practice is better than that pessimism suggests, and the reasons are partly understood:

  • Dauphin et al. (2014), Identifying and attacking the saddle point problem, argue that in high dimensions saddle points, not poor local minima, are the dominant obstacle. A critical point needs every one of its Hessian eigenvalues positive to be a local minimum, which becomes vanishingly unlikely as dimension grows.
  • Choromanska et al. (2015) give evidence that most local minima in large networks reach similar loss values.
  • Zhang et al. (2017), Understanding deep learning requires rethinking generalization, show these networks can fit pure random labels to zero training error. Capacity alone therefore cannot explain generalisation, and this remains an open question.

Practical diagnostics

SymptomLikely causeFirst thing to try
Loss flat from step 1Dead activations, or learning rate far too smallRaise lr by 10x; check for all-zero activations
Loss nanlog(0), overflow, or lr too largeLower lr; use a fused logit loss; clip gradients
Train loss falls, validation risesOverfittingEarly stopping, weight decay, more data
Loss oscillates without settlinglr too large for the curvatureLower lr, or add a decay schedule
  • Rumelhart, Hinton and Williams (1986), Learning representations by back-propagating errors.
  • Bottou, Curtis and Nocedal (2018), Optimization methods for large-scale machine learning — the careful survey of SGD theory.
  • Smith (2017), Cyclical learning rates for training neural networks — the origin of the learning-rate range test.

What to learn next