10 · Capstone — Hand-Derive a Small Network¶
This capstone pulls together every Level 3 module — chain rule backprop, loss derivatives, softmax/cross-entropy, and numerical verification — into one fully hand-derived, fully checked network.
The network¶
A 2-layer network solving binary classification, input \(x\in\mathbb{R}^2\):
Forward pass, concretely¶
Backward pass, using the results from Modules 01 and 06¶
Since \(\hat y=\sigma(z_2)\) with BCE loss, the combined gradient (sigmoid's softmax-for-two-classes special case, Module 06) is:
Backprop into the hidden layer — chain through \(w_2\), then through sigmoid's own derivative \(a_1(1-a_1)\) (Module 01):
Numeric verification¶
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
W1 = np.array([[0.5, -0.3], [0.8, 0.2]])
b1 = np.array([0.0, 0.0])
w2 = np.array([0.6, -0.4])
b2 = 0.0
x = np.array([1.0, 2.0])
y = 1.0
def forward(W1, b1, w2, b2):
z1 = W1 @ x + b1
a1 = sigmoid(z1)
z2 = w2 @ a1 + b2
y_hat = sigmoid(z2)
L = -(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat))
return L, y_hat, a1, z1
L, y_hat, a1, z1 = forward(W1, b1, w2, b2)
print(f"y_hat={y_hat:.4f} L={L:.4f}")
# Analytic backward
delta2 = y_hat - y
dW2 = delta2 * a1
db2 = delta2
delta1 = (delta2 * w2) * (a1 * (1 - a1))
dW1 = np.outer(delta1, x)
db1 = delta1
print(f"analytic dW2={dW2} db2={db2:.4f}")
print(f"analytic dW1=\n{dW1}\ndb1={db1}")
# Finite-difference gradient check on every parameter
h = 1e-6
def loss_only(W1, b1, w2, b2):
return forward(W1, b1, w2, b2)[0]
dW1_num = np.zeros_like(W1)
for i in range(2):
for j in range(2):
Wp, Wm = W1.copy(), W1.copy()
Wp[i, j] += h; Wm[i, j] -= h
dW1_num[i, j] = (loss_only(Wp, b1, w2, b2) - loss_only(Wm, b1, w2, b2)) / (2*h)
dW2_num = np.zeros_like(w2)
for i in range(2):
wp, wm = w2.copy(), w2.copy()
wp[i] += h; wm[i] -= h
dW2_num[i] = (loss_only(W1, b1, wp, b2) - loss_only(W1, b1, wm, b2)) / (2*h)
print(f"numeric dW1=\n{dW1_num}")
print(f"numeric dW2={dW2_num}")
y_hat=0.4944 L=0.7043
analytic dW2=[-0.24016 -0.38863] db2=-0.5056
analytic dW1=
[[-0.0757 -0.1514]
[ 0.036 0.072 ]]
db1=[-0.0757 0.036 ]
numeric dW1=
[[-0.0757 -0.1514]
[ 0.036 0.072 ]]
numeric dW2=[-0.24016 -0.38863]
How It Actually Works¶
Implementing forward and backward passes "from scratch" in this capstone
means, mechanically, building a small computational graph by hand: every
matrix multiply, activation function, and loss evaluation you write in the
forward pass must have a matching, explicitly coded backward function that
you call in reverse order during backprop — exactly the grad_fn
machinery that Module 01 and Level 2's chain-rule modules described a
framework doing automatically. Writing this by hand is instructive
precisely because it exposes the bookkeeping a framework normally hides:
you must decide what to cache from the forward pass (pre-activation values
for ReLU's derivative, the softmax output for the fused cross-entropy
gradient), and you must correctly sum gradients at every node whose output
feeds into more than one downstream computation (a shared weight matrix
used at two points, for instance) — a bug here (overwriting instead of
accumulating) is one of the most common real implementation mistakes when
building autodiff by hand.
The "numeric verification" step in this capstone is not optional scaffolding — it is the single most important debugging tool for exactly this kind of hand-written backward pass: comparing your analytic gradient against a central finite difference, \(\frac{f(\theta+h)-f(\theta-h)}{2h}\) (more accurate than the one-sided version from Module 06 of Level 1, since it cancels the first-order truncation error, leaving \(O(h^2)\) error instead of \(O(h)\)), for a handful of parameters is the standard way real ML engineers catch backward-pass bugs before trusting a training run — matching to 4-6 significant digits is the usual bar, since perfect agreement is impossible given floating- point round-off in both computations.
Exercise¶
- Take one gradient descent step (\(\eta=0.5\)) on all parameters and recompute the forward pass — confirm \(L\) decreased.
- Add L2 regularization (\(\lambda=0.1\)) on \(W_1\) and \(w_2\) and re-derive the gradients (Module 04); verify with finite differences.
- Extend to a batch of 3 examples and show the gradients are the mean of the per-example gradients (this is the "batch" in batch gradient descent — vectorize with matrix operations rather than a Python loop).