Problem
A deep network multiplies its input by one weight matrix after another, with an activation function in between. If each layer makes its output a little larger than its input, say by a factor of 1.5, twenty layers make it 1.5²⁰ ≈ 3300 times larger. If each makes it a little smaller, the signal fades to nothing. Backpropagation multiplies by the same matrices on the way back, so the gradients suffer the same fate: the early layers get gradients that are vanishingly small or enormous, and training stalls or diverges before it starts.
We cannot set the weights to zero either. If all weights in a layer start equal, every unit computes the same function and gets the same gradient, so they stay equal forever. The weights must be random to break the symmetry, and their scale must keep the signal's size roughly constant from layer to layer.
Intuition
A unit adds up n inputs, each multiplied by a random weight. Sums of n independent random terms have variance proportional to n, so a layer with 1000 inputs produces outputs about √1000 ≈ 32 times larger than a layer with one input, for the same weight scale. To cancel that, scale the weights by 1/√n. Then correct for what the activation does: ReLU zeroes half its inputs, which halves the average squared value, so multiply the variance back up by 2. That is Kaiming initialization: weight variance 2/n.
The whole page in one line: keep the per-layer factor at 1.
Mechanics
Forward pass
Take one pre-activation z = Σi=1..n wi hi, with the weights independent of the inputs, zero-mean and of variance Var(w). Each term has mean zero and, using the rules from expectation and variance, variance Var(w) · E[h²]. The terms are uncorrelated, so
Var(z) = n · Var(w) · E[h²]
It is E[h²], not Var(h), because ReLU outputs are not zero-mean. Now z is symmetric around zero, and ReLU keeps the positive half, so E[relu(z)²] = Var(z)/2. Chaining the two,
E[h′²] = (n · Var(w) / 2) · E[h²] ⇒ after L layers: E[hL²] = Πl (nl Var(wl) / 2) · E[h0²]
A product of L factors is stable only if each factor is 1, which gives He et al.'s condition Var(w) = 2/nin. For an activation that is close to the identity near zero, like tanh, the ReLU factor ½ is replaced by about 1, which gives Var(w) = 1/nin.
Backward pass
The gradient with respect to a layer's input is W⊤ times the gradient at its output, times the activation's slope. The same argument now sums over the nout units the input feeds, and ReLU's slope is 1 for half of them and 0 for the rest:
Var(∂L/∂h) = (nout · Var(w) / 2) · Var(∂L/∂h′)
So the backward pass wants Var(w) = 2/nout. When a layer is square the two agree; when it is not, you pick one (mode="fan_in" or "fan_out" in PyTorch).
Glorot and Bengio, deriving for activations in their linear regime, split the difference by using the average of the two fans, Var(w) = 2/(nin + nout).
The named schemes
A uniform distribution on [−a, a] has variance a²/3, so each scheme comes in a normal and a uniform flavour with bound a = √3 · std. PyTorch writes every scheme as a gain divided by the square root of a fan:
- Xavier / Glorot: std = gain · √(2 / (fan_in + fan_out)); uniform bound gain · √(6 / (fan_in + fan_out)).
- Kaiming / He: std = gain / √fan, with fan = fan_in by default; uniform bound gain · √(3 / fan).
- Gains from
calculate_gain: 1 for linear, convolution and sigmoid; 5/3 for tanh; √2 for ReLU; √(2 / (1 + slope²)) for leaky ReLU (slope 0.01 by default); 3/4 for SELU, a value the source says was found empirically. - Fans: for a weight of shape (out, in, k₁, k₂, …), fan_in = in · k₁k₂… and fan_out = out · k₁k₂…, so a 3×3 convolution with 64 input channels has fan_in 576. Note that a
Linearweight is stored as (out_features, in_features).
What PyTorch does by default
nn.Linear.reset_parameters calls kaiming_uniform_(weight, a=math.sqrt(5)). With the default nonlinearity="leaky_relu" and slope √5, the gain is √(2 / (1 + 5)) = √(1/3), so
bound = √3 · gain / √fan_in = √3 · √(1/3) / √fan_in = 1 / √fan_in (fan_in = 64: 0.125)
The source comment says exactly this, and the class docstring documents the weights as U(−√k, √k) with k = 1/in_features. The bias uses the same bound. The convolution layers (_ConvNd) do the same.
The weight variance is 1/(3 fan_in), so in a plain ReLU stack the per-layer factor is fan_in · 1/(3 fan_in) / 2 = 1/6: the default is not Kaiming for ReLU, and the signal shrinks layer by layer.
Modern networks mostly get away with it because normalization layers and residual connections re-establish the scale at every block. nn.Embedding uses normal_, N(0, 1).
Orthogonal, truncated and residual scaling
- Orthogonal (
orthogonal_, Saxe et al.): draw a Gaussian matrix, take its QR decomposition, fix the signs of Q's columns by the signs of R's diagonal (so Q is uniformly distributed), and multiply bygain. Every singular value is exactly 1, so a deep linear network preserves the norm of every input exactly, not just on average. It is common for recurrent weight matrices, which are applied many times. - Truncated normal (
trunc_normal_(mean=0, std=1, a=−2, b=2)): redraws values outside [a, b], to avoid rare huge weights. The cutoffs are absolute values, not multiples ofstd; withstd=0.02the defaults truncate nothing in practice. - Residual scaling: in a residual network x + f(x), each block adds variance, so the stream grows with depth. GPT-2 scaled the weights of the layers feeding the residual stream by 1/√N, with N the number of residual layers, so their sum stays about the same size.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
| Gain | calculate_gain(nonlinearity, param=None) → float |
1, 5/3, √2, √(2/(1+slope²)) or 3/4 by activation. | init.py 173-244 |
| Fans | _calculate_fan_in_and_fan_out(tensor), tensor (out, in, *k) |
fan_in = in · Πk, fan_out = out · Πk; needs ≥ 2 dimensions. | init.py 458-476 |
| Xavier | xavier_uniform_(tensor, gain=1.0), xavier_normal_ |
std = gain · √(2/(fan_in + fan_out)); uniform bound √3 · std. | init.py 479-509, 512-540 |
| Kaiming | kaiming_uniform_(tensor, a=0, mode="fan_in", nonlinearity="leaky_relu"), kaiming_normal_ |
std = gain / √fan; a is the leaky-ReLU slope passed to the gain. |
init.py 554-616, 619-669, 543-551 |
| Orthogonal | orthogonal_(tensor, gain=1), trailing dims flattened |
QR of a Gaussian matrix, sign-corrected by diag(R), times gain. | init.py 672-720 |
| Truncated normal | trunc_normal_(tensor, mean=0.0, std=1.0, a=−2.0, b=2.0) |
N(mean, std²) with values outside [a, b] redrawn. | init.py 301-334 |
| Linear default | nn.Linear(in, out): weight (out, in), bias (out,) |
kaiming_uniform_(a=√5) = U(±1/√in); bias U(±1/√in). |
linear.py 117-128, 53-80 |
| Convolution default | _ConvNd.reset_parameters |
Same kaiming_uniform_(a=√5) and bias bound as Linear. |
conv.py 182-201 |
| Embedding default | nn.Embedding(num, dim): weight (num, dim) |
normal_, N(0, 1); the padding row is zeroed. |
sparse.py 179-181 |
A 3×3 convolution maps 128 channels to 256. What is the Kaiming-normal std for ReLU with the default mode, and with mode="fan_out"?
fan_in = 128 · 9 = 1152, so std = √2 / √1152 = 1/24 ≈ 0.0417. fan_out = 256 · 9 = 2304, so std = √2 / √2304 ≈ 0.0295.
Worked example
One layer by hand. A ReLU layer with fan_in 256 and inputs with E[h²] = 1. Kaiming gives Var(w) = 2/256, std 0.0884. Then Var(z) = 256 · (2/256) · 1 = 2, and ReLU keeps half: E[h′²] = 1. With PyTorch's default, Var(w) = 1/(3 · 256), so Var(z) = 1/3 and E[h′²] = 1/6 ≈ 0.167. With Xavier, Var(w) = 2/(256 + 256) = 1/256, and the factor is 0.500.
A deep network. A batch of 16 Gaussian inputs goes through 20 layers, each 32 units wide with no bias. At the top, a random Gaussian gradient is fed back through the same layers. The table shows the root mean square (RMS) of the activations and of the gradients, and the per-layer factor the derivation predicts for E[h²].
| Scheme | Weight std | Predicted factor per layer | Activation RMS, layer 0 → 20 | Gradient RMS, layer 20 → 0 |
|---|---|---|---|---|
| N(0, 0.01²), ReLU | 0.0100 | 0.0016 | 1 → 4.7e−29 | 0.91 → 4.4e−29 |
| nn.Linear default, U(±1/√n), ReLU | 0.1021 | 0.17 | 1 → 5e−9 | 0.91 → 8.6e−9 |
| Xavier, gain 1, ReLU | 0.1768 | 0.5 | 1 → 0.00042 | 0.91 → 0.00039 |
| Kaiming, gain √2, ReLU | 0.2500 | 1 | 1 → 0.43 | 0.91 → 0.4 |
| N(0, 1), ReLU | 1.0000 | 16 | 1 → 4.7e11 | 0.91 → 4.4e11 |
| tanh, Xavier gain 1 | 0.1768 | ≈ 1 near 0 | 1 → 0.12 | 0.91 → 0.15 |
| tanh, gain 5/3 | 0.2946 | ≈ 2.8 near 0 | 1 → 0.64 | 0.91 → 4.3 |
- With ReLU, every scheme but Kaiming moves the signal geometrically, by about the predicted factor per layer in E[h²] (the square root of it in RMS): N(0, 0.01²) kills it, N(0, 1) blows it up by more than eleven orders of magnitude, and even PyTorch's default and Xavier lose a factor of a thousand or more in twenty layers.
- The gradients mirror the activations because the layers are square: the backward factor nout · Var(w) / 2 equals the forward one, so whatever shrinks the forward signal shrinks the backward one too.
- Kaiming stays in range, but it still drifts to 0.43. The predicted factor is an average; at width 32, each layer's actual factor is random, and the log of the RMS does a random walk whose spread grows with depth/width. Wider layers drift less.
- tanh is gentler: it is linear near zero and saturates for large inputs, so Xavier slowly fades (to 0.12) while gain 5/3 holds the activations near 0.64. The same gain makes the gradient grow toward the input, to 4.3, because on the way back the 5/3 gain more than makes up for tanh's slopes below 1.
Each scheme is a straight line on a log scale with slope log(factor)/2; only a factor of 1 is flat.
Implementation
import math
import torch.nn as nn
def init_weights(module):
if isinstance(module, (nn.Linear, nn.Conv2d)):
nn.init.kaiming_normal_(module.weight, mode="fan_in", nonlinearity="relu")
if module.bias is not None:
nn.init.zeros_(module.bias)
model = nn.Sequential(*[m for _ in range(20) for m in (nn.Linear(512, 512), nn.ReLU())])
model.apply(init_weights) # visits every submodule
# tanh layers: Xavier with the tanh gain
nn.init.xavier_uniform_(tanh_layer.weight, gain=nn.init.calculate_gain("tanh"))
# recurrent weights: orthogonal
nn.init.orthogonal_(rnn.weight_hh_l0)
# GPT-2-style: scale the projections that write into the residual stream
n_residual_layers = 2 * len(blocks) # attention and MLP each add to the stream
for block in blocks:
for proj in (block.attn_out, block.mlp_out):
proj.weight.data /= math.sqrt(n_residual_layers)
The functions end in _ because they fill the tensor in place, under torch.no_grad(), and return it. They take an optional generator for reproducible draws.
When you load pretrained weights, initialization only matters for the layers you add.
Tradeoffs
- Match the activation. Kaiming for ReLU-family, Xavier (with the right gain) for tanh and linear layers. The derivations assume zero-mean, independent weights and inputs; they are a starting point, not a guarantee.
- fan_in or fan_out. fan_in keeps activations stable, fan_out keeps gradients stable. For square layers they agree. Otherwise, choosing one leaves the other pass off by Π nl+1/nl, which telescopes to the ratio of the last width to the first, so neither choice compounds with depth.
- Normalization makes it less critical. With LayerNorm or BatchNorm after each layer the activation scale is reset every block, which is why the PyTorch default rarely causes trouble in modern models. Plain deep stacks without normalization are where initialization decides whether training works at all.
- Average, not exact. The variance rule holds on average over random weights. Narrow layers fluctuate, as the worked example shows; orthogonal initialization removes that fluctuation for linear maps.
- Scale still interacts with the optimizer. The optimizer and learning rate decide how far weights move from the starting point; a good initialization only gives them a well-scaled place to start.
Why can't all weights start at the same nonzero value, say 0.01?
Every unit in a layer would compute the same output and receive the same gradient, so they would stay identical through training: the layer would behave like a single unit. Random initialization breaks this symmetry.
Connections
Read first
Read next
No page builds on this one yet.
References
- Glorot and Bengio. Understanding the difficulty of training deep feedforward neural networks. AISTATS 2010.
- He, Zhang, Ren and Sun. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. ICCV 2015. arXiv:1502.01852
- Saxe, McClelland and Ganguli. Exact solutions to the nonlinear dynamics of learning in deep linear neural networks. 2013. arXiv:1312.6120
- Radford et al. Language Models are Unsupervised Multitask Learners (GPT-2), 2019, section 2.3 (residual scaling).
- PyTorch v2.15.0:
torch/nn/init.py,torch/nn/modules/linear.py.