Block 4.2 Advanced AI
Computing every parameter derivative independently would repeat the same downstream chain-rule calculations thousands or millions of times.
By the end of this lecture, you should be able to:
Training requires repeated gradient-based updates. For neural networks, two practical questions dominate:
Gradient computation: how do we obtain derivatives with respect to every weight and bias without duplicating work?
Initialisation: how do we choose starting weights so that activations and gradients remain numerically well-scaled through many layers?
For trainable model parameters, we follow Prince’s Greek-letter notation:
The familiar affine map
\[ \mat{W}\vect{x}+\vect{b} \]
is written in Prince, Chapter 7, as (Prince 2023)
\[ \vect{f}_k = \layerbias_k+\layerweights_k\hidden_k. \]
The arithmetic is unchanged; the Greek symbols make the learned quantities explicit.
The letters are chosen to be memorable:
For a layer taking \(\dimension_k\) activations to \(\dimension_{k+1}\):
| object | shape |
|---|---|
| \(\hidden_k\) | \(\dimension_k\times1\) |
| \(\layerweights_k\) | \(\dimension_{k+1}\times\dimension_k\) |
| \(\layerbias_k\) | \(\dimension_{k+1}\times1\) |
| \(\vect{f}_k\) | \(\dimension_{k+1}\times1\) |
The weight matrix is read right to left: it takes \(\dimension_k\) numbers in and gives \(\dimension_{k+1}\) out.
For affine layer \(k\):
The repeated pattern is
\[ \hidden_k \longrightarrow \vect{f}_k \longrightarrow \hidden_{k+1}. \]
A deep network alternates affine maps and elementwise nonlinearities. The loss is attached only at the end, but every earlier parameter influences it through the chain of computations.
For one training example \(\vect{x}_i\):
\[ \begin{aligned} \vect{f}_0 &= \layerbias_0+\layerweights_0\vect{x}_i \\ \hidden_1 &= \vactivation{\vect{f}_0} \\ \vect{f}_1 &= \layerbias_1+\layerweights_1\hidden_1 \\ \hidden_2 &= \vactivation{\vect{f}_1} \\ \vect{f}_2 &= \layerbias_2+\layerweights_2\hidden_2 \\ \hidden_3 &= \vactivation{\vect{f}_2} \\ \vect{f}_3 &= \layerbias_3+\layerweights_3\hidden_3 \end{aligned} \]
The per-example loss is
\[ \exloss_i = \exloss(\vect{f}_3,\vect{y}_i). \]
Chapter 7 writes the full training loss as a sum:
\[ \loss[\params] = \sum_{i=1}^{I}\exloss_i. \]
For a batch \(\mathcal B_t\), the SGD update is
\[ \params_{t+1} = \params_t - \alpha \sum_{i\in\mathcal B_t} \nabla_{\params}\exloss_i. \]
Every update therefore needs the derivative of one scalar loss with respect to a very large collection of parameters.
A programmer’s first idea might be numerical differentiation:
\[ \frac{\partial\exloss}{\partial\phi_j} \approx \frac{ \exloss(\params+\varepsilon\vect{e}_j) - \exloss(\params-\varepsilon\vect{e}_j) }{2\varepsilon}. \]
Here \(\vect{e}_j\) is zero everywhere except position \(j\), so only parameter \(\phi_j\) is perturbed.
For \(P\) parameters, centred finite differences require roughly \(2P\) forward evaluations to recover the full gradient.
Finite differences are valuable for checking a gradient, not for computing every training update.
The chain rule can give an exact derivative for any one parameter. For an early scalar weight,
\[ \frac{\partial\exloss_i}{\partial\omega_0} = \frac{\partial f_0}{\partial\omega_0} \frac{\partial h_1}{\partial f_0} \frac{\partial f_1}{\partial h_1} \cdots \frac{\partial\exloss_i}{\partial f_3}. \]
For \(\omega_1\), much of the right-hand side is the same downstream chain again. For \(\omega_2\), it appears again.
The derivatives are exact, but calculating each one independently repeats work.
Parameters at different depths have different path lengths to the loss, but the right-hand suffixes of those paths are shared. Computing every derivative independently would repeatedly rebuild those same suffixes.
Computing each parameter derivative separately rebuilds the same downstream derivative products.
Compute each shared downstream sensitivity once, cache it, and reuse it.
Start at the only quantity we ultimately care about: the loss. Then repeatedly ask one local question:
if this intermediate value changed a little, how much would the loss change?
Each answer becomes an input to the next question one step earlier in the graph.
A neural network has many parameters but one scalar loss:
\[ \exloss:\mathbb{R}^{P}\rightarrow\mathbb{R}. \]
Two derivative-propagation strategies have very different pass counts for the full gradient. Here a seed is the derivative value propagation starts from; reverse mode starts at the loss with \(\partial\exloss/\partial\exloss=1\).
| mode | seed | sweeps for all \(P\) partial derivatives |
|---|---|---|
| forward mode | one parameter direction | \(P\) |
| reverse mode | the scalar loss | \(1\) |
For the eight-parameter toy model: 8 forward-mode seeds versus 1 reverse seed. For \(P=10^6\): the pass-count asymmetry is \(10^6\) versus \(1\).
Each sweep still traverses the computation graph; the advantage is the number of sweeps.
Forward mode seeds one parameter direction and moves toward the loss, so three parameters require three sweeps. Reverse mode seeds the one scalar loss and reaches all three parameters in one reverse sweep.
The forward pass stores intermediate values. The backward pass propagates one cached loss sensitivity at a time in the opposite direction.
The backward pass needs two kinds of stored forward value:
Reusing derivative work therefore requires memory for these forward intermediates.
Use the one-unit-per-layer toy model from Section 7.3:
\[ \begin{aligned} f_0 &= \beta_0+\omega_0x_i \\ h_1 &= \sin(f_0) \\ f_1 &= \beta_1+\omega_1h_1 \\ h_2 &= \exp(f_1) \\ f_2 &= \beta_2+\omega_2h_2 \\ h_3 &= \cos(f_2) \\ f_3 &= \beta_3+\omega_3h_3 \\ \exloss_i &= (f_3-y_i)^2 \end{aligned} \]
The deliberately different \(\sin\), \(\exp\), and \(\cos\) operations are not a proposed network architecture. They force us to apply the chain rule through several different local functions instead of memorising one derivative.
Breaking the composed expression into named intermediate values exposes the local calculations that the chain rule will reuse.
Take
\[ x_i=0.5,\qquad y_i=0.2 \]
and
\[ \begin{aligned} \layerbias &= [0.1,\,-0.2,\,0.05,\,0.15]\transpose \\ \vect{\omega} &= [0.8,\,1.1,\,-0.7,\,0.9]\transpose. \end{aligned} \]
The forward pass gives approximately
| quantity | value |
|---|---|
| \(f_0\) | 0.500 |
| \(h_1\) | 0.479 |
| \(f_1\) | 0.327 |
| \(h_2\) | 1.387 |
| \(f_2\) | -0.921 |
| \(h_3\) | 0.605 |
| \(f_3\) | 0.694 |
| \(\exloss_i\) | 0.244 |
If
\[ u=g(v), \qquad L=q(u), \]
then a small change in \(v\) first changes \(u\), which then changes \(L\).
The local sensitivities multiply:
\[ \frac{dL}{dv} = \frac{du}{dv} \frac{dL}{du}. \]
Read right-to-left:
how much does \(u\) change with \(v\) × how much does \(L\) change with \(u\)?
For every pre-activation, define
\[ \delta_k \equiv \frac{\partial\exloss_i}{\partial f_k}. \]
Read \(\delta_k\) as:
how much does the loss currently care about a small change in \(f_k\)?
The final operation is
\[ \exloss_i=(f_3-y_i)^2. \]
Therefore
\[ \boxed{ \delta_3 = \frac{\partial\exloss_i}{\partial f_3} = 2(f_3-y_i) } \]
For the numerical example,
\[ \delta_3\approx 0.989. \]
This is the one derivative we can compute immediately from the loss.
First pass sensitivity through the affine operation
\[ f_3=\beta_3+\omega_3h_3. \]
Since \(\partial f_3/\partial h_3=\omega_3\),
\[ \frac{\partial\exloss_i}{\partial h_3} = \omega_3\delta_3. \]
Then pass it through
\[ h_3=\cos(f_2), \]
so
\[ \boxed{ \delta_2 = -\sin(f_2)\,\omega_3\,\delta_3. } \]
One backward step is therefore
\[ \text{downstream sensitivity} \times \text{local linear effect} \times \text{local activation derivative}. \]
The next two steps use exactly the same pattern:
\[ \delta_1 = \exp(f_1)\,\omega_2\,\delta_2 \]
and
\[ \delta_0 = \cos(f_0)\,\omega_1\,\delta_1. \]
In general,
\[ \boxed{ \delta_{k-1} = \dactivation{f_{k-1}}\,\omega_k\,\delta_k. } \]
The long chain-rule expression is never rebuilt from scratch.
The forward pass stores numerical values at every intermediate quantity. The backward pass attaches one cached loss sensitivity \(\delta_k\) to each pre-activation.
Each cached scalar sensitivity is obtained from the one to its right by multiplying by one local factor. Repeating factors systematically above or below one can grow or shrink sensitivities through depth.
Once \(\delta_k\) is known, the parameters feeding into \(f_k\) use the same cached sensitivity. For \(k>0\),
\[ f_k=\beta_k+\omega_kh_k. \]
Therefore
\[ \boxed{ \frac{\partial\exloss_i}{\partial\beta_k}=\delta_k, \qquad \frac{\partial\exloss_i}{\partial\omega_k}=h_k\delta_k. } \]
For the first layer, replace the source activation \(h_0\) by the input \(x_i\):
\[ \frac{\partial\exloss_i}{\partial\omega_0}=x_i\delta_0. \]
The gradient of a weight is signal through the connection × loss sensitivity at its destination.
We rejected finite differences as the training algorithm because they require roughly \(2P\) forward evaluations. They are still useful for checking one analytic or automatic derivative:
\[ \frac{\partial\exloss_i}{\partial\omega_0} \approx \frac{ \exloss_i(\omega_0+\varepsilon) - \exloss_i(\omega_0-\varepsilon) }{2\varepsilon}. \]
With \(\varepsilon=10^{-5}\):
The scalar algorithm is now:
Backpropagation stores local forward values and cached local sensitivities instead of expanding one full symbolic derivative.
The scalar equation
\[ f_k=\beta_k+\omega_kh_k \]
becomes
\[ \vect{f}_k = \layerbias_k + \layerweights_k\hidden_k. \]
A layer has several destination pre-activations \(\vect{f}_k\), so it has one loss sensitivity for each destination unit. The source activations \(\hidden_k\) are the values entering the affine layer; the resulting \(\vect{f}_k\) are its pre-activations. Stack the destination sensitivities into a column:
\[ \sensitivity_k \equiv \nabla_{\vect{f}_k}\exloss_i. \]
The same loss-sensitivity idea now has one component per unit. The new feature is that several downstream paths can contribute to one earlier unit.
Take a layer with two source activations and three destination pre-activations:
A small 2→3 affine layer. We will derive the backward rules by asking what each source activation and each weight contributes to the loss.
For this example,
\[ \hidden_k:2\times1, \qquad \layerweights_k:3\times2, \qquad \vect{f}_k,\layerbias_k,\sensitivity_k:3\times1. \]
Suppose the destination sensitivities are
\[ \sensitivity_k = \begin{bmatrix} \delta_{k,1}\\ \delta_{k,2}\\ \delta_{k,3} \end{bmatrix}. \]
Changing source activation \(h_{k,1}\) changes all three destination pre-activations. Therefore all three paths contribute:
\[ \boxed{ \frac{\partial\exloss_i}{\partial h_{k,1}} = \Omega_{k,11}\delta_{k,1} + \Omega_{k,21}\delta_{k,2} + \Omega_{k,31}\delta_{k,3}. } \]
This is just the chain rule adding the contributions from every path out of \(h_{k,1}\).
The second source activation similarly gives
\[ \frac{\partial\exloss_i}{\partial h_{k,2}} = \Omega_{k,12}\delta_{k,1} + \Omega_{k,22}\delta_{k,2} + \Omega_{k,32}\delta_{k,3}. \]
Stack the two answers:
\[ \begin{bmatrix} \partial\exloss_i/\partial h_{k,1}\\ \partial\exloss_i/\partial h_{k,2} \end{bmatrix} = \begin{bmatrix} \Omega_{k,11}&\Omega_{k,21}&\Omega_{k,31}\\ \Omega_{k,12}&\Omega_{k,22}&\Omega_{k,32} \end{bmatrix} \sensitivity_k. \]
The matrix on the right is exactly \(\layerweights_k\transpose\):
\[ \boxed{ \nabla_{\hidden_k}\exloss_i = \layerweights_k\transpose\sensitivity_k. } \]
Forward, one source activation fans out through a column of the weight matrix. Backward, the loss contributions on those same connections are gathered back to that source. Doing the gathering for every source at once is multiplication by the transpose.
For a scalar loss and vector \(\vect{z}\), the gradient dotted with a proposed small change \(\Delta\vect{z}\) predicts the corresponding first-order change in the loss. With column-vector gradients:
\[ \Delta\exloss_i \approx \left(\nabla_{\vect{z}}\exloss_i\right)\transpose\Delta\vect{z}. \]
For the affine layer,
\[ \Delta\vect{f}_k = \layerweights_k\Delta\hidden_k, \]
so
\[ \Delta\exloss_i \approx \sensitivity_k\transpose\layerweights_k\Delta\hidden_k = \left(\layerweights_k\transpose\sensitivity_k\right)\transpose\Delta\hidden_k. \]
This must agree with
\[ \Delta\exloss_i \approx \left(\nabla_{\hidden_k}\exloss_i\right)\transpose\Delta\hidden_k \]
for every \(\Delta\hidden_k\). Therefore the coefficient vectors are equal:
\[ \nabla_{\hidden_k}\exloss_i = \layerweights_k\transpose\sensitivity_k. \]
Suppose a ReLU layer saw
\[ \vect{f}_{k-1} = \begin{bmatrix} -1.2\\ 0.4\\ 2.1 \end{bmatrix} \]
and the sensitivity arriving at its activation vector is
\[ \nabla_{\hidden_k}\exloss_i = \begin{bmatrix} 3\\ -2\\ 5 \end{bmatrix}. \]
The first ReLU was off in the forward pass, so it blocks the backward signal. The other two were on, so they pass it through:
\[ \boxed{ \sensitivity_{k-1} = \begin{bmatrix} 0\\ -2\\ 5 \end{bmatrix}. } \]
For ReLU, backpropagation through the activation is simply a stored on/off mask from the forward pass.
For
\[ \relu(z)=\max(0,z), \]
away from \(z=0\),
\[ \relu'(z) = \begin{cases} 0,&z<0\\ 1,&z>0. \end{cases} \]
Applied elementwise, \(\indicator{\text{condition}}\) is \(1\) where the condition is true and \(0\) otherwise, and \(\odot\) means component-by-component multiplication:
\[ \boxed{ \sensitivity_{k-1} = \indicator{\vect{f}_{k-1}>0} \odot \left( \layerweights_k\transpose\sensitivity_k \right). } \]
Formally, the Jacobian of an elementwise activation is diagonal: each ReLU output depends only on the matching input, so all cross-unit partial derivatives are zero. For ReLU, the diagonal is exactly the \(0/1\) mask above.
Consider the weight \(\Omega_{k,rc}\) connecting source activation \(h_{k,c}\) to destination pre-activation \(f_{k,r}\).
A small weight change produces
\[ \Delta f_{k,r} = h_{k,c}\,\Delta\Omega_{k,rc}. \]
The loss sensitivity at that destination is \(\delta_{k,r}\), so
\[ \boxed{ \frac{\partial\exloss_i}{\partial\Omega_{k,rc}} = \delta_{k,r}h_{k,c}. } \]
Every weight needs exactly two local numbers:
Each cell of the weight-gradient matrix is one destination sensitivity times one source activation. Filling every source–destination pair produces the outer product.
Linear algebra has a name for exactly this construction: an outer product. A dot product combines two vectors into one number; an outer product forms the matrix of all pairwise products.
\[ \boxed{ \nabla_{\layerweights_k}\exloss_i = \sensitivity_k\hidden_k\transpose. } \]
The shape is automatically the shape of the weight matrix:
\[ (\dimension_{k+1}\times1)(1\times\dimension_k) = \dimension_{k+1}\times\dimension_k. \]
Because adding the bias changes the corresponding pre-activation one-for-one,
\[ \Delta\vect{f}_k = \Delta\layerbias_k. \]
Therefore
\[ \boxed{ \nabla_{\layerbias_k}\exloss_i = \sensitivity_k. } \]
The sensitivity already stored at the layer is its bias gradient.
One backward traversal of an affine-plus-ReLU layer does three jobs: form the bias gradient, form the weight gradient, and pass a masked sensitivity to the previous layer.
For a ReLU network,
\[ \boxed{ \begin{aligned} \nabla_{\layerbias_k}\exloss_i &= \sensitivity_k \\ \nabla_{\layerweights_k}\exloss_i &= \sensitivity_k\hidden_k\transpose \\ \sensitivity_{k-1} &= \indicator{\vect{f}_{k-1}>0} \odot \left( \layerweights_k\transpose\sensitivity_k \right) \end{aligned} } \]
Chapter 7, equation 7.25 (Prince 2023)
Read the three lines as:
The first pre-activation uses the input rather than a hidden activation:
\[ \vect{f}_0 = \layerbias_0 + \layerweights_0\vect{x}_i. \]
Once \(\sensitivity_0\) is available,
\[ \boxed{ \nabla_{\layerbias_0}\exloss_i = \sensitivity_0, \qquad \nabla_{\layerweights_0}\exloss_i = \sensitivity_0\vect{x}_i\transpose. } \]
Forward pass
Backward pass
This is the vector form of exactly the scalar algorithm we already ran by hand.
Backpropagation above computes gradients for one training example. Under the sum convention used here,
\[ \nabla_{\params} \sum_{i\in\mathcal B_t}\exloss_i = \sum_{i\in\mathcal B_t} \nabla_{\params}\exloss_i. \]
A batch mean instead divides that sum by \(|\mathcal B_t|\). The local backpropagation rules do not change; only the reduction across examples changes the final scale.
The expensive operations in both passes are matrix multiplications:
Backpropagation avoids repeated chain-rule work by reusing cached sensitivities. The price is memory: the backward pass still needs values saved during the forward pass.
This is the same trade-off we saw at the start:
compute once, store, reuse rather than recompute the same derivative suffix repeatedly.
Backpropagation is reverse-mode algorithmic differentiation applied to a neural-network computation graph.
A framework does not need one giant hand-written derivative for the model. Each primitive operation needs only:
The scalar and vector derivations above are those local rules written out by hand.
Backpropagation is not restricted to a chain. When one quantity influences the loss through several branches, reverse mode adds the sensitivity contributions from every downstream path.
If
\[ u\to a\to\exloss \qquad\text{and}\qquad u\to b\to\exloss, \]
then
\[ \frac{d\exloss}{du} = \frac{\partial a}{\partial u}\frac{\partial\exloss}{\partial a} + \frac{\partial b}{\partial u}\frac{\partial\exloss}{\partial b}. \]
Reverse mode accumulates those contributions at the shared ancestor.
Batching adds one extra axis. A batch is a stack of examples of the same underlying object; the local derivative rules are applied across that additional axis.
The derivative rules are unchanged; batching adds an axis over examples.
In PyTorch-style pseudocode:
model(...) — run the forward graph and save required intermediatesloss.backward() — seed the scalar loss and run reverse-mode accumulationoptimizer.step() — update parameters using the accumulated gradientsloss.backward()The framework is automating the operations we have already derived:
loss.backward() applies these local derivative rules while traversing the computation graph in reverse.
Before the first training update, every weight and bias already needs a value.
Why not initialise everything to zero and let gradient descent take it from there?
Zero initialisation makes all hidden units start identically.
Suppose two hidden units have identical incoming weights and biases.
For every input,
\[ f_a=f_b \qquad\Longrightarrow\qquad h_a=h_b. \]
If the surrounding network is symmetric as well, backpropagation gives
\[ \delta_a=\delta_b. \]
Their incoming-weight gradients are therefore identical:
\[ \nabla_{\vect{\omega}_a}\exloss = \delta_a\hidden\transpose = \delta_b\hidden\transpose = \nabla_{\vect{\omega}_b}\exloss. \]
They take the same update and remain identical.
The only claim here is symmetry: hidden units that start with identical incoming weights remain one function, whereas independently initialised units can take different roles.
Requirement 1: hidden units must not all start identically.
Random weights solve the symmetry problem.
A programmer’s first attempt might be something like:
which samples values in \([0,1)\).
That gives different weights, but it also gives:
Independent random weights are the standard way to give hidden units different starts, but randomness itself is not the mathematical requirement: the requirement is that symmetric units do not start identically.
Once we choose random, zero-centred weights, we still have to choose their scale:
Those all break symmetry.
They do not produce the same signal behaviour through a deep network.
For one affine layer, two counts matter:
Fan-in counts the connections entering one destination unit. Fan-out counts the destinations reached by one source activation. The two counts are shown separately so the shared connection is not visually ambiguous.
For
\[ \mat{\Omega}_k\in\mathbb{R}^{D_{\mathrm{out}}\times D_{\mathrm{in}}}, \]
Forward signal scale depends on fan-in because one destination sums that many incoming contributions.
Backward signal scale depends on fan-out because one earlier unit collects sensitivity from that many downstream units.
The initialisation question is whether each layer systematically shrinks or grows that spread.
A useful initial scale lets a layer transform the signal without systematically narrowing or widening it. ReLU changes the shape, but the next affine map restores a comparable overall spread.
The first engineering target is simple:
a layer should not systematically hand the next layer a much smaller or much larger signal than it received
The correct random scale must therefore depend on what the layer does to the signal.
Assume zero biases and independent zero-mean weights with variance \(\sigma_\Omega^2\). The effect of that variance depends on the layer width: the same \(\sigma_\Omega^2\) can be too small for one fan-in and too large for another.
Each layer roughly multiplies signal scale by a gain factor. Repeated gains below one shrink signals, gains near one preserve them, and gains above one grow them.
The same source activations and the same underlying random draws, scaled three ways. Randomness breaks symmetry in every panel; only the scale determines whether the signal shrinks, survives, or grows.
All three choices are random and zero-centred.
Randomness fixed the symmetry problem. The remaining question is how large the random weights should be.
One destination pre-activation is the sum of one weighted contribution from every incoming connection. Increasing fan-in increases the number of random contributions being combined.
A layer with fan-in \(1000\) sums one hundred times as many contributions as a layer with fan-in \(10\).
The same weight scale cannot be expected to behave identically in both.
Independent zero-centred contributions partly cancel. As more terms are added, the typical width of the sum grows like the square root of the number of terms, not in direct proportion to the count.
So if fan-in multiplies the variance by roughly \(D_{\mathrm{in}}\), the weight variance needs a compensating factor proportional to
\[ \frac{1}{D_{\mathrm{in}}}. \]
Equivalently, the weight standard deviation scales like \(1/\sqrt{D_{\mathrm{in}}}\).
ReLU clips every negative pre-activation to zero and leaves positive values untouched. For a symmetric input, about half of the units become exactly zero.
The initialiser therefore has to compensate for two effects:
To track typical squared signal size, use
\[ \expect{f^2} = \text{average of the squared magnitudes of }f. \]
The formal name for this quantity is the second moment.
For symmetric pairs, the negative and positive values contribute equally to average squared magnitude. ReLU deletes the negative member of every pair, leaving half of that squared contribution.
For a distribution symmetric about zero,
\[ \boxed{ \expect{h^2}=\frac12\expect{f^2} } \qquad h=\relu(f). \]
Fan-in gives a factor of roughly \(D_{\mathrm{in}}\) in variance.
ReLU leaves roughly one-half of the squared magnitude.
To balance those effects, choose
\[ \boxed{ \sigma_\Omega^2 = \frac{2}{D_{\mathrm{in}}} } \qquad\Longleftrightarrow\qquad \sigma_\Omega = \sqrt{\frac{2}{D_{\mathrm{in}}}}. \]
This is He initialisation, also called Kaiming initialisation, for ReLU-style rectifiers.
kaiming_normal_ and kaiming_uniform_The appropriate scaling rule depends on the activation and the assumptions used to model signal propagation.
| family | scaling emphasis | connection to this lecture |
|---|---|---|
| LeCun | fan-in based | related variance-scaling family under different activation assumptions |
| Glorot / Xavier | balances fan-in and fan-out | another widely used signal-preserving rule |
| He / Kaiming | accounts for rectifier behaviour | derived here for ReLU |
Match the initialiser to the activation and architecture.
For one ReLU layer,
\[ f_{k,i} = \sum_{j=1}^{D_{\mathrm{in}}} \Omega_{k,ij}h_{k,j}. \]
Use a deliberately simple model of the signal at initialisation:
Then the variance scale is controlled by three factors:
\[ \underbrace{D_{\mathrm{in}}}_{\text{fan-in terms}} \times \underbrace{\sigma_\Omega^2}_{\text{weight variance}} \times \underbrace{\frac12}_{\text{ReLU leaves half the squared magnitude}}. \]
So
\[ \boxed{ \var{f_{k,i}} \approx \frac12D_{\mathrm{in}}\sigma_\Omega^2 \var{f_{k-1,j}} }. \]
Setting the multiplicative gain to one gives
\[ \boxed{\sigma_\Omega^2=\frac{2}{D_{\mathrm{in}}}.} \]
Chapter 7, equation 7.32 (Prince 2023)
Backpropagation uses
\[ \sensitivity_{k-1} = \indicator{\vect{f}_{k-1}>0} \odot \left(\layerweights_k\transpose\sensitivity_k\right). \]
For one earlier unit,
\[ \delta_{k-1,j} = \indicator{f_{k-1,j}>0} \sum_{i=1}^{D_{\mathrm{out}}} \Omega_{k,ij}\delta_{k,i}. \]
The architectural count has changed:
Use a mean-field approximation for the backward signal: track the typical squared size across many units while ignoring some detailed correlations between them.
Then
\[ \boxed{ \var{\delta_{k-1,j}} \approx \frac12D_{\mathrm{out}}\sigma_\Omega^2 \var{\delta_{k,i}} }. \]
The factor \(1/2\) now has a different meaning:
Keeping backward variance stable gives
\[ \boxed{ \sigma_\Omega^2=\frac{2}{D_{\mathrm{out}}}. } \]
Chapter 7, equation 7.33 (Prince 2023)
For a ReLU layer:
| direction | architectural count | stable-variance target |
|---|---|---|
| forward | fan-in \(D_{\mathrm{in}}\) | \(2/D_{\mathrm{in}}\) |
| backward | fan-out \(D_{\mathrm{out}}\) | \(2/D_{\mathrm{out}}\) |
For an equal-width layer, fan-in = fan-out, so the same Kaiming/He scale serves both directions.
For a rectangular layer, the two targets differ.
Twenty equal-width ReLU layers run forward and backward at three weight scales. Because fan-in equals fan-out here, the middle Kaiming/He scale targets both directions at once.
Per-layer scaling errors compound with depth.
When
\[ D_{\mathrm{in}}\ne D_{\mathrm{out}}, \]
the exact forward and backward targets differ:
\[ \frac{2}{D_{\mathrm{in}}} \qquad\text{versus}\qquad \frac{2}{D_{\mathrm{out}}}. \]
Chapter 7 gives a symmetric compromise using the mean of the two widths:
\[ \boxed{ \sigma_\Omega^2 = \frac{4}{D_{\mathrm{in}}+D_{\mathrm{out}}}. } \]
Chapter 7, equation 7.34 (Prince 2023)
It lies between the two targets; it does not make both gains exactly one.
PyTorch’s Kaiming functions instead let the caller choose whether to preserve the fan-in / forward scale or the fan-out / backward scale.
Start with the rejected alternatives:
For the ReLU networks used here:
A minimal training setup combines the derivation with the framework operations:
model = nn.Sequential(
nn.Linear(D_i, D_h),
nn.ReLU(),
nn.Linear(D_h, D_h),
nn.ReLU(),
nn.Linear(D_h, D_o),
)
# Kaiming/He is used only where ReLU follows the linear layer.
for layer in (model[0], model[2]):
nn.init.kaiming_normal_(layer.weight, mode="fan_in", nonlinearity="relu")
nn.init.zeros_(layer.bias)
# No ReLU follows the output layer, so leave its weight at the framework default.
nn.init.zeros_(model[4].bias)
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)nn.MSELoss() uses mean reduction by default, averaging over all loss elementsnn.MSELoss(reduction="sum") matches the batch-sum equations directlySGD matches the update rule derived in this lecture; momentum would introduce an additional optimiser state and a different update equationPyTorch calls the Kaiming/He rule kaiming_*; here mode="fan_in" matches the forward-pass derivation above for the two ReLU hidden layers. The linear output layer deliberately does not receive that ReLU-specific rule.
Initialisation happens once; backpropagation happens on every training iteration.
One iteration therefore contains:
The forward pass must retain values required later by the backward pass. For a large network and batch, stored activations can dominate memory use. Two ways to trade compute or batch granularity for memory are:
Both preserve the same underlying derivative computation while changing when intermediate values are stored or recomputed.
A deliberately narrow comparison of parameter storage and saved forward activations in a toy dense network. Parameter gradients, optimiser state, allocator overhead, and other framework buffers are omitted.
Checkpointing trades additional forward computation for lower activation memory.
Training alternates a forward numerical computation with a backward sensitivity computation, then the optimiser converts the resulting parameter gradients into an update.
For one example:
\[ \sensitivity_k = \nabla_{\vect{f}_k}\exloss_i \]
\[ \nabla_{\layerbias_k}\exloss_i = \sensitivity_k \]
\[ \nabla_{\layerweights_k}\exloss_i = \sensitivity_k\hidden_k\transpose \]
\[ \sensitivity_{k-1} = \indicator{\vect{f}_{k-1}>0} \odot \left( \layerweights_k\transpose\sensitivity_k \right) \]
Under the initialisation approximations stated in the derivation, for a ReLU layer,
\[ \var{f_{k,i}} \approx \frac12D_{\mathrm{in}}\sigma_\Omega^2\, \var{f_{k-1,j}}. \]
Forward scale is preserved by
\[ \boxed{\sigma_\Omega^2=\frac{2}{D_{\mathrm{in}}}.} \]
Backward scale is preserved approximately by
\[ \boxed{\sigma_\Omega^2=\frac{2}{D_{\mathrm{out}}}.} \]
For unequal fan-in and fan-out, Chapter 7 gives the compromise
\[ \boxed{\sigma_\Omega^2=\frac{4}{D_{\mathrm{in}}+D_{\mathrm{out}}}.} \]
Primary reading:
Associated notebooks:
7_1_Backpropagation_in_Toy_Model7_2_Backpropagation7_3_InitializationUseful locators for revision: