Part II · Chapter 14

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.

Where this sits

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?

Universal Approximation Theorem

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.

Intuition — bending a wire

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.

Figure 1 — a one-hidden-layer ReLU network, built by hand to trace a curve
Target (dashed): $f(x) = \sin(2x) + 0.3x$ on $[0, 3]$. Dots mark the kinks — one per hidden neuron. What to notice: each extra neuron buys one more bend, and the worst-case error in the readout keeps shrinking as $H$ grows. The theorem promises this; it says nothing about how big $H$ must be for a hard function, or whether gradient descent would find these weights.
What the theorem does not say

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:

  1. The objective is non-convex → gradient descent can get stuck in a local minimum.
  2. Stochastic gradient descent takes noisy, pseudo-random steps → that noise helps it pop out of shallow minima.
  3. "Is getting stuck at a local minimum necessarily a bad thing??" — the slide leaves this open. Answered below.
Figure 2 — plain GD vs noisy (stochastic-style) steps on a loss with two valleys
Loss $L(w) = 0.1w^4 - 0.6w^2 + 0.35w$, start $w = 2.5$, 300 steps of size $0.05$; each step uses the true gradient plus Gaussian noise of size $\sigma$ (a stand-in for the noise of a mini-batch gradient). What to notice: with $\sigma = 0$ the path slides into the nearest (shallower) valley and stays — every time. Raise $\sigma$ to about 3 and click "run again" several times: in a 200-run simulation of these exact settings, about 4 runs in 10 hopped the hump into the deeper valley; at $\sigma = 1$, none did. Notice too that at high $\sigma$ the end point jitters around a valley floor instead of sitting on it — noise helps escape, but it also stops you converging precisely, which is why learning rates are usually decayed late in training.
Answer — is a local minimum bad?

Not necessarily. Three reasons:

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:

Key idea

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.

Try itCollapse a two-layer linear network

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

Figure 3 — the debugging flowchart from the lecture
What to notice: you check underfitting first. If the model can't even fit the training data, regularising or adding data won't help — those are fixes for the other problem.
Underfitting (check this first!)

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

Overfitting

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.

Why "really large parameter values" means overfitting

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.

Training loss 0.02, validation loss 0.03, but a human expert gets 0.01 on the same task. Training loss for a bigger network you tried earlier was 0.001 with validation 0.4. What's going on with the current network?
Why: training and validation are close, so there's no overfitting signal. But training loss is above what a human achieves, which is the underfitting signal — the task is learnable to 0.01. The earlier big network shows the other cliff edge (0.001 train, 0.4 validation = overfitting), so the right size is somewhere between. Options A/B mistake a small train–validation gap for overfitting.

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.

Backpropagation = start at the end and reuse

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

Analogy — blame flowing back down an assembly line

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

Figure 4 — forward and backward pass on a 3-layer width-1 chain (sigmoid, $J = \tfrac12(\hat y - y)^2$)
$x = 1$, $y = 1$. Top row (blue): forward values. Bottom row (pink): $\partial J/\partial(\cdot)$, computed right-to-left. The last readout line re-estimates each $\partial J/\partial w_\ell$ by nudging $w_\ell$ by $\pm10^{-6}$ — independently of backprop — and they agree. What to notice: drag $w_3$ to $\pm4$ and watch $\partial J/\partial w_1$ shrink toward zero. Each sigmoid multiplies the backward signal by $g'(z) \le 0.25$, and a saturated sigmoid by much less. That's the vanishing-gradient problem chapter 15's ResNets fix.
Try itOne full forward and backward pass by hand

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.

Figure 5 — one layer's job in the backward pass
What to notice: the layer receives one message from the right, $\partial J/\partial z_{\text{out}}$, and produces two: one to the left (for earlier layers) and one downward (for its own parameter update). It multiplies the incoming message by its local derivatives — it never needs to know what the rest of the network is.

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.

Try itExercise 1 from the slides (left unanswered there)

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

Try itExercise 2 from the slides — with a number at the end

$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
$$ \frac{dy}{dx} = \bigl(4z_1^3e^{z_2} + 5\bigr)\cos x + \bigl(z_1^4 e^{z_2} + 7\bigr)3x^2. $$
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

