Quant Memo
Core

Backprop Through a Two-Layer Network by Hand

Walking every number through a small two-layer network's forward and backward pass, with no shortcuts, is the fastest way to trust that backpropagation is just arithmetic — not magic.

Prerequisites: Backpropagation, Computational Graphs and Automatic Differentiation

Reading about backpropagation and actually pushing real numbers through a real network by hand are two different levels of understanding, and only the second one leaves you able to spot a bug in someone else's implementation. This page does exactly that: one small network, two inputs, two hidden neurons, one output, every single number written down, forward and backward, with a check at the end that proves the gradients are correct.

The analogy: a two-stage supply chain

A raw material's price moves. To know how much that move affects the price of a finished product, you cannot look at the raw material and the finished product alone — the effect passes through an intermediate processing stage first. If a 1riseintherawmaterialadds1 rise in the raw material adds 2 to the cost of an intermediate part, and a 1riseinthatintermediatepartscostadds1 rise in that intermediate part's cost adds 3 to the finished product, then the raw material's total effect on the finished product is 2×3=62 \times 3 = 6 dollars, found by multiplying the effect at each stage. A two-layer network has exactly this structure: an input affects a hidden neuron, and that hidden neuron affects the output, and the input's total effect on the output is the product of the two local effects — chained twice instead of once.

The network

Two inputs x1=1.0x_1 = 1.0, x2=0.5x_2 = 0.5. Two hidden neurons, each a weighted sum of the inputs passed through a sigmoid, σ(z)=1/(1+ez)\sigma(z) = 1/(1+e^{-z}):

z1=w11x1+w12x2+b1,a1=σ(z1)z2=w21x1+w22x2+b2,a2=σ(z2)z_1 = w_{11}x_1 + w_{12}x_2 + b_1, \quad a_1 = \sigma(z_1) \qquad\qquad z_2 = w_{21}x_1 + w_{22}x_2 + b_2, \quad a_2 = \sigma(z_2)

with weights w11=0.3, w12=0.2, b1=0.1w_{11}=0.3,\ w_{12}=-0.2,\ b_1=0.1 and w21=0.4, w22=0.1, b2=0.05w_{21}=0.4,\ w_{22}=0.1,\ b_2=-0.05. A single linear output neuron combines the two hidden activations: y^=v1a1+v2a2+c\hat y = v_1 a_1 + v_2 a_2 + c, with v1=0.5v_1 = 0.5, v2=0.3v_2 = -0.3, c=0.2c = 0.2. Target y=1.0y = 1.0, loss L=12(y^y)2L = \frac{1}{2}(\hat y - y)^2.

Worked example 1: the full forward and backward pass

Forward pass, computed and kept for later:

z1=0.3(1.0)0.2(0.5)+0.1=0.300,a1=σ(0.300)=0.5744z_1 = 0.3(1.0) - 0.2(0.5) + 0.1 = 0.300, \qquad a_1 = \sigma(0.300) = 0.5744 z2=0.4(1.0)+0.1(0.5)0.05=0.400,a2=σ(0.400)=0.5987z_2 = 0.4(1.0) + 0.1(0.5) - 0.05 = 0.400, \qquad a_2 = \sigma(0.400) = 0.5987 y^=0.5(0.5744)0.3(0.5987)+0.2=0.28720.1796+0.2=0.3076\hat y = 0.5(0.5744) - 0.3(0.5987) + 0.2 = 0.2872 - 0.1796 + 0.2 = 0.3076 L=12(0.30761)2=12(0.6924)2=0.2397L = \tfrac{1}{2}(0.3076 - 1)^2 = \tfrac{1}{2}(0.6924)^2 = 0.2397

Backward pass. Start at the output, whose local slope is L/y^=y^y=0.30761=0.6924\partial L/\partial \hat y = \hat y - y = 0.3076 - 1 = -0.6924. Because the output layer is linear, that is also δ3=L/z3=0.6924\delta_3 = \partial L/\partial z_3 = -0.6924.

Output weights — each output weight's gradient is δ3\delta_3 times the hidden activation it multiplied:

Lv1=δ3a1=0.6924(0.5744)=0.3977,Lv2=δ3a2=0.6924(0.5987)=0.4145,Lc=δ3=0.6924\frac{\partial L}{\partial v_1} = \delta_3\, a_1 = -0.6924(0.5744) = -0.3977, \qquad \frac{\partial L}{\partial v_2} = \delta_3\, a_2 = -0.6924(0.5987) = -0.4145, \qquad \frac{\partial L}{\partial c} = \delta_3 = -0.6924

Into the hidden layer — each hidden unit's incoming gradient is δ3\delta_3 times the output weight it fed into, times its own sigmoid's local slope a(1a)a(1-a):

δ1=δ3v1a1(1a1)=(0.6924)(0.5)(0.5744)(0.4256)=0.0846\delta_1 = \delta_3\, v_1\, a_1(1-a_1) = (-0.6924)(0.5)(0.5744)(0.4256) = -0.0846 δ2=δ3v2a2(1a2)=(0.6924)(0.3)(0.5987)(0.4013)=0.0499\delta_2 = \delta_3\, v_2\, a_2(1-a_2) = (-0.6924)(-0.3)(0.5987)(0.4013) = 0.0499

Notice δ2\delta_2 came out positive even though δ3\delta_3 is negative, because v2v_2 is negative — the second hidden neuron helps the output by shrinking, and backprop finds that sign flip automatically, exactly as it must.

Hidden weights — each hidden weight's gradient is its unit's δ\delta times the input it multiplied:

