Normalization & Initialization
BatchNorm, LayerNorm, residual connections, and weight init — what makes deep networks trainable at all. Starts from why a 30-layer network simply refuses to learn, then builds the three fixes that solved it.
Start here — the thing nobody warns you about
You've learned what a neural network is: a stack of layers, each one multiplying by a matrix and bending the result with a nonlinearity. You've learned that gradient descent turns the knobs and backpropagation works out which way to turn them.
So here's a reasonable thing to try: stack thirty of those layers instead of three, and let it rip.
It won't work. The loss will sit there, flat, forever — or it will explode to NaN in the first fifty steps. Not because your code is wrong. Not because thirty layers is too many for the problem. It fails for a reason that is baked into what "stacking layers" means, and for about twenty-five years that reason was the single biggest obstacle in the whole field.
This lesson is about that obstacle and the three fixes that removed it. Every deep network you will ever use — every CNN, every Transformer, every LLM — is built out of those three fixes:
- Initialization — start the weights at exactly the right scale.
- Normalization — re-center and re-scale the signal at every layer, continuously, during training.
- Residual connections — give the signal a shortcut that skips layers entirely.
It starts from first principles and assumes only Neural Networks. Flip the Depth switch at the top for the formal statistics and derivations — they open automatically once you've completed the prerequisites.
The core problem — every layer multiplies
Here is the whole disease in one sentence: a deep network is a long chain of multiplications, and long chains of multiplications go to zero or to infinity.
Photocopy a page. Then photocopy the copy. Then copy that. Each pass is only slightly lossy — 98% faithful, say — but after thirty passes the page is grey mush. Now imagine a machine that instead makes each copy 2% darker; after thirty passes you have a black rectangle. Either way, the information is gone. A deep network passes its signal through thirty layers exactly like this, and only one setting — the razor's edge where each layer neither shrinks nor grows the signal — survives the trip.
Plug a guitar into an amp, plug that amp into another, and so on, thirty amps deep. If each amp's gain is set slightly below 1, the sound at the end is silence. If each is set slightly above 1, the last amp is screaming feedback. Getting one amp's gain right is easy; getting thirty of them to compose to a gain of exactly 1 is the entire engineering problem.
Two names for the two failure modes. When the signal (or the gradient flowing back) shrinks toward zero layer after layer, that's the problem. When it grows without bound, that's the problem.
Forget matrices. Take a single number, 1.0, and pass it through 30 layers where each layer just multiplies by some gain .
- Gain 0.8 (each layer shrinks the signal 20%): after 30 layers the value is . That's , cubed, so roughly — about one-thousandth of what went in.
- Gain 1.2 (each layer grows it 20%): . The signal is 237× larger than it started.
- Gain 0.5: . Nine zeros. In 32-bit floating point this is still representable, but the gradient built from it rounds to nothing.
Notice how violent the difference is. A gain of 0.8 versus 1.2 is a modest per-layer difference — and it produces a 200,000× difference at the output. Depth amplifies small errors of scale into catastrophic ones.
A 30-layer network trains fine, but a 60-layer version of the same network produces NaN losses within a few steps. What is the most likely explanation?
Hint: Think about what happens to the number in the worked example when you double the exponent.
See the collapse happen
This is not a hand-wave — you can compute the exact scale at every depth. Below, a 20-layer network with 256 units per layer and ReLU activations, under three different weight scales. The vertical axis is a log scale, so a straight line means a fixed multiplication per layer.
The middle line is not flat by luck. It is flat because someone solved for the weight scale that makes it flat. That is all initialization is: a formula for the one setting where the signal comes out the far end the same size it went in.
Fix 1 — Initialization: start at the right scale
is the choice of what the weights are before step one. It sounds like a footnote. It is not: it decides whether the network trains at all.
Why not just set everything to zero?
Set every weight in a layer to the same value — zero, or anything else — and every neuron in that layer computes exactly the same thing. Identical outputs mean identical gradients, which mean identical updates, which mean they stay identical after the update. Forever. A 512-neuron layer initialized to a constant behaves like a single neuron copied 512 times, no matter how long you train it.
This is the , and randomness is what breaks it. The weights must be random. The only question is how big.
Why not just "small random numbers"?
The obvious guess — draw from a Gaussian with a small standard deviation like 0.01 — is what everyone tried first, and it fails for a reason you can now compute yourself.
Take a layer with inputs, ReLU, and weights drawn with standard deviation (so variance ).
- The variance gain per layer is .
- Variance shrinks 39× per layer. In terms of the typical size of an activation, that's — each layer shrinks the signal to 16% of what it was.
- After 10 layers: . The activations are a hundred-millionth of their original size, and the gradients that flow back through them are just as tiny.
The network isn't broken. It's just whispering, and by the tenth layer nobody can hear it.
Now do the same arithmetic with the right scale. He initialization says , so for : . Only six times larger than the naive 0.01 — and that six-times difference is the difference between a network that trains and one that doesn't.
The two formulas you'll actually use
There are two standard recipes, and which you pick depends on your activation function.
| Scheme | Weight variance | Use it with | Named after |
|---|---|---|---|
| Xavier / Glorot | tanh, sigmoid | Glorot & Bengio, 2010 | |
| He / Kaiming | ReLU and its relatives | He et al., 2015 |
Run the numbers yourself. This cell propagates a signal through 20 layers under all three scales and prints what survives:
Your ReLU network is initialized with Xavier instead of He. Nothing crashes — what quietly goes wrong, and how bad is it after 20 layers?
Hint: Compare the two variance formulas for a layer where fan-in equals fan-out.
You build a linear layer with 1024 inputs and 256 outputs, using ReLU. What standard deviation does He initialization prescribe for its weights?
The other way signals die — saturation
Bad scaling is one killer. There's a second, and it comes from the activation function itself.
The sigmoid squashes any input into the range 0 to 1. Feed it 5 and you get 0.993; feed it 50 and you get 1.000. Both are essentially "yes." Which means that once an input is large, changing it barely changes the output — the function has gone flat, and a flat function has a gradient of approximately zero. The neuron has stopped listening.
That flatness is called , and it interacts horribly with depth. Play with the curves below: drag along the sigmoid and watch its derivative curve collapse to nothing on either side.
Here is the killer arithmetic. The sigmoid's derivative never exceeds 0.25, even at its very best point. So in the most favourable case imaginable, a 10-layer sigmoid network multiplies the gradient by at most on the way back. In the realistic case, where activations have wandered into the flat regions, the factor is far smaller still.
ReLU's derivative is exactly 1 for every positive input — not 0.25, not 0.9, but 1. Multiplying by 1 many times does nothing at all, which is precisely the property a deep chain needs. That single fact, more than any other, is why ReLU replaced sigmoid as the default activation and made networks past a dozen layers imaginable. (Its cost: for negative inputs the derivative is 0, so a neuron can get stuck permanently off — the so-called dying ReLU.)
Why can a 3-layer sigmoid network train perfectly well while a 15-layer one cannot?
Hint: Raise 0.25 to the third power, then to the fifteenth.
Fix 2 — Normalization: reset the scale, continuously
Good initialization gets you a network whose signal is well-scaled on step zero. But training moves the weights. By step 1,000 the careful scaling has drifted, and by step 100,000 it's gone.
Normalization is the decision to stop hoping the scale stays right and instead enforce it, at every layer, on every forward pass. Take whatever numbers arrive, subtract their mean so they're centered on zero, divide by their standard deviation so their spread is 1, and hand those to the next layer. Whatever drift happened upstream is erased before it can compound.
When you weigh flour then sugar in the same bowl, you press tare in between — resetting the display to zero so the second measurement doesn't inherit the first one's weight. Normalization is tare for a neural network: every layer gets a freshly-zeroed, freshly-scaled reading instead of one carrying twelve layers of accumulated offset.
Here is what that does to a layer's output distribution. Before normalization, a layer deep in a drifting network might be producing values centered around 3 with a spread of 2.5; after, they're centered on 0 with a spread of 1 — every single time, regardless of what the previous layers did.
BatchNorm — normalize each feature across the batch
The first version to work, and the one that changed everything, is . Its choice is: for each feature (each neuron), compute the mean and spread across the examples in the current mini-batch, and standardize using those.
One neuron, a mini-batch of 4 examples. The neuron's outputs are [2, 4, 4, 6].
- Mean: .
- Variance: average of the squared distances from the mean — . So the standard deviation is .
- Subtract the mean:
[-2, 0, 0, 2]. - Divide by the standard deviation:
[-1.414, 0, 0, 1.414].
Check the result: it averages to 0, and its variance is . Exactly centered, exactly unit spread — whatever the neuron happened to be outputting.
- Then the learned part: multiply by and add . If the network has learned , the final output is
[-3.24, 1, 1, 5.24].
Step 5 deserves its own explanation, because it looks like it undoes the whole thing.
Forcing every layer to output mean-0, spread-1 values is a real constraint — and sometimes the wrong one. A sigmoid, for instance, is nearly linear in the range −1 to 1, so a layer locked into that range loses its ability to bend. So BatchNorm adds two learned numbers per feature: a scale and a shift . The network can turn them to any values it likes, including ones that exactly undo the normalization. The point isn't to force mean 0 — it's to make the mean and spread two explicit, directly-learned parameters rather than accidental byproducts of twelve layers of drift.
Compute it yourself and confirm the hand-worked numbers:
BatchNorm behaves differently at training time and at inference time, and forgetting this is the single most common bug involving it.
- Training: it uses the mean and variance of the current mini-batch — so a given example's output depends on which other examples happen to share its batch.
- Inference: there may be no batch (you're classifying one image), and outputs must be deterministic. So BatchNorm instead uses a running average of the statistics it saw during training, accumulated as it went.
In PyTorch this is what model.eval() switches on and model.train() switches off. Forget model.eval() before validating and your accuracy will be mysteriously, inconsistently wrong. A related trap: BatchNorm needs a reasonably-sized batch to estimate statistics from — with a batch size of 1 or 2 the estimates are garbage, which is one reason it fails on memory-hungry models.
Why does BatchNorm include the learned parameters gamma and beta at all, when the whole point was to force mean 0 and variance 1?
LayerNorm — normalize each example across its own features
BatchNorm has a structural weakness: it needs a batch. Its statistics are computed across examples, which makes one example's output depend on its batch-mates, breaks with tiny batches, and gets awkward when examples have different lengths — as sentences do.
makes the opposite choice. Same arithmetic — subtract a mean, divide by a spread — but computed in the perpendicular direction.
Picture the activations as a table: one row per example in the batch, one column per feature.
- BatchNorm averages down the columns — asking how does this one neuron behave across the batch? It needs every row to answer.
- LayerNorm averages across the rows — asking how do this one example's features compare to each other? It needs nothing but that single row.
Same operation, perpendicular axis, and that perpendicularity is the entire difference in their behavior.
One example with 4 features: [1, 2, 3, 10]. LayerNorm never looks at any other example.
- Mean across these 4 features: .
- Variance: , so the standard deviation is .
- Center and scale: .
The four numbers now sum to zero and have unit spread. Note what the outlier did: the 10 dominated the mean and inflated the spread, which squeezed the other three features toward zero. That is LayerNorm working as designed — it reports each feature relative to the others in the same example.
| BatchNorm | LayerNorm | |
|---|---|---|
| Averages over | the batch (down columns) | the features (across a row) |
| Depends on batch size? | Yes — breaks with tiny batches | No — works with a single example |
| Train vs. inference | Different (running stats needed) | Identical |
| Variable-length inputs | Awkward | Natural |
| Standard home | CNNs / vision | Transformers / NLP |
Three properties settle it. Language batches contain sequences of wildly different lengths, so batch statistics are computed over ragged, inconsistent sets. Generation happens one token at a time with an effective batch of one, where batch statistics are meaningless. And distributed training splits a batch across many GPUs, so batch statistics would need syncing across machines every layer. LayerNorm sidesteps all three by never looking outside a single example. See Transformer Block for where it sits in the architecture.
You are generating text one token at a time, so the batch size is 1. Why would BatchNorm fail here while LayerNorm is fine?
Hint: What is the variance of a set containing exactly one number?
Fix 3 — Residual connections: give the gradient a shortcut
Initialization sets the scale right at the start. Normalization keeps it right throughout. Yet even with both, networks past about 20 layers were still getting worse as you added depth — and not from overfitting: their training error went up too. Adding layers was making the model worse at the data it could see.
Here's what makes that absurd. Take a working 20-layer network and add 16 more layers. Those 16 layers could each just copy their input to their output — do nothing — and the deeper network would score exactly the same as the shallower one. So a 36-layer network can never need to be worse. But gradient descent could not find that do-nothing solution, because "output exactly what came in" is a surprisingly awkward function for a stack of matrix multiplies and ReLUs to represent.
The fix is beautiful: stop making the layer learn to copy. Build the copy in, and let the layer learn only the change.
Ordinary layers are a staircase: to reach the top, every step must be climbed, and information has to survive every one. A runs an escalator alongside: whatever you put on at the bottom arrives at the top untouched, no matter what happens on the stairs. The layers are still there and still contribute — but they're now contributing an adjustment to a signal that reaches the top regardless.
The change is one addition. Instead of a block computing , it computes:
- — what came into the block, passed through completely untouched. This is the escalator.
- — whatever the block's layers compute: the residual, the change being proposed.
- — plain addition. That is the entire architectural innovation, and it made 100-plus-layer networks routine.
- Why it matters for training: if the block should do nothing, it only has to drive toward zero — which is trivially easy, since shrinking weights toward zero is exactly what gradient descent does naturally. Compare that with making a stack of matrices reproduce the identity exactly. The residual form makes "do nothing" the default and improvement the deviation.
A 56-layer plain CNN has HIGHER training error than a 20-layer version of the same network. What does this tell you?
Putting the three together — the modern block
Every serious architecture since about 2016 combines all three fixes, and they compose in a specific order. In a Transformer the pattern is:
import torch
import torch.nn as nn
class TransformerBlock(nn.Module):
"""The pre-LN block used by essentially every modern LLM."""
def __init__(self, d_model, n_heads):
super().__init__()
self.norm1 = nn.LayerNorm(d_model) # fix 2: normalize BEFORE the sublayer
self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
self.norm2 = nn.LayerNorm(d_model)
self.mlp = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.GELU(),
nn.Linear(4 * d_model, d_model),
)
# fix 1: PyTorch already applies a sensible init; be explicit for the MLP
for layer in self.mlp:
if isinstance(layer, nn.Linear):
nn.init.normal_(layer.weight, std=0.02) # the GPT-2 convention
nn.init.zeros_(layer.bias)
def forward(self, x):
h = self.norm1(x)
x = x + self.attn(h, h, h, need_weights=False)[0] # fix 3: residual
x = x + self.mlp(self.norm2(x)) # fix 3: residual
return xRead the two lines in forward. Each is x = x + something(norm(x)): normalize, transform, add back to the untouched input. That one line, repeated 32 or 80 or 120 times, is the entire skeleton of a large language model.
- Forgetting
model.eval()before validation, so BatchNorm keeps using batch statistics. Validation accuracy jumps around and looks wrong. - Applying weight decay to and . These are scale and shift parameters, not weights; decaying them toward zero fights the normalization. Exclude norm parameters and biases from weight decay — see Optimization.
- Using BatchNorm with a batch size of 2 or 4 (common when the model barely fits in memory). The batch statistics are pure noise. Use GroupNorm or LayerNorm instead.
- Keeping a bias in a layer immediately followed by BatchNorm. The normalization subtracts the mean, which deletes the bias entirely — it is a parameter that provably cannot affect the output. Set
bias=False. - Initializing with
std=0.01because it looks safe. Compute the He value instead; the worked example above shows how far off the naive guess is.
Explain to a friend why stacking 50 layers used to be impossible, using the photocopy or amplifier picture — no formulas. Then name the three fixes and say in one sentence each what problem it solves. If you cannot explain why a residual connection helps the gradient specifically, that is the section to reread.
- A deep network is a chain of multiplications, so its signal and gradients shrink to zero (vanishing) or blow up (exploding) unless every layer's gain is almost exactly 1.
- Initialization sets that gain right at step zero. He () for ReLU; Xavier () for tanh and sigmoid. Zeros are fatal — every neuron stays identical forever.
- Saturation is the second killer: a sigmoid's derivative never exceeds 0.25, so ten sigmoid layers cut the gradient by a millionfold. ReLU's derivative of exactly 1 is why it won.
- Normalization stops the scale drifting during training by re-centering and re-scaling every layer's output, then handing control back through the learned and .
- BatchNorm normalizes each feature across the batch (great for CNNs; needs a decent batch size; differs between train and eval). LayerNorm normalizes each example across its own features (batch-independent, identical at train and inference — hence every Transformer). RMSNorm drops the mean subtraction for speed.
- Residual connections () make "do nothing" the default and give the gradient an identity highway that structurally cannot vanish.
- The modern block is all three at once:
x = x + sublayer(norm(x)), with a principled init — repeated dozens of times.
Practice — and how to make it stick
• Retrieval practice: before scrolling back, try to state the He formula, the two axes BatchNorm and LayerNorm average over, and what the identity term does to the gradient. Struggling to recall beats rereading.
• Spaced repetition: mark this topic complete to add it to your Review queue, so it resurfaces right before you would have forgotten it.
• Interleaving: mix these with Optimization and Backpropagation problems rather than doing them in a block — initialization, learning rate, and gradient flow are one interlocking system, and practicing them together is what teaches you to tell their failure modes apart.
- By hand: a layer has 2048 inputs and 512 outputs with ReLU. Compute the He standard deviation, then the Xavier one. By what factor do they differ, and what would that do to the signal over 20 layers?
- From scratch: implement LayerNorm's backward pass in NumPy and check it against
torch.nn.LayerNormwithgradcheck. The mean and variance both depend on every input, which makes this a genuinely instructive derivative. - Experiment: train a 20-layer MLP on MNIST three times — no normalization, BatchNorm, LayerNorm — and plot the loss curves together. Then delete the residual connections from a small ResNet and watch the training error get worse with depth.
- Read the source: skim §3 of the ResNet paper and find Figure 1 — the plot of a 56-layer plain network training worse than a 20-layer one. That single figure is the motivation for the entire architecture.
Try it right here — edit and run the code, and if you get stuck or hit an error, ask Ada on the right: she can see your code and terminal output.
Next: with deep networks now trainable, learn how to stop them memorizing the training set in Regularization — then see all three fixes assembled at scale in the Transformer Block.