Weight Initialization¶
Overview¶
Modern networks self-correct during training β but a bad initialization can stall, explode, or permanently poison a network. Initialization is about keeping variance stable as signals flow forward (activations) and backward (gradients).
- Zeroing everything β all neurons compute the same gradient (symmetry breaking never happens).
- Xavier/Glorot β var(w) = 2/(fan_in+fan_out): good for tanh/sigmoid symmetric activations.
- He/Kaiming β var(w) = 2/fan_in (or 2/fan_out): tuned for ReLU family.
- LeCun β var = 1/fan_in (good with LN/self-normalizing nets).
π‘ The single most common bug: initializing with
torch.randnraw (std 1.0) β activations explode; or withstd=1e-4β gradients vanish. Both are silent and GPU-expensive to find.
Why Variance Matters (the math)¶
For a linear layer y = Wx with bias zeroed:
Var(y) = Var(W) * fan_in * Var(x)
To keep Var(y) = Var(x) through the stack you need Var(W) = 1 / fan_in.
Xavier generalizes: Var(W) = 2 / (fan_in + fan_out).
import torch, torch.nn as nn
def check_propagation(n_layers=50, init_fn=None):
torch.manual_seed(0)
x = torch.randn(1000)
for _ in range(n_layers):
w = torch.empty(1000, 1000)
if init_fn: init_fn(w)
else: nn.init.normal_(w, std=0.01) # "small" but wrong
x = w @ x
print(f"std after {n_layers} layers: {x.std().item():.3e}")
check_propagation(init_fn=nn.init.normal_) # explosion with std=1
# xavier keeps std ~1 across 50 layers:
def xav(w): nn.init.xavier_normal_(w)
check_propagation(init_fn=xav)
The Three Main Recipes¶
1. Xavier (Glorot) β sigmoid/tanh¶
nn.init.xavier_uniform_(layer.weight)
nn.init.xavier_normal_(layer.weight) # normal vs uniform: picks tails
2. He (Kaiming) β ReLU family¶
nn.init.kaiming_uniform_(layer.weight, mode='fan_in', nonlinearity='relu')
nn.init.kaiming_normal_(layer.weight, a=0.01, nonlinearity='leaky_relu')
ais the negative slope of LeakyReLU β it changes the optimal variance.
3. Orthogonal β RNNs¶
nn.init.orthogonal_(rnn.weight_hh_l0) # keeps eigenvalues near unit circle
Bias & Residual-Friendly Init¶
def init_linear(m):
if isinstance(m, nn.Linear):
nn.init.kaiming_uniform_(m.weight, a=math.sqrt(5))
if m.bias is not None:
fan_in, _ = nn.init._calculate_fan_in_and_fan_out(m.weight)
bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0
nn.init.uniform_(m.bias, -bound, bound)
- Zero the last-layer bias β if it predicts a distribution, gives a clean "untrained" baseline.
- Small std init for output heads (e.g.,
std=0.02) keeps losses from exploding early.
Common Failure Modes¶
| Symptom | Cause | Fix |
|---|---|---|
| Loss stuck at log(classes) | zero init / symmetric init | small asymmetric init |
| Loss = NaN immediately | weights too large | He/Xavier, reduce lr |
| Hours of flat loss, then works | gradient vanishing | correct var, layer norm |
| Dead ReLU columns forever | big negative bias init | zero/uniform-small bias |
Detecting dead units early¶
def dead_unit_ratio(model, x):
acts = []
def hook(m, i, o): acts.append(o)
h = model[0].register_forward_hook(hook)
model(x)
h.remove()
a = acts[0]
return (a == 0).float().mean().item()
model = nn.Sequential(nn.Linear(32, 64), nn.ReLU())
print("dead ratio:", dead_unit_ratio(model, torch.randn(8, 32)))
Summary Rules¶
- Default init in PyTorch is Kaiming-uniform for most conv/linear β fine as-is for ReLU nets.
- ReLU/LReLU β He (fan_in) Β· tanh/sigmoid β Xavier Β· RNN β Orthogonal.
- Zero-init ONLY for specific last layers (e.g., residual branches:
zero_init_gamma). - Always
applya consistent init function:
model.apply(lambda m: (
nn.init.kaiming_uniform_(m.weight) if isinstance(m, nn.Linear) else None
))
Key Takeaways¶
- Init controls variance flow:
Var(W)Β·fanmust be ~1 to survive 50+ layers. - Match the init to the activation's nonlinearity, not to taste.
- NaN immediately β too large; stuck losses β too small/asymmetric.
- Residual branches can start at zero and learn identity first.
- PyTorch's defaults are well-tuned β don't "improve" them without measurement.