EVERYTHING AIAI engineering, made visual
0/13 complete
LESSON 08 · DEEP LEARNING × AI · BUILD

Initialize wrong
and training never starts.

Weights are the only thing a network gets to choose before it has learned anything. Set them to zero and 512 neurons behave as one; set their scale wrong and the signal is 10²⁷ by layer ten — or a thousandth of itself. This lesson is the five-minute decision that makes the next 50 layers trainable.

45 MIN · 8 CHAPTERSPREREQ · PHASE 3 · LESSONS 04 & 07
FIG. 08 / ONE SIGNAL, FIVE INITIALIZATIONS
stable explodes vanishes identical
LESSON 08TYPE · BUILD~45 MINPREREQ · PHASE 3 · LESSONS 04 & 07ORIGINAL LESSON ↗
THE 60-SECOND VERSIONThen break the symmetry ↓
01 / BREAK THE TIE

Identical weights make identical neurons — forever.

Every neuron does the same job: multiply inputs by weights, add a bias, apply an activation. Start them all at the same value — zero, or any constant — and each one computes the same output, receives the same gradient, and updates by the same amount. A 512-unit layer stays 512 copies of one neuron. Randomness is the brute-force fix; the size of the randomness is the science.

same weights → same gradient → same update
02 / KEEP THE PRODUCT AT 1

Each layer multiplies the signal's variance.

With fan_in inputs, Var(z) = fan_in · Var(w) · Var(x). Set Var(w) = 1 with fan_in = 512 and the factor is 512 per layer — 512¹⁰ ≈ 1.2×10²⁷ by layer ten. Set Var(w) = 0.001 and the factor is 0.512 — down to 1.2×10⁻³ after ten layers. The target is the product fan_in · Var(w) = 1, adjusted for what the activation does to the signal.

fan_in · Var(w) · (activation) = 1
03 / MATCH TO THE ACTIVATION

Xavier for sigmoid/tanh, Kaiming for ReLU — and 1/√(2N) for residuals.

Xavier's Var(w) = 2/(fan_in + fan_out) balances the forward and backward passes when the activation is roughly linear near zero. ReLU zeroes half its outputs, so it needs the extra factor of 2: Var(w) = 2/fan_in. And in transformers, every residual addition pumps variance into the stream — GPT-2 scales branch weights by 1/√(2N) to keep a 126-layer stack bounded.

tanh → Xavier · ReLU → Kaiming · residuals → 1/√(2N)
MENTAL MODEL IN ONE SENTENCE

Initialization is choosing the network’s starting point: random enough that no two neurons are copies, scaled so that one layer’s variance multiplier — fan-in × weight variance × the activation’s factor — is as close to 1 as the architecture allows.

By the end you will be able to implement zero, random, Xavier, Kaiming and orthogonal initialization; explain why zero init wastes a layer and why random scale alone is not enough at depth; derive Var(w) = 2/(fan_in + fan_out) and Var(w) = 2/fan_in from the variance-propagation rule; read activation statistics to diagnose vanishing, exploding and dead networks; and place GPT-2’s 1/√(2N) residual scaling in the same framework — all with numbers you can check by hand.

THE SYMMETRY PROBLEM

Five hundred neurons,
one degree of freedom.

Initialization is the only decision a network makes before it has learned anything. The most tempting starting point — all weights zero — turns out to be the worst one: it makes every neuron in a layer an exact copy of every other, and gradient descent has no way to tell them apart.

Every neuron in a layer does the same arithmetic: multiply its inputs by its weights, add a bias, apply an activation. If every neuron starts with the same weights, they all compute the same output. The backward pass then hands every one of them the same gradient, and the update step moves every one of them by the same amount. The layer never diversifies: after 10,000 epochs it is still 512 copies of a single neuron. You paid for 512 parameters and got one.

This is called symmetry, and random initialization is the brute-force way to break it. Each neuron starts at a different point in weight space, so each one sees a slightly different gradient and learns a slightly different feature. The source’s main.py makes the point with four neurons and two inputs: with zero weights every neuron outputs sigmoid(0) = 0.5, and after ten thousand steps the four outputs are still identical to the last decimal.

zero init, 4 sigmoid neurons, inputs [0.5, −0.3], target 1, MSE step 0 z = 0·0.5 + 0·(−0.3) + 0 = 0 a = σ(0) = 0.5 (all four neurons) dL/da = 2(a − y) = −1.0 dL/dz = dL/da · a(1 − a) = −1.0 × 0.25 = −0.25 dL/dw₁ = −0.25 × 0.5 = −0.125 dL/dw₂ = −0.25 × −0.3 = +0.075 dL/db = −0.25 one step, lr = 0.1 w₁ = 0 + 0.0125 = 0.0125 w₂ = 0 − 0.0075 = −0.0075 b = 0 + 0.025 = 0.025 z = 0.0125×0.5 + (−0.0075)(−0.3) + 0.025 = 0.0335 a = σ(0.0335) = 0.5084 (all four neurons, again) the four neurons share one weight vector forever: 8 weights, 1 function

The failure is not that the output is zero — it is that the output is the same for every unit. A layer of identical neurons has no more expressive power than a layer of width one, no matter how many units you buy.

Four neurons that are really one

