Neural Networks — From One Neuron to a Trained Network

Machine Learning Notes · note 3 of 6 · The network reference ← Logistic regression Multiclass and the softmax →

This is a self-contained reference for the fundamentals of neural networks, built bottom-up. Part I fixes the groundwork on a single neuron — binary classification, notation, logistic regression, the cost that scores it, gradient descent, the derivatives and computation graph the training relies on, and vectorization. Part II stacks neurons into a one-hidden-layer network — layer notation, the forward pass, activation functions, gradient descent, backpropagation, and initialization. Part III takes the same layer block to any depth $L$ — matrix shapes, why depth helps, the forward/backward building blocks, the hyperparameters you tune around them, and what all of it does (and does not) have to do with the brain. Part IV re-derives the whole forward/backward machinery one scalar at a time, so the transposes, outer products and averaging factors of the matrix equations are seen to follow from the chain rule rather than being asserted. The same symbols carry through, so the parts read as one continuous story; new topics can be appended as further parts.

In one line. A neural network is logistic-regression units, layered. Each unit forms a score $z = w^\top x + b$ and squashes it, $a = \sigma(z)$; stacking units into layers and layers into a network gives $z^{[\ell]} = W^{[\ell]}a^{[\ell-1]} + b^{[\ell]}$, $\;a^{[\ell]} = g^{[\ell]}(z^{[\ell]})$, and the whole thing is trained by gradient descent using gradients computed by backpropagation.

Contents

Part I · The single neuron

  1. Binary classification: the task
  2. From image to feature vector
  3. Notation
  4. Logistic regression
  5. Logistic regression cost function
  6. Gradient descent
  7. Derivatives
  8. The computation graph
  9. Derivatives on a computation graph
  10. Logistic regression gradient descent
  11. Gradient descent on $m$ examples
  12. Vectorization
  13. Vectorizing logistic regression
  14. Vectorizing the gradient computation
  15. Broadcasting in Python
  16. Why the loss is what it is: maximum likelihood (optional)

Part II · The shallow network

  1. Notation at a glance
  2. Neural network overview
  3. Neural network representation
  4. Computing a neural network's output
  5. Vectorizing across multiple examples
  6. Activation functions
  7. Gradient descent for a shallow network
  8. The backpropagation equations
  9. Backpropagation intuition (optional)
  10. Random initialization

Part III · Deep networks

  1. What makes a network "deep"?
  2. Notation for an $L$-layer network
  3. Forward propagation in a deep network
  4. Getting the matrix dimensions right
  5. Why deep representations?
  6. Building blocks: forward and backward functions
  7. Forward and backward propagation for a layer
  8. Parameters versus hyperparameters
  9. What does this have to do with the brain?

Part IV · The component view

  1. Indices: naming every scalar
  2. Inside one neuron
  3. Backpropagation, component by component
  4. From components back to matrices
  5. The output seed, for any number of outputs

Roadmap

  1. Where this goes next

Part I · The single neuron

Everything a network is built from, on one unit: turn an input into a probability, score it, and train it with gradient descent.

Binary classification: the task

In binary classification the network looks at an input and answers a single yes/no question. The running example is a cat detector: given an image, output 1 if it contains a cat and 0 if it does not.

Figure 1.1: Binary classification. An input image is fed to a classifier, which outputs a single label $y\in\{0,1\}$ — here 1 = cat, 0 = non-cat.

The label lives in a two-element set:

$$ y \in \{0, 1\}, \qquad y = \begin{cases} 1 & \text{the image is a cat},\\ 0 & \text{the image is not a cat}. \end{cases} \tag{1.1} $$
Equation 1.1: The output is a single binary label

From image to feature vector

A classifier does not work on a picture directly — it works on a vector of numbers. So the first step is to turn the image into one.

A colour image is stored as three channelsred, green, and blue. Each channel is a grid of pixel intensities (values from $0$ to $255$). For a $64\times 64$ image, that is three $64\times 64$ matrices, one per colour.

Figure 2.1: A 64×64 colour image is three stacked 64×64 grids of pixel intensities — one for red, one for green, one for blue. Every grid cell is a number in the range 0–255.

To build the feature vector $\mathbf{x}$, we unroll (flatten) every pixel of every channel into one long column: all the red values, then all the green, then all the blue. The length of that column is the total number of pixel values:

$$ \mathbf{x} \in \mathbb{R}^{n_x}, \qquad n_x = 64 \times 64 \times 3 = 12288 . \tag{2.1} $$
Equation 2.1: Unrolling a 64×64×3 image gives a 12288-dimensional feature vector

The number $n_x$ (often written just $n$) is the input dimension — the number of features the classifier sees. The whole job of the model is then to learn the mapping

$$ \mathbf{x} \;\longrightarrow\; y . \tag{2.2} $$
Equation 2.2: The classifier learns a mapping from features to label

Notation

The rest of this reference reuses one fixed set of symbols. They are collected here.

A single training example

A single example is a pair $(\mathbf{x}, y)$: a feature vector and its label.

$$ (\mathbf{x}, y), \qquad \mathbf{x} \in \mathbb{R}^{n_x}, \qquad y \in \{0, 1\}. \tag{3.1} $$
Equation 3.1: One training example

The whole training set

The training set is a list of $m$ such pairs. A superscript in parentheses, $(i)$, indexes the example — so $\mathbf{x}^{(i)}$ is the $i$-th input and $y^{(i)}$ its label:

$$ \bigl\{\,(\mathbf{x}^{(1)}, y^{(1)}),\ (\mathbf{x}^{(2)}, y^{(2)}),\ \ldots,\ (\mathbf{x}^{(m)}, y^{(m)})\,\bigr\}. \tag{3.2} $$
Equation 3.2: A training set of $m$ examples

When it matters which set is meant, $m$ is written $m_{\text{train}}$ for the number of training examples and $m_{\text{test}}$ for the number of test examples.

Stacking into matrices

For an efficient implementation the examples are packed into matrices. The key convention: each example is a column.

Stack the input vectors $\mathbf{x}^{(1)}, \ldots, \mathbf{x}^{(m)}$ side by side as columns to form the input matrix $\mathbf{X}$:

$$ \mathbf{X} = \begin{bmatrix} \big| & \big| & & \big| \\ \mathbf{x}^{(1)} & \mathbf{x}^{(2)} & \cdots & \mathbf{x}^{(m)} \\ \big| & \big| & & \big| \end{bmatrix} \in \mathbb{R}^{\,n_x \times m}, \qquad \texttt{X.shape} = (n_x,\ m). \tag{3.3} $$
Equation 3.3: Input matrix — one example per column

So $\mathbf{X}$ has $n_x$ rows (one per feature) and $m$ columns (one per example).

Columns, not rows. One could instead put each example in a row (giving an $m \times n_x$ matrix), but that convention is deliberately avoided here. Keeping examples in columns makes the neural-network equations in the following notes noticeably simpler.

The labels are stacked the same way, into a single row vector $\mathbf{Y}$:

$$ \mathbf{Y} = \begin{bmatrix} y^{(1)} & y^{(2)} & \cdots & y^{(m)} \end{bmatrix} \in \mathbb{R}^{\,1 \times m}, \qquad \texttt{Y.shape} = (1,\ m). \tag{3.4} $$
Equation 3.4: Label matrix — a single row of $m$ labels

The shapes to remember:

ObjectSymbolShape
One feature vector$\mathbf{x}^{(i)}$$(n_x,\ 1)$
One label$y^{(i)}$scalar in $\{0,1\}$
Input matrix$\mathbf{X}$$(n_x,\ m)$
Label matrix$\mathbf{Y}$$(1,\ m)$
Table 3.1: Shapes of the core objects. $n_x$ = number of input features; $m$ = number of examples.

Logistic regression

Logistic regression is the simplest model that turns the features $\mathbf{x}$ into a probability. It is also exactly a single neuron — the building block of the networks in the later parts — so it is worth understanding in full.

The model

Given an input $\mathbf{x}\in\mathbb{R}^{n_x}$, we want the output to be the probability that the label is 1, which must lie between $0$ and $1$:

$$ \hat{y} = P(y = 1 \mid \mathbf{x}), \qquad 0 \le \hat{y} \le 1 . \tag{4.1} $$
Equation 4.1: The prediction is a probability

A plain linear score $\mathbf{w}^\top\mathbf{x} + b$ can be any real number, so it cannot be used as a probability on its own. Logistic regression fixes this by passing the score through the sigmoid $\sigma$, which squashes any real number into the range $(0,1)$. The model has two parameters — a weight vector $\mathbf{w}\in\mathbb{R}^{n_x}$ (one weight per feature) and a bias $b\in\mathbb{R}$:

$$ \hat{y} = \sigma(z), \qquad z = \mathbf{w}^\top\mathbf{x} + b . \tag{4.2} $$
Equation 4.2: Logistic regression: a linear score, then a sigmoid

The intermediate quantity $z = \mathbf{w}^\top\mathbf{x} + b$ (the linear score) is called the pre-activation; it reappears under that name throughout the network notes.

The sigmoid function

The sigmoid (or logistic) function is:

$$ \sigma(z) = \frac{1}{1 + e^{-z}} . \tag{4.3} $$
Equation 4.3: The sigmoid function

Its S-shape is what makes it a good probability: it rises smoothly from $0$ to $1$ and passes through $0.5$ at $z = 0$.

Figure 4.1: The sigmoid $\sigma(z) = 1/(1 + e^{-z})$. It maps any real score $z$ into $(0, 1)$: $\sigma(0) = 0.5$, it saturates toward 1 for large positive $z$ and toward 0 for large negative $z$.

The two extremes explain how it encodes confidence:

$$ \sigma(0) = \tfrac{1}{2}, \qquad \underbrace{\lim_{z\to +\infty}\sigma(z) = 1}_{e^{-z}\,\to\,0}, \qquad \underbrace{\lim_{z\to -\infty}\sigma(z) = 0}_{e^{-z}\,\to\,\text{a huge number}} . \tag{4.4} $$
Equation 4.4: Sigmoid limits — a large score means a confident prediction

So a large positive score $z$ gives $\hat{y}\approx 1$ ("confidently a cat"), a large negative score gives $\hat{y}\approx 0$ ("confidently not a cat"), and $z = 0$ sits exactly on the decision boundary at $\hat{y} = 0.5$.

An alternative notation (not used here)

Some textbooks fold the bias into the weight vector. They prepend a constant feature $x_0 = 1$ (so $\mathbf{x}\in\mathbb{R}^{n_x+1}$) and write the model with a single parameter vector $\boldsymbol{\theta}$:

$$ \hat{y} = \sigma\!\bigl(\boldsymbol{\theta}^\top\mathbf{x}\bigr), \qquad \boldsymbol{\theta} = \begin{bmatrix} \theta_0 \\ \theta_1 \\ \theta_2 \\ \vdots \\ \theta_{n_x} \end{bmatrix}, \qquad \underbrace{\theta_0}_{\text{plays the role of } b}, \quad \underbrace{\theta_1,\ \ldots,\ \theta_{n_x}}_{\text{play the role of } \mathbf{w}} . \tag{4.5} $$
Equation 4.5: The θ-convention: bias folded into the parameter vector

This reference deliberately keeps $\mathbf{w}$ and $b$ separate — it makes the neural-network derivations that follow cleaner — so we do not use the $\boldsymbol{\theta}$ convention. It is noted only so the notation is recognisable if you meet it elsewhere.


Logistic regression cost function

The model gives predictions; now we need to measure how wrong they are and turn that into a single number to minimise. Two words are kept distinct throughout:

Recall the per-example prediction, now carrying the example index $(i)$:

$$ \hat{y}^{(i)} = \sigma\!\bigl(z^{(i)}\bigr), \qquad z^{(i)} = \mathbf{w}^\top\mathbf{x}^{(i)} + b . \tag{5.1} $$
Equation 5.1: Prediction for example $i$

Written out by components, $\mathbf{w}$ and $\mathbf{x}^{(i)}$ are column vectors of length $n_x$, and $b$ is a single number:

$$ \mathbf{w} = \begin{bmatrix} w_1 \\ w_2 \\ \vdots \\ w_{n_x} \end{bmatrix} \in \mathbb{R}^{n_x}, \qquad \mathbf{x}^{(i)} = \begin{bmatrix} x_1^{(i)} \\ x_2^{(i)} \\ \vdots \\ x_{n_x}^{(i)} \end{bmatrix} \in \mathbb{R}^{n_x}, \qquad b \in \mathbb{R}. \tag{5.2} $$
Equation 5.2: The objects behind $z^{(i)}$: two length-$n_x$ column vectors and a scalar bias

The score $z^{(i)}$ is then a dot product — the transpose $\mathbf{w}^\top$ turns the weight column into a row, which multiplies the input column to give a single number, shifted by the bias:

$$ z^{(i)} = \mathbf{w}^\top\mathbf{x}^{(i)} + b = \underbrace{\begin{bmatrix} w_1 & w_2 & \cdots & w_{n_x} \end{bmatrix}}_{1 \times n_x} \underbrace{\begin{bmatrix} x_1^{(i)} \\ x_2^{(i)} \\ \vdots \\ x_{n_x}^{(i)} \end{bmatrix}}_{n_x \times 1} +\, b = \sum_{k=1}^{n_x} w_k\, x_k^{(i)} + b . \tag{5.3} $$
Equation 5.3: $z^{(i)}$ written out — row times column (a dot product), plus the bias

The shapes confirm the result is a scalar: $(1 \times n_x)\times(n_x \times 1) = 1\times 1$, and adding $b$ keeps it a single number. Passing it through the sigmoid gives the prediction $\hat{y}^{(i)} = \sigma(z^{(i)})$, again a single number in $(0,1)$.

Given the training set ${(\mathbf{x}^{(1)}, y^{(1)}), \ldots, (\mathbf{x}^{(m)}, y^{(m)})}$, the goal is to make each prediction match its label: $\hat{y}^{(i)} \approx y^{(i)}$.

Loss: measuring one prediction

An obvious first guess for the loss is the squared error:

$$ \mathcal{L}(\hat{y}, y) = \tfrac{1}{2}\,(\hat{y} - y)^2 . \tag{5.4} $$
Equation 5.4: Squared error — intuitive, but not used here

It is the natural choice for linear regression, but for logistic regression it is a poor one. Because the prediction $\hat{y} = \sigma(z)$ is squashed through the sigmoid, feeding the squared error into the cost $J(\mathbf{w}, b)$ produces a non-convex surface — one with many bumps, flat spots, and local minima. Gradient descent only ever walks downhill, so on such a surface it can settle into a shallow dip far from the best solution.

Convex vs. non-convex. Picture the cost as a landscape whose height is the error and whose position is the parameters $(\mathbf{w}, b)$. A convex cost is a single smooth bowl: no matter where you start, walking downhill always leads to the one lowest point (the global minimum). A non-convex cost is a bumpy terrain with several valleys, so downhill steps may get stuck in a poor one. We therefore want a loss whose cost stays convex.

The logistic loss is designed precisely so that the cost remains convex — a single bowl — while still comparing probabilities correctly. It is also called binary cross-entropy:

$$ \mathcal{L}(\hat{y}, y) = -\Bigl(\, y \log \hat{y} \;+\; (1 - y)\log(1 - \hat{y}) \,\Bigr). \tag{5.5} $$
Equation 5.5: Logistic (cross-entropy) loss

This formula is not arbitrary — it falls out of a simple principle:

Where it comes from (maximum likelihood). The model reads $\hat{y}$ as the probability that $y = 1$. So the probability it assigns to the actual label is $\hat{y}$ when $y = 1$ and $1 - \hat{y}$ when $y = 0$. Both cases are captured by the single expression $\hat{y}^{\,y}\,(1 - \hat{y})^{\,1 - y}$. A good model makes this probability large; taking the negative logarithm turns "make the probability large" into "make the loss small," and $-\log\bigl(\hat{y}^{\,y}(1-\hat{y})^{1-y}\bigr) = -\bigl(y\log\hat{y} + (1-y)\log(1-\hat{y})\bigr)$ — exactly the loss above.

Why this loss works

The key is that $y$ is always exactly $0$ or $1$, so inside the sum $y\log\hat{y} + (1-y)\log(1-\hat{y})$ one term is always multiplied by zero and vanishes — if $y = 1$ the factor $(1-y) = 0$ removes the second term, and if $y = 0$ the factor $y = 0$ removes the first:

$$ \mathcal{L}(\hat{y}, y) = \begin{cases} -\log \hat{y} & \text{if } y = 1 \;\Rightarrow\; \text{want } \hat{y} \text{ large (near 1)},\\[4pt] -\log(1 - \hat{y}) & \text{if } y = 0 \;\Rightarrow\; \text{want } \hat{y} \text{ small (near 0)}. \end{cases} \tag{5.6} $$
Equation 5.6: The two cases — each pushes $\hat{y}$ toward the true label

Read each case as a force on the prediction:

So the loss does two jobs at once: it rewards predictions that agree with the label (loss $\to 0$), and it punishes confident mistakes harshly — being sure of the wrong answer costs an unbounded amount. That steep penalty is what makes logistic regression produce well-calibrated probabilities rather than reckless guesses, and (unlike squared error) it does so on a convex, single-bowl cost that gradient descent can reliably minimise.

Figure 5.1: The logistic loss for each label. When $y = 1$ (blue) the loss $-\log \hat{y}$ is small only when $\hat{y}$ is near 1; when $y = 0$ (orange) the loss $-\log(1 - \hat{y})$ is small only when $\hat{y}$ is near 0. A confidently wrong prediction is penalised sharply.

Cost: averaging over the training set

The loss scores one example. The cost $J$ is the average loss across all $m$ training examples — the single number that training minimises over the parameters $\mathbf{w}$ and $b$:

$$ J(\mathbf{w}, b) = \frac{1}{m}\sum_{i=1}^{m} \mathcal{L}\!\bigl(\hat{y}^{(i)}, y^{(i)}\bigr) = -\frac{1}{m}\sum_{i=1}^{m} \Bigl[\, y^{(i)} \log \hat{y}^{(i)} + \bigl(1 - y^{(i)}\bigr)\log\bigl(1 - \hat{y}^{(i)}\bigr) \Bigr]. \tag{5.7} $$
Equation 5.7: Cost function — average logistic loss over the training set

The distinction is worth repeating: the loss $\mathcal{L}$ is defined on a single example $(\mathbf{x}^{(i)}, y^{(i)})$, while the cost $J$ is a function of the parameters $\mathbf{w}, b$ and summarises the model's error over the entire training set. Training the model means finding the $\mathbf{w}$ and $b$ that make $J(\mathbf{w}, b)$ as small as possible — which is the job of gradient descent, the subject of the next section.


Gradient descent

Training the model means choosing the parameters that make the cost $J(\mathbf{w}, b)$ as small as possible. Gradient descent is the algorithm that finds them: start somewhere, then repeatedly take a small step downhill until you reach the bottom.

The cost surface

Picture $J(\mathbf{w}, b)$ as a surface: the horizontal position is the choice of parameters $(\mathbf{w}, b)$ and the height is the resulting cost. Because the logistic loss is convex (§5.1), this surface is a single smooth bowl with exactly one lowest point — the global optimum — and no misleading local minima to fall into. Wherever gradient descent starts on the bowl, stepping downhill leads to that same bottom. (Had we used the squared-error loss, the surface would be bumpy and a downhill walk could stall in a shallow dip — this is the payoff for choosing the cross-entropy loss.)

The update rule

Take the one-parameter picture first: plot the cost $J(w)$ against a single weight $w$. The derivative $\dfrac{dJ}{dw}$ is the slope of that curve — it tells you which way, and how steeply, the cost is changing at the current $w$. Gradient descent repeatedly steps in the opposite direction of the slope:

$$ w := w - \alpha\,\frac{dJ}{dw}. \tag{6.1} $$
Equation 6.1: Gradient descent update (one parameter), repeated until it settles

The symbol $:=$ means "replace $w$ with this new value" (an assignment, not an equation), and $\alpha$ (alpha) is the learning rate — how large a step to take. As a loop, the whole algorithm is just this one line repeated:

Repeat until converged {
    dw = dJ/dw           # the slope of the cost at the current w
    w  := w - α * dw     # step downhill:  new w = old w − (learning rate)·(slope)
}

The derivative is the slope of the tangent. Geometrically, $\dfrac{dJ}{dw}$ is the steepness of the straight tangent line that just grazes the curve at the current $w$ — its rise over run. This has a convenient side effect: far from the minimum the curve is steep, so the slope is large and each step is big; as $w$ nears the flat bottom the tangent levels off, the slope shrinks toward $0$, and the steps automatically become smaller. Gradient descent therefore slows down on its own as it approaches the minimum and settles there, with no extra bookkeeping.

Why it always heads toward the minimum. Because we subtract the slope, the update self-corrects from either side (this is what the two arrows in the sketch show):

Figure 6.1: Gradient descent in one dimension, from two starting points. From either side the update $w := w - \alpha\cdot(\text{slope})$ walks the weight down toward the minimum at $w = 3$; the steps shrink as the slope flattens. Red started on the right (slope $> 0$, moving left); purple started on the left (slope $< 0$, moving right).

A worked example. Take the toy cost $J(w) = \tfrac{1}{2}(w-3)^2 + \tfrac{1}{2}$, whose slope is $\dfrac{dJ}{dw} = w - 3$ (zero exactly at the minimum $w = 3$). With a learning rate $\alpha = 0.4$ and a start at $w = 6$, each step plugs the current slope into the update rule:

iteration$w$slope $\;w-3$new $w = w - 0.4\,(w-3)$
06.0003.0004.800
14.8001.8004.080
24.0801.0803.648
33.6480.6483.389
43.3890.3893.233
$\to 3.000$
Table 6.1: Gradient descent on $J(w)=\tfrac12(w-3)^2+\tfrac12$ with $\alpha=0.4$, starting at $w=6$. The slope shrinks every step, so the steps shrink too and $w$ homes in on the minimum $w=3$.

Notice the slope column ($3.0 \to 1.8 \to 1.08 \to \ldots$) shrinking geometrically: the closer $w$ gets to $3$, the gentler the slope and the smaller the step, so $w$ approaches the minimum quickly at first and then eases in.

Choosing the learning rate $\alpha$. The step size is $\alpha$ times the slope, so $\alpha$ trades off speed against stability: too small and training crawls (tiny steps, many iterations); too large and the steps overshoot the minimum, bouncing back and forth across the valley — and if $\alpha$ is large enough they can even grow and diverge. A well-chosen $\alpha$ is large enough to make steady progress but small enough to settle.

Updating both parameters

The real cost depends on both $\mathbf{w}$ and $b$, so each is updated with its own slope — a partial derivative, which measures how $J$ changes with one parameter while the other is held fixed. On every iteration, update both together:

$$ w := w - \alpha\,\frac{\partial J(\mathbf{w}, b)}{\partial w}, \qquad b := b - \alpha\,\frac{\partial J(\mathbf{w}, b)}{\partial b}. \tag{6.2} $$
Equation 6.2: Gradient descent update (both parameters)

Two notes on notation:

$$ w := w - \alpha\,\texttt{dw}, \qquad b := b - \alpha\,\texttt{db}. \tag{6.3} $$
Equation 6.3: The same update in code notation

Everything now hinges on one remaining question — how do we actually compute dw and db? Finding the derivatives of the cost with respect to each parameter (via the chain rule, and for a full network, backpropagation) is the subject of the next notes.


Derivatives

Gradient descent runs on one ingredient: the derivative $\dfrac{dJ}{dw}$ — the slope of the cost. Before computing it for the logistic model, it helps to be completely clear on what a derivative is, using the simplest possible function.

Slope = height over width

The derivative of a function $f$ at a point measures how much its output changes when you nudge the input by a tiny amount — the slope of the curve there, read as "rise over run." Take the straight line $f(a) = 3a$:

Figure 7.1: The line $f(a) = 3a$ with a slope triangle at $a = 2$. Going a run of 1 to the right raises the line by a rise of 3 (the orange triangle), so the slope — the derivative — is rise / run $= 3$, and it is the same at every point (marked at $a = 2$ and $a = 5$).

To measure the slope, nudge $a$ by a tiny width and see how far the output moves — the height:

point $a$$f(a) = 3a$nudge to $a + 0.001$$f(a+0.001)$height $\Delta f$slope $\dfrac{\Delta f}{0.001}$
262.0016.0030.0033
5155.00115.0030.0033
Table 7.1: Nudging the input of $f(a)=3a$ by a small width $0.001$ raises the output by height $0.003$, so the slope is $0.003 / 0.001 = 3$ — the same at every point.

