1. Why this session exists
Sessions 2.5–2.7 derived the three identities. This one runs them. Take a 2-input, 2-hidden, 1-output network with sigmoid activations, pick a single example, and compute every z, a, δ, ∂L/∂W, ∂L/∂b by hand. Once the numbers come out right, the abstraction is yours.
This is the kind of exercise to do with a calculator and a notebook the first time, then re-check by writing a 15-line PyTorch script that backprops the same example and matches your numbers to 4 decimals.
2. The network
Widths (d0,d1,d2)=(2,2,1). Sigmoid at both the hidden and output layers. Binary cross-entropy loss. One example.
Parameters (chosen for clean arithmetic):
W(1)=(0.100.300.200.40),b(1)=(0.000.00),
W(2)=(0.50−0.60),b(2)=0.10.
One example: x=(1.0,2.0), label y=1.
3. Forward pass
Layer 1 pre-activation:
z(1)=W(1)x+b(1)=(0.10⋅1+0.20⋅20.30⋅1+0.40⋅2)=(0.501.10).
Layer 1 activation:
a(1)=σ(z(1))=(σ(0.50)σ(1.10))≈(0.62250.7503).
(Using σ(0.50)=1/(1+e−0.50)≈0.6225, and similarly σ(1.10)≈0.7503.)
Output pre-activation:
z(2)=W(2)a(1)+b(2)=0.50⋅0.6225+(−0.60)⋅0.7503+0.10≈0.3113−0.4502+0.10=−0.0389.
Output activation (prediction):
y^=σ(z(2))=σ(−0.0389)≈0.4903.
Loss (BCE, label y=1):
L=−logy^≈−log(0.4903)≈0.7126.
4. Backward pass
Output delta (sigmoid + BCE, from 2.5):
δ(2)=y^−y≈0.4903−1=−0.5097.
Layer-2 gradients (a 1×2 row of weights and a scalar bias, from 2.7):
∂W(2)∂L=δ(2)(a(1))⊤≈(−0.5097)⋅(0.6225, 0.7503)≈(−0.3173, −0.3824).
∂b(2)∂L=δ(2)≈−0.5097.
Layer-1 delta (recursion from 2.6):
δ(1)=((W(2))⊤δ(2))⊙σ′(z(1)).
First, (W(2))⊤δ(2):
(W(2))⊤δ(2)=(0.50−0.60)⋅(−0.5097)≈(−0.25490.3058).
And σ′(z(1))=a(1)⊙(1−a(1)):
σ′(z(1))≈(0.6225⋅0.37750.7503⋅0.2497)≈(0.23500.1874).
Hadamard product:
δ(1)≈(−0.2549⋅0.23500.3058⋅0.1874)≈(−0.05990.0573).
Layer-1 gradients:
∂W(1)∂L=δ(1)x⊤≈(−0.05990.0573)⋅(1.0, 2.0)=(−0.05990.0573−0.11980.1146).
∂b(1)∂L=δ(1)≈(−0.05990.0573).
5. The first SGD update
With learning rate α=0.1,
W(1)←W(1)−0.1⋅∂W(1)∂L≈(0.105990.294270.211980.38854).
b(1)←b(1)−0.1⋅δ(1)≈(0.00599−0.00573).
W(2)←W(2)−0.1⋅(−0.3173, −0.3824)≈(0.5317, −0.5618).
b(2)←b(2)−0.1⋅(−0.5097)≈0.1510.
6. Verification in PyTorch
The following PyTorch snippet should reproduce the numbers above to 4 decimals.
import torch
import torch.nn as nn
torch.set_default_dtype(torch.float64)
x = torch.tensor([[1.0, 2.0]])
y = torch.tensor([[1.0]])
W1 = torch.tensor([[0.10, 0.20], [0.30, 0.40]], requires_grad=True)
b1 = torch.tensor([0.0, 0.0], requires_grad=True)
W2 = torch.tensor([[0.50, -0.60]], requires_grad=True)
b2 = torch.tensor([0.10], requires_grad=True)
z1 = x @ W1.T + b1
a1 = torch.sigmoid(z1)
z2 = a1 @ W2.T + b2
y_hat = torch.sigmoid(z2)
L = nn.functional.binary_cross_entropy(y_hat, y)
L.backward()
print("L =", L.item()) # ≈ 0.7126
print("dL/dW2 =", W2.grad) # ≈ [[-0.3173, -0.3824]]
print("dL/db2 =", b2.grad) # ≈ -0.5097
print("dL/dW1 =", W1.grad) # ≈ [[-0.0599, -0.1198], [0.0573, 0.1146]]
print("dL/db1 =", b1.grad) # ≈ [-0.0599, 0.0573]
If your numbers match to four decimals, you've correctly internalised every identity from 2.5–2.7.
7. What this exercise teaches
- Backprop is not magic. It is the chain rule applied carefully.
- Each δ has a clean shape: same as the corresponding z.
- Each ∂L/∂W is an outer product, same shape as the weight.
- Numerical mistakes show up immediately when the PyTorch reference disagrees with your hand-computed values.
8. Takeaways
- Pick a small net, one example, and run the full forward + backward by hand.
- Match every number to a reference implementation.
- After this exercise, the rest of Chapter 2 (vanishing/exploding, init, autodiff) is layered on a foundation you have actually computed.
Self-test(a) Redo the example with y=0 instead of y=1. Predict whether each gradient flips sign before computing, then verify.
(b) Replace the hidden activation with ReLU. Recompute δ(1), noting which entries of g′(z(1)) become 0 or 1, and observe how the gradient sparsifies.
(c) Add a second example x2=(−1.0,0.5), label y2=0, forming a batch of size B=2. Compute the batched gradients and verify they equal 21 of the sum of per-example gradients.
(d) Write the 15-line PyTorch reference and confirm your numbers match. Common pitfalls: zero the gradients between examples, and use float64 to match the manual arithmetic.