ScalarScalar out, vector middleVector 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 wordsone pathone path per $z_j$; add themsame, 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}. $$
Figure 6 — fan-out: one value used by two layers, so its gradient gets two contributions
What to notice: $z_{\text{in}}$ influences $J$ through two boxes. Each box, during its own backward call, only knows about its own path, so it adds its contribution into a shared accumulator that starts at zero. After both have run, the accumulator holds the full multivariate-chain-rule sum. The same thing happens to a parameter used in many places — which is exactly the situation in a convolutional layer (chapter 15).

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$:

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}. $$
Read them in words

Matrix version — two layouts

Stack those into vectors and matrices. Write $\boldsymbol\delta = \partial J/\partial\mathbf z$ for the incoming message.

Denominator layout (gradient has the shape of the thing) $$ \frac{\partial J}{\partial \mathbf a} = W^\top\boldsymbol\delta \quad (M\times 1) $$ $$ \frac{\partial J}{\partial W} = \boldsymbol\delta\,\mathbf a^\top \quad (K\times M) $$ $$ \frac{\partial J}{\partial \mathbf b} = \boldsymbol\delta \quad (K\times 1) $$

$\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.

Numerator layout (gradient is the transpose) $$ \frac{\partial J}{\partial \mathbf a} = \boldsymbol\delta^\top W \quad (1\times M) $$ $$ \frac{\partial J}{\partial W} = \mathbf a\,\boldsymbol\delta^\top \quad (M\times K) $$ $$ \frac{\partial J}{\partial \mathbf b} = \boldsymbol\delta^\top \quad (1\times K) $$

$\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.

Figure 7 — shape check for $\partial J/\partial W = \boldsymbol\delta\,\mathbf a^\top$ (denominator layout), $K = 2$, $M = 3$
What to notice: the gradient for $W$ is an outer product — a column ($K$ entries, one per output) times a row ($M$ entries, one per input). Entry $(i, j)$ is $\delta_i a_j$, exactly the scalar formula. If your shapes don't come out $K\times M$, you've mixed layouts.
Try itBackward through a linear layer

$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
$$ \frac{\partial J}{\partial W} = \begin{bmatrix}0.3\\-0.2\end{bmatrix}\begin{bmatrix}2 & 1 & -1\end{bmatrix} = \begin{bmatrix}0.6 & 0.3 & -0.3\\ -0.4 & -0.2 & 0.2\end{bmatrix}, \qquad \frac{\partial J}{\partial\mathbf b} = \begin{bmatrix}0.3\\-0.2\end{bmatrix}. $$
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.

A linear layer has $W \in \R^{4\times 10}$. In denominator layout, what is the shape of $\partial J/\partial\mathbf a$ (the gradient passed to the layer below)?
Why: $\mathbf a$ is the input, and $W$ is $K\times M = 4\times 10$, so $\mathbf a \in \R^{10}$. In denominator layout a gradient has the shape of the thing it's with respect to: 10 × 1, computed as $W^\top\boldsymbol\delta$ = (10×4)(4×1). "4 × 1" is $\boldsymbol\delta$'s shape — the output side; "4 × 10" is $\partial J/\partial W$.

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.

Common mistakes

11. Recap

  1. 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.
  2. 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.
  3. 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)$.
  4. Debug underfitting first (bad train loss → more capacity), then overfitting (good train, bad validation, large weights → regularise / more data). Size is a hyperparameter.
  5. 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$.
  6. Multivariate chain rule: multiply along paths, add over paths → in code, $+=$.
  7. 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)$.
In a network, a hidden activation $a$ feeds three neurons in the next layer. During backprop you've received $\partial J/\partial a$ contributions of $0.2$, $-0.5$ and $0.1$ from them. What is $\partial J/\partial a$?
Why: three paths from $a$ to $J$, so by the multivariate chain rule you add them: $0.2 - 0.5 + 0.1 = -0.2$. "0.1" is the bug you get from writing $=$ instead of $+=$; averaging has no basis in the chain rule.