Bumping $a$ by $0.001$ (the width) raises $f$ by $0.003$ (the height), so

$$ \text{slope} = \frac{\text{height}}{\text{width}} = \frac{0.003}{0.001} = 3 . $$

That number — how much the output moves per unit of input — is the derivative.

The same slope everywhere

For a straight line the slope is identical at every point: the table gives $3$ at $a = 2$ and at $a = 5$. So the derivative of $f(a) = 3a$ is just the constant $3$, written in either of two equivalent notations:

$$ \frac{d\,f(a)}{da} = 3 = \frac{d}{da}\,f(a). \tag{7.1} $$
Equation 7.1: The derivative of $f(a)=3a$

Read $\frac{d\,f(a)}{da}$ as "the derivative of $f$ with respect to $a$"; the second form $\frac{d}{da}f(a)$ is the same thing with $\frac{d}{da}$ acting as an operator on $f$. The intuition to carry forward: if $a$ increases by a tiny amount $\varepsilon$, then $f$ increases by (derivative)$\,\times \varepsilon$ — here $3 \times 0.001 = 0.003$, exactly the height in the table.

A constant slope is special to straight lines. For a curved function the steepness changes from point to point, so the derivative is different at each $a$ — which is the next example.

Curved functions: a changing slope

A straight line has one slope everywhere; a curved function does not. Its steepness — and therefore its derivative — changes from point to point. Take $f(a) = a^2$, and draw the slope triangle at two places:

Figure 7.2: The curve $f(a) = a^2$. The green triangle at $a = 2$ has rise 4 over run 1 (slope 4); the orange triangle at $a = 5$ has rise 10 over run 1 (slope 10). The triangle's hypotenuse is the tangent at that point, so its slope is the derivative — steeper where the curve is steeper.

To see the actual nudge from the table — moving $a$ from $2$ to $2 + \varepsilon$ — zoom in extremely tight around $a = 2$, so tight that the whole horizontal axis is only $0.005$ wide:

Figure 7.3: A true zoom onto the nudge in the table. The input moves from $a = 2$ (green point) to $a = 2 + \varepsilon$ with $\varepsilon = 0.001$ (orange point), and the output climbs by the height $\Delta f \approx 0.004$. The slope over the nudge is height / width $= 0.004 / 0.001 = 4$ — the derivative. At this scale $a^2$ is indistinguishable from a straight line, so this nudge slope already equals the tangent slope.

The same nudge trick measures each slope. Because the curve bends, the tiny height picked up is different at each point:

point $a$$f(a)=a^2$$f(a+0.001)$height $\Delta f$slope $\dfrac{\Delta f}{0.001}$$2a$
244.004001$\approx 0.004$$\approx 4$4
52525.010001$\approx 0.010$$\approx 10$10
Table 7.2: Nudging $f(a)=a^2$ by $0.001$. The height (and so the slope) grows with $a$ — unlike the line, the slope is not constant. Each slope matches $2a$.

Both slopes match the general power rule for $a^2$:

$$ \frac{d}{da}\,a^2 = 2a \qquad\Longrightarrow\qquad \underbrace{2a\big|_{a=2} = 4}_{\text{slope at }a=2}, \quad \underbrace{2a\big|_{a=5} = 10}_{\text{slope at }a=5}. \tag{7.2} $$
Equation 7.2: The derivative of $f(a)=a^2$ is $2a$

Where does $2a$ come from? Nudge $a$ by a tiny $\varepsilon$ and expand the square:

$$ f(a+\varepsilon) = (a+\varepsilon)^2 = a^2 + \underbrace{2a\,\varepsilon}_{\text{the height}} + \underbrace{\varepsilon^2}_{\text{negligible}}, \qquad \text{slope} = \frac{\Delta f}{\varepsilon} \approx \frac{2a\,\varepsilon}{\varepsilon} = 2a . \tag{7.3} $$
Equation 7.3: Why the slope is $2a$: expand $(a+\varepsilon)^2$ and drop the negligible $\varepsilon^2$

With $\varepsilon = 0.001$ the leftover term $\varepsilon^2 = 0.000001$ is a thousand times smaller than the height $2a\varepsilon$, so it vanishes in the limit — leaving the clean slope $2a$. This is exactly the kind of local, "nudge-and-see" derivative that the chain rule chains together to differentiate the sigmoid and the cost, and that backpropagation applies layer by layer.

More examples: cubes and logarithms

The nudge trick is not special to $a^2$ — the same procedure reads off the slope of any function. Two more are worth having on hand, because they and the power rule cover almost everything the later parts need.

First a cube, $f(a) = a^3$. Bump $a$ from $2$ to $2.001$ and watch the output:

$$ \frac{d}{da}\,a^3 = 3a^2, \qquad f(2.001) = 8.012006\ldots, \qquad \text{slope} = \frac{8.012 - 8}{0.001} \approx 12 = 3\cdot 2^2 . \tag{7.4} $$
Equation 7.4: The derivative of $f(a)=a^3$ is $3a^2$

The measured slope $\approx 12$ matches $3a^2\big|_{a=2} = 3\cdot 4 = 12$ exactly. It comes from the same "expand and drop the tiny term" argument as before: $(a+\varepsilon)^3 = a^3 + 3a^2\varepsilon + 3a\varepsilon^2 + \varepsilon^3$, and everything past $3a^2\varepsilon$ is negligible, leaving the height $3a^2\varepsilon$ and the slope $3a^2$.

Next the natural logarithm, $f(a) = \log_e(a)$ — usually written $\ln(a)$. Its derivative is a particularly clean one:

$$ \frac{d}{da}\,\ln(a) = \frac{1}{a}, \qquad \underbrace{\frac{1}{a}\bigg|_{a=2} = \tfrac{1}{2} = 0.5}_{\text{slope at }a=2}. \tag{7.5} $$
Equation 7.5: The derivative of $f(a)=\ln(a)$ is $1/a$

The nudge confirms it. Going from $a = 2$ to $a = 2.001$, the logarithm rises by about $0.0005$ over a width of $0.001$, so the slope is $0.0005 / 0.001 = 0.5$ — exactly $1/a$ at $a = 2$:

function $f(a)$point $a$$f(a)$$f(a+0.001)$height $\Delta f$slope $\dfrac{\Delta f}{0.001}$rule
$a^3$288.012006$\approx 0.012$$\approx 12$$3a^2 = 12$
$\ln a$20.69310.6936$\approx 0.0005$$\approx 0.5$$1/a = 0.5$
Table 7.3: The nudge trick on a cube and a logarithm. Each measured slope matches the rule in the last column — the power rule $3a^2$ for $a^3$, and $1/a$ for $\ln a$.

Look these up, don't memorise them. The point is not to build a table of derivatives in your head. It is that every one of these rules is just the answer to the same question — "if I nudge the input by a tiny $\varepsilon$, how far does the output move?" — and that a slope is always height over width. When a function is too tangled to nudge in one shot, we break it into simple steps and nudge each one; that is the computation graph, next.


The computation graph

Gradient descent needs the derivative of the cost with respect to every parameter. For anything more complicated than $a^2$, computing that derivative directly is awkward. The computation graph is the tool that makes it systematic: it lays a computation out as a chain of tiny steps so that, later, the chain rule can walk back through them one link at a time.

Breaking a computation into steps

The idea is to take a formula and rewrite it as a sequence of elementary operations, each producing a named intermediate result. A neural network computes its cost in exactly this staged, left-to-right way — first a linear score, then a sigmoid, then a loss — so understanding the pattern on a toy example transfers directly to the real thing.

A worked example

Take a function of three inputs $a$, $b$, $c$:

$$ J(a, b, c) = 3\,(a + bc). \tag{8.1} $$
Equation 8.1: A small function to break into steps

Computing $J$ has three natural stages. Give each intermediate a name:

$$ u = bc, \qquad v = a + u, \qquad J = 3v. \tag{8.2} $$
Equation 8.2: The same computation as three elementary steps

Each step does one operation — a multiply, an add, another multiply — and feeds the next. Drawn as a graph, the inputs flow left to right through the intermediates $u$ and $v$ into the output $J$:

Figure 8.1: The computation graph for J = 3(a + bc). Inputs a, b, c flow left to right: b and c multiply into u = bc, then a is added to give v = a + u, then a final ×3 gives J = 3v. Numbers show the forward pass with a = 5, b = 3, c = 2.

The forward pass

Reading the graph left to right and plugging in $a = 5$, $b = 3$, $c = 2$ fills in every box in order:

$$ u = bc = 3\cdot 2 = 6, \qquad v = a + u = 5 + 6 = 11, \qquad J = 3v = 3\cdot 11 = 33. \tag{8.3} $$
Equation 8.3: Forward pass: evaluate each step in order

This left-to-right sweep that produces the final value is the forward pass. It is the same direction in which a network computes its prediction and cost. The reason to bother staging it this way is the return trip: to get the derivatives $dJ/da$, $dJ/db$, $dJ/dc$, we walk the graph right to left, and that backward sweep is where the chain rule does its work.


Derivatives on a computation graph

The forward pass went left to right and produced $J = 33$. To find how $J$ responds to each input we go the other way — right to left — computing one derivative per step and multiplying them together as we go. This backward sweep is the whole idea behind backpropagation; here it is on the toy graph.

One step back: $dJ/dv$

Start at the output and take a single step back, to $v$. Since $J = 3v$, nudging $v$ by a tiny amount changes $J$ by three times as much:

$$ \frac{dJ}{dv} = 3 . \tag{9.1} $$
Equation 9.1: One step back: $J = 3v$ has slope 3

Check it with a nudge: push $v$ from $11$ to $11.001$. Then $J = 3v$ goes from $33$ to $33.003$ — a change of $0.003$ over a width of $0.001$, giving slope $3$. So increasing $v$ by "one unit" increases $J$ by $3$.

The chain rule: $dJ/da$

Now go one step further back, to the input $a$. Here $a$ does not touch $J$ directly — it affects $v$, and $v$ affects $J$. The chain rule says: to get the overall effect, multiply the effects along the path $a \to v \to J$:

$$ \frac{dJ}{da} = \underbrace{\frac{dJ}{dv}}_{=\,3}\cdot\underbrace{\frac{dv}{da}}_{=\,1} = 3 \cdot 1 = 3 . \tag{9.2} $$
Equation 9.2: Chain rule along the path $a \to v \to J$

The first factor $\dfrac{dJ}{dv} = 3$ we already have. The second, $\dfrac{dv}{da}$, is local to the step $v = a + u$: bumping $a$ by one raises $v = a + u$ by exactly one, so $\dfrac{dv}{da} = 1$. Their product is $3$.

The nudge confirms it end to end: take $a$ from $5$ to $5.001$. Then $v = a + u$ becomes $11.001$, and $J = 3v$ becomes $33.003$ — a change of $0.003$ in $J$ over $0.001$ in $a$, so $\dfrac{dJ}{da} = 3$. Notice this is the same $0.003$ change as when we nudged $v$: because $a$ pushes $J$ only through $v$, the two effects multiply, which is exactly what the chain rule encodes.

Why "multiply along the path." The chain rule is just the nudge picture applied twice. Nudging $a$ by $\varepsilon$ moves $v$ by $\dfrac{dv}{da}\,\varepsilon$ (here $1\cdot\varepsilon$); that change in $v$ then moves $J$ by $\dfrac{dJ}{dv}\times(\text{change in }v) = 3\cdot(1\cdot\varepsilon)$. The two local slopes stack up by multiplication, which is why the backward pass is a running product.

A shorthand for coded gradients

Every quantity we care about on the backward pass is the derivative of the one final output $J$ with respect to some variable. That is a mouthful to write, so it gets a uniform shorthand:

$$ \underbrace{\frac{d(\text{final output})}{d(\text{var})}}_{\text{written }\;\frac{dJ}{d\,\texttt{var}}} \;\;\longrightarrow\;\; \texttt{d}\langle\texttt{var}\rangle . \tag{9.3} $$
Equation 9.3: Backward-pass shorthand: derivative of the final output w.r.t. a variable

So in code the derivative $\dfrac{dJ}{dv}$ is simply the variable dv, $\dfrac{dJ}{da}$ is da, and so on. This is the same convention the logistic-regression code in the next notes uses — dw, db, dz are all "derivative of the final cost with respect to this variable." It is worth stressing: dv does not mean "a little bit of $v$" — it means "how much the final output $J$ moves when $v$ moves."

Finishing the backward pass

The remaining inputs are reached the same way — keep multiplying local slopes along each path back to $J$. Working right to left:

Through $u$. The step is $v = a + u$, so $\dfrac{dv}{du} = 1$, and

$$ \frac{dJ}{du} = \frac{dJ}{dv}\cdot\frac{dv}{du} = 3 \cdot 1 = 3 . \tag{9.4} $$
Equation 9.4: $dJ/du$ — one more link on the chain

To $b$ and $c$. Both feed the step $u = bc$. Its local slopes are $\dfrac{du}{db} = c = 2$ and $\dfrac{du}{dc} = b = 3$ (the derivative of $bc$ with respect to one factor is the other factor). Chaining each through $u$:

$$ \frac{dJ}{db} = \underbrace{\frac{dJ}{du}}_{=\,3}\cdot\underbrace{\frac{du}{db}}_{=\,c\,=\,2} = 3\cdot 2 = 6, \qquad \frac{dJ}{dc} = \underbrace{\frac{dJ}{du}}_{=\,3}\cdot\underbrace{\frac{du}{dc}}_{=\,b\,=\,3} = 3\cdot 3 = 9 . \tag{9.5} $$
Equation 9.5: $dJ/db$ and $dJ/dc$ — chaining through $u = bc$

A direct nudge checks both: taking $b$ from $3$ to $3.001$ makes $u = bc = 6.002$, then $v = 11.002$ and $J = 33.006$ — a change of $0.006$ over $0.001$, so $6$. Likewise nudging $c$ by $0.001$ moves $J$ by $0.009$, giving $9$.

Notice how cheaply the last two gradients came: once $\dfrac{dJ}{du} = 3$ is known, $b$ and $c$ each cost just one extra local multiply. That reuse — compute a node's gradient once, then hand it to everything feeding into it — is the efficiency that makes backpropagation practical on real networks.

The gradients at a glance

The whole backward pass fits in one picture. It is the same graph as the forward pass, read the other way: start at $J$ on the left and let the gradient flow out to the inputs. Every node carries two lines — its forward relation and value on top (what the variable is: $u = bc = 6$, $v = a + u = 11$, and the output $J = 3v = 33$), and its gradient $dJ/d!\cdot$ underneath. Each red edge carries a local slope, and a node's gradient is just the product of the edge labels along the path back from $J$ — that running product is the chain rule made visual.

Figure 9.1: The backward pass for J = 3(a + bc). Each node's top line is its forward relation and value (so J = 3v = 33 is the value from the forward pass); the bottom line is its gradient dJ/d·. Gradients flow out of J (left) to the inputs (right); the red edges are the local slopes, and each node's gradient is the product of the edge labels on the path from J — so dJ/db = 3 · 1 · 2 = 6 and dJ/dc = 3 · 1 · 3 = 9.

Reading it off: the top line traces the forward computation — the inputs $a=5,\,b=3,\,c=2$ build up to $u = bc = 6$, then $v = a + u = 11$, then the output $J = 3v = 33$ (that is where the $33$ comes from). The bottom line and the red edges trace the backward pass: $b$ sits at the end of the path $J \to v \to u \to b$, so its gradient is the product of that path's edge labels, $3\cdot 1\cdot 2 = 6$; likewise $c$ gives $3\cdot 1\cdot 3 = 9$. The table below is the same information in rows.

variablechainlocal slopegradient (= "d·")nudge check: $\Delta J / 0.001$
$v$$J = 3v$$3$$dJ/dv = 3$$0.003 \to 3$
$u$$\frac{dJ}{dv}\cdot\frac{dv}{du}$$dv/du = 1$$dJ/du = 3$$0.003 \to 3$
$a$$\frac{dJ}{dv}\cdot\frac{dv}{da}$$dv/da = 1$$dJ/da = 3$$0.003 \to 3$
$b$$\frac{dJ}{du}\cdot\frac{du}{db}$$du/db = c = 2$$dJ/db = 6$$0.006 \to 6$
$c$$\frac{dJ}{du}\cdot\frac{du}{dc}$$du/dc = b = 3$$dJ/dc = 9$$0.009 \to 9$
Table 9.1: The full backward pass for $J = 3(a+bc)$ at $a=5,\,b=3,\,c=2$. Each gradient is the running product $dJ/dv$ (or $dJ/du$) times one local slope; the nudge column verifies it by moving the variable by $0.001$ and reading the change in $J$.

What a slight move does to $J$

A gradient is not just a number to feed gradient descent — it is a concrete sensitivity: how much the output $J$ moves when you give one input a small push. Because $dJ/da = 3$, nudging $a$ by a small $\delta$ changes $J$ by about $3\delta$; $dJ/db = 6$ makes $b$ twice as influential; and $dJ/dc = 9$ makes $c$ the strongest lever of the three. So the answer to "if I move a parameter a little, what happens to the $33$?" is read straight off the gradients.

When all three move at once by small amounts, their effects simply add up — this is the total change (the total differential):

$$ \Delta J \;\approx\; \frac{dJ}{da}\,\Delta a + \frac{dJ}{db}\,\Delta b + \frac{dJ}{dc}\,\Delta c \;=\; 3\,\Delta a + 6\,\Delta b + 9\,\Delta c . \tag{9.6} $$
Equation 9.6: A simultaneous slight move: the effects add, weighted by each gradient

Proof (by direct expansion). For this function the formula is not just an approximation — we can expand it exactly and see precisely which part is linear and which part is the small error. Perturb all three inputs at once, $a\to a+\Delta a$, $b\to b+\Delta b$, $c\to c+\Delta c$, and substitute into $J = 3(a+bc)$:

$$ J(a+\Delta a,\ b+\Delta b,\ c+\Delta c) = 3\Bigl[(a+\Delta a) + (b+\Delta b)(c+\Delta c)\Bigr]. \tag{9.7} $$
Equation 9.7: Substitute the perturbed inputs

Expand the product $(b+\Delta b)(c+\Delta c) = bc + c\,\Delta b + b\,\Delta c + \Delta b\,\Delta c$ and collect terms:

