Neural networks II: what they can fit, and backprop with vectors
Why a network can approximate anything but might not learn it, what non-convexity really costs you, how to tell underfitting from overfitting, and backpropagation built up from a width-1 chain to a full matrix layer — every blank the slides left for the board, filled in.
Chapter 13 built networks and the chain rule on computation graphs. Lecture 12 (Neural Networks II) asks five questions and answers them in order: Can we fit any function? Non-convex — what about local minima? What if there are no non-linear activations? How many neurons/layers? And: tell me more about calculus. Sections 1–4 are the first four; sections 5–10 are the calculus, ending with the gradient formulas for a linear layer that the slides leave blank.
1. Can a network fit any function?
Start with the worry. Logistic regression draws one straight line. The lecture's demo (Karpathy's ConvNetJS 2-D classifier) shows a small network carving out a triangle, a disc, even an interlocking spiral. Is there any shape it can't do?
A two-layer neural network (one hidden layer plus an output layer) with a sufficient number of neurons can approximate any continuous function to any desired accuracy.
Each ReLU neuron $\max(0, w x + b)$ is a straight line with one kink in it. Adding $H$ of them together gives a piecewise-linear curve with up to $H$ kinks — like bending a piece of wire at $H$ places. Bend it at enough places and you can trace any smooth curve as closely as you like. That's the whole theorem, in one dimension.
Concretely: pick $H+1$ points $x_0 < x_1 < \dots < x_H$ on the curve you want. Join them with straight segments. The joined-up curve is
$$ \hat y(x) = f(x_0) + s_0\,\mathrm{ReLU}(x - x_0) + \sum_{i=1}^{H-1} (s_i - s_{i-1})\,\mathrm{ReLU}(x - x_i), $$where $s_i$ is the slope of segment $i$. Read it as a network: hidden neuron $i$ has weight $1$ and bias $-x_i$; the output neuron's weights are the changes in slope $s_i - s_{i-1}$, and its bias is $f(x_0)$. One hidden layer, $H$ neurons. The figure builds exactly this.
- "Can represent" ≠ "will learn". The weights above were written down by hand. Training has to find them by gradient descent on a non-convex objective (§2). The lecture's reminder slide is exactly this: "Just because it can fit any function, will it?"
- "Sufficient" may be enormous. The theorem doesn't bound $H$. In practice, deeper networks often get the same accuracy with far fewer neurons.
- Fitting anything includes fitting noise. The section title on the slide is "(Over?) Fitting any function" — a model that can fit any function can fit your training set's mistakes too (§4).
2. Non-convex: does getting stuck matter?
Logistic regression's loss is a bowl — one minimum, and gradient descent finds it. A network's loss is not: the hidden-layer weights multiply each other (and you can relabel hidden neurons without changing the function), so the surface has many valleys. The lecture makes three points:
- The objective is non-convex → gradient descent can get stuck in a local minimum.
- Stochastic gradient descent takes noisy, pseudo-random steps → that noise helps it pop out of shallow minima.
- "Is getting stuck at a local minimum necessarily a bad thing??" — the slide leaves this open. Answered below.
Not necessarily. Three reasons:
- We don't actually want the global minimum of training loss. For a big network, the global minimum often means memorising the training set — zero training loss, poor validation loss. A local minimum with slightly higher training loss can generalise better. What matters is validation performance.
- In large networks, most local minima are about equally good. Empirically, different random initialisations land in different minima with very similar loss. A local minimum is only a problem if it's much worse than the alternatives.
- Many directions to escape. With millions of parameters, a point has to curve upward in every direction to trap you. Points that look stuck are more often saddle points, which SGD's noise pushes off.
When it is bad: when training loss is stuck high. That's underfitting, and §4 tells you what to do about it.
3. What if there are no non-linear activations?
Delete every $g$ and keep stacking linear layers. Do you get a more powerful model? No:
A deep network with only linear layers reduces to an exactly equivalent single linear layer.
Two layers: $\mathbf z_1 = W_1\mathbf x + \mathbf b_1$, then $\mathbf z_2 = W_2\mathbf z_1 + \mathbf b_2$. Substitute:
$$ \mathbf z_2 = W_2(W_1\mathbf x + \mathbf b_1) + \mathbf b_2 = \underbrace{(W_2W_1)}_{W}\mathbf x + \underbrace{(W_2\mathbf b_1 + \mathbf b_2)}_{\mathbf b}. $$One matrix, one bias. By induction, any number of linear layers collapses the same way.
$W_1 = \begin{bmatrix}1 & 2\\ 0 & 1\end{bmatrix}$, $\mathbf b_1 = \begin{bmatrix}1\\0\end{bmatrix}$, $W_2 = \begin{bmatrix}1 & -1\end{bmatrix}$, $b_2 = 2$. No activations. Write the network as a single $\hat y = \mathbf w^\top\mathbf x + b$.
Step 1 — What's being asked?
Find the one linear layer that computes the same function for every $\mathbf x$: $W = W_2W_1$ and $b = W_2\mathbf b_1 + b_2$.
Step 2 — Multiply
$W_2W_1 = \begin{bmatrix}1\cdot1 + (-1)\cdot 0 & 1\cdot 2 + (-1)\cdot 1\end{bmatrix} = \begin{bmatrix}1 & 1\end{bmatrix}$. $W_2\mathbf b_1 + b_2 = (1\cdot 1 + (-1)\cdot 0) + 2 = 3$.
Answer & the takeaway
$\hat y = x_1 + x_2 + 3$. Check at $\mathbf x = (1, 1)$: layer 1 gives $(1+2+1,\ 0+1+0) = (4, 1)$, layer 2 gives $4 - 1 + 2 = 5$; the collapsed version gives $1 + 1 + 3 = 5$. ✓ The nonlinearity is what makes depth worth anything. It's also why ReLU's kink in §1 matters: without it, the "bent wire" is a straight wire.
4. How many neurons and layers? Debug it from the losses
There's no formula for the right size. Instead, train something, then read two numbers — training loss and validation loss — and let them tell you which way to move.
Evidence: poor training loss (and poor validation loss).
Baseline: compare with human performance — make sure the task isn't impossible for a human with unlimited resources. If people can't do it either, "poor" may be as good as it gets.
Try: more capacity — wider (more neurons per layer) or deeper (more layers).
Evidence: good training loss, poor validation loss. Also: really large parameter values.
Try: regularisation (penalise large weights — chapter 12), or more data.
"But how do we choose a new network structure?" — the slide's follow-up. Treat width and depth as hyperparameters: pick a few candidates, train each, and compare validation loss, exactly as chapter 12's cross-validation did for $\lambda$. Usual practice is to start from an architecture known to work on a similar problem and adjust.
To pass exactly through noisy points, a model has to make sharp turns, and sharp turns need big weights (a steep slope on a ReLU is a big $w$). Huge weights are the model contorting itself around individual training points.
5. Backprop on a width-1 chain
Now the calculus. The lecture's warm-up is a deliberately "dumb" network: five layers, one neuron each, no biases. It has none of the index mess of a real network, so you can see the structure of the computation clearly.
$$ z_\ell = w_\ell\, a_{\ell-1}, \qquad a_\ell = g(z_\ell), \qquad a_0 = x, \qquad \hat y = a_L, \qquad J = \ell(\hat y, y). $$Written out, $\hat y = g(w_5\, g(w_4\, g(w_3\, g(w_2\, g(w_1 x)))))$ — a nesting of $f$ (multiply by a weight) and $g$ (activation), ten deep.
The question: gradient descent needs $\partial J/\partial w_\ell$ for every $\ell$ so it can update $w_\ell \leftarrow w_\ell - \alpha\,\partial J/\partial w_\ell$. The chain rule along the single path gives, e.g.
$$ \frac{\partial J}{\partial w_2} = \frac{\partial J}{\partial \hat y}\cdot\frac{\partial a_5}{\partial z_5}\frac{\partial z_5}{\partial a_4}\cdot\frac{\partial a_4}{\partial z_4}\frac{\partial z_4}{\partial a_3}\cdot\frac{\partial a_3}{\partial z_3}\frac{\partial z_3}{\partial a_2}\cdot\frac{\partial a_2}{\partial z_2}\cdot\frac{\partial z_2}{\partial w_2}. $$Now write the same thing for $w_1$: it's the same long product with two more factors on the end. Computing each $\partial J/\partial w_\ell$ from scratch repeats almost all the work.
Walk backwards once, keeping a running product. At each layer you hold $\partial J/\partial a_\ell$ and do two cheap things:
$$ \frac{\partial J}{\partial z_\ell} = \frac{\partial J}{\partial a_\ell}\, g'(z_\ell), \qquad \frac{\partial J}{\partial w_\ell} = \frac{\partial J}{\partial z_\ell}\, a_{\ell-1}, \qquad \frac{\partial J}{\partial a_{\ell-1}} = \frac{\partial J}{\partial z_\ell}\, w_\ell. $$The last one is what you hand to the layer below. Every intermediate gradient is computed once. For $L$ layers that's about $L$ steps of work instead of about $L^2/2$.
The final product is off by some amount. The last station works out how much of that is its fault and how much it inherited from upstream, then passes the "inherited" part back to the previous station, which does the same. No station needs to see the whole line; each only needs the blame message from the station after it. (Where it breaks: blame in real backprop can be negative, and it splits and sums when wires fork — §6.)
Three layers, width 1, sigmoid activation $g(z) = \frac{1}{1+e^{-z}}$ (so $g'(z) = g(z)(1 - g(z))$), loss $J = \tfrac12(\hat y - y)^2$. $x = 1$, $y = 1$, $w_1 = 0.5$, $w_2 = -1$, $w_3 = 2$. Find $\partial J/\partial w_1$, $\partial J/\partial w_2$, $\partial J/\partial w_3$.
Step 1 — What's being asked?
Three partial derivatives. The efficient route: one forward pass storing every $z_\ell$ and $a_\ell$, then one backward pass using the three boxed rules.
Step 2 — Forward pass
$z_1 = 0.5\cdot 1 = 0.5$, $a_1 = g(0.5) = 0.6225$.
$z_2 = -1\cdot 0.6225 = -0.6225$, $a_2 = g(-0.6225) = 0.3492$.
$z_3 = 2\cdot 0.3492 = 0.6984$, $a_3 = \hat y = g(0.6984) = 0.6678$.
$J = \tfrac12(0.6678 - 1)^2 = 0.0552$.
Step 3 — Backward through layer 3
$\partial J/\partial a_3 = \hat y - y = -0.3322$.
$\partial J/\partial z_3 = -0.3322 \cdot a_3(1-a_3) = -0.3322 \cdot 0.2218 = -0.0737$.
$\partial J/\partial w_3 = -0.0737 \cdot a_2 = -0.0737\cdot 0.3492 = -0.0257$.
Pass down: $\partial J/\partial a_2 = -0.0737 \cdot w_3 = -0.1474$.
Step 4 — Layers 2 and 1
$\partial J/\partial z_2 = -0.1474\cdot a_2(1-a_2) = -0.1474\cdot 0.2273 = -0.0335$; $\partial J/\partial w_2 = -0.0335 \cdot a_1 = -0.0208$; $\partial J/\partial a_1 = -0.0335\cdot w_2 = 0.0335$.
$\partial J/\partial z_1 = 0.0335\cdot a_1(1-a_1) = 0.0335 \cdot 0.2350 = 0.00787$; $\partial J/\partial w_1 = 0.00787 \cdot x = 0.00787$.
Answer & the takeaway
$\partial J/\partial w_3 = -0.0257$, $\partial J/\partial w_2 = -0.0208$, $\partial J/\partial w_1 = 0.0079$ (finite differences agree to 4 decimal places; Figure 4 starts at these weights). Two things to notice: the sign of $\partial J/\partial a_1$ flipped because $w_2$ is negative, and the magnitude dropped by roughly ×4 per layer — the $g' \le 0.25$ effect. The skill being tested is bookkeeping: store forward values, reuse the running product.
6. The generic layer: the only interface backprop needs
Zoom out. Every layer, whatever it computes, is a box $\mathbf z_{\text{out}} = f(\mathbf z_{\text{in}}, \theta)$. The forward pass pushes values left-to-right through the boxes. The backward pass pushes $\partial J/\partial(\cdot)$ right-to-left. Each box only has to know its own local derivatives.
For a scalar layer the two outputs are
$$ \frac{\partial J}{\partial z_{\text{in}}} = \frac{\partial J}{\partial z_{\text{out}}}\frac{\partial f}{\partial z_{\text{in}}}, \qquad \frac{\partial J}{\partial \theta} = \frac{\partial J}{\partial z_{\text{out}}}\frac{\partial f}{\partial \theta}. $$That's why deep-learning libraries can let you build arbitrary networks from Lego-brick layers: each brick implements forward and backward, and the library just calls them in order, then in reverse.
7. Multivariate chain rule: when paths fork, add
The chain above has exactly one path from each weight to $J$. Real networks fork: one value feeds several neurons. The lecture's first exercise is the simplest fork.
$g_1(x) = 3x$, $g_2(x) = 5x$, $f(z_1, z_2) = 2z_1 + 7z_2$, $y = f(g_1(x), g_2(x))$. Find $dy/dx$.
Step 1 — Draw it
$x$ splits into two paths: $x \to g_1 \to f$ and $x \to g_2 \to f$. The rule: multiply along each path, then add the paths: $\frac{dy}{dx} = \frac{\partial f}{\partial g_1}\frac{dg_1}{dx} + \frac{\partial f}{\partial g_2}\frac{dg_2}{dx}$.
Step 2 — Local derivatives
$\frac{dg_1}{dx} = 3$, $\frac{dg_2}{dx} = 5$, $\frac{\partial f}{\partial z_1} = 2$, $\frac{\partial f}{\partial z_2} = 7$.
Answer & the takeaway
$\frac{dy}{dx} = 2\cdot 3 + 7\cdot 5 = 6 + 35 = 41$. Sanity check by substitution: $y = 2(3x) + 7(5x) = 41x$. ✓ Forgetting a path gives 6 or 35 — a classic error.
$z_1 = \sin x$, $z_2 = x^3$, $y = f(z_1, z_2) = z_1^4 e^{z_2} + 5z_1 + 7z_2$. Find $dy/dx$, then evaluate it at $x = 1$.
Step 1 — Four local derivatives
$\frac{dz_1}{dx} = \cos x$, $\frac{dz_2}{dx} = 3x^2$, $\frac{\partial f}{\partial z_1} = 4z_1^3 e^{z_2} + 5$, $\frac{\partial f}{\partial z_2} = z_1^4 e^{z_2} + 7$. (In $\partial f/\partial z_1$, $e^{z_2}$ is a constant; in $\partial f/\partial z_2$, $z_1^4$ is.)
Step 2 — Combine
Step 3 — Evaluate at $x = 1$
$z_1 = \sin 1 = 0.8415$, $z_2 = 1$. First term: $(4\cdot 0.5958\cdot 2.7183 + 5)(0.5403) = (6.478 + 5)(0.5403) = 6.202$. Second: $(0.5014\cdot 2.7183 + 7)(3) = (1.363 + 7)(3) = 25.089$. Total $\approx 31.29$.
Answer & the takeaway
$dy/dx\big|_{x=1} \approx 31.290$; a central finite difference on $y(x) = \sin^4 x\, e^{x^3} + 5\sin x + 7x^3$ gives $31.290$ too. Note the answer is written in terms of $z_1, z_2$ — values you already computed on the forward pass. That's the reuse that makes backprop cheap.
Three versions of the chain rule
| Scalar | Scalar out, vector middle | Vector out, vector middle | |
|---|---|---|---|
| Setup | $y = f(z)$, $z = g(x)$ | $y = f(\mathbf z)$, $\mathbf z = g(x)$ | $\mathbf y = f(\mathbf z)$, $\mathbf z = g(\mathbf x)$ |
| Rule | $\dfrac{dy}{dx} = \dfrac{dy}{dz}\dfrac{dz}{dx}$ | $\dfrac{dy}{dx} = \displaystyle\sum_j \dfrac{\partial y}{\partial z_j}\dfrac{\partial z_j}{\partial x}$ | $\dfrac{\partial y_i}{\partial x_k} = \displaystyle\sum_j \dfrac{\partial y_i}{\partial z_j}\dfrac{\partial z_j}{\partial x_k}$ |
| In words | one path | one path per $z_j$; add them | same, for every (output $i$, input $k$) pair |
The third column is the one to remember — the other two are special cases. The sum over $j$ is "add over every intermediate the influence can flow through".
Why the updated layer says "+="
The lecture's "Generic Layer Implementation (updated)" slide changes $=$ to $+=$:
$$ \frac{\partial J}{\partial z_{\text{in}}} \mathrel{+}= \frac{\partial J}{\partial z_{\text{out}}}\frac{\partial f}{\partial z_{\text{in}}}, \qquad \frac{\partial J}{\partial \theta} \mathrel{+}= \frac{\partial J}{\partial z_{\text{out}}}\frac{\partial f}{\partial \theta}. $$8. A layer with a vector output
Now let the layer output $K$ numbers, $\mathbf z_{\text{out}} \in \R^K$, with parameters $\boldsymbol\theta \in \R^M$. The slide leaves the formulas blank on its first version. Fill them in with the third column of the table: $z_{\text{in}}$ reaches $J$ through each of the $K$ outputs, so sum over them.
$$ \frac{\partial J}{\partial z_{\text{in}}} = \sum_{k=1}^{K}\frac{\partial J}{\partial z_{\text{out},k}}\frac{\partial z_{\text{out},k}}{\partial z_{\text{in}}}, \qquad \frac{\partial J}{\partial \theta_j} = \sum_{k=1}^{K}\frac{\partial J}{\partial z_{\text{out},k}}\frac{\partial z_{\text{out},k}}{\partial \theta_j}. $$Here $j$ indexes a single parameter; you compute this once per $j = 1,\dots,M$. The incoming message is now a vector $\partial J/\partial\mathbf z_{\text{out}} \in \R^K$, one number per output.
9. The linear layer: $\mathbf z = W\mathbf a + \mathbf b$
The workhorse. Input $\mathbf a \in \R^M$, weights $W \in \R^{K\times M}$, bias $\mathbf b \in \R^K$, output $\mathbf z \in \R^K$. Entry by entry:
$$ z_k = b_k + \sum_{j=1}^{M} W_{k,j}\, a_j. $$The slides pose three blanks: $\partial J/\partial a_j$, $\partial J/\partial W_{i,j}$, $\partial J/\partial b_i$. Here they are, filled.
Scalar version — derive each one
Step 1, the vector-output rule: every derivative is $\sum_k \frac{\partial J}{\partial z_k}\cdot(\text{how much } z_k \text{ moves})$.
Step 2, the local derivatives from $z_k = b_k + \sum_j W_{k,j}a_j$:
- $\frac{\partial z_k}{\partial a_j} = W_{k,j}$ — every output depends on every input.
- $\frac{\partial z_k}{\partial W_{i,j}} = a_j$ if $k = i$, and $0$ otherwise — weight $W_{i,j}$ only appears in output $i$.
- $\frac{\partial z_k}{\partial b_i} = 1$ if $k = i$, and $0$ otherwise.
Step 3, plug in. The sum over $k$ survives for $\mathbf a$, and collapses to a single term for $W$ and $\mathbf b$:
$$ \frac{\partial J}{\partial a_j} = \sum_{k} \frac{\partial J}{\partial z_k}W_{k,j}, \qquad \frac{\partial J}{\partial W_{i,j}} = \frac{\partial J}{\partial z_i}\,a_j, \qquad \frac{\partial J}{\partial b_i} = \frac{\partial J}{\partial z_i}. $$- Input gradient: $a_j$ feeds every output, so it collects blame from all of them, weighted by the connection strength $W_{k,j}$.
- Weight gradient: $W_{i,j}$ connects input $j$ to output $i$. Its blame = (how wrong output $i$ is) × (how active input $j$ was). An input that was zero gets no blame for anything — it didn't contribute.
- Bias gradient: the bias passes the output's blame straight through.
Matrix version — two layouts
Stack those into vectors and matrices. Write $\boldsymbol\delta = \partial J/\partial\mathbf z$ for the incoming message.
$\boldsymbol\delta$ is a column vector. $\partial J/\partial W$ has the same shape as $W$, so the update $W \leftarrow W - \alpha\,\partial J/\partial W$ just works.
$\boldsymbol\delta^\top$ is a row vector. Everything is transposed relative to the left box — you must transpose $\partial J/\partial W$ back before the update.
$W = \begin{bmatrix}1 & 2 & 0\\ -1 & 0 & 3\end{bmatrix}$, $\mathbf b = \begin{bmatrix}0.5\\-1\end{bmatrix}$, $\mathbf a = \begin{bmatrix}2\\1\\-1\end{bmatrix}$, and the layer above sends back $\boldsymbol\delta = \partial J/\partial\mathbf z = \begin{bmatrix}0.3\\-0.2\end{bmatrix}$. Find $\mathbf z$, $\partial J/\partial\mathbf a$, $\partial J/\partial W$, $\partial J/\partial\mathbf b$ (denominator layout).
Step 1 — What's being asked, and the shapes
$K = 2$ outputs, $M = 3$ inputs. Expect $\mathbf z$: 2×1, $\partial J/\partial\mathbf a$: 3×1, $\partial J/\partial W$: 2×3, $\partial J/\partial\mathbf b$: 2×1. Write the shapes down before any arithmetic.
Step 2 — Forward
$z_1 = 1\cdot2 + 2\cdot1 + 0\cdot(-1) + 0.5 = 4.5$, $z_2 = -1\cdot 2 + 0 + 3\cdot(-1) - 1 = -6$. So $\mathbf z = (4.5, -6)$.
Step 3 — Input gradient $W^\top\boldsymbol\delta$
Column $j$ of $W$ dotted with $\boldsymbol\delta$: $j=1$: $1(0.3) + (-1)(-0.2) = 0.5$; $j=2$: $2(0.3) + 0 = 0.6$; $j=3$: $0 + 3(-0.2) = -0.6$. So $\partial J/\partial\mathbf a = (0.5, 0.6, -0.6)$.
Step 4 — Weight and bias gradients
Answer & the takeaway
Notice $\mathbf z$ never appeared in the gradients — for a linear layer the local derivatives don't depend on the output. What you need stored from the forward pass is the input $\mathbf a$ (for $\partial J/\partial W$). The test here is shapes: transpose $W$ for the input gradient; outer product for the weight gradient.
10. Putting it together: backprop for a whole network
A fully connected network is linear layers alternating with element-wise activation layers. You now have both bricks. The activation brick $\mathbf a = g(\mathbf z)$ acts on each entry separately, so output $k$ depends only on input $k$, and the sum over $k$ collapses to one term:
$$ \frac{\partial J}{\partial z_k} = \frac{\partial J}{\partial a_k}\, g'(z_k) \quad\Longleftrightarrow\quad \frac{\partial J}{\partial\mathbf z} = \frac{\partial J}{\partial\mathbf a}\odot g'(\mathbf z), $$where $\odot$ means multiply entry by entry. Chain the bricks:
forward(x):
a[0] = x
for l = 1..L:
z[l] = W[l] @ a[l-1] + b[l] # linear brick (store a[l-1])
a[l] = g(z[l]) # activation brick (store z[l])
return J = loss(a[L], y)
backward():
dA = dJ/da[L] # from the loss
for l = L..1:
dZ = dA * g'(z[l]) # activation brick, element-wise
dW[l] = dZ @ a[l-1].T # outer product
db[l] = dZ
dA = W[l].T @ dZ # message to the layer below
# then: W[l] -= alpha * dW[l], b[l] -= alpha * db[l] for every l
Set every width to 1 and this is exactly §5's chain. That's the payoff of the lecture's "dumb" warm-up: the structure was already there.
- Using the updated weight in the backward pass. Compute all gradients first, using the weights from the forward pass, then update.
- Forgetting $g'$ on the way through an activation, or applying it at the wrong $z$ (it's evaluated at that layer's pre-activation $z_\ell$).
- $=$ instead of $+=$ when a value or parameter is used in more than one place. The gradients from other paths silently disappear.
- Mixing layouts. Pick denominator layout and check every product's shape.
11. Recap
- Universal approximation: one hidden layer with enough neurons can approximate any continuous function — ReLUs are kinks in a bendable wire. Says nothing about learnability or size.
- Non-convex loss → local minima; SGD's noise helps escape. Local minima aren't necessarily bad: we care about validation loss, and in big nets most minima are similar.
- No nonlinearity → one linear layer: $W_2(W_1\mathbf x + \mathbf b_1) + \mathbf b_2 = (W_2W_1)\mathbf x + (W_2\mathbf b_1 + \mathbf b_2)$.
- Debug underfitting first (bad train loss → more capacity), then overfitting (good train, bad validation, large weights → regularise / more data). Size is a hyperparameter.
- Backprop = chain rule, computed right-to-left, reusing the running product. Each layer: receive $\partial J/\partial z_{\text{out}}$, emit $\partial J/\partial z_{\text{in}}$ and $\partial J/\partial\theta$.
- Multivariate chain rule: multiply along paths, add over paths → in code, $+=$.
- Linear layer: $\partial J/\partial\mathbf a = W^\top\boldsymbol\delta$, $\partial J/\partial W = \boldsymbol\delta\mathbf a^\top$, $\partial J/\partial\mathbf b = \boldsymbol\delta$ (denominator layout). Activation: $\boldsymbol\delta = \partial J/\partial\mathbf a \odot g'(\mathbf z)$.