Views: 5
The wrong starting weights can make a deep network’s activations vanish to zero or explode to millions before training even begins. Module 3 of From Zero to Agents derives Xavier and He initialization from a variance formula and proves it in a real 15-layer network.
Episode 03.02 ended by asking what happens if a deep network’s initial weights are drawn from a poorly-chosen distribution before training even begins. The answer turns out to be dramatic, measurable, and entirely preventable — a badly scaled starting point can make activations vanish to numerically zero or explode past a hundred million, across a mere 15 layers, before a single gradient step has been taken. This episode derives exactly how to avoid both failure modes.
1. Theory: two distinct initialization failures
1.1 The symmetry problem — why identical weights (including all-zero) are catastrophic. It’s tempting to think initializing all weights to zero is a safe, neutral starting point. It’s actually a complete failure, for a structural reason: if every weight feeding into a layer’s neurons is identical, every neuron in that layer computes the exact same output for any given input — they’re computing the same function. Backpropagation then assigns them the exact same gradient, because the math computing that gradient depends only on the (identical) weights and the (identical) forward computation. Every neuron updates identically, forever — a layer with 100 neurons, initialized this way, behaves for the rest of training exactly like a layer with 1 neuron, just wastefully computed 100 times. This is called a symmetry-breaking problem, and it’s why weights are always initialized randomly, not to a fixed value — randomness is what lets different neurons in the same layer diverge into computing genuinely different things.
1.2 The scale problem — random, but how random? Randomness alone fixes symmetry, but doesn’t fix everything. Draw weights from a distribution with the wrong spread (variance), and Episode 03.02‘s vanishing/exploding gradient story replays — except now happening to the forward-pass activations themselves, before training has even started. Too-small weights shrink the signal toward zero, layer after layer; too-large weights amplify it, layer after layer, toward numbers that quickly overflow any sane range. What’s needed is a variance just right to keep the signal’s scale roughly stable as it passes through many layers — and this can be derived exactly, not guessed at.
2. Math: deriving the correct variance from first principles
2.1 The variance of a weighted sum. Consider one neuron’s raw output (before activation): , where is the number of inputs (the “fan-in”), and — the key modeling assumption — the weights and inputs are independent random variables with mean zero. For independent, zero-mean random variables, variance of a sum equals the sum of variances, and the variance of a product of two independent zero-mean variables is the product of their variances:
(assuming all share the same variance , and likewise for , which is exactly the setup when weights are drawn i.e. from one fixed initialization distribution). This single formula is the entire foundation of every principled initialization scheme.
2.2 Solving for the “just right” weight variance. We want the output’s variance to roughly match the input’s variance — — so the signal’s scale is preserved passing through the layer, rather than shrinking or growing. Setting in §2.1’s formula and solving:
This is the core intuition behind Xavier/Glorot initialization (Glorot & Bengio, 2010): scale the initial weight variance inversely with the number of inputs. The actual published formula additionally balances against the number of outputs (fan-out), to keep gradients well-scaled on the backward pass too, not just activations on the forward pass:
2.3 He/Kaiming initialization — the ReLU correction. Xavier’s derivation implicitly assumes activations stay roughly symmetric around zero. ReLU (Episode 03.02 §1.3) breaks that assumption on purpose — it zeros out every negative input, meaning roughly half the variance computed in §2.1 gets discarded at every layer. He et al. (2015) derived the correction: to compensate for that lost half, double the variance:
This is now the standard default for ReLU-family networks — precisely because it accounts for a property of the activation function itself, not initialization in the abstract.
3. Decoding real notation — straight from the papers
Glorot & Bengio’s paper states the initialization as a uniform distribution:
This looks different from §2.2’s formula only superficially: for a uniform distribution on , the variance is (a standard fact about the uniform distribution). Setting and solving for a gives exactly — the paper’s formula is §2.2’s variance target, just expressed as a uniform-distribution range instead of a normal-distribution variance. Recognizing this conversion — “a paper’s notation is a repackaged variance target” — is a genuinely useful, reusable skill; the same variance-to-range conversion shows up constantly. He et al.’s paper states their version directly as a normal distribution, — already in the variance form §2.3 derived.
4. Code: both failure modes demonstrated, then the fix, verified against PyTorch
4.1 From scratch — the symmetry problem, watched failing to break
import torch
import torch.nn as nn
torch.manual_seed(0)
net = nn.Sequential(nn.Linear(4, 4), nn.ReLU(), nn.Linear(4, 1))
with torch.no_grad():
net[0].weight.fill_(0.3) # every weight in layer 1 IDENTICAL (not even zero -- the point is symmetry, not zero)
net[2].weight.fill_(0.5) # every weight in layer 2 also identical
x = torch.randn(1, 4)
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
for _ in range(20):
loss = (net(x) - torch.tensor([[1.0]]))**2
optimizer.zero_grad(); loss.backward(); optimizer.step()
rows = net[0].weight.data
print("Layer 1 neurons after 20 training steps:")
print(rows)
print("Still identical to each other?", all(torch.allclose(rows[0], rows[i], atol=1e-6) for i in range(1, 4)))
Layer 1 neurons after 20 training steps:
tensor([[0.2668, 0.3174, 0.3499, 0.2960],
[0.2668, 0.3174, 0.3499, 0.2960],
[0.2668, 0.3174, 0.3499, 0.2960],
[0.2668, 0.3174, 0.3499, 0.2960]])
Still identical to each other? True
Twenty full gradient-descent steps, and all four “neurons” moved — but moved in perfect lockstep, remaining identical to six decimal places. This confirms §1.1 precisely: symmetric initialization doesn’t just start bad, it’s structurally incapable of ever un-tying itself through gradient descent alone. This layer will behave like one neuron for the rest of training, no matter how long it runs.
4.2 Verifying the variance formula from §2.1, empirically
import numpy as np
rng = np.random.default_rng(0)
n = 50 # fan-in
trials = 200000
y_samples = [np.sum(rng.normal(0, 1, n) * rng.normal(0, 1, n)) for _ in range(trials)]
print("Empirical Var(y):", np.var(y_samples))
print("Analytical n * Var(w) * Var(x):", n * 1.0 * 1.0)
Empirical Var(y): 50.116
Analytical n * Var(w) * Var(x): 50.0
Two hundred thousand simulated neurons, each with 50 random inputs and 50 random weights (both variance 1), and the measured output variance matches §2.1’s derived formula almost exactly.
4.3 The real payoff — activation scale across 15 layers, three initialization choices
def build_net(n_layers=15, width=100):
layers = []
for _ in range(n_layers):
layers += [nn.Linear(width, width, bias=False), nn.ReLU()]
return nn.Sequential(*layers)
def report(name, std):
torch.manual_seed(0)
net = build_net()
with torch.no_grad():
for m in net:
if isinstance(m, nn.Linear):
m.weight.normal_(0, std)
activations = torch.randn(1, 100)
print(f"\n{name} (std={std:.4f}):")
for layer in net:
activations = layer(activations)
if isinstance(layer, nn.ReLU):
print(f" std = {activations.std().item():.6g}")
fan_in = 100
report("Too small", 0.01)
report("He/Kaiming (sqrt(2/fan_in))", (2.0/fan_in)**0.5)
report("Too large", 0.5)
Too small (std=0.0100):
std = 0.082292 ... (shrinking every layer) ... std = 0.000000 [by layer 5, fully dead]
He/Kaiming (std=0.1414):
std = 1.163784 ... std = 1.293513 ... std = 1.201446 [stays roughly 1.2-1.6 across all 15 layers]
Too large (std=0.5000):
std = 4.114597 ... std = 16.168905 ... std = 202548512.0 [by layer 15, over 200 MILLION]
This is the entire episode in one experiment. Same architecture, same random input, same 15 layers of ReLU — the only thing that changed between runs is the initial weight standard deviation. Too small, and every activation is numerically indistinguishable from zero by layer 5 — nothing downstream receives any signal at all. Too large, and the activation scale roughly quadruples every layer, reaching over 200 million by layer 15 — a scale where numerical precision itself starts breaking down, long before considering training. The He/Kaiming-scaled run — using exactly 2/fan_in from §2.3 — keeps the activation scale in a stable, narrow band across the entire depth, exactly as derived.
4.4 Confirming against PyTorch’s built-in initializers
layer = nn.Linear(100, 100, bias=False)
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
print("torch kaiming_normal_ std:", layer.weight.std().item())
print("He formula sqrt(2/fan_in):", (2.0/100)**0.5)
torch kaiming_normal_ std: 0.1425
He formula sqrt(2/fan_in): 0.1414
Matching closely (small difference is expected — a finite sample of 10,000 weights won’t hit the theoretical variance exactly), confirming nn.init.kaiming_normal_ implements precisely the formula derived in §2.3, not a different heuristic under a similar name.
5. Module 03 progress: where this leaves us
Three episodes in, and every foundational failure mode of a naive neural network has now been diagnosed and fixed with derivable, verifiable mathematics rather than folklore: a single perceptron’s linear ceiling (03.00), fixed by nonlinear composition (03.01); vanishing gradients from saturating activations (03.02), fixed by ReLU-family functions; and today, symmetric and badly-scaled initial weights, fixed by variance-matched random initialization. Every one of these was measured directly in real code, not merely asserted.
6. Before Episode 03.04
We now have working layers (03.01), a good activation function (03.02), and a good starting point (03.03). What’s still entirely missing is the actual training loop that ties a full multi-layer network together end to end: forward pass, loss computation, backward pass through every layer at once (not the two-layer toy case from 03.01), and a full update step — repeated until the network genuinely learns something non-trivial. What do you think changes, mechanically, going from backpropagating through 2 layers to backpropagating through many?
That’s the on-ramp into Episode 03.04 — implementing backpropagation properly, from scratch, through an arbitrarily deep network.
Previous: Episode 03.02 — Activation Functions and the Vanishing Gradient Problem Next: Episode 03.04 — Backpropagation Through a Full Network, From Scratch


Leave a Reply