$$ \begin{align} J(a+\Delta a,\ b+\Delta b,\ c+\Delta c) &= 3\bigl[\,a + bc\,\bigr] \;+\; 3\bigl[\,\Delta a + c\,\Delta b + b\,\Delta c\,\bigr] \;+\; 3\,\Delta b\,\Delta c \tag{9.8.1} \\ &= \underbrace{J(a,b,c)}_{\text{original}} \;+\; \underbrace{3\,\Delta a + 3c\,\Delta b + 3b\,\Delta c}_{\text{first order (linear in the }\Delta\text{'s})} \;+\; \underbrace{3\,\Delta b\,\Delta c}_{\text{second order}} . \tag{9.8.2} \end{align} $$
Equation 9.8: Expand and group by order of smallness

Subtracting the original value gives the exact change:

$$ \Delta J = \underbrace{3\,\Delta a + 3c\,\Delta b + 3b\,\Delta c}_{\text{linear}} \;+\; \underbrace{3\,\Delta b\,\Delta c}_{\text{remainder}} . \tag{9.9} $$
Equation 9.9: The exact change, split into a linear part and a second-order remainder

Now recognise the coefficients: they are exactly the partial derivatives computed on the backward pass, $\dfrac{\partial J}{\partial a} = 3$, $\dfrac{\partial J}{\partial b} = 3c$, $\dfrac{\partial J}{\partial c} = 3b$. So the linear part is the gradient-weighted sum, and the entire error is the single term $3\,\Delta b\,\Delta c$:

$$ \Delta J = \frac{\partial J}{\partial a}\,\Delta a + \frac{\partial J}{\partial b}\,\Delta b + \frac{\partial J}{\partial c}\,\Delta c \;+\; \underbrace{3\,\Delta b\,\Delta c}_{\text{quadratic — negligible for small moves}} . \tag{9.10} $$
Equation 9.10: The change equals the total differential plus a second-order remainder

The remainder $3\,\Delta b\,\Delta c$ is a product of two small quantities. If each step is of size $\delta$, it is $\sim 3\delta^2$, which shrinks far faster than the linear $\sim\delta$ terms as $\delta\to 0$ — at $\delta = 0.01$ the linear part is order $0.01$ while the remainder is order $0.0003$, a hundred times smaller. Dropping it leaves exactly the total differential (eq. eq-total-diff). $\blacksquare$

The general statement. This is one instance of the multivariable first-order Taylor expansion: for any smooth $f$, $f(\mathbf{x} + \Delta\mathbf{x}) = f(\mathbf{x}) + \sum_i \frac{\partial f}{\partial x_i}\,\Delta x_i + O(\lVert\Delta\mathbf{x}\rVert^2)$. One clean way to see it is to change the inputs one at a time and telescope: $\Delta J = \bigl[f(a{+}\Delta a, b{+}\Delta b, c{+}\Delta c) - f(a, b{+}\Delta b, c{+}\Delta c)\bigr] + \bigl[f(a, b{+}\Delta b, c{+}\Delta c) - f(a, b, c{+}\Delta c)\bigr] + \bigl[f(a, b, c{+}\Delta c) - f(a, b, c)\bigr]$. Each bracket varies only one input, so by the single-variable derivative it equals that partial times its $\Delta$ (plus higher-order terms), and the three partials add up. The nudge picture from §7, applied once per variable.

If each input moves by the same small step $\delta$, this becomes $\Delta J \approx (3 + 6 + 9)\,\delta = 18\,\delta$ — so the output moves about 18 times as far as the inputs. (Working it out exactly gives $J = 33 + 18\delta + 3\delta^2$; the extra $3\delta^2$ is the tiny curvature from $b$ and $c$ multiplying each other, and for a slight move it is negligible — which is exactly why the local slopes, the gradients, are what matter.)

Figure 9.2: How $J$ responds to a slight move $\delta$ in the inputs, starting from $J = 33$ at $\delta = 0$. Moving one input at a time gives a straight line whose slope is that input's gradient: $a \to 3$, $b \to 6$, $c \to 9$. Moving all three together (red) has slope $3 + 6 + 9 = 18$ near $\delta = 0$ and bends up slightly (the $b\cdot c$ interaction); the grey dashed line is the gradient's linear estimate $33 + 18\delta$, which the red curve hugs for small $\delta$.

The chart makes the ranking visual: the steeper the line, the more that input matters, so $c$ (slope $9$) swings $J$ hardest and $a$ (slope $3$) the gentlest — precisely the gradients from the backward pass. And moving everything together (red) is just the three slopes stacked into one, $18$, with a barely-perceptible bend near the middle.

That is the entire mechanics of backpropagation on a small graph: one forward sweep to fill in the values, one backward sweep multiplying local slopes to fill in the gradients — and those gradients tell you exactly how a slight move in any parameter moves the final output. The next section runs this exact procedure on the logistic-regression graph — $z = \mathbf{w}^\top\mathbf{x} + b$, then $\hat{y} = \sigma(z)$, then the loss $\mathcal{L}$ — to produce the dw and db that gradient descent needs.


Logistic regression gradient descent

Everything is now in place to answer the question left open in §6: how do we actually compute dw and db for logistic regression? The answer is the computation-graph backward pass from §9, applied to the logistic model instead of the toy function. We do it first for a single training example; averaging over $m$ examples (§11) is a short step from there.

The logistic-regression computation graph

Recall the model and its loss, all three stages named. To keep the picture concrete we use just two features $x_1, x_2$ (so two weights $w_1, w_2$ and a bias $b$); the pattern for $n_x$ features is identical:

$$ z = w_1 x_1 + w_2 x_2 + b, \qquad a = \hat{y} = \sigma(z), \qquad \mathcal{L}(a, y) = -\bigl(y\log a + (1-y)\log(1-a)\bigr). \tag{10.1} $$
Equation 10.1: Logistic regression as three chained steps

These are exactly the steps of a computation graph: the parameters and inputs combine into the score $z$, the sigmoid turns $z$ into the prediction $a$, and the loss compares $a$ to the true label $y$.

Figure 10.1: The forward computation graph for one example of logistic regression. The weights w₁, w₂, bias b and features x₁, x₂ combine into z; the sigmoid maps z to the prediction a = ŷ; the loss L compares a with the label y. This is the same left-to-right structure as the toy graph J = 3(a + bc).

The backward pass: $da$, $dz$, $dw$, $db$

We want the derivative of the loss with respect to each parameter. Following the graph right to left, we compute one link at a time, reusing the d-shorthand from §9.3: da means $\dfrac{d\mathcal{L}}{da}$, dz means $\dfrac{d\mathcal{L}}{dz}$, and so on.

Step 1 — da, the loss's slope in $a$. Differentiate $\mathcal{L} = -(y\log a + (1-y)\log(1-a))$ with respect to $a$, using $\frac{d}{da}\log a = \frac1a$ and $\frac{d}{da}\log(1-a) = -\frac{1}{1-a}$:

$$ \texttt{da} = \frac{d\mathcal{L}}{da} = -\frac{y}{a} + \frac{1-y}{1-a}. \tag{10.2} $$
Equation 10.2: Derivative of the loss with respect to the prediction $a$

Step 2 — dz, back through the sigmoid. By the chain rule, $\dfrac{d\mathcal{L}}{dz} = \dfrac{d\mathcal{L}}{da}\cdot\dfrac{da}{dz}$. The local factor $\dfrac{da}{dz}$ is the derivative of the sigmoid, which has the clean form $a(1-a)$:

$$ \frac{da}{dz} = \sigma'(z) = \sigma(z)\bigl(1 - \sigma(z)\bigr) = a(1-a). \tag{10.3} $$
Equation 10.3: The sigmoid's derivative

Why $\sigma'(z) = a(1-a)$. Write $\sigma(z) = (1 + e^{-z})^{-1}$. Then $\sigma'(z) = -(1+e^{-z})^{-2}\cdot(-e^{-z}) = \dfrac{e^{-z}}{(1+e^{-z})^{2}}$. Split it as $\dfrac{1}{1+e^{-z}}\cdot\dfrac{e^{-z}}{1+e^{-z}}$; the first factor is $\sigma(z)$, and the second is $\dfrac{e^{-z}}{1+e^{-z}} = 1 - \dfrac{1}{1+e^{-z}} = 1 - \sigma(z)$. Hence $\sigma'(z) = \sigma(z)(1-\sigma(z)) = a(1-a)$.

Now multiply the two factors. The algebra collapses to something remarkably simple:

$$ \texttt{dz} = \frac{d\mathcal{L}}{dz} = \underbrace{\left(-\frac{y}{a} + \frac{1-y}{1-a}\right)}_{\texttt{da}}\cdot\underbrace{a(1-a)}_{da/dz} = -y(1-a) + (1-y)\,a = a - y. \tag{10.4} $$
Equation 10.4: Back through the sigmoid — the terms cancel to $a - y$

The cancellation is worth seeing in full: $-\dfrac{y}{a}\,a(1-a) = -y(1-a)$ and $\dfrac{1-y}{1-a}\,a(1-a) = (1-y)a$; adding gives $-y + ya + a - ya = a - y$. So the gradient of the loss with respect to the score is just prediction minus label — the same tidy form the squared error would give, and the reason the cross-entropy loss pairs so naturally with the sigmoid.

Step 3 — dw₁, dw₂, db, back to the parameters. The last step is the linear map $z = w_1x_1 + w_2x_2 + b$, whose local slopes are $\dfrac{\partial z}{\partial w_1} = x_1$, $\dfrac{\partial z}{\partial w_2} = x_2$, and $\dfrac{\partial z}{\partial b} = 1$. Chaining each through dz:

$$ \texttt{dw}_1 = x_1\,\texttt{dz}, \qquad \texttt{dw}_2 = x_2\,\texttt{dz}, \qquad \texttt{db} = \texttt{dz}, \qquad\text{where } \texttt{dz} = a - y. \tag{10.5} $$
Equation 10.5: Gradients with respect to the parameters

Drawn as a backward graph — the mirror of the forward one — each red edge is a local slope and each node carries its gradient, exactly as in the toy example:

Figure 10.2: The backward pass for one example. Starting from the loss L, the gradient flows back: da = −y/a + (1−y)/(1−a), then through the sigmoid (×a(1−a)) it simplifies to dz = a − y, then to the parameters dw₁ = x₁·dz, dw₂ = x₂·dz, db = dz.

The single-example update

With the three gradients in hand, one gradient-descent step on this single example is the update rule from §6, now with concrete values to plug in:

$$ w_1 := w_1 - \alpha\,\texttt{dw}_1, \qquad w_2 := w_2 - \alpha\,\texttt{dw}_2, \qquad b := b - \alpha\,\texttt{db}. \tag{10.6} $$
Equation 10.6: One gradient-descent step (single example)

As straight-line pseudocode, the whole per-example computation is six lines:

z  = w1*x1 + w2*x2 + b        # forward: score
a  = σ(z)                     # forward: prediction
dz = a − y                    # backward: through loss + sigmoid (a − y)
dw1 = x1 * dz                 # backward: to the weights …
dw2 = x2 * dz
db  = dz                      # … and the bias
# then step:  w1 −= α*dw1;  w2 −= α*dw2;  b −= α*db

This trains on one example. Real training minimises the cost over the whole set — the average loss over all $m$ examples — which is the next section.


Gradient descent on $m$ examples

A single example gives one noisy tug on the parameters. Training uses the cost $J$ — the average loss over all $m$ examples (§5.3) — so the gradient we actually descend is the derivative of that average.

The cost is the average loss

Recall the cost and the per-example predictions:

$$ J(\mathbf{w}, b) = \frac{1}{m}\sum_{i=1}^{m} \mathcal{L}\bigl(a^{(i)}, y^{(i)}\bigr), \qquad a^{(i)} = \sigma\bigl(z^{(i)}\bigr) = \sigma\bigl(\mathbf{w}^\top\mathbf{x}^{(i)} + b\bigr). \tag{11.1} $$
Equation 11.1: Cost as the average loss over $m$ examples

Because differentiation is linear — the derivative of a sum is the sum of the derivatives — the gradient of the cost is just the average of the per-example gradients:

$$ \frac{\partial J}{\partial w_1} = \frac{1}{m}\sum_{i=1}^{m} \frac{\partial}{\partial w_1}\,\mathcal{L}\bigl(a^{(i)}, y^{(i)}\bigr) = \frac{1}{m}\sum_{i=1}^{m} \texttt{dw}_1^{(i)} , \tag{11.2} $$
Equation 11.2: The cost's gradient is the average of the single-example gradients

and likewise for $w_2$ and $b$. Here $\texttt{dw}_1^{(i)} = x_1^{(i)}\,\texttt{dz}^{(i)}$ is exactly the single-example gradient from §10, computed for example $i$. So the recipe is: compute each example's gradients, add them up, divide by $m$, then take one step.

The algorithm

Written out with explicit accumulators, one step of gradient descent over the whole training set is a loop that sweeps every example and averages:

J = 0;  dw1 = 0;  dw2 = 0;  db = 0            # accumulators

for i = 1 to m:                               # ── loop over examples ──
    z  = w1*x1(i) + w2*x2(i) + b              # forward
    a  = σ(z)
    J  += −[ y(i)*log(a) + (1−y(i))*log(1−a) ]   # accumulate cost
    dz = a − y(i)                             # backward: a − y
    dw1 += x1(i) * dz                         # accumulate gradients
    dw2 += x2(i) * dz
    db  += dz

J /= m;  dw1 /= m;  dw2 /= m;  db /= m         # averages → ∂J/∂w1, ∂J/∂w2, ∂J/∂b

w1 := w1 − α*dw1                               # one gradient-descent step
w2 := w2 − α*dw2
b  := b  − α*db

After the loop, dw1, dw2, db hold the cost's gradients $\frac{\partial J}{\partial w_1}, \frac{\partial J}{\partial w_2}, \frac{\partial J}{\partial b}$, and the last three lines take one downhill step. To actually train, this whole block is repeated many times.

Why this is slow: two for-loops

The code above hides a second loop. With $n_x$ features there is not just dw1 and dw2 but dw1, dw2, …, dw$n_x$, each with its own accumulation dw$_k$+= x$_k^{(i)}$*dz — a loop over the $n_x$ features nested inside the loop over the $m$ examples:

$$ \underbrace{\text{for } i = 1 \ldots m}_{\text{examples}} \;\Bigl\{\; \underbrace{\text{for } k = 1 \ldots n_x}_{\text{features}} \;\bigl\{\; \texttt{dw}_k \mathrel{+}= x_k^{(i)}\,\texttt{dz}^{(i)} \;\bigr\} \;\Bigr\}. \tag{11.3} $$
Equation 11.3: Two nested explicit loops — the bottleneck

Explicit for-loops in Python are slow, and deep learning runs on large $m$ and large $n_x$ — so two nested loops per gradient step is exactly the wrong thing to do. The fix is vectorization: replace the loops with matrix and vector operations that compute all examples and all features at once. That is the subject of the next section.


Vectorization

The training loop just built has two nested for-loops, and in Python that is a performance disaster. Vectorization is the art of getting rid of explicit loops by expressing the same computation as operations on whole vectors and matrices. It is not a minor optimisation — on real problems it is the difference between code that runs in seconds and code that runs in hours.

What is vectorization?

Take the single most common operation in all of this reference: the score $z = \mathbf{w}^\top\mathbf{x} + b$, where $\mathbf{w}, \mathbf{x}\in\mathbb{R}^{n_x}$. The dot product $\mathbf{w}^\top\mathbf{x} = \sum_{k=1}^{n_x} w_k x_k$ can be computed two ways.

The non-vectorized way spells out the sum as a loop:

z = 0
for k in range(n_x):
    z += w[k] * x[k]      # one multiply-add per iteration
z += b

The vectorized way hands the whole two vectors to a single library call that computes $\mathbf{w}^\top\mathbf{x}$ in one shot:

z = np.dot(w, x) + b      # the entire dot product, no Python loop

Both compute exactly the same number. But np.dot does the work inside optimised, compiled code instead of the Python interpreter — and that is where the speed comes from.

Why it is faster: SIMD

A Python for-loop processes one pair $w_k x_k$ per interpreter step. A vectorized call instead ships the whole array to routines that exploit SIMDSingle Instruction, Multiple Data — hardware, where one instruction multiplies (and adds) many elements in parallel. Both CPUs and GPUs have SIMD units; GPUs simply have far more of them, which is why they are so well suited to deep learning.

The practical effect is dramatic. A standard benchmark — one dot product of two million-element vectors — looks roughly like this:

approachcodetimerelative
explicit for-loopfor k: z += w[k]*x[k]≈ 474 ms
vectorizednp.dot(w, x)≈ 1.5 ms≈ 300× faster
Table 12.1: A typical timing of one dot product of two 1,000,000-element vectors (a typical library benchmark). The vectorized call is on the order of 300× faster — for the exact same result.

Same answer, ~300× less time. Scale that up by the millions of dot products a training run performs and vectorization stops being optional.

The guideline: avoid explicit for-loops

This gives the single most useful rule of thumb in numerical deep-learning code:

Neural-network programming guideline. Whenever possible, avoid explicit for-loops. If a library function (or a matrix/vector operation) can do it, use that instead of writing the loop yourself.

As an example beyond the dot product, take a matrix–vector product $\mathbf{u} = A\mathbf{v}$, whose entries are $u_i = \sum_j A_{ij}\,v_j$. The literal translation is a double loop:

u = np.zeros((n, 1))
for i in range(n):
    for j in range(n):
        u[i] += A[i][j] * v[j]

Vectorized, the whole thing is one call — and much faster:

u = np.dot(A, v)

Element-wise functions

The same principle covers applying a function to every element of a vector. Suppose you need the exponential of each entry, turning $\mathbf{v} = [v_1,\ldots,v_n]^\top$ into $\mathbf{u} = [e^{v_1},\ldots,e^{v_n}]^\top$. The loop version calls math.exp element by element:

u = np.zeros((n, 1))
for i in range(n):
    u[i] = math.exp(v[i])

NumPy applies exp to the whole array at once:

import numpy as np
u = np.exp(v)             # element-wise, no loop

and the same holds for a whole family of element-wise operations:

operationNumPyeffect (per element)
exponentialnp.exp(v)$v_i \mapsto e^{v_i}$
natural lognp.log(v)$v_i \mapsto \ln v_i$
absolute valuenp.abs(v)$v_i \mapsto \lvert v_i\rvert$
ReLU / clamp at 0np.maximum(v, 0)$v_i \mapsto \max(v_i, 0)$
squarev ** 2$v_i \mapsto v_i^2$
reciprocal1 / v$v_i \mapsto 1/v_i$
Table 12.2: Common element-wise NumPy operations. Each applies to every entry of the array at once, replacing a for-loop. np.maximum(v, 0) is exactly the ReLU activation used in later parts.

Vectorizing logistic regression

Now apply the guideline to the training loop from §11.2. It had two loops: an inner one over the $n_x$ features and an outer one over the $m$ examples. Vectorization removes them one at a time.

Removing the inner loop over features

The inner loop was accumulating one weight-gradient per feature — dw1 += x1*dz, dw2 += x2*dz, …, dw$n_x$ += x$n_x$*dz. That is exactly a vector operation: make dw a single vector and update it all at once.

$$ \underbrace{\texttt{dw}_1 \mathrel{+}= x_1\texttt{dz},\ \ \ldots,\ \ \texttt{dw}_{n_x} \mathrel{+}= x_{n_x}\texttt{dz}}_{\text{inner loop over } n_x \text{ features}} \qquad\Longrightarrow\qquad \underbrace{\texttt{dw} \mathrel{+}= \mathbf{x}^{(i)}\,\texttt{dz}^{(i)}}_{\text{one vector update}} . \tag{13.1} $$
Equation 13.1: The per-feature updates are one vector update

The training step then keeps only the outer loop over examples:

J = 0;  dw = np.zeros((n_x, 1));  db = 0      # dw is now a VECTOR, not dw1, dw2, …

for i in range(1, m + 1):                      # only ONE loop left (over examples)
    z  = np.dot(w.T, x_i) + b
    a  = sigmoid(z)
    J  += -(y_i * np.log(a) + (1 - y_i) * np.log(1 - a))
    dz = a - y_i
    dw += x_i * dz          # whole gradient vector at once — inner loop gone
    db += dz

J /= m;  dw /= m;  db /= m                      # dw holds ∂J/∂w,  db holds ∂J/∂b

Compare this with §11.2: the block of dw1 += …, dw2 += … lines has collapsed into the single line dw += x_i * dz, no matter how many features there are.

Removing the outer loop: the forward pass

The outer loop still computes the $m$ scores one at a time: $z^{(i)} = \mathbf{w}^\top\mathbf{x}^{(i)} + b$ and $a^{(i)} = \sigma(z^{(i)})$ for each $i$. We can do all $m$ at once using the input matrix $\mathbf{X}$ from §3.3, which stacks the examples as columns ($\mathbf{X}\in\mathbb{R}^{\,n_x\times m}$).

Multiplying the row vector $\mathbf{w}^\top$ (shape $1\times n_x$) by $\mathbf{X}$ (shape $n_x\times m$) produces a $1\times m$ row of all the scores in one matrix product; adding $b$ to every entry finishes it:

$$ \mathbf{Z} = \begin{bmatrix} z^{(1)} & z^{(2)} & \cdots & z^{(m)} \end{bmatrix} = \mathbf{w}^\top \mathbf{X} + \underbrace{\begin{bmatrix} b & b & \cdots & b \end{bmatrix}}_{1\times m} = \begin{bmatrix} \mathbf{w}^\top\mathbf{x}^{(1)}+b & \cdots & \mathbf{w}^\top\mathbf{x}^{(m)}+b \end{bmatrix} \in \mathbb{R}^{\,1\times m}. \tag{13.2} $$
Equation 13.2: All m scores at once — one matrix product plus a broadcast bias

Then a single element-wise sigmoid turns every score into its prediction:

$$ \mathbf{A} = \begin{bmatrix} a^{(1)} & a^{(2)} & \cdots & a^{(m)} \end{bmatrix} = \sigma(\mathbf{Z}) \in \mathbb{R}^{\,1\times m}. \tag{13.3} $$
Equation 13.3: All m predictions at once — one element-wise sigmoid

In code the entire forward pass over the whole training set is two lines with no loop at all:

Z = np.dot(w.T, X) + b     # (1, m):  all m scores;  the scalar b is broadcast to every column
A = sigmoid(Z)             # (1, m):  all m predictions, element-wise

The addition of the scalar b to the $1\times m$ vector — where Python quietly copies b across all $m$ columns — is called broadcasting, a NumPy convenience covered in the next note.

Figure 13.1: The vectorized forward pass. The whole input matrix X (features × examples) becomes the row of scores Z in one matrix product w·X + b, then the row of predictions A in one element-wise sigmoid — computing all m examples at once, with no for-loop.

That removes the outer loop from the forward pass. What remains is to vectorize the backward pass across examples too — computing all the gradients $\texttt{dz}^{(i)}$, and the averaged dw, db, without a loop — which is next.


Vectorizing the gradient computation

The forward pass over all $m$ examples is now two loop-free lines producing the score row $\mathbf{Z}$ and the prediction row $\mathbf{A}$. The backward pass — the gradients dz, dw, db — vectorizes just as cleanly, and once it does, an entire step of gradient descent over the whole training set has no inner loop at all.

All the $dz$'s at once: $d\mathbf{Z} = \mathbf{A} - \mathbf{Y}$

For one example the gradient of the loss with respect to the score was the tidy $\texttt{dz}^{(i)} = a^{(i)} - y^{(i)}$ (§10.2). Stack the $m$ of them into a $1\times m$ row $d\mathbf{Z}$, alongside the prediction row $\mathbf{A}$ and a label row $\mathbf{Y} = [\,y^{(1)} \cdots y^{(m)}\,]$. Then every $\texttt{dz}^{(i)}$ is computed by a single elementwise subtraction:

$$ d\mathbf{Z} = \begin{bmatrix} \texttt{dz}^{(1)} & \cdots & \texttt{dz}^{(m)} \end{bmatrix} = \mathbf{A} - \mathbf{Y} = \begin{bmatrix} a^{(1)} - y^{(1)} & a^{(2)} - y^{(2)} & \cdots & a^{(m)} - y^{(m)} \end{bmatrix} \in \mathbb{R}^{\,1\times m}. \tag{14.1} $$
Equation 14.1: All m score-gradients in one subtraction

$dw$ and $db$ as matrix operations

Recall the averaged gradients from §11.1: $\texttt{db} = \frac1m\sum_i \texttt{dz}^{(i)}$ and $\texttt{dw} = \frac1m\sum_i \mathbf{x}^{(i)}\,\texttt{dz}^{(i)}$. Both sums are hiding a loop over examples — and both are exactly what a matrix operation does.

The bias gradient is just the sum of the entries of $d\mathbf{Z}$:

$$ \texttt{db} = \frac{1}{m}\sum_{i=1}^{m}\texttt{dz}^{(i)} = \frac{1}{m}\,\texttt{np.sum}(d\mathbf{Z}). \tag{14.2} $$
Equation 14.2: db is the mean of the dz row

The weight gradient $\frac1m\sum_i \mathbf{x}^{(i)}\texttt{dz}^{(i)}$ is a matrix–vector product in disguise: multiplying $\mathbf{X}$ (shape $n_x\times m$) by the column $d\mathbf{Z}^\top$ (shape $m\times 1$) sums the columns $\mathbf{x}^{(i)}$ each weighted by $\texttt{dz}^{(i)}$ — precisely the sum we want:

$$ \texttt{dw} = \frac{1}{m}\,\mathbf{X}\,d\mathbf{Z}^\top = \frac{1}{m} \underbrace{\begin{bmatrix} \big| & & \big| \\ \mathbf{x}^{(1)} & \cdots & \mathbf{x}^{(m)} \\ \big| & & \big| \end{bmatrix}}_{n_x\times m} \underbrace{\begin{bmatrix} \texttt{dz}^{(1)} \\ \vdots \\ \texttt{dz}^{(m)} \end{bmatrix}}_{m\times 1} = \frac{1}{m}\Bigl(\mathbf{x}^{(1)}\texttt{dz}^{(1)} + \cdots + \mathbf{x}^{(m)}\texttt{dz}^{(m)}\Bigr) \in \mathbb{R}^{\,n_x\times 1}. \tag{14.3} $$
Equation 14.3: dw is one matrix–vector product

The shapes check out — $(n_x\times m)(m\times 1) = (n_x\times 1)$, the same shape as $\mathbf{w}$ — and no loop over examples appears anywhere.

The full vectorized implementation

Putting the forward pass (§13.2) and this backward pass together, one full iteration of gradient descent over all $m$ examples is six loop-free lines. The only loop left is the outer one that repeats the gradient-descent step itself — which is inherent to the algorithm and cannot be vectorized away:

for iteration in range(num_iterations):     # the ONE unavoidable loop (GD steps)
    Z  = np.dot(w.T, X) + b                  # (1, m)  forward: all scores
    A  = sigmoid(Z)                          # (1, m)  forward: all predictions
    dZ = A - Y                               # (1, m)  backward: all dz's at once
    dw = (1 / m) * np.dot(X, dZ.T)           # (n_x, 1)  averaged weight gradient
    db = (1 / m) * np.sum(dZ)                # scalar     averaged bias gradient
    w  = w - alpha * dw                      # gradient-descent update
    b  = b - alpha * db

Set that beside the version we started with in §11.2 — an outer loop over $m$ examples wrapped around an inner loop over $n_x$ features — and the payoff is stark:

quantitylooped version (§11.2)vectorized version
scoresz = wᵀx(i)+b inside the example loopZ = np.dot(w.T, X) + b
predictionsa = σ(z) per exampleA = sigmoid(Z)
score gradientsdz = a − y(i) per exampledZ = A − Y
weight gradientdw1 += …; dw2 += … (feature loop)dw = (1/m)·np.dot(X, dZ.T)
bias gradientdb += dz per exampledb = (1/m)·np.sum(dZ)
Table 14.1: From two nested for-loops to none. Each per-example / per-feature accumulation becomes a single matrix or vector operation covering the whole training set at once.

Both nested loops are gone; a single gradient step now runs as a handful of matrix operations on the whole dataset. One line above quietly relied on a convenience we have used without naming it — adding the scalar b to the $1\times m$ array Z. That is broadcasting, and it deserves its own section.


Broadcasting in Python

Broadcasting is the rule that lets NumPy combine arrays of different but compatible shapes by automatically stretching the smaller one to fit. We already leaned on it twice — adding the scalar b to the row Z, and it is about to make a normalisation trick a one-liner.

A worked example: calorie percentages

Here is the calories (in a 100 g serving) from carbs, proteins, and fats for four foods, as a $3\times 4$ matrix $A$ — rows are the three nutrients, columns are the foods:

ApplesBeefEggsPotatoes
Carb56.00.04.468.0
Protein1.2104.052.08.0
Fat1.8135.099.00.9
total59.0239.0155.476.9
Table 15.1: Calories from carbs, proteins, and fats per 100 g of four foods. The goal: turn each column into the percentage of that food's calories coming from each nutrient — without an explicit for-loop.

We want each entry as a percentage of its column's total — e.g. an apple's carbs are $56/59 \approx 94.9\%$ of its calories. Two lines do it for the whole matrix:

cal = A.sum(axis=0)                 # (1, 4): column sums = total calories per food
percentage = 100 * A / cal.reshape(1, 4)    # (3, 4) / (1, 4)  → broadcast → (3, 4)

The division is between a $(3,4)$ matrix and a $(1,4)$ row. Broadcasting copies the row down to all three rows so the shapes match, dividing every nutrient in a column by that column's total:

$$ 100 \cdot \underbrace{\begin{bmatrix} 56.0 & 0.0 & 4.4 & 68.0 \\ 1.2 & 104.0 & 52.0 & 8.0 \\ 1.8 & 135.0 & 99.0 & 0.9 \end{bmatrix}}_{(3,\,4)} \;\Big/\; \underbrace{\begin{bmatrix} 59.0 & 239.0 & 155.4 & 76.9 \end{bmatrix}}_{(1,\,4)} = \begin{bmatrix} 94.9 & 0.0 & 2.8 & 88.4 \\ 2.0 & 43.5 & 33.5 & 10.4 \\ 3.1 & 56.5 & 63.7 & 1.2 \end{bmatrix}\% \tag{15.1} $$
Equation 15.1: The (1,4) totals row is broadcast down to divide each (3,4) column

Each column now sums to $100\%$ — the whole normalisation done with no loop over foods or nutrients. (The reshape(1, 4) is defensive; A.sum(axis=0) is already the right shape, but making it explicit costs nothing and guards against shape bugs.)

The general principle

The rule generalises. When you combine two arrays with $+$, $-$, $\times$, or $\div$ and one of them is missing a dimension (has size $1$ where the other has size $m$ or $n$), NumPy copies it along that dimension to match:

operationsmaller operand is…stretched toresult
$(m, n)$ with scalara single numbercopied to every entry$(m, n)$
$(m, n)$ with $(1, n)$a rowcopied down $m$ rows$(m, n)$
$(m, n)$ with $(m, 1)$a columncopied across $n$ columns$(m, n)$
Table 15.2: Broadcasting shape rules. A dimension of size 1 (or a bare scalar) is stretched — copied — to match the other operand. Applies to +, −, ×, and ÷ alike.

Two small cases make the pattern concrete — a scalar added to a column, and a row added to a matrix:

$$ \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} + 100 = \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} + \begin{bmatrix} 100 \\ 100 \\ 100 \end{bmatrix} = \begin{bmatrix} 101 \\ 102 \\ 103 \end{bmatrix}. \tag{15.2} $$
Equation 15.2: A scalar is copied to every entry
$$ \underbrace{\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}}_{(2,\,3)} + \underbrace{\begin{bmatrix} 100 & 200 & 300 \end{bmatrix}}_{(1,\,3)} = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} + \begin{bmatrix} 100 & 200 & 300 \\ 100 & 200 & 300 \end{bmatrix} = \begin{bmatrix} 101 & 202 & 303 \\ 104 & 205 & 306 \end{bmatrix}. \tag{15.3} $$
Equation 15.3: A (1,3) row is copied down to each of the 2 rows

In short. A size-$1$ dimension is a wildcard: NumPy repeats it as many times as needed so the two shapes line up, then does the operation elementwise. It is what makes Z = np.dot(w.T, X) + b and the calorie normalisation work without a loop. (Users of MATLAB/Octave will know the same idea as bsxfun.)

Broadcasting is powerful but also a common source of silent bugs — a stray $(1, n)$ where you meant $(n, 1)$ will still "work" and broadcast to a wrongly-shaped result rather than raise an error. Two cheap habits keep shapes unambiguous: prefer explicit two-dimensional shapes like $(n, 1)$ over one-dimensional "rank-1" arrays, and drop an assert(A.shape == (n, m)) next to any operation whose shape you care about.


Why the loss is what it is: maximum likelihood (optional)

Back in §5.1 the logistic loss $\mathcal{L}(\hat{y}, y) = -\bigl(y\log\hat{y} + (1-y)\log(1-\hat{y})\bigr)$ was introduced with a promise that it "falls out of maximum likelihood." This section makes good on that promise. It is optional — nothing in the algorithm needs it — but it explains where every term of the loss and cost actually comes from, and why the minus sign and the logarithm are there at all.

The prediction as a probability

The whole derivation rests on one interpretation, stated back in §4.1: the model's output is the probability that the label is 1,

$$ \hat{y} = \sigma(\mathbf{w}^\top\mathbf{x} + b) = P(y = 1 \mid \mathbf{x}). \tag{16.1} $$
Equation 16.1: The prediction is read as a probability

Because $y$ is binary, that single number fixes the probability of both outcomes: if the true label is $1$ the model assigns it probability $\hat{y}$; if the true label is $0$ the model assigns it the remaining probability $1 - \hat{y}$:

$$ \text{if } y = 1:\quad P(y \mid \mathbf{x}) = \hat{y}, \qquad\qquad \text{if } y = 0:\quad P(y \mid \mathbf{x}) = 1 - \hat{y}. \tag{16.2} $$
Equation 16.2: The probability the model assigns to the actual label, in each case

One formula for both cases

Those two cases can be folded into a single expression using $y$ and $1-y$ as exponents — a standard trick for binary variables:

$$ P(y \mid \mathbf{x}) = \hat{y}^{\,y}\,(1 - \hat{y})^{\,1 - y}. \tag{16.3} $$
Equation 16.3: Both cases in one formula

It works because an exponent of $0$ makes a factor equal to $1$, switching it off. Check both labels:

$$ y = 1:\ \ \hat{y}^{1}(1-\hat{y})^{0} = \hat{y}\cdot 1 = \hat{y}, \qquad y = 0:\ \ \hat{y}^{0}(1-\hat{y})^{1} = 1\cdot(1-\hat{y}) = 1 - \hat{y}. \tag{16.4} $$
Equation 16.4: The exponents switch the right factor on

Both recover exactly the cases above, so the one formula is faithful.

Taking the log gives the loss

We want the model to assign the actual label a high probability. Maximising $P(y\mid\mathbf{x})$ is the same as maximising its logarithm — $\log$ is monotonically increasing, so it does not move the location of the maximum, and it turns the product into a sum that is far easier to work with:

$$ \log P(y\mid\mathbf{x}) = \log\!\Bigl(\hat{y}^{\,y}(1-\hat{y})^{\,1-y}\Bigr) = y\log\hat{y} + (1-y)\log(1-\hat{y}) = -\,\mathcal{L}(\hat{y}, y). \tag{16.5} $$
Equation 16.5: The log-probability is exactly the negative loss

There is the loss. The log-probability of the correct label is precisely $-\mathcal{L}$, so maximising the probability is the same as minimising $\mathcal{L}$ — and since the convention is to minimise a cost, the loss is defined as the negative log-probability. That is exactly where the leading minus sign in the logistic loss comes from.

From one example to the cost

Finally, extend from one example to the whole training set. Assuming the $m$ examples are drawn independently, the probability of getting all the training labels right is the product of the individual probabilities:

$$ P(\text{all labels}) = \prod_{i=1}^{m} P\bigl(y^{(i)} \mid \mathbf{x}^{(i)}\bigr). \tag{16.6} $$
Equation 16.6: Likelihood of the whole training set (independent examples)

Maximum-likelihood estimation chooses $\mathbf{w}, b$ to make this as large as possible. Taking the log turns the product into a sum, and each term is a negative loss from the previous step:

$$ \log P(\text{all labels}) = \sum_{i=1}^{m} \log P\bigl(y^{(i)}\mid\mathbf{x}^{(i)}\bigr) = -\sum_{i=1}^{m} \mathcal{L}\bigl(\hat{y}^{(i)}, y^{(i)}\bigr). \tag{16.7} $$
Equation 16.7: Log-likelihood is the negative sum of the per-example losses

Maximising this log-likelihood is therefore the same as minimising the sum $\sum_i \mathcal{L}(\hat{y}^{(i)}, y^{(i)})$. Scaling by $\frac{1}{m}$ to get a mean (which does not change where the minimum is) gives back exactly the cost function from §5.3:

$$ J(\mathbf{w}, b) = \frac{1}{m}\sum_{i=1}^{m} \mathcal{L}\bigl(\hat{y}^{(i)}, y^{(i)}\bigr). \tag{16.8} $$
Equation 16.8: The cost is the (scaled) negative log-likelihood

So the cost is not an arbitrary choice: minimising $J$ is maximum-likelihood estimation for the probabilistic model $\hat{y} = P(y=1\mid\mathbf{x})$. The minus sign, the logarithm, and the sum are all fixed by that single principle — which closes the loop on the loss the rest of this reference has been minimising all along.

Part I in one arc. An image becomes a feature vector (§2); logistic regression turns it into a probability (§4); maximum likelihood dictates the cross-entropy loss and cost (§5, and this section); gradient descent (§6) minimises that cost using derivatives (§7) computed by the computation-graph backward pass (§8§10); and vectorization (§12§15) makes the whole thing fast enough to run on real data. Every later part builds on these same pieces.


Part II · The shallow network

Stack the single unit into a hidden layer and one output unit. The forward pass, the activation choice, and the backward pass that trains all four parameter blocks.

Notation at a glance

Everything here hinges on reading these symbols correctly, so here is the full set used here, once. A quantity can carry three kinds of index at once, in three distinct positions:

So $a_4^{[1]{}(i)}$ means unit 4, layer 1, example $i$ — all at once. The bracket shape is the whole distinction: $a^{[1]}$ (the layer-1 activations) is not $a^{(1)}$ (the first example).

SymbolMeaningShapeConcrete example (the $3\to4\to1$ net)
$x = a^{[0]}$input feature vector — the "layer 0" activations$(n_x,\ 1)$$x = \big[x_1\ x_2\ x_3\big]^\top$ is $(3,1)$
$x_1,\ x_2,\ x_3$the individual input featuresscalare.g. pixel values of one image
$[\ell]$  (superscript)index of layer $\ell$$W^{[1]}$ = hidden layer, $W^{[2]}$ = output layer
$(i)$  (superscript)index of training example $i$$x^{(5)}$ = the 5th image; $a^{[1]{}(5)}$ its hidden activations
$j$  (subscript)index of unit $j$ within a layer$a_2^{[1]}$ = output of the 2nd hidden unit
$n^{[\ell]}$number of units in layer $\ell$  ($n^{[0]}=n_x$)scalar$n^{[0]}{=}3,\ n^{[1]}{=}4,\ n^{[2]}{=}1$
$L$number of computing layers (input layer excluded)scalar$L = 2$ (one hidden + one output)
$z^{[\ell]}$pre-activation (score) of layer $\ell$$(n^{[\ell]},\ 1)$$z^{[1]}$ is $(4,1)$;   $z^{[2]}$ is $(1,1)$
$a^{[\ell]}$activation (output) of layer $\ell$; $a^{[\ell]}=\sigma(z^{[\ell]})$$(n^{[\ell]},\ 1)$$a^{[1]}$ is $(4,1)$;   $a^{[2]}$ is $(1,1)$
$W^{[\ell]}$weight matrix of layer $\ell$  (one row per unit)$(n^{[\ell]},\ n^{[\ell-1]})$$W^{[1]}$ is $(4,3)$;   $W^{[2]}$ is $(1,4)$
$w_j^{[\ell]\top}$weight row of unit $j$ in layer $\ell$ (a row of $W^{[\ell]}$)$(1,\ n^{[\ell-1]})$$w_2^{[1]\top}$ is $(1,3)$ — the 2nd row of $W^{[1]}$
$b^{[\ell]}$bias vector of layer $\ell$  (one bias per unit)$(n^{[\ell]},\ 1)$$b^{[1]}$ is $(4,1)$;   $b^{[2]}$ is $(1,1)$
$\sigma$sigmoid activation, $\sigma(z)=\dfrac{1}{1+e^{-z}}$element-wiseapplied to each entry of $z^{[\ell]}$ separately
$\hat{y} = a^{[2]}$the network's prediction (last-layer activation)$(n^{[2]},\ 1)$a single number in $(0,1)$ — the predicted probability
$m$number of training examplesscalare.g. $m = 1000$ images
$X = \big[x^{(1)}\ \cdots\ x^{(m)}\big]$input matrix — examples stacked as columns$(n_x,\ m)$$(3, 1000)$ for $1000$ images
$Z^{[\ell]},\ A^{[\ell]}$layer $\ell$'s pre-activations / activations for all $m$ examples (columns)$(n^{[\ell]},\ m)$$A^{[1]}$ is $(4, 1000)$ — one column per image
Table 16.1: Notation for a one-hidden-layer network, as used throughout Part II. Shapes are given for the running $3\to 4\to 1$ example: $n^{[0]}=n_x=3$ inputs, $n^{[1]}=4$ hidden units, $n^{[2]}=1$ output. $\ell$ ranges over the computing layers $1,\dots,L$ (here $L=2$).

The last three rows ($m$, $X$, $Z^{[\ell]}$, $A^{[\ell]}$) belong to the vectorized form covered in §21 and the next note; everything above them is used right away, below.

Neural network overview

By the end of Part I a single unit did the whole job: take an input $x$, form the score $z = w^\top x + b$, squash it with the sigmoid to get $a = \sigma(z) = \hat{y}$, and score the prediction with a loss $\mathcal{L}(a, y)$. A neural network is that same computation repeated and layered.

Stacking neurons into a network

Draw the single neuron as its two-box computation — the linear score, then the activation — and the loss that follows it:

Figure 16.1: A single logistic-regression unit. The score $z = w^\top x + b$ feeds the sigmoid $a = \sigma(z) = \hat{y}$, and the loss $\mathcal{L}(a, y)$ compares the prediction with the label. This is the whole Part I model.

A neural network puts a whole column of these units side by side to form a hidden layer, then feeds their outputs into one more unit that produces the final prediction. Each circle is still exactly a logistic-regression neuron — a score followed by a sigmoid — but now there are two rounds of it:

Figure 16.2: A one-hidden-layer network is two rounds of the single-neuron computation. The input $x$ drives a layer of hidden units (layer 1), whose activations $a^{[1]}$ drive the output unit (layer 2), giving $\hat{y} = a^{[2]}$.

Two things are worth noticing before we even define the symbols. First, layer 1 works on the input $x$, but layer 2 works on layer 1's output $a^{[1]}$ — the network is a chain, each layer consuming the one before it. Second, the score and activation now come with superscripts in square brackets: $z^{[1]}$, $a^{[1]}$, $z^{[2]}$, $a^{[2]}$. That bracket is the one genuinely new symbol in Part II.

The bracket notation for layers

The one genuinely new symbol is that square-bracket superscript $[\ell]$, and it is worth pausing on because two other indices look almost identical. As the notation table in §17 lays out, a quantity can carry a layer index $[\ell]$, an example index $(i)$, and a unit subscript $j$ — all at once, in three different positions. So $a_4^{[1]{}(i)}$ reads "the activation of the 4th unit, in layer 1, for the $i$-th example." The trap to avoid: $a^{[1]}$ (layer-1 activations) is not $a^{(1)}$ (the first example) — the bracket shape is the whole distinction. With that fixed, we can draw the network properly.


Neural network representation

Here is the standard picture of a one-hidden-layer network with three inputs, four hidden units, and a single output:

Figure 16.3: A one-hidden-layer neural network. Three input features feed four hidden units (layer 1), which feed one output unit (layer 2). Every hidden unit receives every input — the layer is fully connected. The output $a^{[2]}$ is the prediction $\hat{y}$.

Input, hidden, and output layers

The picture has three named parts:

$$ a^{[0]} = x, \qquad \hat{y} = a^{[2]}. \tag{16.9} $$
Equation 16.9: The input is layer 0's activation; the last layer's activation is the prediction

Why it is called a "2-layer" network

By convention we do not count the input layer when we count layers — it holds data, it computes nothing. So this network, with an input layer, one hidden layer, and an output layer, is called a 2-layer neural network: hidden layer ($\ell=1$) plus output layer ($\ell=2$). The layers that do computation are the ones that count.

The parameters and their shapes

Each computing layer $\ell$ owns a weight matrix $W^{[\ell]}$ and a bias vector $b^{[\ell]}$. Their shapes follow one rule: a layer's weight matrix has one row per unit in that layer and one column per input it receives (i.e. per unit in the previous layer).

For our $3 \to 4 \to 1$ network — with $n^{[0]} = 3$ inputs, $n^{[1]} = 4$ hidden units, and $n^{[2]} = 1$ output — that gives:

ParameterRoleShape
$W^{[1]}$hidden-layer weights (4 units, 3 inputs each)$(4,\ 3)$
$b^{[1]}$hidden-layer biases (one per unit)$(4,\ 1)$
$W^{[2]}$output-layer weights (1 unit, 4 inputs)$(1,\ 4)$
$b^{[2]}$output-layer bias$(1,\ 1)$
Table 16.2: Parameter shapes for the $3\to 4\to 1$ network. Rule: $W^{[\ell]}$ is $\left(n^{[\ell]},\ n^{[\ell-1]}\right)$ — units of layer $\ell$ by units of layer $\ell-1$ — and $b^{[\ell]}$ is $\left(n^{[\ell]},\ 1\right)$.

Where the shapes come from. Layer 1 has 4 units, so it produces a $(4,1)$ output $z^{[1]}$; to make $W^{[1]}x$ come out $(4,1)$ from a $(3,1)$ input $x$, the matrix must be $(4,3)$. Layer 2 has 1 unit, takes the $(4,1)$ vector $a^{[1]}$, and returns a $(1,1)$ scalar, so $W^{[2]}$ is $(1,4)$. The bias always matches the layer's own unit count, $\left(n^{[\ell]}, 1\right)$.


Computing a neural network's output

We now write down exactly what the network computes. The plan is to look at one hidden unit, see that it does precisely what a logistic-regression neuron does, and then stack the four units so the whole layer is a single matrix equation.

One neuron = two steps

Zoom into any single circle of the network and you find a short chain of operations. Each input $x_k$ is multiplied by its own weight $w_k$; those products (together with the bias $b$) are summed into the pre-activation $z = w^\top x + b$; and $z$ is passed through the activation $\sigma$ to give $a = \sigma(z)$. Drawing each operation as its own node makes the whole neuron explicit:

BIAS MULTIPLY SUM ACTIVATION OUTPUT $x_1$ $x_2$ $x_3$ $1$ $\times\, w_1$ $\times\, w_2$ $\times\, w_3$ $\times\, b$ $\displaystyle\sum$ $z = w^\top x + b$ $z$ $\sigma$ $a = \sigma(z)$ $a$ $a = \hat{y}$
Figure 16.4: One neuron, every operation shown. Each input $x_k$ enters a multiply unit that scales it by the weight $w_k$ (the bias unit scales the constant $1$ by $b$); the summation unit $\Sigma$ adds the four products into the pre-activation $z = w^\top x + b$; the activation unit applies the sigmoid $a = \sigma(z)$; and $a = \hat{y}$ leaves as the output.

Multiply, sum, activate — those three operations are the neuron. In the network diagram all of this is collapsed into a single circle to save space, but the arithmetic inside every circle is always this chain, and $a = \sigma(w^\top x + b)$ is the whole of it.

Each hidden unit's computation

Take the four hidden units one at a time. Unit $j$ in layer 1 has its own weight row $w_j^{[1]}$ (a length-3 vector, since there are 3 inputs) and its own bias $b_j^{[1]}$. It computes exactly the logistic-regression pair:

$$ \begin{align} z_1^{[1]} &= w_1^{[1]\top}x + b_1^{[1]}, & a_1^{[1]} &= \sigma\left(z_1^{[1]}\right), \tag{16.10.1} \\ z_2^{[1]} &= w_2^{[1]\top}x + b_2^{[1]}, & a_2^{[1]} &= \sigma\left(z_2^{[1]}\right), \tag{16.10.2} \\ z_3^{[1]} &= w_3^{[1]\top}x + b_3^{[1]}, & a_3^{[1]} &= \sigma\left(z_3^{[1]}\right), \tag{16.10.3} \\ z_4^{[1]} &= w_4^{[1]\top}x + b_4^{[1]}, & a_4^{[1]} &= \sigma\left(z_4^{[1]}\right). \tag{16.10.4} \end{align} $$
Equation 16.10: The four hidden units, each a logistic-regression neuron on the same input $x$

Why the transpose $w_j^{[1]\top}$? By convention every vector — the input $x$ and each unit's weights $w_j^{[1]}$ — is stored as a column, shape $(3,1)$. The score a neuron needs is the dot product $\sum_k w_{j,k}\,x_k$: multiply matching entries and add. Matrix multiplication only does that when the left factor is a row and the right factor a column — a $(1,3)$ times a $(3,1)$ gives a $(1,1)$ scalar. Writing $w_j^{[1]}x$ as-is would be $(3,1)\times(3,1)$, which is not a legal product. Transposing the column $w_j^{[1]}$ into the row $w_j^{[1]\top}$ (shape $(1,3)$) is exactly what lines the two vectors up so the $(1,3)\times(3,1)$ product returns the single number we want: $\,w_j^{[1]\top}x = w_{j,1}x_1 + w_{j,2}x_2 + w_{j,3}x_3\,$ — the weighted sum.

That is four scores and four activations. If we coded this literally, it would be a for-loop over the four units — and, as in Part I, an explicit loop is exactly what we want to avoid. So we stack.

Vectorizing the layer: stacking into $W^{[1]}$

Stack the four weight rows into a single matrix, one row per unit. Each row $w_j^{[1]\top}$ is a $(1, 3)$ row, and there are four of them, so the matrix is $(4, 3)$ — exactly the shape from §19.3:

$$ W^{[1]} = \begin{bmatrix} \text{---}\ w_1^{[1]\top}\ \text{---}\\ \text{---}\ w_2^{[1]\top}\ \text{---}\\ \text{---}\ w_3^{[1]\top}\ \text{---}\\ \text{---}\ w_4^{[1]\top}\ \text{---} \end{bmatrix}_{(4,3)}, \qquad b^{[1]} = \begin{bmatrix} b_1^{[1]}\\ b_2^{[1]}\\ b_3^{[1]}\\ b_4^{[1]} \end{bmatrix}_{(4,1)}. \tag{16.11} $$
Equation 16.11: Stacking the four weight rows into $W^{[1]}$, and the four biases into $b^{[1]}$

Now the matrix–vector product $W^{[1]}x$ computes all four scores at once: row $j$ of the product is $w_j^{[1]\top}x$, and adding $b^{[1]}$ finishes each score. The sigmoid is applied element-wise to the whole vector, giving all four activations together:

$$ z^{[1]} = W^{[1]}x + b^{[1]}, \qquad a^{[1]} = \sigma\left(z^{[1]}\right), \tag{16.12} $$
Equation 16.12: The entire hidden layer in two lines — no loop over units

where $z^{[1]}$ and $a^{[1]}$ are both $(4, 1)$ columns holding the four units' values. Writing it out confirms each row is the per-unit equation from §20.2:

$$ \underbrace{\begin{bmatrix} z_1^{[1]}\\ z_2^{[1]}\\ z_3^{[1]}\\ z_4^{[1]} \end{bmatrix}}_{z^{[1]}\,(4,1)} = \underbrace{\begin{bmatrix} \text{---}\ w_1^{[1]\top}\ \text{---}\\ \text{---}\ w_2^{[1]\top}\ \text{---}\\ \text{---}\ w_3^{[1]\top}\ \text{---}\\ \text{---}\ w_4^{[1]\top}\ \text{---} \end{bmatrix}}_{W^{[1]}\,(4,3)} \underbrace{\begin{bmatrix} x_1\\ x_2\\ x_3 \end{bmatrix}}_{x\,(3,1)} + \underbrace{\begin{bmatrix} b_1^{[1]}\\ b_2^{[1]}\\ b_3^{[1]}\\ b_4^{[1]} \end{bmatrix}}_{b^{[1]}\,(4,1)} = \begin{bmatrix} w_1^{[1]\top}x + b_1^{[1]}\\ w_2^{[1]\top}x + b_2^{[1]}\\ w_3^{[1]\top}x + b_3^{[1]}\\ w_4^{[1]\top}x + b_4^{[1]} \end{bmatrix}. \tag{16.13} $$
Equation 16.13: The stacked layer, written row by row — each row is one unit's score

The full forward pass for one example

The output layer does the same thing on its input, which is $a^{[1]}$ (not $x$). Its one unit has a $(1,4)$ weight row $W^{[2]}$ and a scalar bias $b^{[2]}$, so $z^{[2]} = W^{[2]}a^{[1]} + b^{[2]}$ is a $(1,1)$ scalar and $a^{[2]} = \sigma\left(z^{[2]}\right)$ is the prediction. Putting both layers together, the entire network's output for one input $x$ is four lines:

$$ \begin{align} z^{[1]} &= W^{[1]}\,x + b^{[1]} & &\bigl[(4,1) = (4,3)(3,1) + (4,1)\bigr] \tag{16.14.1} \\ a^{[1]} &= \sigma\left(z^{[1]}\right) & &\bigl[(4,1)\bigr] \tag{16.14.2} \\ z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]} & &\bigl[(1,1) = (1,4)(4,1) + (1,1)\bigr] \tag{16.14.3} \\ a^{[2]} &= \sigma\left(z^{[2]}\right) & &\bigl[(1,1)\bigr] \tag{16.14.4} \end{align} $$
Equation 16.14: Forward pass for a single example. Given $x$, four lines compute the prediction $\hat{y} = a^{[2]}$; the annotations give each object's shape.

That is the whole computation. Notice its shape: it is the same two-line pattern $z = Wa_{\text{prev}} + b, a = \sigma(z)$ applied twice, with $a^{[0]} = x$ feeding layer 1 and $a^{[1]}$ feeding layer 2. Written that way it barely matters that there are two layers — the same block repeats, and a deeper network would simply repeat it more times.

Why $w^\top x$ for a unit but $W^{[1]}x$ for a layer. A single neuron's weight $w$ is stored as a column, so its score needs the transpose, $w^\top x$. When we stack the units, each $w_j^{[1]\top}$ becomes a row of $W^{[1]}$ — the transpose is already baked into the stacking — so the layer equation is the plain product $W^{[1]}x$, no transpose in sight. Same arithmetic, tidier bookkeeping.


Vectorizing across multiple examples