The same inputs feed four independent sigmoid neurons, each with its own weights. Give them identical starting weights and press step: they receive identical gradients and stay identical forever. Randomize and watch them separate.

init all weights 0, biases 0 gradient step 0 of 3 (lr 0.1) outputs a₁ a₂ a₃ a₄ 0.5000 0.5000 0.5000 0.5000 spread 0.00e+0 → identical outputs AND identical dL/dz: the eight weights produce one function. Effective parameters: 1, not 8.

Constant weights fail the same way zero does — it is theshared value, not the value zero, that creates the tie. Random is the brute-force fix; the next chapters decide howlarge that randomness should be.

Worked example A — why random beats zero, in one gradient step

Take two neurons with the same inputs, one started at zero and one started at random weights. Compare what one gradient step does to thedifference between them.

neuron A: w = (0, 0), b = 0 ← the zero-init twin neuron B: w = (0.4, −0.2), b = 0 ← a random start neuron A z = 0.0000 a = 0.5000 dL/dz = −0.2500 neuron B z = 0.2600 a = 0.5646 dL/dz = −0.2141 after one step (lr = 0.1): A: w = (0.0125, −0.0075), b = 0.0250 B: w = (0.4107, −0.2064), b = 0.0214 gap before: (0.40, −0.20) gap after: (0.3982, −0.1989) ← still there, ~0.45 wide The update rule w ← w − lr·dL/dw is a function of the current weights. Two neurons with identical weights are the same input to that function and stay identical forever; neurons with a gap keep their gap because they see different gradients (−0.2141 vs −0.2500) and therefore move by different amounts.

The numbers are ordinary arithmetic — no luck involved. Randomness gives the units different starting points, and different points see different gradients, which keeps them apart. The rest of this lesson is about choosing the scale of that randomness.

Quick check

Your colleague initializes a hidden layer with every weight equal to 0.01 and says 'at least it's not zero, so the neurons will differ.' What actually happens?

VARIANCE THROUGH DEPTH

Every layer multiplies.
Aim the product at 1.

Random weights break symmetry. Wrong-sized random weights break everything else. One line of probability — the variance of a weighted sum — explains why a scale that works at depth 3 detonates or dies at depth 50, and it produces every formula in the rest of the lesson.

Look at a single neuron before any activation: z = w₁x₁ + w₂x₂ + … + w_n x_n. If the inputs and weights are independent, zero-mean random variables, the variance of the sum is the sum of the variances of the terms, and each term contributes Var(w)·Var(x):

Var(z) = Var(w₁x₁) + Var(w₂x₂) + … + Var(w_n x_n) = fan_in · Var(w) · Var(x) "each of the fan_in connections scales the input's variance by Var(w)"

That single line is the whole engine. The layer’s variance multiplier is fan_in · Var(w), and the danger is that it compounds: after L layers the signal is multiplied by (fan_in · Var(w))^L. A factor of 1.05 sounds harmless until you raise it to the 50th power. The design goal is exact: pick Var(w) so the product equals 1 — ideally on both the way forward and the way back.

Worked example B — fan_in = 512, two weight scales, ten layers

Take a 512-input layer and input variance 1, exactly the source’s opening scenario. Watch what one layer does, then ten.

fan_in = 512, Var(x) = 1 Var(w) = 1 per-layer factor 512 × 1 = 512 ten layers: 512^10 = 2^90 ≈ 1.24×10^27 → "your signal has exploded" Var(w) = 0.001 per-layer factor 512 × 0.001 = 0.512 ten layers: 0.512^10 ≈ 1.24×10^-3 → ~800× smaller every ten layers; by layer 50, 0.512^50 ≈ 2.9×10^-15 — gone the fix Var(w) = 1/fan_in = 1/512 ≈ 0.00195 per-layer factor 512 × 0.00195 = 1.000 10 layers: 1, 50 layers: 1 check the arithmetic: log₁₀(0.512) = −0.2907, so log₁₀(0.512^10) = −2.907 → 10^−2.907 = 1.24×10^-3 ✓

Note that the source prints 0.00013 for the ten-layer shrinkage; careful arithmetic gives 1.24×10⁻³ (0.512 multiplied by itself ten times). Neither number changes the story: with variance 0.001 the signal shrinks by roughly an order of magnitude every four layers, and depth finishes the job. The lesson’s lab recomputes the real thing with Monte-Carlo samples.

The 50-layer variance experiment

Push random data through 50 same-width layers, measuring the mean squared activation each layer. Pick an initialization and an activation: the healthy curves stay inside the band for the whole trip; the broken ones leave the chart in either direction.

N(0, 1) + relu per-layer factor (linear model) 16.0000 (half of the raw weights) initial input variance 1.0 mean a² at layer 1 14.97 10 4.09×10^11 25 2.25×10^29 50 7.33×10^58 layer 50: mean a² = 7.33×10^58 mean |a| = 1.37×10^29 → exploded: early layers will saturate or NaN

Simulation, not theorem: one seed, 32 units wide, 96 samples. Xavier and Kaiming preserve the expected variance; any single deep run wanders, which is the honest reason practitioners still measure activation statistics instead of trusting the formula blind.

Two refinements turn fan_in · Var(w) = 1 into the formulas practitioners use. First, the activation is not free: sigmoid and tanh are roughly linear near zero — where well-initialized activations live — so they pass variance through unchanged, but ReLU zeroes every negative value and multiplies the variance by about ½. Second, the backward pass has its own fan-in: a neuron’s gradient depends on its fan_out, so protecting the gradient suggests a different constant than protecting the forward signal. Xavier and Kaiming are the two ways of resolving those refinements.

Quick check

A 512-input layer is initialized with weights drawn from N(0, 1) — standard normal. What happens to the activation variance after ten such layers?

XAVIER / GLOROT

Balance the forward
and the backward.

Protecting the signal on its way in wants Var(w) = 1/fan_in. Protecting the gradient on its way back wants Var(w) = 1/fan_out. Glorot and Bengio’s answer was to take the harmonic mean of the two — the formula that made sigmoid networks trainable at depth.

Chapter 02 asked for fan_in · Var(w) = 1, which reads as Var(w) = 1/fan_in. That is the forward ideal: it keeps the activation variance constant as data flows toward the loss. But backpropagation has its own variance budget: a neuron’s gradient is a weighted sum over its fan_out outgoing connections, so keeping the gradient variance stable asks for Var(w) = 1/fan_out. When a layer is square (fan_in = fan_out) the two ideals agree; when it is rectangular they conflict.

Xavier/Glorot initialization (2010) resolves the conflict by choosing the harmonic mean of the two ideals: 2/(fan_in + fan_out). The harmonic mean leans toward the smaller number, which is the larger of the two fans — exactly the edge of the layer where the signal is most at risk. In practice the weights are drawn either normally with that variance, or uniformly on ±√(6/(fan_in + fan_out)), which is constructed to have the same variance.

Xavier assumes the activation is roughly linear near zero — true for sigmoid and tanh in their responsive range, which is where properly initialized activations live. That assumption is why it is the right answer for sigmoid/tanh networks and one factor of two short for ReLU, the subject of the next chapter.

Worked example C — the harmonic mean, checked by hand
layer: 400 inputs → 400 outputs (square) forward ideal 1/fan_in = 1/400 = 0.002500 backward ideal 1/fan_out = 1/400 = 0.002500 harmonic mean 2/(fan_in + fan_out) = 2/800 = 0.002500 ✓ normal draw w ~ N(0, √0.0025) = N(0, 0.05) uniform draw L = √(6/(fan_in + fan_out)) = √(6/800) = 0.08660 w ~ U(−L, L) Var(U) = L²/3 = 0.0075/3 = 0.002500 ✓ same variance forward factor fan_in × Var(w) = 400 × 0.0025 = 1.0000 backward factor fan_out × Var(w) = 400 × 0.0025 = 1.0000 ten layers 1.0000^10 = 1 (signal and gradient both preserved) ---------------- rectangular layer: 400 → 100 ---------------- Xavier variance 2/(400 + 100) = 0.004000, std = √0.004 = 0.06325 forward factor 400 × 0.004 = 1.600 → 1.6^10 = 110× after ten layers backward factor 100 × 0.004 = 0.400 → 0.4^10 = 1.05×10⁻⁴ after ten average (1.6 + 0.4)/2 = 1.0 ← "balanced on average" Xavier cannot make both directions equal when the shape is not square; it deliberately splits the error between them.

That split is the honest reading of the formula: 2/(fan_in + fan_out) is not magic, it is a compromise. For square layers — the common case inside a transformer block or an MLP trunk — the compromise is exact and the factor is 1 in both directions.

The init calculator: what should √ be?

Give a layer shape and an activation; read the standard deviation each scheme prescribes. The forward and backward columns are the variance factors fan_in × Var(w) and fan_out × Var(w) — a scheme that balances both is the whole point of Xavier’s harmonic mean.

fan_in = 400, fan_out = 400. Factor 1 means the variance survives the layer; above 1 grows, below 1 shrinks. The 10-layer column is the forward factor raised to the 10th power.
schemestdvalueVar(w)fwd ×bwd ×10 layers
N(0, 1) — the naive default11.00001.000400.0400.01.05×10^26
N(0, 0.01)0.010.01001.00×10^-40.040000.040001.05×10^-14
Xavier normal√(2/(fan_in + fan_out))0.05000.0025001.0001.0001.000
Kaiming normal√(2/fan_in)0.07070.0050002.0002.0001024
LeCun normal√(1/fan_in)0.05000.0025001.0001.0001.000
Xavier uniform L = √(6/(fan_in+fan_out))±L0.08660.0025001.0001.0001.000
forward ideal 1/fan_in = 0.002500 backward ideal 1/fan_out = 0.002500 Xavier var 2/(fan_in + fan_out) = 0.002500 = harmonic mean of the two ideals recommended kaiming · The gate zeroes about half the outputs, so the raw factor fan_in × Var(w) is cut in half. The factor of 2 puts it back: Var(w) = 2/fan_in.

Simplified teaching model: it tracks variance through the weighted sum and post-activation scaling, ignoring correlations and finite-width effects. The empirical check on the right measures the real thing on 400 inputs × 20,000 samples.

