6.5.2 · D2Research Frontiers & Practice

Visual walkthrough — Implementing models from scratch

2,676 words12 min readBack to topic

We will need the ideas of a neuron layer, the chain rule, matrix multiplication, and backpropagation — but we will re-derive each piece as we go.


Step 1 — One neuron, one number

WHAT. Forget matrices. Start with the smallest possible piece: one input number , one weight , one bias , giving one output number

WHY. Every big formula in the parent note is this line copied many times. If we understand how the loss (a single number measuring "how wrong we are") depends on and here, the matrix version is just bookkeeping.

PICTURE. Below, the input enters on the left, gets multiplied by , has added, and out comes . Think of as the slope of a straight line and as where the line crosses zero.

Figure — Implementing models from scratch

Step 2 — The single question backprop answers

WHAT. We want to nudge and to make smaller. To nudge intelligently we must know: "If I increase by a tiny amount, does go up or down, and how fast?" That number is the derivative .

WHY a derivative and not something else? Because "how fast does change as changes" is exactly the definition of a derivative — the slope of the -vs- curve at our current spot. No other tool answers "which way is downhill" locally.

PICTURE. Plot against . At our current , draw the tangent line. Its slope is . If the slope is positive, increasing raises — so we must go the other way. That sign is everything.

Figure — Implementing models from scratch

Step 3 — The activation, and how the error is actually born

WHAT. Between the neuron's raw output and the loss sits one more stage we must name: the activation function . It takes and bends it into , the neuron's final "firing strength". A common choice is . So the real chain is three links long:

WHY do we need at all? Without it, stacking layers collapses to one big linear map (the parent note's argument). injects the bend that lets networks learn curves. And because sees only through , the error signal at must pass through 's slope first.

PICTURE. Three boxes in a row: . The error travels right-to-left; at the box it gets multiplied by , the local steepness of the bend.

Figure — Implementing models from scratch

Now compute by the chain rule, one link at a time. Suppose the next stage hands us (how loss reacts to this neuron's output). Then:

WHY multiply by ? only reaches through . If the bend is flat there (), wiggling does nothing to , hence nothing to — so . This is the missing piece: is not handed down from heaven; it is passed through the activation's slope.


Step 4 — Chain rule: from down to and

WHAT. Now that is computed, reaches only through , so one more chain-rule link finishes the job:

WHY multiply? If wiggling moves by 3 units, and each unit of moves by units, then moves by . Chained effects multiply — this is the core reason the whole algorithm is called backpropagation: we carry one number () backward and keep multiplying.

PICTURE. Two gears in a chain. Turn the -gear; it drives the -gear; that (through ) drives . Total gear ratio = product of the two.

Figure — Implementing models from scratch

From , differentiate treating everything else as constant:

WHY these values? Bump by a tiny : changes by , so the rate is . Bump by : changes by exactly , so the rate is .

Plug in:


Step 5 — Widen it: many inputs, many outputs (the matrix appears)

WHAT. Real layers have many inputs and many outputs . Each output is Step-1's line, but summing over all inputs: Here is the weight from input to output — a whole grid of numbers, the matrix . The bias becomes a column vector — one bias per output.

WHY a grid? Every (output, input) pair needs its own knob, and there are such pairs. Stacking them makes — the compact form.

PICTURE. A bipartite graph: input dots on the left, output dots on the right, one arrow per weight . The grid of arrows is the matrix.

Figure — Implementing models from scratch

Now Step 4's rule, applied to entry , becomes (the error on output times the input that fed it). Writing every such product at once is exactly an outer product:


Step 6 — Sending the error further back: the other transpose

WHAT. To keep going backward we need — how the loss depends on each input, so the previous layer can use it (through its own ) as its . Input feeds every output (look again at the fan of arrows), so its influences add up:

WHY sum over ? One input pushes on many outputs; the chain rule adds all those paths. That sum is exactly row of dotted with :

PICTURE. Reverse every arrow. Forward, sent ; backward, the same weights read the other direction () carry the error .

Figure — Implementing models from scratch

Step 7 — Batches, and where the really lives

WHAT. We rarely feed one sample. Stack samples as columns: , . First we must be honest about what means over a batch. We define the batch loss as the average of the per-sample losses, where is the loss on sample . Because the lives in the definition of , it automatically appears in every gradient — we do not sprinkle it in by hand. With that convention:

\frac{\partial L}{\partial \mathbf{b}} = \frac{1}{B}\sum_{\text{columns}}\boldsymbol{\delta}.$$ **WHY sum the bias, but matrix-multiply the weight?** The outer product $\boldsymbol\delta\mathbf X^T$ *already* sums over the batch (the shared column index does that). The bias has no input to multiply, so every sample contributes its raw $\delta_i$ and we sum the $\delta$ columns directly. Both then carry the same $\tfrac1B$ inherited from $L$'s definition — nothing extra. **PICTURE.** $B$ transparent copies of the layer stacked in depth; the bias gradient is the vertical column-sum of all their error signals, then scaled by $1/B$. ![[deepdives/dd-ai-ml-6.5.02-d2-s07.png]] > [!mistake] Forgetting `keepdims` > Our shape convention (fixed in Step 5) is $\mathbf b\in\mathbb R^{m\times 1}$ — a **column**. > But `np.sum(dZ, axis=1)` collapses to shape $(m,)$ — a flat 1-D array with **no** second axis. > Adding a $(m,)$ update to an $(m,1)$ bias makes NumPy broadcast into an $(m,m)$ grid silently, and > you update the wrong thing. `keepdims=True` preserves the $(m,1)$ column so shapes match. --- ## Step 8 — The degenerate cases (never skip these) **WHAT & WHY.** Every real training run hits edges. Here is what each formula *does* at the edge: | Situation | What happens | Why | |---|---|---| | $x_j = 0$ (dead input) | $\partial L/\partial W_{ij}=\delta_i\cdot 0=0$ | that weight saw no signal, so it gets **no** update — correct. | | $\partial L/\partial a = 0$ (output already right) | $\delta_i=0$, its whole weight-gradient row is $0$ | leave that output alone. | | ReLU with $z_i<0$ | $\sigma'(z_i)=0$, so $\delta_i=\tfrac{\partial L}{\partial a}\cdot 0=0$ | the neuron was "off"; changing $z$ wouldn't have changed $a$. | | $B=1$ (single sample) | the $\tfrac1B$ is just $1$; formulas unchanged | batch is a generalisation, not a new rule. | | all inputs equal, $\alpha$ too big | update overshoots, $L$ rises | Step 2's tangent is only local; step must be small. | **PICTURE.** The ReLU gate: for $z<0$ the "valve" is shut and the backward error $\delta$ is zeroed by $\sigma'=0$; for $z>0$ it passes untouched ($\sigma'=1$). ![[deepdives/dd-ai-ml-6.5.02-d2-s08.png]] > [!intuition] "Dead neuron" made visible > A neuron with $z\le 0$ outputs $0$ *and* blocks its own gradient (since $\sigma'(z)=0$ kills > $\delta$). If it stays negative for all data it never learns again — the famous **dying ReLU**. > The picture is a permanently shut valve. --- ## Step 9 — The update: what we finally do with the gradients **WHAT.** We now have $\partial L/\partial\mathbf W$ and $\partial L/\partial\mathbf b$. Collect **all** the tunable numbers of the layer — every entry of $\mathbf W$ and $\mathbf b$ — into one big bundle we call $\boldsymbol\theta$ (theta), the ==parameters==. Its matching bundle of gradients is written $\nabla_{\boldsymbol\theta}L$ (read "grad L"): same shape as $\boldsymbol\theta$, holding $\partial L/\partial\theta_k$ in each slot. Then take one step *downhill* (Step 2's rule): $$\boldsymbol\theta \;\leftarrow\; \boldsymbol\theta \;-\; \underbrace{\alpha}_{\text{step size}}\;\underbrace{\nabla_{\boldsymbol\theta}L}_{\text{uphill direction}}.$$ **WHY these symbols?** - $\boldsymbol\theta$ ::: just a name for "all weights and biases piled together" so we can write one update instead of two. - $\nabla_{\boldsymbol\theta}L$ ::: the pile of slopes $\partial L/\partial\theta_k$ — the direction of steepest *increase* of $L$. - $\alpha$ ::: the ==learning rate==, a small positive number choosing how far we step. Too big overshoots (Step 8's last row); too small crawls. The **minus sign** turns "steepest uphill" into "step downhill", exactly Step 2's picture generalised to many parameters at once. This is [[4.2.02-Gradient-Descent|gradient descent]]; smarter variants live in [[6.3.04-Optimization-Algorithms|optimization algorithms]]. **PICTURE.** One figure, the full round-trip: forward $\mathbf{x}\xrightarrow{\mathbf W}\mathbf z\xrightarrow{\sigma}\mathbf a\to L$, then backward $\delta=\tfrac{\partial L}{\partial a}\sigma'(z)$ flowing right-to-left, spawning $\partial L/\partial\mathbf W$ and $\partial L/\partial\mathbf b$, then the step $\boldsymbol\theta \leftarrow \boldsymbol\theta - \alpha\nabla_{\boldsymbol\theta}L$. ![[deepdives/dd-ai-ml-6.5.02-d2-s09.png]] > [!recall]- Feynman retelling — say it like a story > A number $x$ walks into a machine, gets multiplied by a weight $w$ and shifted by a bias $b$ — out > pops $z$. An activation $\sigma$ then bends $z$ into $a$, and only through $a$ does the loss $L$ > see the neuron. To learn, the far end hands back $\partial L/\partial a$; we pass it through the > bend's slope $\sigma'(z)$ to get the error at $z$, called $\delta = \partial L/\partial a\cdot\sigma'(z)$. > To update a weight we ask the chain rule again: the answer is $\delta$ (the error) times $x$ (the > input that weight saw) — big input, big update; zero input, zero update. Stack all those products > and you get $\boldsymbol\delta\mathbf X^T$; the transpose is just the shape that makes > column-times-row into the weight grid. The bias (a column $\mathbf b\in\mathbb R^{m\times 1}$) has > no input to multiply, so it simply **sums** the errors across the batch. To keep the story going > into the previous layer, we run the *same* weights in reverse — that is $\mathbf W^T\boldsymbol\delta$. > Finally we pile all weights and biases into $\boldsymbol\theta$ and step each a little bit > *downhill* — opposite $\nabla_{\boldsymbol\theta}L$, by an amount $\alpha$. Because $L$ is defined > as the *average* over the batch, the $1/B$ is already baked in. Repeat millions of times and the > machine learns. No magic — pass the error through the bend, multiply the gear ratios, and always > walk downhill. > [!recall]- Quick self-test > How is the error $\delta$ at $z$ computed from above? ::: $\delta = \dfrac{\partial L}{\partial a}\cdot\sigma'(z)$ — the incoming gradient passed through the activation's slope. > Why is the weight gradient an outer product and not element-wise? ::: Because each weight $W_{ij}$ needs its own $\delta_i x_j$, and column $\boldsymbol\delta$ times row $\mathbf x^T$ produces exactly that grid. > Why does the backward pass use $\mathbf W^T$? ::: Forward, outputs sum over inputs (rows); backward, inputs sum over outputs (columns = rows of $\mathbf W^T$). > Where does the $1/B$ come from? ::: From defining the batch loss $L$ as the average of per-sample losses; it is inherited by every gradient automatically. > What are $\boldsymbol\theta$, $\alpha$, and $\nabla_{\boldsymbol\theta}L$ in the update? ::: All parameters piled together, the learning-rate step size, and the pile of slopes $\partial L/\partial\theta_k$.