Everything so far runs the network on one input $x$. A real training set has $m$ of them, $x^{(1)},\dots,x^{(m)}$, and we need a prediction $\hat{y}^{(i)}$ for each. The obvious way is a loop; the better way — exactly as in Part I — is to stack the examples into matrices and push the whole set through at once, with no explicit loop.

The naive way: a loop over examples

To run the four-line forward pass on every example, give every quantity a round-bracket example index $(i)$ and wrap it in a loop over $i = 1,\dots,m$:

$$ \begin{align} z^{[1]{}(i)} &= W^{[1]} x^{(i)} + b^{[1]}, & a^{[1]{}(i)} &= \sigma\left(z^{[1]{}(i)}\right), \tag{16.15.1} \\ z^{[2]{}(i)} &= W^{[2]} a^{[1]{}(i)} + b^{[2]}, & a^{[2]{}(i)} &= \sigma\left(z^{[2]{}(i)}\right) = \hat{y}^{(i)}. \tag{16.15.2} \end{align} $$
Equation 16.15: The forward pass repeated for each example $i$. Every symbol gains an $(i)$; the last activation $a^{[2]{}(i)}$ is that example's prediction $\hat{y}^{(i)}$.

Read the double index carefully: $a^{[2]{}(i)}$ is "layer 2, example $i$." As pseudocode this is a for loop:

for i = 1 to m:
    z[1](i) = W[1] x(i)   + b[1]        # (4,1)
    a[1](i) = σ( z[1](i) )              # (4,1)
    z[2](i) = W[2] a[1](i) + b[2]       # (1,1)
    a[2](i) = σ( z[2](i) )              # (1,1)  = ŷ(i)

It is correct, but that explicit Python for over $m$ examples is precisely the bottleneck vectorization removes. We want to lose the loop.

Stacking examples into matrices

Take the $m$ input column-vectors and set them side by side as the columns of one big matrix $X$ — the same convention as Part I, one column per example. Do the same with the per-example $z$ and $a$ vectors of each layer, giving capital-letter matrices:

$$ X = \big[\, x^{(1)}\ \ x^{(2)}\ \ \cdots\ \ x^{(m)}\, \big], \qquad Z^{[1]} = \big[\, z^{[1]{}(1)}\ \cdots\ z^{[1]{}(m)}\, \big], \qquad A^{[1]} = \big[\, a^{[1]{}(1)}\ \cdots\ a^{[1]{}(m)}\, \big]. \tag{16.16} $$
Equation 16.16: Stacking the examples column-by-column. $X$ is $(n_x, m) = (3, m)$; each layer's $Z$ and $A$ are $(n^{[\ell]}, m)$ — here $(4, m)$ for the hidden layer.

The two axes of every capital matrix carry a fixed, opposite meaning — worth memorising because it is the source of most shape confusion:

DirectionIndex it runs overMeaning
horizontal — across the columnsexample $i = 1,\dots,m$different training examples
vertical — down the rowsunit $j = 1,\dots,n^{[\ell]}$different hidden units (or input features)
Table 16.3: What the two directions of a stacked matrix mean. Going across selects the training example; going down selects the unit. So the entry in row $j$, column $i$ of $A^{[1]}$ is $a_j^{[1]{}(i)}$.

Two consequences to keep: the input matrix is layer 0's activation, $X = A^{[0]}$; and the network's full output is $A^{[2]} = \hat{Y} = \big[\hat{y}^{(1)} \cdots \hat{y}^{(m)}\big]$, a $(1, m)$ row of predictions.

The vectorized forward pass

With the examples stacked, the entire loop collapses into the same four lines, now in capitals:

$$ \begin{align} Z^{[1]} &= W^{[1]} X + b^{[1]} & &\bigl[(4,m) = (4,3)(3,m) + (4,1)\bigr] \tag{16.17.1} \\ A^{[1]} &= \sigma\left(Z^{[1]}\right) & &\bigl[(4,m)\bigr] \tag{16.17.2} \\ Z^{[2]} &= W^{[2]} A^{[1]} + b^{[2]} & &\bigl[(1,m) = (1,4)(4,m) + (1,1)\bigr] \tag{16.17.3} \\ A^{[2]} &= \sigma\left(Z^{[2]}\right) & &\bigl[(1,m)\bigr] \tag{16.17.4} \end{align} $$
Equation 16.17: Vectorized forward pass over all $m$ examples. Identical to the single-example version with lowercase→uppercase; the bias broadcasts across the $m$ columns.

with $X = A^{[0]}$. Notice the bias: $b^{[1]}$ is $(4,1)$ but is added to the $(4,m)$ matrix $W^{[1]}X$. Python broadcasts it — copying the same bias column across all $m$ columns — exactly the broadcasting rule from Part I. In code the four lines are literal:

Z1 = W1 @ X  + b1          # (4, m)   one matmul does all m examples
A1 = sigmoid(Z1)           # (4, m)
Z2 = W2 @ A1 + b2          # (1, m)
A2 = sigmoid(Z2)           # (1, m)   = Yhat, one column per example

The whole rule in a sentence: lowercase = one example (a column vector); uppercase = all $m$ examples (a matrix, one column per example). The mathematics is unchanged — the same $z = W a_{\text{prev}} + b, a = \sigma(z)$ block twice — but the for-loop is gone, replaced by two matrix multiplies.

Figure 16.5: The vectorized forward pass as a chain. The input matrix $X = A^{[0]}$ (all $m$ examples) flows through the two layers to $A^{[2]} = \hat{Y}$; each arrow is one matrix multiply plus a broadcast bias, then an element-wise sigmoid.

Why it works: $W X$ stacks the columns

Why is it legitimate to swap $x$ for $X$ and read off all the answers at once? Because of one property of matrix multiplication: multiplying a matrix by a matrix of stacked columns is the same as multiplying it by each column separately. Set the bias aside for a moment; then

$$ W^{[1]} X = W^{[1]} \big[\, x^{(1)}\ \cdots\ x^{(m)}\, \big] = \big[\, W^{[1]}x^{(1)}\ \cdots\ W^{[1]}x^{(m)}\, \big] = \big[\, z^{[1]{}(1)}\ \cdots\ z^{[1]{}(m)}\, \big] = Z^{[1]}. \tag{16.18} $$
Equation 16.18: Matrix multiplication acts column-by-column. The $i$-th column of $W^{[1]}X$ is $W^{[1]}$ times the $i$-th column of $X$ — which is exactly $z^{[1]{}(i)}$ from the loop.

So the single product $W^{[1]}X$ already contains all $m$ pre-activation vectors, one per column; adding $b^{[1]}$ (broadcast to every column) completes them, giving $Z^{[1]} = W^{[1]}X + b^{[1]}$. The identical argument runs at layer 2 with $A^{[1]}$ in place of $X$. That single fact — matrix-times-stacked-columns — is the whole reason the loop-free version is not just faster but exactly the same computation.

The one thing to remember. Every vector became a matrix by stacking examples as columns, and every per-example equation became the same equation in capitals. If you can read a capital matrix as "one column per training example," the vectorized forward pass needs no new ideas beyond the single-example one in §20.


Activation functions

Every unit so far ends with the sigmoid $\sigma$. But the sigmoid is only one choice of a general activation function $g$ — the non-linear squashing step applied to the pre-activation $z$. This section swaps $\sigma$ for a general $g$, meets the four activations you will actually use, shows why $g$ must be non-linear, and lists the derivatives each one needs for training.

The activation is a choice: a general $g$

Rewrite the forward pass with a general $g$ in place of $\sigma$, and let each layer pick its own activation $g^{[\ell]}$:

$$ \begin{align} z^{[1]} &= W^{[1]}x + b^{[1]}, & a^{[1]} &= g^{[1]}\big(z^{[1]}\big), \tag{16.19.1} \\ z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]}, & a^{[2]} &= g^{[2]}\big(z^{[2]}\big). \tag{16.19.2} \end{align} $$
Equation 16.19: The forward pass with a general activation. The layer superscript on $g^{[\ell]}$ signals that different layers may use different activations; the sigmoid is just the special case $g = \sigma$.

So "using the sigmoid everywhere" was the choice $g^{[1]} = g^{[2]} = \sigma$. In practice the hidden layer and the output layer are usually given different activations, for reasons we come to below.

Four common activations

Four functions cover almost everything you will do:

$$ \underbrace{\sigma(z) = \frac{1}{1+e^{-z}}}_{\text{sigmoid}}, \qquad \underbrace{\tanh(z) = \frac{e^{z}-e^{-z}}{e^{z}+e^{-z}}}_{\text{tanh}}, \qquad \underbrace{\max(0,\,z)}_{\text{ReLU}}, \qquad \underbrace{\max(0.01z,\,z)}_{\text{leaky ReLU}} . \tag{16.20} $$
Equation 16.20: The four standard activation functions.

One panel each tells the whole story — two saturating S-curves and two ramps:

Figure 16.6: The four activations $g(z)$, one panel each. Sigmoid $\in(0,1)$ and tanh $\in(-1,1)$ are S-shaped and saturate for large $|z|$; ReLU $=\max(0,z)$ and leaky ReLU $=\max(0.01z,z)$ are ramps of slope $1$ for $z>0$. In the leaky ReLU panel the negative side is drawn at slope $0.1$ so the 'leak' below zero is visible alongside the full ramp; its true slope is $0.01$ — ten times gentler still, and nearly flat at this scale.

How to choose (rules of thumb):

ActivationRangeWhere to use it
$\sigma(z)$  (sigmoid)$(0,\ 1)$output layer for binary classification ($\hat{y}$ is a probability); almost never a hidden layer
$\tanh(z)$$(-1,\ 1)$hidden layers — zero-centred, so it usually beats the sigmoid there
ReLU  $\max(0,z)$$[0,\ \infty)$hidden layers — the default choice; fast because it does not saturate for $z>0$
leaky ReLU  $\max(0.01z,z)$$(-\infty,\ \infty)$hidden layers — like ReLU but keeps a small slope for $z<0$ so units never fully "die"
Table 16.4: Choosing an activation. The output layer is fixed by the task; hidden layers default to ReLU. The one rule to remember: don't use the sigmoid in a hidden layer.

Two ideas drive that table. First, tanh beats sigmoid in hidden layers because its outputs are centred on $0$ rather than $0.5$, which keeps the next layer's inputs balanced; in fact $\tanh(z) = 2\sigma(2z) - 1$, a stretched-and-shifted sigmoid. Second, both $\sigma$ and $\tanh$ saturate: for large $|z|$ the curve goes flat, its slope $\to 0$, and gradient descent nearly stops learning there (the "vanishing gradient"). ReLU has slope exactly $1$ for all $z>0$, so it never saturates on that side and trains much faster — which is why it is the default hidden-layer activation.

Why the activation must be non-linear

Why bother with $g$ at all — why not a linear activation $g(z) = z$ (the identity)? Because then the hidden layer does nothing: the whole network collapses to a single linear map. Substitute $g(z)=z$ into both layers and expand:

$$ \begin{align} a^{[1]} &= z^{[1]} = W^{[1]}x + b^{[1]}, \tag{16.21.1} \\ a^{[2]} &= z^{[2]} = W^{[2]}a^{[1]} + b^{[2]} = W^{[2]}\big(W^{[1]}x + b^{[1]}\big) + b^{[2]} \tag{16.21.2} \\ &= \underbrace{\big(W^{[2]}W^{[1]}\big)}_{W'}\,x + \underbrace{\big(W^{[2]}b^{[1]} + b^{[2]}\big)}_{b'} = W'x + b' . \tag{16.21.3} \end{align} $$
Equation 16.21: A linear activation collapses the network. With $g(z)=z$, two layers reduce to one linear function $W'x + b'$ — the hidden layer buys nothing.

A composition of linear functions is just another linear function, so no matter how many hidden layers you stack, a linear activation gives you nothing more than plain linear (or, with a sigmoid output, logistic) regression. The non-linearity in the hidden layers is the entire reason a neural network can represent more than a straight line.

The one place a linear activation is fine: the output of a regression. If you are predicting a real number $\hat{y}\in\mathbb{R}$ — say a house price anywhere from 0 to 1,000,000 dollars — the output unit can use $g(z)=z$ so $\hat{y}$ is not squashed into $(0,1)$. (If the target is known non-negative, ReLU works too.) The hidden layers, though, must still be non-linear.

Derivatives of the activations

Training the network by gradient descent needs the slope $g'(z)$ of each activation — the backward pass (next note) multiplies by it at every unit. All four have tidy derivatives, and the two S-curves express theirs in terms of the activation $a = g(z)$ itself:

Activation $g(z)$Derivative $g'(z)$In terms of $a=g(z)$
$\sigma(z) = \dfrac{1}{1+e^{-z}}$$\sigma(z)\big(1-\sigma(z)\big)$$a(1-a)$
$\tanh(z)$$1 - \big(\tanh z\big)^2$$1 - a^2$
ReLU $= \max(0,z)$$0$ if $z<0$;   $1$ if $z>0$
leaky ReLU $= \max(0.01z,z)$$0.01$ if $z<0$;   $1$ if $z\ge 0$
Table 16.5: Derivatives of the four activations. Writing $a = g(z)$, the sigmoid and tanh slopes are $a(1-a)$ and $1-a^2$ — cheap to reuse in the backward pass since $a$ is already computed in the forward pass.

Plotted on their own, the derivatives make the difference between the families obvious:

Figure 16.7: The derivatives $g'(z)$, one panel each. The S-curve derivatives are bumps that vanish at the tails — $\sigma'$ peaks at only $0.25$, $\tanh'$ at $1$ — the saturation that slows learning. ReLU's derivative is a clean $0/1$ step: exactly $0$ for $z<0$. Leaky ReLU's derivative jumps to $1$ for $z>0$ too, but for $z<0$ it is a small non-zero constant (drawn here at $0.1$; the true value is $0.01$) — that is what stops leaky-ReLU units from 'dying'.

Sanity-check the two S-curves at the ends and the middle. For the sigmoid, $g'(z) = a(1-a)$: at $z=0$, $a=\tfrac12$ so $g' = \tfrac12\cdot\tfrac12 = \tfrac14$ (its steepest point); as $z\to\pm\infty$, $a\to 1$ or $0$ and $g'\to 0$ (flat tails). For tanh, $g'(z) = 1-a^2$: at $z=0$, $a=0$ so $g'=1$; as $z\to\pm\infty$, $a\to\pm1$ and $g'\to 0$. Both slopes vanishing at the tails is exactly the saturation that slows learning. For ReLU the slope is a clean $1$ wherever $z>0$ and $0$ where $z<0$ (the single point $z=0$ is non-differentiable, but in code it is simply set to $0$ or $1$ — $z$ is never exactly $0$ in practice).

Why $\sigma'=a(1-a)$ and $\tanh' = 1-a^2$. The sigmoid identity was proved in logistic regression: differentiating $(1+e^{-z})^{-1}$ and regrouping gives $\sigma(z)\big(1-\sigma(z)\big)$. For tanh, $\tanh = \frac{\sinh}{\cosh}$ and the quotient rule gives $\tanh'(z) = \frac{\cosh^2 - \sinh^2}{\cosh^2} = 1 - \tanh^2 z$, using $\cosh^2 - \sinh^2 = 1$. Writing $a = \tanh z$ turns it into $1 - a^2$.


Gradient descent for a shallow network

Training the network means picking the four parameters — $W^{[1]}, b^{[1]}, W^{[2]}, b^{[2]}$ — that make the predictions $\hat{y}$ as close to the labels $y$ as possible. As in Part I, this is done by gradient descent: measure how wrong the network is with a cost, compute the slope of that cost with respect to each parameter, and step every parameter a little downhill. Nothing about the recipe changes; there are simply four parameter blocks to update instead of two.

The parameters and the cost

The four parameters keep the shapes fixed in §19.3, written here for the running $n^{[0]} \to n^{[1]} \to n^{[2]}$ net:

$$ \underbrace{W^{[1]}}_{(n^{[1]},\, n^{[0]})}, \qquad \underbrace{b^{[1]}}_{(n^{[1]},\, 1)}, \qquad \underbrace{W^{[2]}}_{(n^{[2]},\, n^{[1]})}, \qquad \underbrace{b^{[2]}}_{(n^{[2]},\, 1)}. \tag{16.22} $$
Equation 16.22: The four learnable parameter blocks of a one-hidden-layer network, with their shapes.

With a binary classifier ($n^{[2]} = 1$) the network's average error over the training set is the cost — the mean of the per-example cross-entropy losses:

$$ J\left(W^{[1]}, b^{[1]}, W^{[2]}, b^{[2]}\right) = \frac{1}{m} \sum_{i=1}^{m} \mathcal{L}\left(\hat{y}^{(i)}, y^{(i)}\right), \qquad \hat{y}^{(i)} = a^{[2]{}(i)}. \tag{16.23} $$
Equation 16.23: The cost is the average loss over the $m$ training examples; only $\hat{y}$ is now produced by two layers instead of one.

The loss $\mathcal{L}$ is the same log-loss used for logistic regression in logistic regression: $\mathcal{L}(a, y) = -\big(y\log a + (1-y)\log(1-a)\big)$. What has changed is only how $\hat{y}$ is computed — by two layers now — not how it is scored.

The gradient-descent loop

One pass of training repeats the familiar three beats: predict, differentiate, update.

Repeat until converged:
    # 1. forward pass: compute predictions for all m examples
    A2 = forward(X)                     # = Yhat        (see section 8)

    # 2. backward pass: gradient of the cost w.r.t. each parameter
    dW1 = dJ/dW1      db1 = dJ/db1
    dW2 = dJ/dW2      db2 = dJ/db2

    # 3. update: one downhill step, learning rate alpha
    W1 := W1 - alpha * dW1      b1 := b1 - alpha * db1
    W2 := W2 - alpha * dW2      b2 := b2 - alpha * db2

Each $dW^{[\ell]}$ here is shorthand for the matrix $\partial J / \partial W^{[\ell]}$ — the same shape as $W^{[\ell]}$ itself, one partial derivative per weight — and likewise $db^{[\ell]}$ matches $b^{[\ell]}$. The learning rate $\alpha$ is the single step-size knob. The only hard part is step 2: computing those four gradients. That computation is backpropagation, and the next section gives its equations outright.


The backpropagation equations

Backpropagation runs the computation graph in reverse. The forward pass sent $X \to Z^{[1]} \to A^{[1]} \to Z^{[2]} \to A^{[2]}$; the backward pass starts from the output error and walks the same chain right-to-left, handing each layer an error signal $dZ^{[\ell]}$ from which its parameter gradients fall out. This section states the equations; §25 derives them.

Here is the full computation graph the two passes run over — every input, parameter, and operation. Each score node $z^{[\ell]}$ is fed by three things: the previous activation, the weights $W^{[\ell]}$, and the bias $b^{[\ell]}$; the loss compares the final activation with the label $y$. Backpropagation is exactly this graph traversed right-to-left, and the mirror below it shows where each gradient comes from.

Figure 16.8: The full forward computation graph of the one-hidden-layer network. Inputs and parameters (blue) feed the two score nodes $z^{[\ell]} = W^{[\ell]}a^{[\ell-1]} + b^{[\ell]}$ (yellow); each is squashed to an activation $a^{[\ell]}$; the output $a^{[2]} = \hat{y}$ (green) and the label $y$ feed the loss (grey). Backprop traverses this same graph in reverse.
Figure 16.9: The same graph in reverse (red). From the output error $dz^{[2]} = a^{[2]} - y$, each score node reads off its two parameter gradients (green) — weight = error × the layer's input, bias = the error itself — and relays the error one layer back through $W^{[2]\top}(\cdot)\odot g^{[1]\prime}$ to $dz^{[1]}$, which does the same.

Forward pass, then six gradients

For a single training example, the forward pass is the four-line block from §20.4, and the backward pass is six lines that mirror it (lowercase = one example):

$$ \begin{align} dz^{[2]} &= a^{[2]} - y \tag{16.24.1} \\ dW^{[2]} &= dz^{[2]}\, a^{[1]\top} \tag{16.24.2} \\ db^{[2]} &= dz^{[2]} \tag{16.24.3} \\ dz^{[1]} &= W^{[2]\top} dz^{[2]} \;\odot\; g^{[1]\prime}\left(z^{[1]}\right) \tag{16.24.4} \\ dW^{[1]} &= dz^{[1]}\, x^{\top} \tag{16.24.5} \\ db^{[1]} &= dz^{[1]} \tag{16.24.6} \end{align} $$
Equation 16.24: The backward pass for one example. $\odot$ is the element-wise (Hadamard) product; $g^{[1]\prime}$ is the hidden layer's activation derivative (e.g. $1-a^2$ for tanh, from §22.4).

Read the six lines top to bottom as one sweep backward:

Notice the rhythm: at each layer, given the error $dz^{[\ell]}$, the weight gradient is $dz^{[\ell]}$ times the layer's input transposed, the bias gradient is $dz^{[\ell]}$ itself, and the error is sent one layer further back by $W^{[\ell]\top} dz^{[\ell]} \odot g^{\prime}$. The same three moves at every layer — the pattern that makes backprop scale to deep networks — the full backward mirror is @nn-fig-compgraph-back above.

Vectorizing across all $m$ examples

Exactly as the forward pass went lowercase → uppercase, the six equations vectorize by stacking examples as columns. Divide the two weight/bias gradients by $m$ (the cost is an average over examples) and sum the bias terms across the $m$ columns:

$$ \begin{align} dZ^{[2]} &= A^{[2]} - Y \tag{16.25.1} \\ dW^{[2]} &= \tfrac{1}{m}\, dZ^{[2]} A^{[1]\top} \tag{16.25.2} \\ db^{[2]} &= \tfrac{1}{m}\, \texttt{np.sum}\big(dZ^{[2]},\ \text{axis}=1,\ \text{keepdims}\big) \tag{16.25.3} \\ dZ^{[1]} &= W^{[2]\top} dZ^{[2]} \;\odot\; g^{[1]\prime}\left(Z^{[1]}\right) \tag{16.25.4} \\ dW^{[1]} &= \tfrac{1}{m}\, dZ^{[1]} X^{\top} \tag{16.25.5} \\ db^{[1]} &= \tfrac{1}{m}\, \texttt{np.sum}\big(dZ^{[1]},\ \text{axis}=1,\ \text{keepdims}\big) \tag{16.25.6} \end{align} $$
Equation 16.25: The vectorized backward pass over all $m$ examples. The $\tfrac1m$ averages the gradient; the bias becomes a row-sum of the error over the example columns.

Two changes from the single-example version, both from the sum in the cost:

dZ2 = A2 - Y                                      # (1,  m)
dW2 = (1/m) * dZ2 @ A1.T                          # (1,  n1)
db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True)  # (1,  1)

dZ1 = (W2.T @ dZ2) * gprime(Z1)                   # (n1, m)   * = element-wise
dW1 = (1/m) * dZ1 @ X.T                           # (n1, n0)
db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True)  # (n1, 1)

Reading the shapes

Every gradient has the same shape as the thing it is the gradient of — the quickest correctness check there is. Track the hidden-error line, the trickiest one:

$$ \underbrace{dZ^{[1]}}_{(n^{[1]},\, m)} = \underbrace{W^{[2]\top}}_{(n^{[1]},\, n^{[2]})}\, \underbrace{dZ^{[2]}}_{(n^{[2]},\, m)} \;\odot\; \underbrace{g^{[1]\prime}\left(Z^{[1]}\right)}_{(n^{[1]},\, m)}. \tag{16.26} $$
Equation 16.26: Shapes along the hidden-error line. The matrix product carries the output error onto the $n^{[1]}$ hidden units; the element-wise $\odot$ then needs the identical $(n^{[1]}, m)$ shape.

The product $W^{[2]\top} dZ^{[2]}$ is $(n^{[1]}, n^{[2]}) \times (n^{[2]}, m) = (n^{[1]}, m)$ — the output error carried back onto the $n^{[1]}$ hidden units, for all $m$ examples. The element-wise $\odot$ then needs its right operand to be the same $(n^{[1]}, m)$ shape, which $g^{[1]\prime}(Z^{[1]})$ is; the result $(n^{[1]}, m)$ matches $Z^{[1]}$, as it must. The two weight gradients check the same way: $dW^{[2]} = \tfrac1m dZ^{[2]} A^{[1]\top}$ is $(n^{[2]}, m)(m, n^{[1]}) = (n^{[2]}, n^{[1]})$, the shape of $W^{[2]}$; $dW^{[1]} = \tfrac1m dZ^{[1]} X^{\top}$ is $(n^{[1]}, m)(m, n^{[0]}) = (n^{[1]}, n^{[0]})$, the shape of $W^{[1]}$. If a shape does not line up, an operand needs transposing.