the layer’s activation
layer 400 → 400 activation ReLU / GELU / Swish recommended kaiming · std 0.0707 Xavier normal 0.0500 Xavier uniform ±0.0866 Kaiming normal 0.0707 LeCun normal 0.0500 analytic forward factor (recommended) fan_in × Var(w) ÷ 2 (ReLU) = 1.000 empirical check: press the button

Try the 400 → 100 preset with each family. Xavier keeps the arithmetic balance between 1.6 forward and 0.4 backward; Kaiming and LeCun each bet on one direction. There is no universally right answer — only the right pairing with your activation.

Four ways to initialize a weight matrix · main.py, Step 1python
import math
import random


def zero_init(fan_in, fan_out):
    return [[0.0 for _ in range(fan_in)] for _ in range(fan_out)]


def random_init(fan_in, fan_out, scale=1.0):
    return [[random.gauss(0, scale) for _ in range(fan_in)] for _ in range(fan_out)]


def xavier_init(fan_in, fan_out):
    std = math.sqrt(2.0 / (fan_in + fan_out))
    return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]


def kaiming_init(fan_in, fan_out):
    std = math.sqrt(2.0 / fan_in)
    return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
Rows are neurons, columns are inputs: fan_in columns, fan_out rows. Only the standard deviation changes between schemes — the shape and the loop stay the same, which is why swapping an init is a one-line decision with fifty-layer consequences.

Notice what did not change: every function returns a list of lists with the same shape. Initialization is not a new architecture or a new optimizer — it is three characters in a formula. xavier_init(400, 100) draws from N(0, 0.06325²), and those weights already know their layer’s fan-in and fan-out. That is the entire trick: let the starting scale depend on the layer’s shape instead of hoping one constant works everywhere.

KAIMING / HE

ReLU zeroes half.
Pay the other half back.

Xavier assumes the activation passes variance through. ReLU does not: it turns every negative value into zero, which cuts the layer’s signal in half. He et al. (2015) added the factor that compensates — and with it, the initialization that made deep ReLU networks finally trainable.

Here is the ReLU problem in one line: ReLU(z) = max(0, z) deletes every negative value. With symmetric, zero-mean pre-activations, that deletes roughly half of them — and variance is an average of squares, so deleting half the values halves the variance. The layer’s multiplier is no longer fan_in · Var(w) but fan_in · Var(w) / 2. Set that equal to 1 and solve:

Xavier (assumes activation passes variance through) fan_in · Var(w) = 1 → Var(w) = 1 / fan_in Kaiming (knows ReLU halves the signal) fan_in · Var(w) / 2 = 1 → Var(w) = 2 / fan_in std = √(2 / fan_in) without the 2: 0.5 per layer → 0.5^10 = 9.77×10^-4 → 0.5^50 = 8.88×10^-16 at the source's depth

The factor of 2 is not a fudge — it is the measured halving, undone. The same logic covers the whole ReLU family: Leaky ReLU with negative slope a keeps a fraction a of the negative side, so the correction becomes Var(w) = 2 / ((1 + a²) · fan_in); GELU and Swish are smooth enough that the same 2/fan_in works in practice, which is why PyTorch and HuggingFace models reach for Kaiming for every modern transformer-style block.

Worked example D — the factor of 2, measured in one layer

Take fan_in = 400 and compare Xavier and Kaiming before and after the ReLU, using the single-layer variance experiment (the lab’s numbers, 20,000 Monte-Carlo samples):

fan_in = 400, inputs x ~ N(0, 1) Xavier Var(w) = 2/800 = 0.0025, std = 0.0500 E[z²] = fan_in × Var(w) = 1.00 after ReLU: E[a²] ≈ 0.50 ← half the signal, every layer Kaiming Var(w) = 2/400 = 0.0050, std = √0.005 = 0.0707 E[z²] = fan_in × Var(w) = 2.00 ← the deliberate ×2 after ReLU: E[a²] ≈ 1.00 ← restored measured at fan_in = 64 (lab, 20,000 samples): Xavier E[z²] = 0.9800 → E[a²] = 0.4933 (≈ half, as predicted) Kaiming E[z²] = 1.9599 → E[a²] = 0.9866 (≈ 1, as designed) ten Kaiming-scale layers: 1.00^10 = 1 ten naive-scale layers: 0.50^10 = 9.77×10^-4 ← the leak Xavier never charged for

This is a rare case in deep learning where theory and measurement agree to within a couple of percent, because the two approximations — symmetric pre-activations and independence across units — are essentially exact at initialization.

Activation statistics across 20 layers

One column per layer, one row per magnitude decade: color shows where the activations live. A healthy init keeps the mass near 10⁰ in every column; a bad one drifts upward, collapses to the floor, or freezes at a single value.

N(0, 0.01) + tanh layer 1 mean a² 0.003529 layer 10 mean a² 9.28×10^-26 layer 20 mean a² 1.18×10^-50 inspected layer 20 mean |a| 7.79×10^-26 value exactly 0 0% of units a > 1 0% of units vanishing: almost nothing survives to layer 20

This is the diagnostic a practitioner runs first: forward a batch of real data and look at each layer’s activation statistics. The source’s rule of thumb — mean magnitude between 0.5 and 2.0 through all layers — is exactly this heat-map staying on the middle rows.

A different idea: orthogonal initialization. Everything so far has tuned the variance of an iid Gaussian matrix so that its average row length is 1. An orthogonal matrix does that exactly, by construction: its columns are perpendicular unit vectors, so QᵀQ = I, every singular value is 1, and multiplying by Q rotates the input without stretching it in any direction. The check is short enough to do by hand:

Q = [ cos 45° −sin 45° 0 ] columns: (cos, sin, 0), (−sin, cos, 0), (0, 0, 1) [ sin 45° cos 45° 0 ] norms: cos²+sin² = 1 ✓ dot products: 0 ✓ [ 0 0 1 ] Qx with x = (1, 1, 0): z = (cos − sin, sin + cos, 0) = (0, 1.4142, 0) |z|² = 2 = |x|² ← no stretch, in any direction iid Gaussian needed Var(w) = 1/fan_in to make an average row unit-length; Q's rows are unit-length exactly. For ReLU, divide by √2 as with Kaiming.

Orthogonal init keeps the signal norm exactly intact at any depth — it was standard in recurrent networks and very deep non-residual nets — but it costs an SVD or QR decomposition per layer and constrains the whole spectrum uniformly, which is why the big models in practice mostly reach for Kaiming and normalization layers. It is a good reminder that the goal is norm preservation; variance tuning is just the cheap way to approximate it.

RESIDUALS & TRANSFORMERS

Every addition
adds variance.

A residual connection is a gradient highway: it lets the signal skip a block unchanged. But it also lets variance pile up, block after block, and 126 of them will bury an otherwise perfect initialization. GPT-2’s 1/√(2N) is the scaling term that keeps the pile bounded.

A transformer block is built around a residual stream: the input x flows through, and each sub-layer adds its output back to it. x = x + sublayer(x). For the gradient, this is wonderful — the + passes it through unchanged, and Chapter 03’s vanishing-gradient problem disappears.

For the variance, it is a leak. Each addition injects the sub-layer’s output variance into the stream, and the stream never gets a chance to shrink it back. If each branch contributes one unit of variance, the stream after N transformer blocks — two additions each, attention and MLP — carries 1 + 2N units. At GPT-2-small’s 12 blocks that is already 25×; at Llama 3’s 126 blocks and 405 billion parameters it is 253×, and the activations would be far from the range the rest of the initialization was designed for.

GPT-2’s fix is one constant: multiply the output weights of each residual branch by 1/√(2N). Squaring the scale makes each branch contribute 1/(2N) units instead of one, so the worst-case growth over the whole stack is 2N × 1/(2N) = 1 extra unit — a stream that doubles at most, no matter how deep the model. (The original paper states 1/√N per sub-block; with two sub-blocks per layer the two bookkeepings carry the same constant.)

Worked example E — a 12-block and a 126-block stack

The simplified teaching model from the lab: the stream starts at variance 1, every residual addition contributes a fixed branch variance of 1 unit, and a transformer block contains two additions.

GPT-2-small: N = 12 blocks → 24 residual additions unscaled: 1 + 2N = 25× scale 1/√(2N) = 1/√24 = 0.2041 (branch variance ×0.04167) scaled: 1 + 2N/(2N) = 2× Llama 3 depth: N = 126 blocks → 252 additions unscaled: 1 + 2N = 253× scale 1/√(2N) = 1/√252 = 0.0630 scaled: 1 + 2N/(2N) = 2× half a stack (63 blocks): unscaled 127× · scaled 1.5× the scaled result does not depend on N: 1 + 2N · (1/(2N)) = 2, always.

Simplified teaching model, stated plainly: real transformers have LayerNorm inside each branch, branch gains that vary with attention entropy, and depth-dependent scaling schemes beyond GPT-2’s (LayerScale, DeepNorm, μP). What survives every version is the accounting: residual additions accumulate variance, so something must scale with depth to cancel it.

Why GPT-2 scales residual weights

Every residual addition pumps a little variance into the stream. Left alone it accumulates linearly with depth; scaled by 1/√(2N) it stops mattering how deep you go. Slide the depth from GPT-2-small to Llama-3-large.

N blocks 12 residual additions 24 GPT-2 scale 1/√(2N) 0.2041 scale² 0.041667 final stream variance unscaled 1 + 2N = 25 scaled 2.000 capped at 2.000 (one extra unit) → even at 12 blocks the unscaled stream has doubled three times over.

The model is a deliberately simple bookkeeping device: every branch contributes a fixed unit of variance and each block has two branches. Real transformers mix in LayerNorm, attention entropy and branch gains — but the qualitative fact survives: unscaled residual variance grows with depth, and 1/√(2N) is the term that cancels it.

DIAGNOSING A BAD INIT

The failure has
three fingerprints.

Initialization bugs do not throw exceptions. They leave statistics behind: activation magnitudes that climb, collapse, freeze, or look perfectly healthy while the gradients quietly die. Learning to read those numbers is cheaper than learning to guess.

The source’s health check is one sentence: verify that activation magnitudes stay between 0.5 and 2.0 through all layers. That band is not a law of nature — it is the range where most activations still behave usefully. One forward pass over a batch, recording the mean absolute activation per layer, tells you almost everything:

healthy mean |a| ≈ 0.5 … 2.0 in every layer loss falls from the first epochs identical every unit the same value, gradients identical (zero or constant init — symmetry never broke) vanishing mean |a| falls by a constant factor per layer, e.g. ×0.04: 10⁻¹⁴ by layer 10 (weights too small for fan_in, or Xavier on ReLU) exploding mean |a| grows by a constant factor per layer, e.g. ×4: 10¹⁵ by layer 25, then inf / NaN (weights too large for fan_in) saturated magnitudes look fine (σ output ≈ 1) but the derivative σ(1−σ) ≈ 0 — gradients die while the chart looks healthy

The last row is the one that fools people. A saturating sigmoid outputs values near 0 or 1: not tiny, not huge — and its derivative a(1−a) collapses toward zero anyway. If you only watch magnitudes you will see a healthy network; if you watch the gradient at layer one you will see 10⁻⁹. That difference — magnitude versus derivative — is the diagnostic skill this chapter is after. It is also why the same diagnostic run on a trained network can show a bimodal histogram (many units at 0, many at 1) — the failure mode is units parked on the flat tails.

The init clinic: read the symptoms

Six forwards of a 32-wide stack, six fingerprints. Pick an observed symptom and read the diagnosis — the same chart a practitioner squints at when a network refuses to learn.

OBSERVATION

A 512-wide sigmoid layer: every unit outputs exactly 0.5000 on the first batch, and every unit's gradient is identical.

0layer 1 → 24 · y = log₁₀ mean |a|

Diagnosis. Zero (or constant) weight initialization. The units are the same function, so backprop hands them the same gradient and the symmetry never breaks — the layer is effectively 1 neuron wide with 512 copies.

Prescription. Initialize weights randomly. Biases can stay at zero; it is the weight matrix that must differ across units. Confirm by checking that outputs differ across units on the first batch.

w ~ N(0, σ²), σ > 0 · b = 0
case identical units depth 24 · width 32 · 64 samples chart y = log10 mean |a| start -0.30 end -0.30 healthy target from the source: 0.5 ≤ mean |a| ≤ 2.0 at every layer

The zero-init, small-init, too-large-init and Xavier-plus-ReLU sparklines are computed live from the same simulation as the other labs. The dead-ReLU curve is illustrative: a permanently dark unit is a per-unit statistic, not a layer average.

Order the diagnoses by cost of being wrong: identical units waste the entire layer; vanishing units waste the depth; exploding units waste the run.

Biases deserve their own footnote, because they are initialized separately and follow different conventions. The default is zero everywhere; biases are not where the capacity lives, and a zero bias keeps a ReLU unit’s fate decided by its inputs rather than a constant offset. Two well-known exceptions: the classic LSTM forget-gate bias is often set to 1 so the memory starts wide open (a constant bias cannot break symmetry between units, but it can set a useful operating point), and transformer normalization layers initialize their scale at 1 and shift at 0. The general rule stands: randomness in the weights, constants in the biases.

Finally, note what initialization cannot fix. If a network is broken because of a bug, bad data, or a learning rate that is off by 100×, better starting weights will not rescue it. But if training diesimmediately — flat loss, NaNs in the first epochs, gradients that are zero or enormous before any learning happens — the starting scale is the first suspect, and it is the cheapest one to test.

Quick check

A 30-layer sigmoid network is stuck: mean |a| ≈ 1.0 at every layer — perfectly healthy-looking — but the first layer's gradients are ~10⁻⁹ and the loss will not move. What is the most likely cause?

INIT MEETS TRAINING

The decision shows up
in the first loss curve.

The source closes with a real training run: 200 points, a 2-8-1 network, six initialization/activation pairs, 300 epochs each. Small networks forgive more than deep ones — but the loss curves still tell you which starting points let the signal do its job.

The experiment is deliberately modest: a circle dataset (label 1 inside a radius, 0 outside), a network with 8 hidden units, one output sigmoid, squared error, per-sample gradient descent at learning rate 0.1, 300 epochs. Nothing about the data, the architecture or the optimizer changes between rows — only the initial weights and the activation. The port in this lesson follows the source’s training_comparison() line for line, with a deterministic JavaScript RNG, so the numbers in the lab are the numbers below.

config start loss end loss improvement N(0,0.01) + Sigmoid 0.223072 0.210843 5.5% N(0,1.0) + Sigmoid 0.222099 0.021914 90.1% Xavier + Sigmoid 0.222009 0.024337 89.0% N(0,0.01) + ReLU 0.229423 0.012299 94.6% N(0,1.0) + ReLU 0.180464 0.006868 96.2% Kaiming + ReLU 0.184231 0.006570 96.4% reference: always predicting the class prior (31.5% positives) costs 0.315 × 0.685 = 0.2158 — the plateau the first row sits on.

Read the first row carefully, because it is the chapter’s whole point in miniature: the tiny-init sigmoid run improves 5.5% and stops at 0.2108 — barely under the 0.2158 of always predicting the class prior. It is not fitting; it is outputting a constant. The same tiny weights paired with ReLU recover (94.6%), because ReLU’s derivative is 1 where it is active, so the signal can regrow — exactly the opposite of the sigmoid row, whose derivative is at most 0.25. And notice that the two large-init runs also learn here: this network is 2 layers deep and 8 units wide, where a bad scale bends the curve rather than breaking it. Fifty layers, the setting of chapter 02’s experiment, is where they break.

This gap between “shallow networks forgive” and “deep networks do not” is why initialization gets a reputation for being folklore. People try three layers, see everything work, and conclude the choice does not matter. Depth is the variable that turns a 5% annoyance into a dead run — and modern models are nothing but depth.

Six inits, one tiny training run

The source’s training comparison, computed live: a 2-8-1 network on 200 circle-shaped points, six initialization/activation pairs, 300 epochs each. Same data, same learning rate — only the starting weights change.

selected Xavier + Sigmoid start 0.222009 epoch 299 0.024337 end 0.024337 improvement 89.0% final losses, best first 0.0066 Kaiming + ReLU 0.0069 N(0,1.0) + ReLU 0.0123 N(0,0.01) + ReLU 0.0219 N(0,1.0) + Sigmoid 0.0243 Xavier + Sigmoid 0.2108 N(0,0.01) + Sigmoid → the matched pairing: the signal reaches every layer from epoch one.

Honest caveats: this network is tiny and the problem is easy, so several bad initializations still find their way — small-init ReLU recovers because ReLU’s derivative is 1, not because tiny weights are safe. The 50-layer experiment is where they stop recovering. Re-roll the seeds to see which conclusions are robust.

In production you rarely write these formulas by hand. Every framework ships them — and, importantly, already applies a fan-in-aware default when you use a standard layer. PyTorch’s nn.Linear(512, 256) initializes its weight uniformly on ±1/√fan_in, a Kaiming-uniform relative whose bound is tied to the layer’s shape. That default is why most simple networks “just work.” You override it when you build custom architectures, when you go deeper than roughly 20 layers, or when you use an activation the default was not designed around — and you always own the depth terms, like residual scaling, that the framework cannot know about.

The same strategies in PyTorch · main.py, 'Use It'python
import torch
import torch.nn as nn

layer = nn.Linear(512, 256)

nn.init.xavier_uniform_(layer.weight)          # sigmoid / tanh
nn.init.xavier_normal_(layer.weight)

nn.init.kaiming_uniform_(layer.weight, nonlinearity='relu')   # ReLU / GELU
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')

nn.init.zeros_(layer.bias)                     # biases stay at zero
One init call per layer. The framework computes fan_in and fan_out from the tensor's shape, which is why swapping schemes is a one-liner in practice and why forgetting to think about it at all is so common.

The practical recipe. For each layer: (1) draw weights randomly so no two units are copies — never zeros or constants; (2) choose the scale by activation — Xavier if the gate is roughly linear near zero (sigmoid, tanh), Kaiming if it zeroes half its range (ReLU, GELU, Swish), LeCun if the design assumes unit variance (SELU); (3) set biases to zero unless a specific component (an LSTM forget gate, a normalization scale) needs otherwise; (4) if the architecture has residual connections, add the depth term 1/√(2N); and (5) verify with one forward pass that the mean activation magnitude stays between 0.5 and 2.0 across all layers. Five rules, five minutes, and the next 50 layers have a chance.

CHECK YOURSELF

Five questions.
Then the terms worth keeping.

Answer before you look. The Kaiming formula and the Xavier-pairing questions are the two that separate memorized acronyms from a working understanding of why the factors are there.

0 / 5 answered · 0 correct

01What happens if you initialize every weight in a neural network to zero?

02Why does the scale of random weight initialization matter?

03What is the formula for Kaiming/He initialization variance?

04When should you use Xavier/Glorot initialization instead of Kaiming/He?

05Why does GPT-2 scale residual layer weights by 1/√(2N)?

Key terms, demystified

Click a card to swap the lazy description for what it actually means.

Exercises from the lesson

Four problems with exact numbers — add LeCun init and compare it to Xavier, implement GPT-2’s residual scaling, watch the safe window narrow as fan-in grows, and verify orthogonal initialization by hand. Try first; a worked answer is one click away.

  1. Add LeCun initialization (Var(w) = 1/fan_in, designed for SELU). Run the 50-layer experiment with LeCun + tanh and compare it to Xavier + tanh. When do the two schemes actually disagree, and by how much?
    Show one worked answer

    LeCun's standard deviation is 1/√fan_in. For the source's square 64→64 layers, Xavier gives √(2/(64+64)) = √(2/128) = 0.125 and LeCun gives 1/√64 = 0.125 — identical, because for fan_in = fan_out the harmonic mean 2/(n+n) equals 1/n. So the 50-layer run at width 64 shows no difference in the linear regime (both hold E[a²] near 1; measurement noise and tanh saturation account for the drift). The schemes separate only when the layer is rectangular: for 64→256, Xavier std = √(2/320) = 0.0791 while LeCun std = 0.125. The forward factors diverge accordingly: LeCun keeps 64 × 0.015625 = 1, Xavier gives 64 × 0.00625 = 0.4 per layer — 0.4¹⁰ ≈ 1.0×10⁻⁴ after ten layers. LeCun keeps the forward signal exactly; Xavier deliberately gives some of that up to protect the backward pass, which is the right trade when the gradient is the scarce resource.

  2. Implement GPT-2's residual scaling: multiply each residual branch's output weights by 1/√(2N). Run N = 12 and N = 126 blocks with and without the scale, and report how fast the residual-stream variance grows.
    Show one worked answer

    Model each transformer block as two residual additions (attention and MLP), each adding one unit of branch variance to a stream that starts at 1. Unscaled, 2N additions give Var = 1 + 2N: ×25 for N = 12, ×253 for N = 126 (Llama 3's depth). Scaling the branch weights by 1/√(2N) multiplies each branch's variance by 1/(2N), so the stream grows to 1 + 2N/(2N) = 2 — one extra unit, no matter how deep. Concretely: scale = 1/√24 = 0.2041 for GPT-2-small and 1/√252 = 0.0630 for the 126-layer version. The check is that the scaled final value is independent of N while the unscaled one grows linearly with N.

  3. Run the random-init experiment with fan_in = 16 and fan_in = 1024. Xavier and Kaiming adapt their scale to fan-in; a fixed-scale random init does not. Show how the gap between 'works' and 'breaks' widens with larger layers.
    Show one worked answer

    Take a fixed weight standard deviation of 0.1, so Var(w) = 0.01. The per-layer factor is fan_in × Var(w): 16 × 0.01 = 0.16 for the small layer (0.16¹⁰ ≈ 1.1×10⁻⁸ — vanished by layer ten) and 1024 × 0.01 = 10.24 for the large one (10.24¹⁰ ≈ 1.3×10¹⁰ — exploded). Same weights, opposite failures, purely because fan-in changed by 64×. The scheme that adapts, std ≈ 1/√fan_in, gives 0.25 for fan_in = 16 and 0.03125 for fan_in = 1024. In practice you do not need the exact point: a factor between about 0.63 and 1.58 keeps a 10-layer stack within a 100× band, which is a std window of [0.198, 0.315] at fan_in 16 but only [0.025, 0.039] at fan_in 1024 — an 8× narrower target. That is why large layers need the formula, not a guess.

  4. Implement orthogonal initialization: draw a random matrix, compute its SVD, and use U as the weight matrix. Compare it to Kaiming for a ReLU network at 50 layers, and verify the norm-preservation property by hand on a 3×3 example.
    Show one worked answer

    An orthogonal matrix has orthonormal columns, so QᵀQ = I and every singular value is 1 — the matrix rotates and reflects but never stretches. Hand check with θ = 45°: Q = [[cos θ, −sin θ, 0], [sin θ, cos θ, 0], [0, 0, 1]]. Columns have norm 1 (cos²+sin² = 1) and are mutually perpendicular. Applying Q to x = [1, 1, 0] gives z = [cos θ − sin θ, sin θ + cos θ, 0] = [0, 1.4142, 0], and |z|² = 2 = |x|². For variance: Var(z) = Var(x) exactly, with no 1/fan_in factor — the iid Gaussian matrix needed Var(w) = 1/fan_in only to make its row norm √(fan_in × Var(w)) = 1. For ReLU, divide the orthogonal weights by √2 to pay for the halving, just as Kaiming does. At 50 layers orthogonal init keeps the signal at unit norm by construction; the catch is cost (an SVD or QR decomposition per layer) and that it constrains all singular values equally rather than tuning the spectrum, which is why practice usually reaches for Kaiming.

Terms this lesson borrows from later lessons (or outside)

You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.

  • varianceVar(aX) = a²·Var(X), and variances of independent variables add. Those two rules give the whole chapter its one-line engine: Var(z) = fan_in · Var(w) · Var(x). (Phase 1, Lesson 15)
  • normal distributionEvery scheme here draws weights from a Gaussian — N(0, 0.01), N(0, √(2/fan_in)) — and only the standard deviation changes. The bell curve is the starting point of every training run. (Phase 1, Lesson 06)
  • weighted sumz = w₁x₁ + … + w_n x_n. The more terms you add, the more variance accumulates — which is exactly why fan-in appears in every formula. (Phase 1, Lesson 02)
  • vanishing and exploding gradientsThe backward-pass failure mode from Lesson 03: a product of per-layer factors greater or smaller than 1 compounds with depth. Initialization picks the starting factors. (Phase 3, Lesson 03)
  • activation functionsSigmoid saturates and ReLU zeroes half its inputs. The size of those effects — 1× vs ½× — is the only difference between the Xavier and Kaiming formulas. (Phase 3, Lesson 04)
KEEP GOING

A picture is a start.
Practice is the rest.

This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.

Lesson text adapted from AI Engineering from Scratch (Phase 03, Lesson 08) and the Math Foundations Notebook reference build. The seven labs (four-neuron symmetry stepper, 50-layer variance experiment, fan-in/fan-out calculator with a Monte-Carlo variance check, activation-magnitude heat map, GPT-2 residual-scaling curve, init clinic, and the six-configuration training sweep) are original to this page, as are the worked one-step symmetry ledger, the corrected 0.512¹⁰ arithmetic, the two Xavier worked examples, the uniform-variance check L²/3, the single-layer Kaiming measurements, the 12/126-block residual table, the LSTM forget-gate note, and the orthogonal-init hand check. Every number shown is computed live by the labs or verified by hand in the prose.