Qm
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.

Discussion

Sign in to join the discussion · reading is open to everyone

💡 Discussion rules

  1. Ask and answer about this concept. Off-topic gets removed.
  2. No homework dumps. Show what you tried first.
  3. Corrections are welcome. Cite a source when you claim an error.

Loading discussion…

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