The one operator to get right. $W^{[2]\top} dZ^{[2]}$ is a matrix multiply (inner dimensions must chain: $\dots\, n^{[2]} \mid n^{[2]}\, \dots$); the $\odot$ with $g^{[1]\prime}(Z^{[1]})$ is element-wise (dimensions must match exactly). In NumPy those are @ and * respectively — swapping them is the single most common backprop bug.


Backpropagation intuition (optional)

The six equations were stated; this section shows where they come from. It is the optional reading, and everything below is one tool applied twice: the chain rule on the computation graph. If you are happy to take the equations on faith, skip to §26.

Warm-up: gradients for logistic regression

Start with the single neuron of Part I — one score, one sigmoid, one loss — and differentiate the loss backward along the graph $z \to a \to \mathcal{L}$. The chain rule multiplies the local slope on each link.

Figure 16.10: The logistic-regression computation graph. Forward (left→right) it computes the loss; backprop (right→left) multiplies the local derivatives written on the arrows to get each parameter's gradient.

The first slope back from the loss is the derivative of the cross-entropy in $a$:

$$ da \;\equiv\; \frac{\partial \mathcal{L}}{\partial a} = \frac{\partial}{\partial a}\Big(-y\log a - (1-y)\log(1-a)\Big) = -\frac{y}{a} + \frac{1-y}{1-a}. \tag{16.27} $$
Equation 16.27: The loss's slope with respect to the activation $a$.

Push this through the sigmoid with $a = \sigma(z)$, whose derivative is $\sigma'(z) = a(1-a)$ (proved in §22.4):

$$ dz \;\equiv\; \frac{\partial \mathcal{L}}{\partial z} = da \cdot \sigma'(z) = \left(-\frac{y}{a} + \frac{1-y}{1-a}\right) a(1-a) = -y(1-a) + (1-y)a = a - y. \tag{16.28} $$
Equation 16.28: The messy $da$ collapses: the sigmoid's $a(1-a)$ cancels the denominators, leaving the clean $dz = a - y$.

The $a(1-a)$ from the sigmoid cancels both denominators in $da$, leaving the clean $dz = a - y$. The last two links — $z = w^\top x + b$, whose slopes in $w$ and $b$ are $x$ and $1$ — then give the parameter gradients:

$$ dw = dz \cdot x = (a-y)\,x, \qquad db = dz = a - y. \tag{16.29} $$
Equation 16.29: The parameter gradients for one logistic-regression unit.

Why $dz = a - y$ comes out so clean. It is not luck. The cross-entropy loss is chosen precisely as the partner of the sigmoid so that $da \cdot \sigma'(z)$ telescopes to $a - y$. Pair a sigmoid output with a squared-error loss instead and the cancellation fails — $dz$ picks up an extra $a(1-a)$ factor that vanishes when the unit saturates, stalling learning. The same pairing is what makes $dZ^{[2]} = A^{[2]} - Y$ the output-error line in §24.

The same graph, one layer deeper

The network just adds a second copy of the block in front of the first. The graph is now $x \to z^{[1]} \to a^{[1]} \to z^{[2]} \to a^{[2]} \to \mathcal{L}$, and backprop applies the identical chain rule down its whole length.

Figure 16.11: The two-layer computation graph. The output block ($z^{[2]}\to a^{[2]}\to\mathcal{L}$) is identical to logistic regression; reaching layer 1 means crossing two more links — back through the weights $W^{[2]}$ and through the hidden activation $g^{[1]}$.

The output block is identical to logistic regression — same sigmoid, same loss — so the same algebra gives the same result at layer 2:

$$ dz^{[2]} = a^{[2]} - y, \qquad dW^{[2]} = dz^{[2]} a^{[1]\top}, \qquad db^{[2]} = dz^{[2]}. \tag{16.30} $$
Equation 16.30: Layer-2 gradients. Identical to the logistic-regression case, except the block's input is now the hidden activation $a^{[1]}$, not $x$.