Lw11=δ1x1=0.0846,Lw12=δ1x2=0.0423,Lb1=δ1=0.0846\frac{\partial L}{\partial w_{11}} = \delta_1 x_1 = -0.0846, \quad \frac{\partial L}{\partial w_{12}} = \delta_1 x_2 = -0.0423, \quad \frac{\partial L}{\partial b_1} = \delta_1 = -0.0846 Lw21=δ2x1=0.0499,Lw22=δ2x2=0.0250,Lb2=δ2=0.0499\frac{\partial L}{\partial w_{21}} = \delta_2 x_1 = 0.0499, \quad \frac{\partial L}{\partial w_{22}} = \delta_2 x_2 = 0.0250, \quad \frac{\partial L}{\partial b_2} = \delta_2 = 0.0499

Nine numbers, six weights and three biases, all obtained from one forward pass and one backward sweep — never re-running the network from scratch for each one.

x₁=1.0 x₂=0.5 a₁=0.574δ₁=−0.085 a₂=0.599δ₂=0.050 ŷ=0.308δ₃=−0.692 values flow right; δ adjoints flow left, each multiplied by a local slope
Each δ is the gradient arriving at that unit's pre-activation, computed once and reused for every weight feeding into it — that reuse is the entire efficiency gain over checking each weight one at a time.

Worked example 2: verifying two gradients by finite difference

Nudge v1v_1 up by 0.0010.001 to 0.5010.501, holding everything else fixed, and redo only the parts of the forward pass that change: y^=0.501(0.5744)0.3(0.5987)+0.2=0.3082\hat y' = 0.501(0.5744) - 0.3(0.5987) + 0.2 = 0.3082, giving L=12(10.3082)2=0.2393L' = \frac{1}{2}(1 - 0.3082)^2 = 0.2393.

LL0.001=0.23930.23970.001=0.392\frac{L' - L}{0.001} = \frac{0.2393 - 0.2397}{0.001} = -0.392

That is within rounding of the backprop value L/v1=0.3977\partial L/\partial v_1 = -0.3977 computed above — a genuine, independent check, not a restatement of the same calculation. Now nudge w11w_{11} up by 0.0010.001 to 0.3010.301 instead: z1=0.301z_1' = 0.301, a1=σ(0.301)=0.5747a_1' = \sigma(0.301) = 0.5747, y^=0.5(0.5747)0.3(0.5987)+0.2=0.3077\hat y' = 0.5(0.5747) - 0.3(0.5987) + 0.2 = 0.3077, L=12(10.3077)2=0.2396L' = \frac{1}{2}(1-0.3077)^2 = 0.2396.

LL0.001=0.23960.23970.001=0.084\frac{L' - L}{0.001} = \frac{0.2396 - 0.2397}{0.001} = -0.084

Matching backprop's L/w11=0.0846\partial L/\partial w_{11} = -0.0846. Both checks land within finite-difference rounding error of the analytic values, confirming the backward sweep is correct — and this is exactly the gradient-checking routine every hand-rolled implementation should run once before trusting it.

Every weight's gradient is the product of a chain of local slopes running from that weight all the way to the loss. A single backward sweep computes every one of those chains at once by reusing each unit's δ\delta for every weight that feeds into it — that reuse, not any deeper trick, is what makes backprop cheap.

Gradient descent
parameter →
position -0.623loss -0.141gradient -0.930converging

Every one of the nine gradients above feeds into exactly the kind of step the explorer above shows: a small move opposite the gradient's direction, one number at a time. Drag the step size and watch how the same mechanics that update v1v_1 and w11w_{11} by hand here can overshoot or crawl depending on how big a step is taken.

What this means in practice

The one-step gradient descent update at learning rate 0.10.1 moves every parameter a small amount opposite its gradient — v10.50.1(0.3977)=0.5398v_1 \to 0.5 - 0.1(-0.3977) = 0.5398, w110.30.1(0.0846)=0.3085w_{11} \to 0.3 - 0.1(-0.0846) = 0.3085, and so on for all nine. Recomputing the forward pass with the updated weights would show the loss has dropped from 0.23970.2397, confirming the step moved in a genuinely useful direction. Real networks repeat this exact nine-number (or nine-million-number) update, unchanged in kind, thousands of times per training run; nothing about a production-scale network is different in principle from what happened by hand above, only in size.

Practice

  1. Using the deltas already computed, find L/w22\partial L / \partial w_{22} and check your arithmetic against the value given in the worked example.
  2. Suppose v2v_2 had been +0.3+0.3 instead of 0.3-0.3. Redo the sign of δ2\delta_2 only, using the chain δ2=δ3v2a2(1a2)\delta_2 = \delta_3 v_2 a_2(1-a_2), and explain in words what that sign change means for how hidden neuron 2 should move.
  3. Why does the finite-difference check use a small perturbation like 0.0010.001 rather than, say, 1.01.0?

It is easy to think each hidden neuron's gradient depends only on the weight connecting it to the output, but δ1\delta_1 and δ2\delta_2 both also depend on the sigmoid's own local slope, a(1a)a(1-a), which is largest near a=0.5a=0.5 and shrinks toward zero as aa approaches 0 or 1. A hidden neuron whose activation is saturated near 0 or 1 will pass almost no gradient backward no matter how large its output weight is — this is the small-scale version of the vanishing-gradient problem, visible here in a network with only two hidden units.

Related concepts

Practice in interviews

Further reading

  • Rumelhart, Hinton & Williams, Learning Representations by Back-Propagating Errors (1986)
  • Nielsen, Neural Networks and Deep Learning, ch. 2
ShareTwitterLinkedIn