The only difference from Part I is what plays the role of "input" to this block: not $x$, but the hidden activation $a^{[1]}$ — hence $dW^{[2]} = dz^{[2]} a^{[1]\top}$ (the block's error times its input, transposed).

To reach layer 1 the error must cross two more links — back through the weights $W^{[2]}$ that connect $a^{[1]}$ to $z^{[2]}$, and back through the hidden activation $g^{[1]}$. The chain rule multiplies both:

$$ dz^{[1]} = \underbrace{W^{[2]\top} dz^{[2]}}_{\text{back through the weights}\ =\ da^{[1]}} \;\odot\; \underbrace{g^{[1]\prime}\left(z^{[1]}\right)}_{\text{back through the activation}}. \tag{16.31} $$
Equation 16.31: Pushing the error back to the hidden pre-activation. The first factor is $da^{[1]}$; the second passes it through the hidden unit's own slope, element-wise.

The first factor is $da^{[1]}$, the error landed on the hidden activations: since $z^{[2]} = W^{[2]} a^{[1]} + b^{[2]}$ makes $a^{[1]}$'s slope onto $z^{[2]}$ equal to $W^{[2]}$, the transpose $W^{[2]\top}$ carries the $dz^{[2]}$ error back onto $a^{[1]}$. The second factor passes it through the hidden unit's slope $g^{[1]\prime}(z^{[1]})$ — element-wise, because each hidden unit has its own independent activation. With $dz^{[1]}$ in hand, the layer-1 gradients are the same "error times input" pattern, the input now being $x$:

$$ dW^{[1]} = dz^{[1]} x^{\top}, \qquad db^{[1]} = dz^{[1]}. \tag{16.32} $$
Equation 16.32: Layer-1 gradients — the same shape and pattern as layer 2, with the network input $x$ as the block's input.

These are exactly the six single-example equations of §24.1; stacking examples into columns and averaging (the $\tfrac1m$ and the np.sum) turns them into the vectorized set. So backprop is nothing more than the Part I derivation run twice — once for the output block unchanged, once more to push the error back through the extra layer.

The pattern that generalizes. Every layer does the same three things to the error signal: (1) receive $dz^{[\ell]}$; (2) read off its gradients $dW^{[\ell]} = dz^{[\ell]} a^{[\ell-1]\top}$ and $db^{[\ell]} = dz^{[\ell]}$; (3) hand the layer before it $dz^{[\ell-1]} = W^{[\ell]\top} dz^{[\ell]} \odot g^{[\ell-1]\prime}(z^{[\ell-1]})$. Two layers here, but the recipe is layer-count-agnostic — which is exactly how it extends to the deep networks of a later part.

The whole backward pass, one derivative at a time

Putting the two warm-ups together, here is the entire backward pass as a chain of single derivative steps. Each step multiplies by exactly one node's local slope and hands its result to the next — the mechanical recipe you would follow by hand. The graph below is the full gradient computation graph: start at the loss and walk left to right (this is the backward direction), applying the local factor written on each edge.

Figure 16.12: The full gradient computation graph. Every intermediate derivative is its own node; each red edge carries the local slope you multiply by. Blue = seed / carried error, yellow = pre-activation error $dz$, green = a parameter gradient you keep. An activation node contributes an element-wise $\odot$ by its slope; a linear node $Wa+b$ either reads off a parameter gradient (error × input) or carries the error back through $W^{\top}$.

Now the same walk written out, one derivative per step:

Step 1 — the loss's own slope (seed the pass). Differentiate the loss in its argument $a^{[2]}$. This is the only step that touches the loss formula; everything after is mechanical chain rule. $$ da^{[2]} = \frac{\partial \mathcal{L}}{\partial a^{[2]}} = -\frac{y}{a^{[2]}} + \frac{1-y}{1-a^{[2]}}. $$

Step 2 — through the output activation. Multiply by the sigmoid's local slope $\sigma'(z^{[2]}) = a^{[2]}(1-a^{[2]})$. As in §25.1 the product telescopes: $$ dz^{[2]} = da^{[2]} \odot \sigma'(z^{[2]}) = a^{[2]} - y. $$

Step 3 — read off the output-layer parameters. At the linear node $z^{[2]} = W^{[2]}a^{[1]} + b^{[2]}$ the slope in $W^{[2]}$ is the input $a^{[1]}$ and the slope in $b^{[2]}$ is $1$: $$ dW^{[2]} = dz^{[2]} a^{[1]\top}, \qquad db^{[2]} = dz^{[2]}. $$

Step 4 — carry the error back through the output weights. The same node also feeds its input $a^{[1]}$; the slope onto $a^{[1]}$ is $W^{[2]}$, so transpose to send the error back: $$ da^{[1]} = W^{[2]\top} dz^{[2]}. $$

Step 5 — through the hidden activation. Multiply by the hidden unit's local slope, element-wise: $$ dz^{[1]} = da^{[1]} \odot g^{[1]\prime}\left(z^{[1]}\right) = W^{[2]\top} dz^{[2]} \odot g^{[1]\prime}\left(z^{[1]}\right). $$

Step 6 — read off the hidden-layer parameters. At $z^{[1]} = W^{[1]}x + b^{[1]}$ the input is $x$, so the pattern of step 3 repeats: $$ dW^{[1]} = dz^{[1]} x^{\top}, \qquad db^{[1]} = dz^{[1]}. $$

That is the whole pass. There are really only two kinds of step, and every layer just alternates them: an activation node contributes an element-wise $\odot$ by its local slope (steps 2 and 5), while a linear node $Wa+b$ either reads off a parameter gradient — error × input, transposed (steps 3 and 6) — or carries the error back through the weights, $\times\, W^{\top}$ (step 4). Knowing which move each node type demands is all you need to differentiate any network by hand.

Forward nodeLocal derivative(s)Backward step
$\mathcal{L}\left(a^{[2]}, y\right)$$\dfrac{\partial \mathcal{L}}{\partial a^{[2]}}$seed $da^{[2]} = -\dfrac{y}{a^{[2]}} + \dfrac{1-y}{1-a^{[2]}}$
$a^{[2]} = \sigma\left(z^{[2]}\right)$$\sigma'\left(z^{[2]}\right) = a^{[2]}\left(1-a^{[2]}\right)$$dz^{[2]} = da^{[2]} \odot \sigma'\left(z^{[2]}\right) = a^{[2]} - y$
$z^{[2]} = W^{[2]}a^{[1]} + b^{[2]}$$a^{[1]}$  (in $W^{[2]}$),   $1$  (in $b^{[2]}$),   $W^{[2]}$  (in $a^{[1]}$)$dW^{[2]} = dz^{[2]} a^{[1]\top}$;   $db^{[2]} = dz^{[2]}$;   $da^{[1]} = W^{[2]\top} dz^{[2]}$
$a^{[1]} = g^{[1]}\left(z^{[1]}\right)$$g^{[1]\prime}\left(z^{[1]}\right)$$dz^{[1]} = da^{[1]} \odot g^{[1]\prime}\left(z^{[1]}\right)$
$z^{[1]} = W^{[1]}x + b^{[1]}$$x$  (in $W^{[1]}$),   $1$  (in $b^{[1]}$)$dW^{[1]} = dz^{[1]} x^{\top}$;   $db^{[1]} = dz^{[1]}$
Table 16.6: The per-node recipe. Walk the forward graph in reverse; at each node apply its local derivative to the incoming error. This table generates every line of the backward pass.

Collecting the "keep" lines ($dz^{[2]}, dW^{[2]}, db^{[2]}, dz^{[1]}, dW^{[1]}, db^{[1]}$) gives back exactly the six equations of §24.1; the vectorized set (§24.2) is the same six with examples stacked into columns and the $\tfrac1m$ average. The intermediate $da^{[2]}$ and $da^{[1]}$ never need storing — they are folded into $dz^{[2]}$ and $dz^{[1]}$ — which is why the compact mirror in §24 jumps straight from $dz^{[2]}$ to $dz^{[1]}$.


Random initialization

One choice remains before the loop of §23.2 can run: the starting values of the parameters. For logistic regression, starting at all-zeros was fine. For a neural network it is fatal — and the reason is a subtle symmetry that random initialization exists to break.

Why all-zeros fails: the symmetry trap

Take the $2 \to 2 \to 1$ net and set every weight and bias to zero. Now watch the two hidden units through one full iteration.

$$ a^{[1]}_1 = a^{[1]}_2 \;\Longrightarrow\; dz^{[1]}_1 = dz^{[1]}_2 \;\Longrightarrow\; dW^{[1]} = \begin{pmatrix} u & v \\ u & v \end{pmatrix} \;\Longrightarrow\; \text{rows of } W^{[1]} \text{ stay equal.} \tag{16.33} $$
Equation 16.33: The symmetry is self-preserving: equal activations force equal errors, which force equal gradient rows, which keep the weight rows equal after the update — forever.

By induction the two hidden units remain the same function after any number of iterations. A network whose hidden units are forced to be identical is no more expressive than one with a single hidden unit — the extra units are dead weight. This is the symmetry problem: units that start identical and are trained identically can never differentiate.

What zero actually breaks. Only the weight symmetry matters. The biases $b^{[1]}, b^{[2]}$ can safely start at zero — a bias does not multiply an input, so equal biases alone don't clone the units once the weights differ. It is the weight rows being equal that makes the units identical. Break the weight symmetry and the biases sort themselves out.

The fix: small random weights

The cure is to start the weights at small random values so no two units begin alike; the biases can stay at zero.

W1 = np.random.randn(n1, n0) * 0.01   # small random -> breaks symmetry
b1 = np.zeros((n1, 1))                 # bias may start at zero
W2 = np.random.randn(n2, n1) * 0.01   # small random
b2 = np.zeros((n2, 1))

np.random.randn(...) draws each entry from a standard Gaussian, so the weight rows differ with probability 1 — the units diverge from the very first step and go on to learn distinct features. The biases start at zero because, once the weights differ, the symmetry is already broken.

Why the weights must be small

Why multiply by $0.01$ — why small? Because of where large weights land you on the activation curve. If $W$ is large, the scores $z = Wx + b$ are large in magnitude, and for a sigmoid or tanh a large $|z|$ sits far out on a flat tail where the slope $g'(z) \approx 0$ (the saturation seen in §22.4). A near-zero slope means a near-zero gradient — $dz^{[1]} = W^{[2]\top}dz^{[2]} \odot g^{[1]\prime}(z^{[1]})$ is throttled by that tiny $g^{[1]\prime}$ — so learning crawls from the first step. Small weights keep $z$ near $0$, in the steep central region where $g'$ is largest and gradients flow freely.

Is $0.01$ magic? No — it is a reasonable default for a shallow network, small enough to keep tanh/sigmoid units off their flat tails. For very deep networks a fixed $0.01$ is too blunt; the scale is then chosen per layer from the fan-in (He / Xavier initialization) so signal neither vanishes nor explodes through many layers. That refinement belongs to a later part; for one hidden layer, $\times\,0.01$ is enough. Note the constant multiplies only the weights — the zero biases are left untouched.


Part III · Deep networks

The one-hidden-layer network of Part II already contains every idea a deep network needs: depth just repeats the same layer block more times. This part fixes the $L$-layer notation, writes the forward pass for any depth, pins down the matrix shapes so the code actually runs, argues why depth is worth having at all, and assembles the layer blocks into a complete training step. It closes with the two questions every practitioner asks next: which numbers do you choose rather than learn, and how much of this is really about brains.

What makes a network "deep"?

"Shallow" and "deep" are two ends of one axis: how many layers of computation sit between the input and the prediction. We count the computing layers — the hidden layers plus the output layer — and leave out the input, exactly the convention fixed for the two-layer network in §19. By that count logistic regression is a one-layer network (a single output unit, no hidden layers) — as shallow as it gets — and each hidden layer you add moves one step toward "deep."

NetworkHidden layersDepth $L$ (computing layers)Informal name
Logistic regression$0$$1$the shallowest network
One hidden layer$1$$2$a "shallow" network
Two hidden layers$2$$3$
Five hidden layers$5$$6$a "deep" network
Table 16.7: Depth is a spectrum, not a switch. $L$ counts the computing layers (hidden + output); the input layer is layer $0$ and is not counted. There is no hard threshold — 'deep' just means several hidden layers.

The important point is mechanical: depth changes nothing about what one layer does — it only repeats the score-then-activate block. Every equation below is the Part II layer computation with the index $\ell$ now allowed to run all the way to $L$. (Why extra depth is worth having — that some functions are far cheaper to represent with many thin layers than one wide one — is a separate topic left for a later part.)

Notation for an $L$-layer network

Take the bracket notation from §17 and let the layer index run to $L$. Nothing is redefined; the two-layer case was just $L = 2$.

The running example throughout this part is the four-layer network below: $n^{[0]} = 3$ inputs, hidden layers of $n^{[1]} = 5$, $n^{[2]} = 5$, $n^{[3]} = 3$ units, and a single output unit $n^{[4]} = 1$, so $L = 4$.

Figure 16.13: The four-layer running example ($L = 4$), drawn neuron by neuron: $n^{[0]} = 3$ inputs, hidden layers of $5$, $5$, $3$ units, and a single output unit $\hat{y}$. Every circle is one unit; every arrow is one weight; the shaded bands mark the layers.
SymbolMeaningShape (one example)
$L$number of layers (hidden + output; input excluded)scalar
$n^{[\ell]}$units in layer $\ell$;   $n^{[0]} = n_x$,   $n^{[L]}$ = output sizescalar
$a^{[0]} = x$the input — "layer 0" activations$(n^{[0]},\ 1)$
$z^{[\ell]}$pre-activation (score) of layer $\ell$$(n^{[\ell]},\ 1)$
$a^{[\ell]} = g^{[\ell]}\!\left(z^{[\ell]}\right)$activations of layer $\ell$;   $a^{[L]} = \hat{y}$$(n^{[\ell]},\ 1)$
$W^{[\ell]}$weights of layer $\ell$$(n^{[\ell]},\ n^{[\ell-1]})$
$b^{[\ell]}$bias of layer $\ell$$(n^{[\ell]},\ 1)$
$g^{[\ell]}$activation of layer $\ell$ (may differ per layer)element-wise
Table 16.8: $L$-layer notation. Every symbol is the Part II one with the layer index $\ell$ ranging over $1, \dots, L$. Shapes are for a single example; the vectorized shapes ($m$ examples) appear in §30.

Forward propagation in a deep network

The per-layer computation is exactly the block from §20, now written for a general layer $\ell$: take the previous layer's activation, form a score, squash it.

$$ z^{[\ell]} = W^{[\ell]} a^{[\ell-1]} + b^{[\ell]}, \qquad a^{[\ell]} = g^{[\ell]}\left(z^{[\ell]}\right), \qquad a^{[0]} = x. \tag{16.34} $$
Equation 16.34: The layer-$\ell$ forward step for one example. Identical to the two-layer case; only the index changes. The recursion is seeded by $a^{[0]} = x$.

To run the whole network, apply this for $\ell = 1, 2, \dots, L$ in order — each layer consuming the activation the previous one produced:

a[0] = x
for l = 1 to L:
    z[l] = W[l] @ a[l-1] + b[l]
    a[l] = g[l](z[l])
yhat = a[L]

Vectorized across all $m$ examples, stack them as columns ($X = A^{[0]}$) exactly as in §21; every lowercase vector becomes an uppercase matrix:

$$ Z^{[\ell]} = W^{[\ell]} A^{[\ell-1]} + b^{[\ell]}, \qquad A^{[\ell]} = g^{[\ell]}\left(Z^{[\ell]}\right), \qquad A^{[0]} = X, \quad \hat{Y} = A^{[L]}. \tag{16.35} $$
Equation 16.35: The vectorized deep forward pass. Lowercase $\to$ uppercase; the bias $b^{[\ell]}$ broadcasts across the $m$ columns. $A^{[0]} = X$ seeds it and $\hat{Y} = A^{[L]}$ reads off the predictions.
Figure 16.14: The vectorized forward pass as a chain of layer blocks. The input matrix $X = A^{[0]}$ flows through the $L$ layers to $A^{[L]} = \hat{Y}$; each arrow is one matrix multiply plus a broadcast bias, then an element-wise activation.

One honest caveat about vectorization: it removed the loop over examples (that became the $m$ columns), but the loop over layers $\ell = 1, \dots, L$ stays — each layer needs the previous layer's output, so the layers cannot run in parallel. That explicit for l in range(1, L+1) is expected and fine; $L$ is small.

Getting the matrix dimensions right

The fastest way to debug a deep network is to check that every shape lines up. Two rules generate all of them, and they follow directly from the forward equation.

$$ \underbrace{z^{[\ell]}}_{(n^{[\ell]},\,1)} = \underbrace{W^{[\ell]}}_{(n^{[\ell]},\, n^{[\ell-1]})}\, \underbrace{a^{[\ell-1]}}_{(n^{[\ell-1]},\,1)} + \underbrace{b^{[\ell]}}_{(n^{[\ell]},\,1)}. \tag{16.36} $$
Equation 16.36: Shapes along one forward step. For the product to land in $(n^{[\ell]},\,1)$ with $a^{[\ell-1]}$ of size $(n^{[\ell-1]},\,1)$, the weight matrix must be $(n^{[\ell]},\,n^{[\ell-1]})$ and the bias $(n^{[\ell]},\,1)$.
Figure 16.15: The five-layer example drawn neuron by neuron, with unit counts $n = [\,2,3,5,4,2,1\,]$. Read each layer's width straight off the picture: the arrows into layer $\ell$ form $W^{[\ell]}$, of shape $(n^{[\ell]},\ n^{[\ell-1]})$ — the two counts the arrows connect. The exact shapes are tabulated below.
Layer $\ell$$n^{[\ell-1]} \to n^{[\ell]}$$W^{[\ell]}$  $(n^{[\ell]},\, n^{[\ell-1]})$$b^{[\ell]}$  $(n^{[\ell]},\, 1)$
$1$$2 \to 3$$(3,\ 2)$$(3,\ 1)$
$2$$3 \to 5$$(5,\ 3)$$(5,\ 1)$
$3$$5 \to 4$$(4,\ 5)$$(4,\ 1)$
$4$$4 \to 2$$(2,\ 4)$$(2,\ 1)$
$5$$2 \to 1$$(1,\ 2)$$(1,\ 1)$
Table 16.9: Every weight and bias shape for a five-layer example with unit counts $n = [\,2, 3, 5, 4, 2, 1\,]$ (so $n^{[0]} = n_x = 2$ up to $n^{[5]} = 1$). Each $W^{[\ell]}$ is $(n^{[\ell]}, n^{[\ell-1]})$ — 'this layer's size × the previous layer's size.'

Going from one example to all $m$ of them changes only the second dimension, and only for the quantities that are per-example. The parameters do not grow with $m$ — there is one $W^{[\ell]}$ and one $b^{[\ell]}$ no matter how many examples you feed in — which is exactly why the bias has to broadcast.

ObjectOne exampleAll $m$ examples
input$x = a^{[0]}$   $(n^{[0]},\ 1)$$X = A^{[0]}$   $(n^{[0]},\ m)$
pre-activation$z^{[\ell]}$   $(n^{[\ell]},\ 1)$$Z^{[\ell]}$   $(n^{[\ell]},\ m)$
activation$a^{[\ell]}$   $(n^{[\ell]},\ 1)$$A^{[\ell]}$   $(n^{[\ell]},\ m)$
weights$W^{[\ell]}$   $(n^{[\ell]},\ n^{[\ell-1]})$   — unchanged
bias$b^{[\ell]}$   $(n^{[\ell]},\ 1)$   — unchanged, broadcast across the $m$ columns
weight gradient$dW^{[\ell]}$   $(n^{[\ell]},\ n^{[\ell-1]})$
bias gradient$db^{[\ell]}$   $(n^{[\ell]},\ 1)$
error signals$dz^{[\ell]}, da^{[\ell]}$   $(n^{[\ell]},\ 1)$$dZ^{[\ell]}, dA^{[\ell]}$   $(n^{[\ell]},\ m)$
Table 16.10: One example versus $m$ examples. The per-example quantities $z, a$ (and their gradients) gain a column per example; the parameters $W^{[\ell]}, b^{[\ell]}$ keep their shapes. A gradient always matches the shape of what it differentiates — so $dZ^{[\ell]}$ is shaped like $Z^{[\ell]}$, and $db^{[\ell]}$ stays $(n^{[\ell]}, 1)$ even though $dZ^{[\ell]}$ has $m$ columns (the sum over examples collapses them).

The one habit to keep. Before running anything, write the shape under every matrix in the forward (and backward) equations and confirm the inner dimensions chain: $(n^{[\ell]},\, n^{[\ell-1]}) \times (n^{[\ell-1]},\, m) = (n^{[\ell]},\, m)$. A mismatch is almost always a $W^{[\ell]}$ that needs transposing or a wrong $n^{[\ell]}$ — caught in seconds on paper, versus a confusing broadcast bug at runtime.

Why deep representations?

Depth is not just "more of the same" — it changes what the network can represent cheaply. Two views make this concrete: an intuition about feature hierarchies, and a result from circuit theory.

Layers build simple features into complex ones

The standard intuition: early layers detect simple patterns, and each later layer composes the previous layer's features into something more complex. On images, the first hidden layer picks out edges and gradients; the next assembles edges into parts (an eye, a nose); the next assembles parts into whole objects (a face). The same story holds for audio — raw waveform → phonemes → words → phrases — and for language.

Figure 16.16: Depth as a feature hierarchy (a face recogniser). Each layer composes the previous layer's features into a more complex one — edges, then parts, then whole faces — so the representation grows from 'simple' to 'complex' with depth.

That schematic is borne out by what the layers of a real trained network actually learn:

Three grids of trained features: oriented edge filters, then facial parts (eyes, noses), then whole faces
Figure 16.17: What the layers of a face recogniser actually learn (a visualisation of trained hidden units). Left — the first hidden layer responds to oriented edges. Middle — a deeper layer combines edges into parts: eyes, noses, mouths. Right — a still-deeper layer assembles parts into whole faces. Simple to complex, left to right, exactly as the schematic above predicts.

A shallow network can approximate the same function, but it must do in one wide layer what a deep network does in stages — and that turns out to be dramatically more expensive.

Circuit theory: depth saves exponential width

Informally: there are functions a small $L$-layer deep network can compute that a shallow network needs exponentially more hidden units to match. The classic example is the parity function — the XOR of $n$ inputs, $x_1 \oplus x_2 \oplus \cdots \oplus x_n$.

On a log scale the difference is stark: the deep tree's size grows linearly in $n$ while the single hidden layer grows exponentially — a straight line rocketing away.

Figure 16.18: Units needed to compute the parity of $n$ inputs, on a logarithmic vertical axis. The deep tree ($\sim n$ units) stays near the bottom; the single hidden layer ($\sim 2^{n-1}$ units) climbs a straight line on the log scale — i.e. exponential growth. By $n = 20$ that is $\sim 20$ units versus over half a million.

The exact counts:

Inputs $n$Deep tree — depth $O(\log n)$Deep tree — total units $\approx n$One hidden layer — units $\approx 2^{\,n-1}$
$4$$2$$3$$8$
$8$$3$$7$$128$
$16$$4$$15$$32{,}768$
$32$$5$$31$$\approx 2.1 \times 10^{9}$
Table 16.11: Computing the parity (XOR) of $n$ inputs. A deep tree of XOR gates stays tiny; a single-hidden-layer network blows up exponentially — the concrete gap circuit theory predicts.

The takeaway. Depth buys expressive efficiency: a few extra layers can replace an exponentially wide single layer. That is a large part of why "deep" networks — rather than merely "wide" ones — became the default. (It is an argument about what is representable cheaply, not a guarantee that training will find it.)

Building blocks: forward and backward functions

To organise the code for an $L$-layer network, think of each layer as two functions that share a small cache. This is the mental model that makes deep backprop tidy.

One layer, two functions

Figure 16.19: One layer as a forward/backward pair sharing a cache. The forward function turns $a^{[\ell-1]}$ into $a^{[\ell]}$ and stores $z^{[\ell]}$; the backward function reuses that cache to turn the incoming error $da^{[\ell]}$ into $da^{[\ell-1]}$ and the kept gradients $dW^{[\ell]}, db^{[\ell]}$.

Chaining the blocks across the network

Chaining these blocks gives the whole algorithm. Take a concrete three-layer network — ReLU, ReLU, sigmoid — the shape of almost every binary classifier in this reference. The forward pass runs the forward functions $\ell = 1 \dots L$ left to right, each one caching what its partner will need:

Figure 16.20: The forward chain for a three-layer network (ReLU $\to$ ReLU $\to$ sigmoid). Each block consumes the previous activation, emits the next one, and stores its cache. The final activation $A^{[3]} = \hat{Y}$ is scored by the loss.

The loss then seeds the error, and the backward pass runs the backward functions $\ell = L \dots 1$ — the same chain traversed in reverse, each block consuming its cache, emitting the parameter gradients it owns, and handing $dA^{[\ell-1]}$ to the block before it:

Figure 16.21: The backward chain (red), the mirror image of the figure above. Each block reuses its cached $Z^{[\ell]}$, keeps $dW^{[\ell]}, db^{[\ell]}$, and passes $dA^{[\ell-1]}$ back. The last output, $dA^{[0]}$, is discarded — the input layer has no parameters to train.

Notice what each half stores. The forward pass caches $z^{[\ell]}$ because the backward function needs the local slope $g^{[\ell]\prime}\left(z^{[\ell]}\right)$ at exactly the point the forward pass passed through; in code it is convenient to cache $W^{[\ell]}, b^{[\ell]}$ alongside it, so a backward block receives everything it needs in one object. And the very last thing the chain produces, $dA^{[0]}$ — how the inputs would like to change — is thrown away: $x$ is data, not a parameter.

Finally every layer takes the gradient-descent step from §6, now once per layer:

$$ W^{[\ell]} := W^{[\ell]} - \alpha\, dW^{[\ell]}, \qquad b^{[\ell]} := b^{[\ell]} - \alpha\, db^{[\ell]}, \qquad \ell = 1, \dots, L. \tag{16.37} $$
Equation 16.37: The parameter update, applied to all $L$ layers after one forward/backward sweep. $\alpha$ is the learning rate; one sweep plus one update is a single iteration of gradient descent.

Forward and backward propagation for a layer

Here are the two functions written out, for a general layer $\ell$. The forward half is §29; the backward half is Part II's derivation (§25) with the index generalised from ${1,2}$ to any $\ell$.

Forward (layer $\ell$). Input $a^{[\ell-1]}$, output $a^{[\ell]}$, cache $z^{[\ell]}$: $$ z^{[\ell]} = W^{[\ell]} a^{[\ell-1]} + b^{[\ell]}, \qquad a^{[\ell]} = g^{[\ell]}\left(z^{[\ell]}\right). $$

Backward (layer $\ell$). Input $da^{[\ell]}$ and the cached $z^{[\ell]}$; output $da^{[\ell-1]}, dW^{[\ell]}, db^{[\ell]}$:

$$ \begin{align} dz^{[\ell]} &= da^{[\ell]} \odot g^{[\ell]\prime}\left(z^{[\ell]}\right) \tag{16.38.1} \\ dW^{[\ell]} &= dz^{[\ell]}\, a^{[\ell-1]\top} \tag{16.38.2} \\ db^{[\ell]} &= dz^{[\ell]} \tag{16.38.3} \\ da^{[\ell-1]} &= W^{[\ell]\top} dz^{[\ell]} \tag{16.38.4} \end{align} $$
Equation 16.38: The four backward-function lines for one example at layer $\ell$. These are the §25 equations with $\ell$ in place of the specific layers 1 and 2; $\odot$ is the element-wise product.

The last line is what makes it a chain: $da^{[\ell-1]}$ is exactly the input the layer before needs. Substituting it back one step gives the pure recursion in the $dz$'s,

$$ dz^{[\ell]} = W^{[\ell+1]\top} dz^{[\ell+1]} \odot g^{[\ell]\prime}\left(z^{[\ell]}\right), \tag{16.39} $$
Equation 16.39: The error recursion: layer $\ell$'s error is the next layer's error carried back through $W^{[\ell+1]}$ and the local slope $g^{[\ell]\prime}$. Run it down from $\ell = L$ to $\ell = 1$.

which is the general form of the "push the error back one layer" line first seen at §24.1. Like any recursion it needs a first value, and that value comes from the loss at the output layer — see §33.1 below.

Figure 16.22: The backward function at a general layer $\ell$ (red). The incoming error $da^{[\ell]}$ passes through the local slope to $dz^{[\ell]}$, which reads off $dW^{[\ell]}, db^{[\ell]}$ and is carried back through $W^{[\ell]\top}$ to $da^{[\ell-1]}$ — the input to layer $\ell-1$.

Vectorized over all $m$ examples (uppercase, with the $\tfrac1m$ average and the bias row-sum from §24.2):

$$ \begin{align} dZ^{[\ell]} &= dA^{[\ell]} \odot g^{[\ell]\prime}\left(Z^{[\ell]}\right) \tag{16.40.1} \\ dW^{[\ell]} &= \tfrac{1}{m}\, dZ^{[\ell]} A^{[\ell-1]\top} \tag{16.40.2} \\ db^{[\ell]} &= \tfrac{1}{m}\, \texttt{np.sum}\big(dZ^{[\ell]},\ \text{axis}=1,\ \text{keepdims}\big) \tag{16.40.3} \\ dA^{[\ell-1]} &= W^{[\ell]\top} dZ^{[\ell]} \tag{16.40.4} \end{align} $$
Equation 16.40: The vectorized backward function at layer $\ell$. Stack examples as columns; average the parameter gradients over $m$; sum the bias across the example columns.
dZl  = dAl * gprime_l(Zl)                          # (n_l,   m)   * = element-wise
dWl  = (1/m) * dZl @ A_prev.T                       # (n_l,   n_{l-1})
dbl  = (1/m) * np.sum(dZl, axis=1, keepdims=True)   # (n_l,   1)
dA_prev = Wl.T @ dZl                                # (n_{l-1}, m)   -> input to layer l-1

Every shape is the §30 rule at work: $dW^{[\ell]}$ matches $W^{[\ell]}$ at $(n^{[\ell]}, n^{[\ell-1]})$, $db^{[\ell]}$ matches $b^{[\ell]}$ at $(n^{[\ell]}, 1)$, and $dA^{[\ell-1]}$ comes out $(n^{[\ell-1]}, m)$ — ready for the next backward block.

Seeding the recursion at the output

The backward chain is a recursion, and a recursion needs a first value. That value comes from the loss: differentiate $\mathcal{L}(\hat{y}, y) = -y\log a^{[L]} - (1-y)\log(1 - a^{[L]})$ with respect to the network's own output $a^{[L]} = \hat{y}$.

$$ da^{[L]} = -\frac{y}{a^{[L]}} + \frac{1-y}{1-a^{[L]}}, \qquad dA^{[L]} = \begin{bmatrix} -\dfrac{y^{(1)}}{a^{[L]{}(1)}} + \dfrac{1-y^{(1)}}{1-a^{[L]{}(1)}} & \cdots & -\dfrac{y^{(m)}}{a^{[L]{}(m)}} + \dfrac{1-y^{(m)}}{1-a^{[L]{}(m)}} \end{bmatrix}. \tag{16.41} $$
Equation 16.41: The seed of the backward pass, for one example (left) and stacked across the $m$ examples (right) — one column per example. This is $da$ from §10.2, now wearing the layer index $L$.

Feed that $dA^{[L]}$ into the layer-$L$ backward function and the first line does something pleasant. With a sigmoid output, $g^{[L]\prime}\left(z^{[L]}\right) = a^{[L]}(1 - a^{[L]})$, so the messy quotient cancels against the slope and collapses — exactly as proved in §25.1 — to

$$ dZ^{[L]} = dA^{[L]} \odot g^{[L]\prime}\left(Z^{[L]}\right) = A^{[L]} - Y. $$

In practice you skip $dA^{[L]}$ altogether and start the loop at $dZ^{[L]} = A^{[L]} - Y$: one subtraction, no division, and no risk of dividing by an $a^{[L]}$ that has saturated to $0$ or $1$. (Write it out as $dA^{[L]}$ only when the output activation is not sigmoid, since then the cancellation no longer happens.)

With the forward pass of §29, this per-layer backward function is a complete $L$-layer training step: run forward caching each $Z^{[\ell]}$, seed $dZ^{[L]} = A^{[L]} - Y$, run backward for $\ell = L \dots 1$, then update every $W^{[\ell]}, b^{[\ell]}$ with the rule in §32.2.

Parameters versus hyperparameters

Everything trained so far — every $W^{[\ell]}$ and $b^{[\ell]}$ — is a parameter: gradient descent learns it from the data. But look back at the algorithm and you will find a second set of numbers that gradient descent never touches, because you chose them before it ran: the learning rate $\alpha$, how many iterations to run, how many layers $L$, how wide each layer $n^{[\ell]}$ is, which activation $g^{[\ell]}$ to use.

These are the hyperparameters, and the name is precise: they are the knobs that control the parameters. $\alpha$ decides how far each update moves $W^{[\ell]}$; $L$ and $n^{[\ell]}$ decide how many $W^{[\ell]}$'s there are and how big; the choice of $g$ decides the slope $g'$ that the gradient flows through. Change a hyperparameter and you get a different set of learned parameters.

The knobs you set yourself

QuantitySymbolWho sets itWhat it controls
Weights$W^{[1]}, \dots, W^{[L]}$learned — gradient descentthe function the network computes
Biases$b^{[1]}, \dots, b^{[L]}$learned — gradient descenteach unit's threshold
Learning rate$\alpha$youhow far each update step moves
Iterationsyouhow long training runs
Number of layers$L$youthe depth of the network
Units per layer$n^{[1]}, \dots, n^{[L-1]}$youthe width of each hidden layer
Activation choice$g^{[\ell]}$youthe non-linearity, hence $g'$
— and, once the later parts arrive —
Momentum$\beta$youhow much past gradient carries forward
Mini-batch sizeyouexamples per update step
Regularization$\lambda$youhow hard overfitting is penalised
Table 16.12: Parameters are learned by gradient descent; hyperparameters are chosen by you and determine which parameters get learned. The bottom group are knobs introduced by later topics — listed here so the distinction, not the list, is what you remember.

Why the distinction matters. You cannot differentiate the cost with respect to $L$ — "half a layer" is meaningless — so hyperparameters cannot be learned by the same machinery that learns $W$. They have to be searched over: pick values, train, measure, repeat. That outer loop, not the inner gradient-descent loop, is where most of the real work of applied deep learning goes.

Applied deep learning is an empirical process

There is no formula that returns the right $\alpha$ for your problem. What is optimal depends on the data, the architecture, even the hardware — and intuitions transfer poorly across domains, so a learning rate that is excellent for vision may be useless for speech. The honest method is a cycle:

Figure 16.23: The outer loop of applied deep learning. You guess a hyperparameter setting, implement it, measure the resulting cost curve, and use what you saw to guess better. Unlike gradient descent, this loop is run by you — and it is where most of the time goes.

The single most informative experiment is to plot the cost $J$ against the iteration count for a few learning rates. That one picture diagnoses $\alpha$ immediately:

Figure 16.24: Gradient descent on a simple convex cost, run from the same starting point with four learning rates. $\alpha = 0.52$ overshoots every step and the cost climbs — it leaves the top of the frame by iteration 10. $\alpha = 0.02$ is stable but crawls: after 16 iterations it has only reached $J \approx 0.40$. $\alpha = 0.12$ and $\alpha = 0.30$ both drive $J$ to near zero, the larger one faster. Rising means $\alpha$ is too big; a slow steady decline means it is too small.

Read the curve, not the theory. If $J$ goes up, $\alpha$ is too large — each step overshoots the minimum and lands further up the far wall of the bowl. If $J$ goes down but barely, $\alpha$ is too small. The useful range is often a factor of ten wide, so search it multiplicatively ($0.001, 0.01, 0.1, \dots$) rather than in equal steps. And re-search it when anything else changes: a network that trained happily at $\alpha = 0.05$ can need a different value after you add a layer, because the same step size now travels through a differently-shaped cost surface.

What does this have to do with the brain?

It is worth collecting the whole algorithm in one place before answering that question, because the answer is essentially "less than the name suggests, and the equations are why."

Forward propagationBackward propagation
$Z^{[1]} = W^{[1]} X + b^{[1]}$$dZ^{[L]} = A^{[L]} - Y$
$A^{[1]} = g^{[1]}\left(Z^{[1]}\right)$$dW^{[L]} = \frac{1}{m}\, dZ^{[L]} A^{[L-1]\top}$
$Z^{[2]} = W^{[2]} A^{[1]} + b^{[2]}$$db^{[L]} = \frac{1}{m}\, \texttt{np.sum}\left(dZ^{[L]},\ \text{axis}=1,\ \text{keepdims}\right)$
$A^{[2]} = g^{[2]}\left(Z^{[2]}\right)$$dZ^{[L-1]} = W^{[L]\top} dZ^{[L]} \odot g^{[L-1]\prime}\left(Z^{[L-1]}\right)$
$\vdots$$\vdots$
$A^{[L]} = g^{[L]}\left(Z^{[L]}\right) = \hat{Y}$$dZ^{[1]} = W^{[2]\top} dZ^{[2]} \odot g^{[1]\prime}\left(Z^{[1]}\right)$
$dW^{[1]} = \frac{1}{m}\, dZ^{[1]} A^{[0]\top}$
$db^{[1]} = \frac{1}{m}\, \texttt{np.sum}\left(dZ^{[1]},\ \text{axis}=1,\ \text{keepdims}\right)$
Table 16.13: The complete $L$-layer training step, vectorized over $m$ examples: the forward pass on the left, the backward pass on the right. Everything in Part III, side by side. The forward pass is seeded by $A^{[0]} = X$ and the backward pass by $dZ^{[L]} = A^{[L]} - Y$; the middle layers repeat the same two (respectively three) lines with the index shifted.

A shape check worth doing. The back-propagated error is carried by $W^{[\ell]\top}$, not by $dW^{[\ell]\top}$ — a tempting typo, since the two sit side by side. The shape rule from §30 catches it instantly: both are $(n^{[\ell]}, n^{[\ell-1]})$, so both transpose to a legal shape and NumPy will happily run the wrong one. Reason from meaning instead: $dW$ is a destination (a gradient you keep), $W$ is the road the error travels back along. Likewise $dW^{[1]}$ pairs with $A^{[0]\top} = X^\top$, the activation below layer 1 — never $A^{[1]}$.

The analogy, and where it breaks

Here is the resemblance that gave the field its name. A single unit takes several inputs, combines them, and emits one output if the combination is strong enough — which is roughly what a biological neuron does: dendrites collect signals from other neurons, the cell body integrates them, and if the total crosses a threshold the neuron fires a spike down its axon to the neurons downstream.

Illustration of a biological neuron: dendrites, cell body with nucleus, and a myelinated axon ending in terminals
Figure 16.25: A biological neuron: dendrites (left) collect incoming signals, the cell body integrates them, and the axon (right) carries the output to other neurons. The artificial unit $a = g\left(w^\top x + b\right)$ is a caricature of this — inputs in, one number out — and the resemblance stops at about that level of detail.

Line the two up and the mapping is easy to state — and easy to over-read:

Artificial unitLoose biological counterpartWhere it breaks down
inputs $x_1, x_2, x_3$signals arriving at dendritesreal inputs are spike trains in time, not fixed real numbers
weighted sum $w^\top x + b$the cell body integrating its inputsreal integration is non-linear, leaky, and stateful
activation $g(z)$the firing thresholda neuron emits discrete spikes, not a smooth $g(z)$
output $a$ sent onwardthe axon's spike to the next neuron
learning by backpropagation???no known biological mechanism
Table 16.14: The analogy at its most generous. Each row is a real correspondence; none of them survives close inspection, which is why the analogy is a teaching aid rather than a model of the brain.

That last row is the whole story. Forward propagation is a defensible caricature; the learning rule is not. Nobody knows how a real neuron could compute anything like $dW^{[\ell]}$ — backpropagation requires each unit to know the weights of the units above it and to send an error signal precisely backwards along the forward connections, and brains do not appear to do that. Neuroscience has no consensus account of how a single neuron learns, let alone one that matches the equations above.

Why keep the metaphor at all? It is a good first sentence and a bad second one. "It's like a neuron in the brain" gets someone from zero to a mental picture in five seconds, which is why it survives in press releases and introductory lectures. But the deeper you go, the less it earns: the ideas that actually make these networks work — the chain rule, the convexity of the loss, the shapes lining up, the learning rate — come from calculus and optimization, not from biology. This reference is what a neural network is: a composition of matrix multiplies and non-linearities, fitted by gradient descent. The brain inspired it. It does not explain it.


Part IV · The component view

Parts I–III worked with whole vectors and matrices: $z^{[\ell]} = W^{[\ell]}a^{[\ell-1]} + b^{[\ell]}$ hides a great deal of index bookkeeping inside one product. That compression is exactly what makes the equations implementable — but it also means you have never seen which weight multiplies which activation, or why a transpose appears in the backward pass rather than being asserted.

This part drops one level of resolution. Every quantity gets a subscript, every equation is written for a single scalar, and backpropagation is derived from the chain rule one component at a time. Nothing new is proved — every result here collapses back into an equation you already have. What changes is that you can see inside the matrix products, which is the level you need when deriving a new layer type from scratch or when a shape mismatch will not resolve itself.

In one line. A matrix equation is a bundle of scalar equations. Unbundle them, apply the chain rule to each, and re-bundle — the transposes, the outer products, and the $\tfrac{1}{m}$ all appear on their own.

Indices: naming every scalar

A single symbol in a neural network has to answer four questions at once: which layer, which neuron, which training example, and whether the object is a scalar, a vector, or a matrix. The convention already used throughout this reference answers all four, and here we finally use every slot of it.

So $a_j^{[\ell]{}(i)}$ is one number: the activation of neuron $j$ in layer $\ell$ for example $i$. Its column is $a^{[\ell]{}(i)}$, and stacking those columns for all $m$ examples gives $A^{[\ell]}$.

One symbol needs care. Up to now $L$ has meant the number of layers, and it also commonly denotes the loss. From here on the loss on one example is $\mathcal{L}$ and the cost over the batch is $J = \tfrac{1}{m}\sum_i \mathcal{L}^{(i)}$, exactly as in §5 — never $L$, which remains the depth.

ConceptOne componentVector / matrixMeaning
Indices and sizes
Layer index$[\ell]$$\ell = 1, \dots, L$; layer $0$ is the input
Example index$(i)$$i = 1, \dots, m$
Neuron index$j$, $k$$j$ in layer $\ell$; $k$ in layer $\ell-1$
Layer width$n^{[\ell]}$units in layer $\ell$; $n^{[0]} = n_x$
Forward — activations
Input$x_k^{(i)} = a_k^{[0]{}(i)}$$x^{(i)} \in \mathbb{R}^{n^{[0]}}$,   $X = A^{[0]} \in \mathbb{R}^{n^{[0]} \times m}$feature $k$ of example $i$
Pre-activation$z_j^{[\ell]{}(i)}$$z^{[\ell]{}(i)} \in \mathbb{R}^{n^{[\ell]}}$,   $Z^{[\ell]} \in \mathbb{R}^{n^{[\ell]} \times m}$weighted sum plus bias, before $g$
Activation$a_j^{[\ell]{}(i)}$$a^{[\ell]{}(i)} \in \mathbb{R}^{n^{[\ell]}}$,   $A^{[\ell]} \in \mathbb{R}^{n^{[\ell]} \times m}$output of neuron $j$, after $g$
Forward — parameters (no $(i)$: shared across examples)
Weight$w_{j,k}^{[\ell]}$$W^{[\ell]} \in \mathbb{R}^{n^{[\ell]} \times n^{[\ell-1]}}$from neuron $k$ of layer $\ell-1$ into neuron $j$ of layer $\ell$
Weight row$w_j^{[\ell]} \in \mathbb{R}^{1 \times n^{[\ell-1]}}$row $j$ of $W^{[\ell]}$: every weight feeding neuron $j$
Bias$b_j^{[\ell]}$$b^{[\ell]} \in \mathbb{R}^{n^{[\ell]}}$one offset per neuron
Activation function$g^{[\ell]}(\cdot)$applied element-wise; may differ per layer
Backward — gradients ($d\,\square$ means $\partial \mathcal{L} / \partial \square$)
Activation gradient$da_j^{[\ell]{}(i)}$$da^{[\ell]{}(i)} \in \mathbb{R}^{n^{[\ell]}}$,   $dA^{[\ell]} \in \mathbb{R}^{n^{[\ell]} \times m}$the signal arriving from layer $\ell+1$
Pre-activation gradient$dz_j^{[\ell]{}(i)}$$dz^{[\ell]{}(i)} \in \mathbb{R}^{n^{[\ell]}}$,   $dZ^{[\ell]} \in \mathbb{R}^{n^{[\ell]} \times m}$the shared error signal at neuron $j$
Weight gradient$\dfrac{\partial \mathcal{L}}{\partial w_{j,k}^{[\ell]}}$$dW^{[\ell]} \in \mathbb{R}^{n^{[\ell]} \times n^{[\ell-1]}}$same shape as $W^{[\ell]}$
Bias gradient$\dfrac{\partial \mathcal{L}}{\partial b_j^{[\ell]}}$$db^{[\ell]} \in \mathbb{R}^{n^{[\ell]}}$same shape as $b^{[\ell]}$
Loss, cost, and operators
Loss (one example)$\mathcal{L}^{(i)}$error on example $i$ alone
Cost (the batch)$J$$J = \frac{1}{m}\sum_{i=1}^{m} \mathcal{L}^{(i)}$ — the source of every $\frac{1}{m}$
Hadamard product$\odot$element-wise multiplication
Ones vector$\mathbf{1}_m \in \mathbb{R}^{m}$sums across the example columns
Table 16.15: Every symbol at three resolutions: one scalar component, the column vector for one example, and the matrix for the whole batch. Subscript $j$ indexes a neuron of layer $\ell$; subscript $k$ a neuron of layer $\ell-1$; superscript $(i)$ the example. Parameters carry no $(i)$ — the same weights and biases serve every example.

The distinction between the loss $\mathcal{L}^{(i)}$ and the cost $J$ is worth pausing on, because it is the entire origin of the $\tfrac{1}{m}$ factors in §33. Each example produces its own loss and therefore its own gradient for every parameter. The cost averages those losses, so the parameter gradient is the average of the per-example gradients. Activation gradients carry no $\tfrac{1}{m}$: each example keeps its own error signal all the way back.

Here is the whole apparatus on a concrete network — three inputs, two hidden layers of four units, and two outputs — drawn for one example $i$, so every activation carries $(i)$ while no weight or bias does.

Figure 16.26: A fully-connected network in component notation, drawn for a single example $i$: input layer $0$ ($n^{[0]} = 3$), two hidden layers ($n^{[1]} = n^{[2]} = 4$), output layer $L = 3$ ($n^{[3]} = 2$). Each solid edge is one weight $w_{j,k}^{[\ell]}$ running from source neuron $k$ in layer $\ell-1$ to destination neuron $j$ in layer $\ell$ — row $j$ = destination, column $k$ = source. Only the first row of each $W^{[\ell]}$ is labelled; every other edge carries its own weight the same way. Each dashed bias unit holds the constant $1$ and broadcasts one component $b_j^{[\ell]}$ of the bias vector to every neuron of its layer. Counting the solid edges gives the parameter count: $4 \cdot 3 + 4 \cdot 4 + 2 \cdot 4 = 36$ weights.

Inside one neuron

The circles above hide the two operations every neuron performs. Unfold one — neuron $3$ of hidden layer $1$ — and the forward pass becomes elementary arithmetic: scale each incoming activation by its weight, add them together with the bias, then apply $g$.

Figure 16.27: Inside hidden neuron $a_3^{[1]{}(i)}$: one circle of the network figure, expanded. Each input $a_k^{[0]{}(i)}$ is scaled by its weight $w_{3,k}^{[1]}$ at a $\times$ node; the three products and the bias $b_3^{[1]}$ meet at the $+$ node to form the pre-activation $z_3^{[1]{}(i)}$; the nonlinearity $g$ turns it into the activation. Every neuron in every layer is this same multiply–sum–activate unit.

Written out, that neuron computes a dot product followed by a shift:

$$ z_j^{[\ell]{}(i)} = \sum_{k=1}^{n^{[\ell-1]}} w_{j,k}^{[\ell]}\, a_k^{[\ell-1]{}(i)} + b_j^{[\ell]}, \qquad a_j^{[\ell]{}(i)} = g^{[\ell]}\left(z_j^{[\ell]{}(i)}\right). \tag{16.42} $$
Equation 16.42: The pre-activation of one neuron: a weighted sum over the previous layer's neurons, plus that neuron's own bias. This single scalar equation, applied for every $j$, is all that $z^{[\ell]} = W^{[\ell]}a^{[\ell-1]} + b^{[\ell]}$ says.

Notice which indices the sum runs over. The neuron $j$ is fixed; the sum sweeps $k$ across every neuron of the previous layer. That is the row of $W^{[\ell]}$ belonging to neuron $j$, dotted with the incoming activation column — which is precisely why the matrix form is a product.

The matrices, entry by entry

Now stack those scalars. The layout convention is fixed once and never varies: for parameters, row = destination, column = source; for anything indexed by examples, rows are neurons, columns are examples.

$$ W^{[\ell]} = \begin{bmatrix} w_{1,1}^{[\ell]} & w_{1,2}^{[\ell]} & \cdots & w_{1,n^{[\ell-1]}}^{[\ell]} \\ w_{2,1}^{[\ell]} & w_{2,2}^{[\ell]} & \cdots & w_{2,n^{[\ell-1]}}^{[\ell]} \\ \vdots & \vdots & \ddots & \vdots \\ w_{n^{[\ell]},1}^{[\ell]} & w_{n^{[\ell]},2}^{[\ell]} & \cdots & w_{n^{[\ell]},n^{[\ell-1]}}^{[\ell]} \end{bmatrix} \in \mathbb{R}^{n^{[\ell]} \times n^{[\ell-1]}}, \qquad b^{[\ell]} = \begin{bmatrix} b_1^{[\ell]} \\ b_2^{[\ell]} \\ \vdots \\ b_{n^{[\ell]}}^{[\ell]} \end{bmatrix} \in \mathbb{R}^{n^{[\ell]}}. \tag{16.43} $$
Equation 16.43: The weight matrix of layer $\ell$. Row $j$ collects every weight feeding neuron $j$ of layer $\ell$; column $k$ collects every weight leaving neuron $k$ of layer $\ell-1$. Mapping $n^{[\ell-1]}$ inputs to $n^{[\ell]}$ outputs forces exactly this shape.

Every example-indexed object shares one layout. The input matrix, the pre-activations, and the activations are all "neurons down, examples across":

$$ Z^{[\ell]} = \begin{bmatrix} z_1^{[\ell]{}(1)} & z_1^{[\ell]{}(2)} & \cdots & z_1^{[\ell]{}(m)} \\ z_2^{[\ell]{}(1)} & z_2^{[\ell]{}(2)} & \cdots & z_2^{[\ell]{}(m)} \\ \vdots & \vdots & \ddots & \vdots \\ z_{n^{[\ell]}}^{[\ell]{}(1)} & z_{n^{[\ell]}}^{[\ell]{}(2)} & \cdots & z_{n^{[\ell]}}^{[\ell]{}(m)} \end{bmatrix} \in \mathbb{R}^{n^{[\ell]} \times m}. \tag{16.44} $$
Equation 16.44: The pre-activation matrix, entry by entry. Row $j$ is neuron $j$; column $i$ is example $i$. $X = A^{[0]}$ and $A^{[\ell]}$ have exactly this layout, which is why one column of any of these equations is the single-example equation.

Bias broadcasting, written honestly

In $Z^{[\ell]} = W^{[\ell]}A^{[\ell-1]} + b^{[\ell]}$ the product already has shape $(n^{[\ell]}, m)$, but $b^{[\ell]}$ is only $(n^{[\ell]}, 1)$. NumPy's broadcasting (§15) silently copies the bias across all $m$ columns. The exact statement of what it copies is an outer product with a row of ones:

$$ b^{[\ell]}\mathbf{1}_m^\top = \begin{bmatrix} b_1^{[\ell]} \\ \vdots \\ b_{n^{[\ell]}}^{[\ell]} \end{bmatrix} \begin{bmatrix} 1 & 1 & \cdots & 1 \end{bmatrix} = \begin{bmatrix} b_1^{[\ell]} & b_1^{[\ell]} & \cdots & b_1^{[\ell]} \\ \vdots & \vdots & \ddots & \vdots \\ b_{n^{[\ell]}}^{[\ell]} & b_{n^{[\ell]}}^{[\ell]} & \cdots & b_{n^{[\ell]}}^{[\ell]} \end{bmatrix} \in \mathbb{R}^{n^{[\ell]} \times m}. \tag{16.45} $$
Equation 16.45: What broadcasting means, spelled out. The outer product $b^{[\ell]}\mathbf{1}_m^\top$ replicates the bias column $m$ times. NumPy does not build this matrix — it adds the bias to each column in place — but the algebra is what the code performs.

So the fully explicit forward pass is $Z^{[\ell]} = W^{[\ell]}A^{[\ell-1]} + b^{[\ell]}\mathbf{1}_m^\top$, and in code it stays one line, Z = W @ A_prev + b. Keep the $\mathbf{1}_m$ in mind — it returns in §39.2, where the same ones vector is what sums the bias gradient back down to one column.

Backpropagation, component by component

Now the derivation. Backpropagation was stated at the matrix level in §33 and motivated by a two-layer computation graph in §25; here it is obtained from the chain rule applied to individual scalars, which is where the transposes come from.

Before the algebra, the picture. The forward pass sends information left to right; the backward pass sends an error signal right to left, along the very same edges, scaled by the very same weights.

Figure 16.28: The backward pass runs the network in reverse. Each neuron carries its activation (upper symbol) and its gradient (lower symbol); each dotted edge is labelled with the weight that scales the signal travelling back along it — the same $w_{j,k}^{[\ell]}$ as the forward pass, used in the opposite direction. Each hidden neuron's returning signal has its own colour. Where the three colours converge on an input neuron, that neuron's gradient is their sum — the sum derived in §38.2.

The four scalar derivatives

Assume the signal $da_j^{[\ell]} = \partial\mathcal{L}/\partial a_j^{[\ell]}$ has already arrived from the layer above (the example index $(i)$ is suppressed — every line below is for one example). Four derivatives follow, each a direct application of the chain rule.

One. The activation acts on each neuron independently: $a_j^{[\ell]} = g^{[\ell]}(z_j^{[\ell]})$ involves no other neuron. So the chain rule contributes exactly one local factor, the slope of $g$ at that neuron's own pre-activation:

$$ dz_j^{[\ell]} = \underbrace{\frac{\partial \mathcal{L}}{\partial a_j^{[\ell]}}}_{da_j^{[\ell]}} \cdot \underbrace{\frac{\partial a_j^{[\ell]}}{\partial z_j^{[\ell]}}}_{g^{[\ell]\prime}(z_j^{[\ell]})} = da_j^{[\ell]}\; g^{[\ell]\prime}\left(z_j^{[\ell]}\right). \tag{16.46} $$
Equation 16.46: Back through the activation, for one neuron. Neuron $j$'s activation depends on neuron $j$'s pre-activation and on nothing else, so the chain rule contributes a single local factor — the slope of $g$ at that neuron.

Because each $j$ is scaled by its own slope and nothing else, collecting these over $j$ gives an element-wise product, not a matrix one. That is the origin of the $\odot$.

Two. The weight $w_{j,k}^{[\ell]}$ appears in the pre-activation of §37 exactly once — inside $z_j^{[\ell]}$, multiplying $a_k^{[\ell-1]}$. So $\partial z_j^{[\ell]} / \partial w_{j,k}^{[\ell]} = a_k^{[\ell-1]}$ and

$$ \frac{\partial \mathcal{L}}{\partial w_{j,k}^{[\ell]}} = \frac{\partial \mathcal{L}}{\partial z_j^{[\ell]}}\,\frac{\partial z_j^{[\ell]}}{\partial w_{j,k}^{[\ell]}} = dz_j^{[\ell]}\; a_k^{[\ell-1]}. \tag{16.47} $$
Equation 16.47: The gradient of one weight: the error signal at the neuron it feeds, times the activation it carried. The middle factor is $\partial z_j / \partial w_{j,k} = a_k^{[\ell-1]}$, because that weight multiplies that activation and appears nowhere else.

In words: a weight's gradient is the error at the neuron it feeds, times the activation it carried. A weight that pushed a large input into a badly wrong neuron gets a large gradient — exactly the parameter most worth changing.

Three. The bias enters $z_j^{[\ell]}$ additively, so $\partial z_j^{[\ell]} / \partial b_j^{[\ell]} = 1$ and the factor simply vanishes:

$$ \frac{\partial \mathcal{L}}{\partial b_j^{[\ell]}} = \frac{\partial \mathcal{L}}{\partial z_j^{[\ell]}} \cdot 1 = dz_j^{[\ell]}. \tag{16.48} $$
Equation 16.48: The gradient of one bias. Because the bias is added straight into $z_j^{[\ell]}$, the local derivative is exactly $1$ and the error signal passes through unchanged.

Four. The last one is different, and it is the only place addition appears.

Why one gradient is a sum

An activation $a_k^{[\ell-1]}$ does not feed one neuron of the next layer — it feeds every one of them. The loss therefore depends on it through $n^{[\ell]}$ separate paths, and the multivariable chain rule says: add up what comes back along each path.

Figure 16.29: One neuron $a_k^{[\ell-1]}$ sends its signal forward along a separate path into every neuron of the next layer (three here). Its gradient is the total error returning along all of them, each scaled by the weight it travelled through. Paths and labels share a colour so they can be matched. This is the only sum in the backward rules — every other step is a plain multiplication.

Since $\partial z_j^{[\ell]} / \partial a_k^{[\ell-1]} = w_{j,k}^{[\ell]}$, summing the paths gives

$$ da_k^{[\ell-1]} = \sum_{j=1}^{n^{[\ell]}} \frac{\partial \mathcal{L}}{\partial z_j^{[\ell]}}\,\frac{\partial z_j^{[\ell]}}{\partial a_k^{[\ell-1]}} = \sum_{j=1}^{n^{[\ell]}} dz_j^{[\ell]}\; w_{j,k}^{[\ell]}. \tag{16.49} $$
Equation 16.49: The gradient handed back to one neuron of the previous layer: the total error it caused, summed over every neuron it fed. Read the indices carefully — the sum runs over $j$ (the destinations), and $k$ (the source) is held fixed. That is a sum down a column of $W^{[\ell]}$, which is a row of $W^{[\ell]\top}$.

That last observation is the transpose. In the forward pass, computing $z_j$ meant summing over $k$ — along a row of $W^{[\ell]}$. In the backward pass, computing $da_k$ means summing over $j$ — down a column. Same numbers, other index summed, hence $W^{[\ell]\top}$. The transpose is not a trick to make the shapes fit; it is the statement that a weight carries signal forward and error backward through the same connection.

One neuron's backward pass

All four results, on the neuron we opened up earlier. One incoming gradient enters, one shared error signal is formed, and everything else is that signal multiplied by a local factor.

Figure 16.30: Everything backpropagation does at hidden neuron $a_3^{[1]}$. The incoming gradient $da_3^{[1]}$ passes back through the activation — multiply by the local slope — to become the shared error signal $dz_3^{[1]}$. From that one number the neuron reads off its two weight gradients (multiply by each input), its bias gradient (multiply by $1$), and its share of the gradient sent back to each input (multiply by the weight it travelled through). Green boxes are kept for the update; blue boxes travel further back.

Note the economy that makes backpropagation cheap: $dz_j^{[\ell]}$ is computed once and reused by all three downstream quantities. A naive implementation would re-multiply the whole chain from the loss down to each individual weight; backpropagation multiplies it once per layer and shares the result.

From components back to matrices

Re-bundle the four scalar results and the matrix equations of §33 appear — now derived rather than asserted.

Collecting the indices

StepComponent formCollected form (one example)Why that operation
through the activation$dz_j^{[\ell]} = da_j^{[\ell]}\, g^{[\ell]\prime}(z_j^{[\ell]})$$dz^{[\ell]} = da^{[\ell]} \odot g^{[\ell]\prime}(z^{[\ell]})$each $j$ uses its own slope — element-wise
weight gradient$\dfrac{\partial \mathcal{L}}{\partial w_{j,k}^{[\ell]}} = dz_j^{[\ell]} a_k^{[\ell-1]}$$dW^{[\ell]} = dz^{[\ell]} \left(a^{[\ell-1]}\right)^{\top}$one free index $j$, one free index $k$ — an outer product
bias gradient$\dfrac{\partial \mathcal{L}}{\partial b_j^{[\ell]}} = dz_j^{[\ell]}$$db^{[\ell]} = dz^{[\ell]}$the local derivative is $1$
pass back$da_k^{[\ell-1]} = \sum_j dz_j^{[\ell]} w_{j,k}^{[\ell]}$$da^{[\ell-1]} = \left(W^{[\ell]}\right)^{\top} dz^{[\ell]}$the sum runs over $j$, not $k$ — a transpose
Table 16.16: Each scalar rule and the matrix expression that collects it. Reading right to left tells you what a matrix operation is really doing: $\odot$ because the activation is per-neuron, an outer product because a weight gradient pairs one output index with one input index, and a transpose because the backward sum runs over the other index.

The shapes confirm each line. The outer product is $(n^{[\ell]}, 1) \times (1, n^{[\ell-1]}) = (n^{[\ell]}, n^{[\ell-1]})$, matching $W^{[\ell]}$ as any weight gradient must. The pass-back is $(n^{[\ell-1]}, n^{[\ell]}) \times (n^{[\ell]}, 1) = (n^{[\ell-1]}, 1)$ — a column for the previous layer, exactly the input its own §38.1 expects. This is the §30 habit at work, one index at a time.

Where the $\tfrac{1}{m}$ and the ones vector come from

Everything so far concerned one example. The batch is trained on the cost $J = \tfrac{1}{m}\sum_i \mathcal{L}^{(i)}$, so each parameter gradient is the average of its per-example gradients:

$$ \frac{\partial J}{\partial w_{j,k}^{[\ell]}} = \frac{1}{m}\sum_{i=1}^{m} dz_j^{[\ell]{}(i)}\, a_k^{[\ell-1]{}(i)}, \qquad \frac{\partial J}{\partial b_j^{[\ell]}} = \frac{1}{m}\sum_{i=1}^{m} dz_j^{[\ell]{}(i)}. \tag{16.50} $$
Equation 16.50: The batch gradient of a single weight and a single bias: an average over the examples. Every $\frac{1}{m}$ in the vectorized equations traces back to these two lines, and to nothing else.

Both sums over $i$ are performed for free by matrix algebra. In $dZ^{[\ell]} \left(A^{[\ell-1]}\right)^{\top}$ the inner dimension is $m$, so forming the product contracts the example index away — the summation over examples is the matrix multiplication. For the bias there is no second matrix to contract against, so the sum is written with the ones vector from §37.2, now used in the opposite direction: $\mathbf{1}_m^\top$ copied the bias out across columns; $\mathbf{1}_m$ sums the gradient back in.

$$ \begin{align} dZ^{[\ell]} &= dA^{[\ell]} \odot g^{[\ell]\prime}\left(Z^{[\ell]}\right) \tag{16.51.1} \\ dW^{[\ell]} &= \tfrac{1}{m}\, dZ^{[\ell]} \left(A^{[\ell-1]}\right)^{\top} \tag{16.51.2} \\ db^{[\ell]} &= \tfrac{1}{m}\, dZ^{[\ell]} \mathbf{1}_m \tag{16.51.3} \\ dA^{[\ell-1]} &= \left(W^{[\ell]}\right)^{\top} dZ^{[\ell]} \tag{16.51.4} \end{align} $$
Equation 16.51: The vectorized backward function, with each factor now accounted for. The $\frac{1}{m}$ appears on the two parameter gradients because the cost averages the losses; it does not appear on $dA^{[\ell-1]}$, because each example keeps its own error signal as it travels back.

In NumPy the ones vector is spelled np.sum(..., axis=1, keepdims=True), which is the same contraction: db = (1/m) * np.sum(dZ, axis=1, keepdims=True).

The whole sweep, in code

Chain the layer functions and the algorithm is complete: a forward pass that caches, a backward sweep that consumes the caches from last layer to first, and one update.

# forward: cache what the backward pass will need
caches = {}
A = X                                            # A[0] = X
for l in range(1, L + 1):
    A_prev = A
    Z = W[l] @ A_prev + b[l]                     # broadcast bias  (comp-eq-broadcast)
    A = g[l](Z)
    caches[l] = (A_prev, W[l], Z)                # a^(l-1), W^(l), z^(l)

# backward: seed at the output, then sweep down
grads = {}
dA = A - Y                                       # dZ^[L] seed for sigmoid + cross-entropy
for l in reversed(range(1, L + 1)):
    A_prev, Wl, Z = caches[l]
    dZ = dA * g_prime[l](Z)                       # element-wise   (the odot)
    grads[f"dW{l}"] = (1 / m) * dZ @ A_prev.T     # outer product, examples contracted
    grads[f"db{l}"] = (1 / m) * dZ.sum(axis=1, keepdims=True)
    dA = Wl.T @ dZ                                # the transpose  -> input to layer l-1

# update every parameter
for l in range(1, L + 1):
    W[l] -= alpha * grads[f"dW{l}"]
    b[l] -= alpha * grads[f"db{l}"]

One caveat in that seed. The line dA = A - Y is really $dZ^{[L]}$, not $dA^{[L]}$ — it is the collapsed form that only holds for a sigmoid output with the cross-entropy loss (§33.1). Writing it into dA and then multiplying by g_prime[L](Z) on the first iteration would apply the sigmoid slope twice. Real implementations either seed dZ directly and skip the first activation step, or seed the honest $dA^{[L]}$ of §40. The shortcut above is the common one and the common bug.

The output seed, for any number of outputs

Every backward step assumes $da^{[\ell]}$ is already known. The recursion's first value comes from differentiating the loss with respect to the network's own output.

For binary classification the output layer has $n^{[L]} = 1$ neuron, $a_1^{[L]{}(i)} = \hat{y}^{(i)}$ is a predicted probability, and the loss is the cross-entropy of §5.1. Differentiating it with respect to that one activation gives the seed already met in §33.1.

For several outputs — multi-label prediction, or one unit per class — nothing changes except that each output neuron carries its own seed, indexed by $j$:

$$ da_j^{[L]{}(i)} = \frac{\partial \mathcal{L}^{(i)}}{\partial a_j^{[L]{}(i)}} = -\frac{y_j^{(i)}}{a_j^{[L]{}(i)}} + \frac{1 - y_j^{(i)}}{1 - a_j^{[L]{}(i)}}. \tag{16.52} $$
Equation 16.52: The seed at output neuron $j$ for example $i$, when the layer has $n^{[L]} > 1$ sigmoid units and each is scored by its own cross-entropy term. For $n^{[L]} = 1$ this is the binary seed of §33.1, and the index $j$ disappears.

Stack those scalars in the standard layout — neurons down, examples across — and the seed for the whole batch is one element-wise expression on two $(n^{[L]}, m)$ matrices:

$$ dA^{[L]} = -\frac{Y}{A^{[L]}} + \frac{1 - Y}{1 - A^{[L]}} \;=\; \begin{bmatrix} da_1^{[L]{}(1)} & da_1^{[L]{}(2)} & \cdots & da_1^{[L]{}(m)} \\ da_2^{[L]{}(1)} & da_2^{[L]{}(2)} & \cdots & da_2^{[L]{}(m)} \\ \vdots & \vdots & \ddots & \vdots \\ da_{n^{[L]}}^{[L]{}(1)} & da_{n^{[L]}}^{[L]{}(2)} & \cdots & da_{n^{[L]}}^{[L]{}(m)} \end{bmatrix}. \tag{16.53} $$
Equation 16.53: The output-layer gradient matrix. Every operation is element-wise; entry $(j, i)$ is the scalar seed above. $A^{[L]}$ holds the predictions and $Y$ the labels, both with one row per output neuron and one column per example. Feed this into the layer-$L$ backward function and the sweep begins.

When $n^{[L]} = 1$ both matrices collapse to a single row of $m$ predictions and $m$ labels, and — because the output is a sigmoid — the whole expression collapses one step further, to $dZ^{[L]} = A^{[L]} - Y$. That is the complete cycle: predict forward, measure the loss, seed the gradient at the output, and propagate it back to every parameter.


Where this goes next

The pieces above are a complete, trainable $L$-layer classifier: a forward pass (§29), shapes that catch most bugs before they run (§30), a reason to prefer depth (§31), the building-block/cache view (§32), the general per-layer backward function (§33), and the knobs you tune around it (§34) — with Part IV supplying the component-level derivation underneath all of it. Each block is reusable, so extending this reference is a matter of appending new parts that lean on the same notation and the same forward/backward machinery. Natural next topics:

House style for new parts. Keep one ## per topic with an explicit {#id}; wrap every display equation, figure, and table in a :::caption block; draw computation graphs with neuralflow (forward left-to-right, backward as a red mirror); and write math as KaTeX, words in prose. Numbering and the §N.M cross-references continue automatically across parts.

Machine Learning Notes · note 3 of 6 · The network reference ← Logistic regression Multiclass and the softmax →