Weight Initialization: Xavier, He & Variance Preservation

Michael BrenndoerferApril 25, 202553 min read

Part of Language AI Handbook

Explains why weight initialization matters for training neural networks. Topics include Xavier and He initialization, orthogonal init, BERT and GPT schemes.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Weight Initialization

Training a neural network means finding good values for its weights, but where do you start? The answer matters more than you might expect. If you initialize weights poorly, activations can explode to infinity or vanish to zero before a single gradient update is applied, making learning impossible before it even begins. The right initialization sets the stage for stable, efficient training.

This chapter explores why weight initialization matters and how techniques like Xavier and He initialization solve the problems that plagued early deep networks. We'll derive these methods from first principles, implement them from scratch, see how modern architectures like BERT and GPT approach initialization, and compare empirically how each method performs in practice. By the end, you'll have the conceptual foundations to reason about initialization decisions for any architecture you encounter, and you'll understand precisely when the choice of initialization scheme makes the difference between a network that trains and one that doesn't.

Historical Context: When Initialization Was an Art

Before principled initialization schemes existed, training deep neural networks was largely a matter of craft and intuition. Practitioners in the early 2000s knew that random initialization was necessary for symmetry breaking, but the question of how much randomness and what scale remained largely unanswered by theory. Researchers would manually tune initialization hyperparameters, sometimes spending significant compute searching for settings that produced reasonable gradient magnitudes in the first few iterations.

This difficulty was one of the primary reasons deep networks were considered impractical for much of the 1990s and early 2000s. Shallow networks with one or two hidden layers could be trained reliably, but adding more layers introduced instabilities that were difficult to diagnose. When a 10-layer network failed to learn, the culprit could be any combination of bad initialization, inappropriate learning rates, the wrong activation function, or all three simultaneously.

The field began to change with a pair of foundational papers. In 2010, Xavier Glorot and Yoshua Bengio published "Understanding the Difficulty of Training Deep Feedforward Neural Networks," which provided the first rigorous analysis of how activation variance propagates through layers and derived the initialization scheme now known as Xavier or Glorot initialization. Five years later, Kaiming He and colleagues extended this analysis to ReLU networks in "Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification," introducing He initialization and demonstrating that it enabled training of networks much deeper than had previously been practical. Together, these papers transformed initialization from an empirical art into a theoretically grounded engineering discipline.

Understanding what these authors figured out, and why it took the field so long to get there, gives the derivations that follow their proper context. The math is not difficult, but the framing is everything.

The Symmetry Problem: Why Zero Initialization Fails

The most intuitive initialization strategy is to set all weights to the same constant value. Zero seems like a natural, neutral starting point. But this approach is fundamentally broken.

Consider what happens when all weights in a layer are zero. Every neuron receives the same input (zero dot product) and produces the same output. During backpropagation, every neuron also receives identical gradients and applies identical updates. No matter how long you train, every neuron in the layer remains identical to every other. A 512-neuron layer with zero initialization behaves exactly like a 1-neuron layer.

Symmetry Breaking

The requirement that neurons in the same layer be initialized differently so they can learn distinct features. Without symmetry breaking, all neurons compute the same function regardless of how wide the network is.

This is the symmetry problem: neurons that start identical stay identical. The network's representational capacity collapses. Any initialization scheme that makes all weights in a layer the same value, whether zero, one, or any constant, triggers this failure mode.

The intuition here is worth dwelling on. Imagine a network designed to recognize different types of objects in images. You want early neurons to detect different low-level features: edges at different orientations, color contrasts, texture patterns. If all neurons start the same and receive the same gradient, they can only ever develop the same feature detector, no matter how many neurons you use or how long you train. The entire point of a wide network is to represent many features in parallel, and this capacity is completely destroyed by symmetric initialization.

The fix is randomness. If we initialize each weight independently from a random distribution, neurons start with different values, receive different gradient signals, and diverge to learn different features. But random initialization introduces a new challenge: what scale should those random values be?

Vanishing and Exploding Activations

Suppose we fix the symmetry problem by using random initialization. We draw weights from a standard normal distribution N(0,1)\mathcal{N}(0, 1). At first this seems reasonable, but a deep network reveals a severe problem.

In a layer with ninn_{\text{in}} inputs, each pre-activation sums ninn_{\text{in}} weighted inputs. If each weight has variance 1 and each input has variance 1, then the pre-activation has variance ninn_{\text{in}}. Its standard deviation is nin\sqrt{n_{\text{in}}}. For a layer with 256 inputs, every layer amplifies the signal by a factor of 256=16\sqrt{256} = 16.

After 10 layers, the signal has grown by a factor of 1610101216^{10} \approx 10^{12}. This is exploding activations: values become astronomically large, causing numerical overflow or saturating activation functions.

The opposite problem occurs with too-small initialization. If weights are drawn from N(0,0.012)\mathcal{N}(0, 0.01^2), each layer reduces the signal by a factor of roughly 0.01nin0.01 \cdot \sqrt{n_{\text{in}}}. For 256 inputs this is 0.01×16=0.160.01 \times 16 = 0.16. After 10 layers, the signal shrinks to 0.16101080.16^{10} \approx 10^{-8}. This is vanishing activations: the network loses the ability to distinguish inputs, and gradients vanish with the activations.

Both failure modes prevent learning. The goal is to find an initialization scale that keeps activations in a reasonable range throughout the network, regardless of depth.

The gradient side of the picture is equally important and often more immediately damaging to training. When activations vanish, the gradients that flow backward during backpropagation vanish with them. This happens because the gradient of the loss with respect to a weight in an early layer requires multiplying many terms together through the chain rule, and if any activation is near zero, its contribution to the gradient chain is near zero. This is the vanishing gradient problem, and it is what made networks with more than a few layers so notoriously difficult to train before principled initialization and techniques like batch normalization were introduced.

When activations explode, the gradients can explode too, driving weight updates to extreme values and destabilizing the optimization. In practice, activation saturation with sigmoid or tanh is another failure mode: once activations reach the flat regions of these functions, their derivatives are essentially zero, and gradients stop flowing regardless of whether the pre-activations have exploded or not.

In[5]:
Code
import numpy as np

np.random.seed(42)


def forward_pass(x, weights, activation="tanh"):
    """Propagate input through multiple layers and return all activations."""
    activations = [x]
    for W in weights:
        x = x @ W
        if activation == "tanh":
            x = np.tanh(x)
        elif activation == "relu":
            x = np.maximum(0, x)
    activations.append(x)
    return activations


# Simulate a 10-layer network with different initialization scales
n_layers = 10
layer_size = 256
batch_size = 100
scales = [0.01, 0.1, 1.0, 2.0]
Out[6]:
Console
Effect of initialization scale on activations (tanh network):
------------------------------------------------------------
Scale 0.01: Final layer std = 0.000000
Scale 0.10: Final layer std = 0.633119
Scale 1.00: Final layer std = 0.973512
Scale 2.00: Final layer std = 0.988212

The output reveals the two failure modes. Scale 0.01 produces nearly zero activations after just 10 layers. Scale 2.0 saturates tanh immediately, because activations hit the ±1\pm 1 saturation zone where gradients are nearly zero. Scales 0.1 and 1.0 are better, but neither is precisely calibrated.

Out[7]:
Visualization
Violin plot showing activation distributions narrowing toward zero across 10 layers.
Activation distributions across 10 layers with small initialization (scale=0.01). Each violin shows the distribution of activations in that layer. Activations progressively collapse toward zero, making gradients vanish and preventing learning in deeper layers.
Violin plot showing bimodal activation distributions concentrated at plus and minus one.
Activation distributions with large initialization (scale=2.0). Activations saturate at the extremes of tanh (near plus or minus 1), where the gradient is nearly zero. This also halts learning, just for a different reason.

The violin plots make the failure modes visible. Small initialization compresses all activations toward zero layer by layer. Large initialization immediately saturates them into two spikes near ±1\pm 1. We need a principled way to find the scale that keeps activations in the useful middle range.

Variance Analysis of Forward Propagation

The experiments reveal the symptoms, but to find the cure we need to understand the cause mathematically. The key question is: how does the variance of activations change as they pass through each layer?

If we can answer this question, we can work backward to determine what weight variance preserves activation variance across layers. This is the central insight behind principled initialization: treat variance preservation as a design constraint, then solve for the weight scale that satisfies it. The beauty of this approach is that it converts a vague intuition ("weights shouldn't be too big or too small") into a concrete formula with a clear derivation.

The Pre-Activation Equation

Consider a single layer with ninn_{\text{in}} input neurons. Each output neuron computes a weighted sum of its inputs before applying the activation function. For a single output neuron, this pre-activation value is:

z=i=1ninwixiz = \sum_{i=1}^{n_{\text{in}}} w_i x_i

where:

  • zz: the pre-activation value (the raw weighted sum before any activation function is applied)
  • wiw_i: the weight connecting input neuron ii to this output neuron
  • xix_i: the activation value from input neuron ii
  • ninn_{\text{in}}: the number of input neurons (the fan-in)

The pre-activation zz is a sum of ninn_{\text{in}} random terms. We want to understand how its variance relates to the variances of the weights and inputs.

Deriving the Variance Relationship

To derive how variance propagates, we make two assumptions that hold approximately during the early stages of training: weights wiw_i and inputs xix_i are independent random variables with zero mean.

Step 1: Variance is additive for independent random variables. Because the wixiw_i x_i terms are independent of each other (each uses a different weight-input pair):

Var(z)=Var ⁣(i=1ninwixi)=i=1ninVar(wixi)\text{Var}(z) = \text{Var}\!\left(\sum_{i=1}^{n_{\text{in}}} w_i x_i\right) = \sum_{i=1}^{n_{\text{in}}} \text{Var}(w_i x_i)

Step 2: Variance of a product of two independent zero-mean variables. For any two independent, zero-mean random variables AA and BB:

Var(AB)=E[(AB)2](E[AB])2=E[A2]E[B2]0=Var(A)Var(B)\text{Var}(AB) = \mathbb{E}[(AB)^2] - (\mathbb{E}[AB])^2 = \mathbb{E}[A^2]\mathbb{E}[B^2] - 0 = \text{Var}(A) \cdot \text{Var}(B)

Step 3: Combine. Assuming all weights share variance Var(w)\text{Var}(w) and all inputs share variance Var(x)\text{Var}(x):

Var(z)=i=1ninVar(wi)Var(xi)=ninVar(w)Var(x)\text{Var}(z) = \sum_{i=1}^{n_{\text{in}}} \text{Var}(w_i) \cdot \text{Var}(x_i) = n_{\text{in}} \cdot \text{Var}(w) \cdot \text{Var}(x)

where:

  • ninn_{\text{in}}: the number of input neurons (fan-in)
  • Var(w)\text{Var}(w): the variance of each weight, assumed identical across all weights in the layer
  • Var(x)\text{Var}(x): the variance of each input activation, assumed identical across all inputs

This formula immediately explains the explosion problem. With nin=256n_{\text{in}} = 256 and unit-variance weights (Var(w)=1\text{Var}(w) = 1), the output variance is 256×Var(x)256 \times \text{Var}(x). Each layer multiplies variance by ninn_{\text{in}}, producing exponential growth.

Fan-in and Fan-out

For a weight matrix connecting two layers, fan-in (ninn_{\text{in}}) is the number of input connections per output neuron, and fan-out (noutn_{\text{out}}) is the number of output connections per input neuron. For a fully connected layer with an nin×noutn_{\text{in}} \times n_{\text{out}} weight matrix, fan-in equals the input dimension and fan-out equals the output dimension.

The assumptions we made (independence, zero mean) are strictly true only at initialization, before any training has occurred. As training proceeds, weights and activations become correlated. However, the variance relationships remain approximately valid throughout early training, which is precisely the period when initialization matters most. By the time the network has trained for many epochs, the optimization landscape has been significantly reshaped and initialization conditions matter much less.

Solving for the Optimal Weight Variance

To maintain stable variance across layers, we want Var(z)=Var(x)\text{Var}(z) = \text{Var}(x). Starting from:

Var(z)=ninVar(w)Var(x)\text{Var}(z) = n_{\text{in}} \cdot \text{Var}(w) \cdot \text{Var}(x)

Setting Var(z)=Var(x)\text{Var}(z) = \text{Var}(x) and dividing both sides by Var(x)\text{Var}(x):

1=ninVar(w)1 = n_{\text{in}} \cdot \text{Var}(w)

Solving for the weight variance:

Var(w)=1nin\text{Var}(w) = \frac{1}{n_{\text{in}}}

The weight variance should scale inversely with the number of input connections. Summing ninn_{\text{in}} independent random terms amplifies variance by ninn_{\text{in}}, so each weight's variance must be 1/nin1/n_{\text{in}} to counteract this amplification.

This result is sometimes called LeCun initialization, after Yann LeCun who described it in his 1998 work on convolutional networks. It is the optimal forward-pass initialization for networks with linear or nearly-linear activation functions, and it forms the theoretical foundation for both Xavier and He initialization.

In[8]:
Code
def analyze_variance_propagation(n_layers, layer_size, weight_variance):
    """Track variance across layers with specified weight variance (no activation)."""
    weights = [
        np.random.randn(layer_size, layer_size) * np.sqrt(weight_variance)
        for _ in range(n_layers)
    ]
    x = np.random.randn(1000, layer_size)
    x = x / np.std(x)  # Normalize to unit variance
    variances = [np.var(x)]
    for W in weights:
        x = x @ W
        variances.append(np.var(x))
    return variances


variance_choices = {
    "Too small (1/n^2)": 1 / (layer_size**2),
    "Just right (1/n)": 1 / layer_size,
    "Too large (1)": 1.0,
}
Out[9]:
Console
Variance propagation through 10 layers (no activation):
Layer size: 256
------------------------------------------------------------

Too small (1/n^2):
  Initial variance: 1.0000
  Final variance:   8.2928e-25
  Ratio (final/initial): 8.2928e-25

Just right (1/n):
  Initial variance: 1.0000
  Final variance:   1.0596e+00
  Ratio (final/initial): 1.0596e+00

Too large (1):
  Initial variance: 1.0000
  Final variance:   1.1612e+24
  Ratio (final/initial): 1.1612e+24

With weight variance 1/n1/n, the output variance stays close to the input variance, confirming our derivation. The other choices cause variance to explode or collapse exponentially with depth.

Out[10]:
Visualization
Line plot showing variance across 10 layers for three initialization strategies on a log scale, with 1/n remaining stable.
Variance propagation through 10 layers with three different weight initialization scales, measured without activation functions to isolate the linear component. Only the 1/n variance (green) maintains stable variance across all layers. The too-small choice causes exponential decay and the too-large choice causes exponential growth, both visible on the log-scale y-axis.

Xavier/Glorot Initialization

Our variance analysis derived the optimal weight variance for forward propagation. But training a neural network involves both forward and backward propagation, and gradients face the same variance amplification problem in reverse.

Xavier Glorot and Yoshua Bengio, in their 2010 paper "Understanding the Difficulty of Training Deep Feedforward Neural Networks," recognized that we must consider both signal directions simultaneously. If we optimize only for forward variance, gradients might explode or vanish during backpropagation. The key insight was that a good initialization must care about the health of the gradient signal just as much as the health of the forward activations, because training depends on both flowing cleanly through the entire network.

Forward and Backward Requirements

The forward pass constraint from our derivation above gives:

Var(w)=1nin\text{Var}(w) = \frac{1}{n_{\text{in}}}

where ninn_{\text{in}} is the fan-in (number of input connections to each neuron).

During backpropagation, gradients flow in the opposite direction. Each input neuron receives gradient contributions from all noutn_{\text{out}} output neurons. By the same reasoning applied to the gradient equations, maintaining gradient variance during the backward pass requires:

Var(w)=1nout\text{Var}(w) = \frac{1}{n_{\text{out}}}

where noutn_{\text{out}} is the fan-out (number of output connections from each input neuron).

To see why the backward pass requirement involves noutn_{\text{out}}, recall the chain rule during backpropagation. The gradient of the loss with respect to an input activation xix_i is:

Lxi=j=1noutwijLzj\frac{\partial L}{\partial x_i} = \sum_{j=1}^{n_{\text{out}}} w_{ij} \cdot \frac{\partial L}{\partial z_j}

This is a weighted sum of noutn_{\text{out}} gradient terms, structurally identical to the forward pass equation but with fan-out replacing fan-in. The same variance analysis applies, giving the backward requirement Var(w)=1/nout\text{Var}(w) = 1/n_{\text{out}}.

The Glorot Compromise

We face a dilemma. The forward pass requires Var(w)=1/nin\text{Var}(w) = 1/n_{\text{in}}, but the backward pass requires Var(w)=1/nout\text{Var}(w) = 1/n_{\text{out}}. These two constraints conflict whenever ninnoutn_{\text{in}} \neq n_{\text{out}}, which is the common case for layers that change dimensionality.

Glorot and Bengio proposed taking the harmonic mean of the two fan values:

Var(w)=2nin+nout\text{Var}(w) = \frac{2}{n_{\text{in}} + n_{\text{out}}}

where:

  • ninn_{\text{in}}: fan-in (input dimension of the weight matrix)
  • noutn_{\text{out}}: fan-out (output dimension of the weight matrix)
  • 2nin+nout\frac{2}{n_{\text{in}} + n_{\text{out}}}: a compromise variance that partially satisfies both the forward and backward variance preservation constraints

Notice that when nin=noutn_{\text{in}} = n_{\text{out}}, this formula reduces to 1/nin1/n_{\text{in}}, recovering the LeCun result. For layers where the input and output dimensions differ substantially, the formula takes a middle path between the two competing requirements. This is an elegant engineering compromise: it is not optimal for either direction alone, but it avoids catastrophically failing in either direction.

This is Xavier initialization (also called Glorot initialization). To sample weights, we convert this target variance to the parameters of either a normal or uniform distribution.

For a normal distribution N(0,σ2)\mathcal{N}(0, \sigma^2), the variance is σ2\sigma^2. Setting σ2=2/(nin+nout)\sigma^2 = 2/(n_{\text{in}} + n_{\text{out}}) and taking the square root gives the standard deviation:

σ=2nin+nout\sigma = \sqrt{\frac{2}{n_{\text{in}} + n_{\text{out}}}}

Weights are drawn from:

wN ⁣(0,2nin+nout)w \sim \mathcal{N}\!\left(0, \sqrt{\frac{2}{n_{\text{in}} + n_{\text{out}}}}\right)

For a uniform distribution U[a,a]U[-a, a], the variance is a2/3a^2/3. Setting a2/3=2/(nin+nout)a^2/3 = 2/(n_{\text{in}} + n_{\text{out}}) and solving for aa:

a=6nin+nouta = \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}

Weights are drawn from:

wU ⁣[6nin+nout,6nin+nout]w \sim U\!\left[-\sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}\right]
Xavier/Glorot Initialization

A weight initialization scheme where weights are drawn from a distribution with variance 2/(nin+nout)2/(n_{\text{in}} + n_{\text{out}}). Designed for networks using symmetric activations like tanh or sigmoid, where the activation function is approximately linear near zero and does not systematically distort variance across layers.

In practice, the uniform variant is the more commonly referenced form, particularly in the original Glorot and Bengio paper. Both versions achieve similar results in practice because the variance formula is the same; only the shape of the distribution differs. The uniform variant is bounded, so it avoids the (very small) probability of drawing a very large outlier weight from the tails of a normal distribution.

Why This Works for Tanh and Sigmoid

The derivation assumes the activation function does not distort the variance of the signal. Tanh and sigmoid satisfy this assumption near zero, where they are both approximately linear. Specifically:

  • tanh(z)z\tanh(z) \approx z for small z|z|
  • σ(z)0.25z+0.5\sigma(z) \approx 0.25 z + 0.5 for small z|z|, with derivative 0.25\approx 0.25

For tanh, the derivative near zero is approximately 1, meaning the function barely affects variance. For sigmoid, the derivative near zero is about 0.25, which does systematically reduce variance. This is why PyTorch's implementation includes an empirical correction factor of 5/35/3 for tanh (accounting for the actual saturation properties) and why some implementations of Xavier for sigmoid include additional adjustment. The original Glorot and Bengio paper noted that sigmoid is more problematic than tanh, because its non-zero mean output shifts the input distribution seen by subsequent layers.

Implementation

In[11]:
Code
def xavier_uniform(shape):
    """Xavier initialization with uniform distribution."""
    fan_in, fan_out = shape
    limit = np.sqrt(6 / (fan_in + fan_out))
    return np.random.uniform(-limit, limit, shape)


def xavier_normal(shape):
    """Xavier initialization with normal distribution."""
    fan_in, fan_out = shape
    std = np.sqrt(2 / (fan_in + fan_out))
    return np.random.randn(*shape) * std


def compare_initializations(n_layers, layer_size, init_func, activation="tanh"):
    """Track activation standard deviation through network with given initialization."""
    weights = [init_func((layer_size, layer_size)) for _ in range(n_layers)]
    x = np.random.randn(1000, layer_size)
    stds = [np.std(x)]
    for W in weights:
        x = x @ W
        if activation == "tanh":
            x = np.tanh(x)
        elif activation == "relu":
            x = np.maximum(0, x)
        stds.append(np.std(x))
    return stds
Out[12]:
Console
Activation standard deviations across layers (tanh network):
------------------------------------------------------------

Naive (std=1.0):
  Layer  0: std = 0.9999
  Layer  2: std = 0.9740
  Layer  4: std = 0.9738
  Layer  6: std = 0.9740
  Layer  8: std = 0.9740
  Layer 10: std = 0.9738

Small (std=0.01):
  Layer  0: std = 1.0004
  Layer  2: std = 0.0250
  Layer  4: std = 0.0006
  Layer  6: std = 0.0000
  Layer  8: std = 0.0000
  Layer 10: std = 0.0000

Xavier normal:
  Layer  0: std = 0.9991
  Layer  2: std = 0.4858
  Layer  4: std = 0.3593
  Layer  6: std = 0.2948
  Layer  8: std = 0.2531
  Layer 10: std = 0.2255

Xavier uniform:
  Layer  0: std = 1.0009
  Layer  2: std = 0.4855
  Layer  4: std = 0.3560
  Layer  6: std = 0.2921
  Layer  8: std = 0.2546
  Layer 10: std = 0.2241

Xavier initialization keeps activation standard deviation stable across layers, while naive (too large) and small (too small) initializations diverge in opposite directions.

Out[13]:
Visualization
Line plot comparing activation standard deviations across 10 layers for four initialization methods in a tanh network.
Activation standard deviation across 10 layers for four initialization strategies in a tanh network. Xavier initialization (both uniform and normal variants) maintains stable activation magnitudes throughout. Naive initialization (std=1.0) causes saturation in early layers where tanh is pushed to its extremes, while small initialization (std=0.01) causes activations to collapse toward zero in deeper layers.

He Initialization for ReLU Networks

Xavier initialization was a breakthrough for networks with tanh or sigmoid activations. But by the mid-2010s, ReLU (Rectified Linear Unit) had become the dominant activation function due to its simplicity and its resistance to the vanishing gradient problem. Unfortunately, Xavier initialization performs poorly in ReLU networks.

The problem is fundamental. Xavier's derivation assumes the activation function is approximately linear near zero, so it does not systematically distort variance. Tanh and sigmoid satisfy this: near zero, both behave approximately like the identity function. ReLU does not. It zeros all negative inputs, keeping only the positive half of the distribution. This asymmetry means ReLU is far from linear around zero, and the linear approximation underlying Xavier's derivation breaks down completely.

The practical consequence is severe. A deep ReLU network initialized with Xavier will see its activations slowly collapse toward zero as the depth increases, because each ReLU layer systematically discards half the information by zeroing all negative values. The network can still learn, but it starts from a much worse position than necessary.

ReLU's Variance Halving Effect

Consider a pre-activation zz drawn from a symmetric distribution centered at zero with variance σ2\sigma^2. ReLU applies ReLU(z)=max(0,z)\text{ReLU}(z) = \max(0, z), zeroing all negative values while keeping positive values unchanged. Since the distribution is symmetric around zero, approximately half the values are negative and become zero.

The variance of the ReLU output is:

Var(ReLU(z))σ22\text{Var}(\text{ReLU}(z)) \approx \frac{\sigma^2}{2}

where:

  • ReLU(z)=max(0,z)\text{ReLU}(z) = \max(0, z): the rectified linear unit function that passes positive inputs and zeros negative ones
  • σ2\sigma^2: the variance of the input pre-activation zz before applying ReLU
  • 12\frac{1}{2}: the variance reduction factor, because ReLU eliminates roughly half the distribution (all values below zero)

To see this more carefully, consider the expected squared output of ReLU applied to zN(0,σ2)z \sim \mathcal{N}(0, \sigma^2):

E[ReLU(z)2]=E[z21z>0]=12E[z2]=σ22\mathbb{E}[\text{ReLU}(z)^2] = \mathbb{E}[z^2 \cdot \mathbf{1}_{z > 0}] = \frac{1}{2}\mathbb{E}[z^2] = \frac{\sigma^2}{2}

The indicator 1z>0\mathbf{1}_{z > 0} is 1 when zz is positive and 0 when negative. Since zz has zero mean, E[ReLU(z)]=E[z1z>0]=σ/2π\mathbb{E}[\text{ReLU}(z)] = \mathbb{E}[z \cdot \mathbf{1}_{z > 0}] = \sigma / \sqrt{2\pi}, which is nonzero. The variance therefore equals E[ReLU(z)2](E[ReLU(z)])2\mathbb{E}[\text{ReLU}(z)^2] - (\mathbb{E}[\text{ReLU}(z)])^2. For large networks where the number of neurons is large, the mean contribution to variance is much smaller than σ2/2\sigma^2/2, and the approximation Var(ReLU(z))σ2/2\text{Var}(\text{ReLU}(z)) \approx \sigma^2/2 holds well.

This variance halving has severe consequences for deep networks. After LL ReLU layers, variance shrinks by a factor of 2L2^L. A 10-layer ReLU network reduces activation variance by 210=10242^{10} = 1024. A 20-layer network reduces it by over a million. Xavier initialization, calibrated assuming no such reduction, produces vanishing activations in deep ReLU networks.

Deriving He Initialization

Kaiming He and colleagues addressed this in their 2015 paper "Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification." Their derivation modifies the Xavier variance equation to include ReLU's variance reduction factor.

Starting from the variance propagation equation with the ReLU correction:

Var(a)=12ninVar(w)Var(x)\text{Var}(a) = \frac{1}{2} \cdot n_{\text{in}} \cdot \text{Var}(w) \cdot \text{Var}(x)

where:

  • Var(a)\text{Var}(a): variance of the activation output, that is, the distribution after applying ReLU
  • 12\frac{1}{2}: the variance reduction factor from ReLU zeroing all negative pre-activation values
  • ninn_{\text{in}}: fan-in (number of input connections)
  • Var(w)\text{Var}(w): variance of the weights in this layer
  • Var(x)\text{Var}(x): variance of the input activations from the previous layer

To maintain Var(a)=Var(x)\text{Var}(a) = \text{Var}(x), we set:

Var(x)=12ninVar(w)Var(x)\text{Var}(x) = \frac{1}{2} \cdot n_{\text{in}} \cdot \text{Var}(w) \cdot \text{Var}(x)

Dividing both sides by Var(x)\text{Var}(x):

1=12ninVar(w)1 = \frac{1}{2} \cdot n_{\text{in}} \cdot \text{Var}(w)

Solving for the weight variance:

Var(w)=2nin\text{Var}(w) = \frac{2}{n_{\text{in}}}

This is He initialization (also called Kaiming initialization). The factor of 2 compensates precisely for ReLU's variance-halving effect. Notice that He uses only ninn_{\text{in}} rather than averaging ninn_{\text{in}} and noutn_{\text{out}} as Xavier does. He et al. found that matching forward pass variance was the more critical constraint for deep ReLU networks in practice, and empirical results confirmed that omitting the fan-out term worked better for the very deep convolutional networks they were studying.

He/Kaiming Initialization

A weight initialization scheme where weights are drawn from a distribution with variance 2/nin2/n_{\text{in}}. Designed specifically for ReLU networks, the factor of 2 in the numerator exactly compensates for the variance-halving effect of setting negative activations to zero.

Converting to distribution parameters:

Normal distribution:

wN ⁣(0,2nin)w \sim \mathcal{N}\!\left(0, \sqrt{\frac{2}{n_{\text{in}}}}\right)

where 2/nin\sqrt{2/n_{\text{in}}} is the standard deviation (the square root of the target variance).

Uniform distribution: Using the same derivation as for Xavier, where the variance of U[a,a]U[-a, a] equals a2/3a^2/3, we solve a2/3=2/nina^2/3 = 2/n_{\text{in}} to obtain a=6/nina = \sqrt{6/n_{\text{in}}}:

wU ⁣[6nin,6nin]w \sim U\!\left[-\sqrt{\frac{6}{n_{\text{in}}}}, \sqrt{\frac{6}{n_{\text{in}}}}\right]

Implementation and Comparison

In[14]:
Code
def he_normal(shape):
    """He initialization with normal distribution."""
    fan_in, fan_out = shape
    std = np.sqrt(2 / fan_in)
    return np.random.randn(*shape) * std


def he_uniform(shape):
    """He initialization with uniform distribution."""
    fan_in, fan_out = shape
    limit = np.sqrt(6 / fan_in)
    return np.random.uniform(-limit, limit, shape)
Out[15]:
Console
Comparing Xavier and He initialization on ReLU network:
------------------------------------------------------------

Xavier normal:
  Layer  0: std = 0.9990
  Layer  1: std = 0.5859
  Layer  2: std = 0.4019
  Layer  3: std = 0.2649
  Layer  4: std = 0.1818
  Layer  5: std = 0.1210
  Layer  6: std = 0.0794
  Layer  7: std = 0.0576
  Layer  8: std = 0.0443
  Layer  9: std = 0.0320
  Layer 10: std = 0.0238

He normal:
  Layer  0: std = 1.0011
  Layer  1: std = 0.8270
  Layer  2: std = 0.8115
  Layer  3: std = 0.8722
  Layer  4: std = 0.9080
  Layer  5: std = 0.9697
  Layer  6: std = 0.9750
  Layer  7: std = 1.0024
  Layer  8: std = 1.0809
  Layer  9: std = 1.1509
  Layer 10: std = 1.1800

He initialization maintains stable activation magnitudes in ReLU networks, while Xavier causes activations to decay layer by layer because it does not account for the variance reduction from ReLU.

Out[16]:
Visualization
Line plot showing activation standard deviation across 10 layers, with He initialization stable and Xavier decaying in a ReLU network.
Activation standard deviation across 10 layers for Xavier and He initialization in a ReLU network. Xavier initialization causes activations to progressively decay because it does not account for ReLU's variance-halving effect. He initialization compensates exactly for this reduction and maintains stable activation magnitudes throughout the entire network.

Initialization for Different Activations

We've seen Xavier for symmetric activations and He for ReLU. The deep learning toolkit includes many more activations, each with its own variance characteristics. The general principle remains: analyze how the activation distorts variance and adjust the initialization accordingly. This generalization is what PyTorch's nn.init.calculate_gain() function systematizes. This provides gain factors for each activation that can be plugged into the LeCun base formula.

The following table summarizes the recommended initialization for common activations:

Recommended initialization schemes for common activation functions. The variance formulas assume sampling from a normal distribution; multiply variance by 3 if using a uniform distribution instead.
ActivationRecommended InitWeight Variance
LinearXavier2nin+nout\frac{2}{n_{\text{in}} + n_{\text{out}}}
TanhXavier2nin+nout\frac{2}{n_{\text{in}} + n_{\text{out}}}
SigmoidXavier2nin+nout\frac{2}{n_{\text{in}} + n_{\text{out}}}
ReLUHe2nin\frac{2}{n_{\text{in}}}
Leaky ReLU (α\alpha)He (adjusted)2(1+α2)nin\frac{2}{(1 + \alpha^2) n_{\text{in}}}
SELULeCun1nin\frac{1}{n_{\text{in}}}
GELUHe (approximate)2nin\frac{2}{n_{\text{in}}}

LeCun Initialization and SELU

The SELU activation (Scaled Exponential Linear Unit) is a special case that deserves its own mention. SELU is designed to be self-normalizing: under the right conditions, activations in a SELU network converge to a fixed-point distribution with mean zero and unit variance regardless of network depth. The activation function itself applies a specific scaling and shift to maintain this fixed-point property.

For SELU to work correctly, the weights must be initialized with LeCun initialization, Var(w)=1/nin\text{Var}(w) = 1/n_{\text{in}}. This is the simplest of all the initialization schemes and the theoretical precursor to Xavier. The SELU paper by Klambauer et al. (2017) provides rigorous proofs that the combination of LeCun initialization, SELU activations, and specific rescaling factors produces self-normalizing behavior, eliminating the need for batch normalization in networks designed around this activation function.

Leaky ReLU

For Leaky ReLU with negative slope α\alpha (typically 0.01 or 0.2), negative inputs are scaled by α\alpha rather than zeroed. This preserves some variance from the negative half of the distribution. The corrected variance formula is:

Var(w)=2(1+α2)nin\text{Var}(w) = \frac{2}{(1 + \alpha^2) \cdot n_{\text{in}}}

where:

  • α\alpha: the negative slope of Leaky ReLU (the multiplier applied to negative pre-activations)
  • (1+α2)(1 + \alpha^2): a correction factor accounting for variance from both positive inputs (coefficient 1, so contribution is 12=11^2 = 1) and negative inputs (coefficient α\alpha, so contribution is α2\alpha^2)
  • ninn_{\text{in}}: fan-in (number of input connections)

When α=0\alpha = 0 (standard ReLU), the factor (1+0)=1(1 + 0) = 1 recovers He initialization. When α=1\alpha = 1 (linear activation), the factor (1+1)=2(1 + 1) = 2 gives Var(w)=1/nin\text{Var}(w) = 1/n_{\text{in}}, approaching LeCun initialization for the forward-only case. For typical Leaky ReLU with α=0.01\alpha = 0.01, the correction factor is barely distinguishable from He initialization, which is why many practitioners simply use He for Leaky ReLU without the explicit correction.

GELU and Modern Activations

GELU (Gaussian Error Linear Unit), used in BERT and GPT, applies a smooth, probabilistic gating that scales each input by the probability that a standard normal random variable is less than the input. Like ReLU, it zeros or nearly zeros a fraction of inputs, reducing variance by roughly half. He initialization works well for GELU networks in practice, as the variance reduction is similar in magnitude to standard ReLU.

Swish, another smooth activation used in some architectures, has a similar profile to GELU. Both can be approximated as having a gain factor of 2\sqrt{2}, identical to ReLU, making He initialization a reasonable default. The key insight is that any activation function that zeros or heavily suppresses roughly half its inputs requires the 2/nin2/n_{\text{in}} variance formula to compensate, while activations that are closer to linear around zero (tanh, sigmoid) do not need this correction factor.

In[17]:
Code
def init_weights(
    shape, activation="relu", mode="fan_in", distribution="normal"
):
    """
    Initialize weights based on activation function.

    Parameters:
    - shape: (fan_in, fan_out)
    - activation: 'linear', 'tanh', 'sigmoid', 'relu', 'leaky_relu', 'selu', 'gelu'
    - mode: 'fan_in', 'fan_out', or 'fan_avg'
    - distribution: 'normal' or 'uniform'
    """
    fan_in, fan_out = shape

    if mode == "fan_in":
        fan = fan_in
    elif mode == "fan_out":
        fan = fan_out
    else:  # fan_avg
        fan = (fan_in + fan_out) / 2

    # Gain factors derived from each activation's variance behavior
    gain = {
        "linear": 1.0,
        "tanh": 5 / 3,  # Empirical correction for tanh nonlinearity
        "sigmoid": 1.0,
        "relu": np.sqrt(2),  # Compensates for variance halving
        "leaky_relu": np.sqrt(2 / (1 + 0.01**2)),  # Negative slope alpha=0.01
        "selu": 1.0,  # LeCun initialization: variance = 1/n
        "gelu": np.sqrt(2),  # Similar to ReLU in practice
    }.get(activation, 1.0)

    std = gain / np.sqrt(fan)

    if distribution == "normal":
        return np.random.randn(*shape) * std
    else:
        limit = np.sqrt(3) * std
        return np.random.uniform(-limit, limit, shape)
Out[18]:
Console
Initialization statistics for different activations (shape 256x256):
------------------------------------------------------------
relu        : std = 0.0883, range = [-0.354, 0.357]
tanh        : std = 0.1036, range = [-0.428, 0.423]
leaky_relu  : std = 0.0885, range = [-0.391, 0.404]
selu        : std = 0.0628, range = [-0.264, 0.260]
gelu        : std = 0.0883, range = [-0.339, 0.423]

ReLU and GELU initializations have the largest standard deviation because they use the 2\sqrt{2} gain factor to compensate for variance halving. Tanh uses a gain of approximately 5/3. SELU uses LeCun initialization with a gain of 1, giving variance 1/nin1/n_{\text{in}}.

Orthogonal Initialization

Xavier and He initialization focus on setting the right variance. Orthogonal initialization takes a different approach: it constructs weight matrices whose columns are perpendicular unit vectors, a property called orthogonality.

An orthogonal matrix WW satisfies WTW=IW^T W = I (the identity matrix). This means the transformation preserves the length of vectors: Wx2=x2\|Wx\|_2 = \|x\|_2 for any input vector xx. In terms of variance, orthogonal initialization guarantees exact variance preservation during the forward pass, with no dependence on the input distribution or layer size.

Orthogonal Initialization

A weight initialization scheme that constructs square weight matrices to be orthogonal, satisfying WTW=IW^T W = I. Since orthogonal transformations preserve vector norms, this guarantees exact variance preservation during the forward pass and is particularly valuable for recurrent neural networks where the same matrix is applied repeatedly across time steps.

The construction uses singular value decomposition (SVD). Starting from a random matrix AA with entries drawn from N(0,1)\mathcal{N}(0, 1), we compute the SVD A=UΣVTA = U \Sigma V^T and use UU (or VTV^T) as the initial weight matrix. Both UU and VV are unitary matrices satisfying the orthogonality condition.

Why Orthogonality Matters for Gradient Flow

Beyond variance preservation, orthogonal matrices have an important property: their eigenvalues all have magnitude exactly 1. For a general matrix WW, the eigenvalues can be larger or smaller than 1 in magnitude. When a matrix with eigenvalues much larger than 1 is applied repeatedly (as in a recurrent network processing a long sequence), the signal grows exponentially. When eigenvalues are much smaller than 1, the signal decays exponentially. This is the spectral radius perspective on exploding and vanishing gradients.

An orthogonal matrix, with eigenvalues of magnitude exactly 1, neither amplifies nor attenuates the signal regardless of how many times it is applied. This makes orthogonal initialization particularly well-suited for recurrent neural networks, where the same hidden-to-hidden weight matrix is multiplied at every time step. Even for very long sequences (hundreds or thousands of steps), an orthogonally initialized recurrent matrix maintains bounded signal propagation.

In[19]:
Code
def orthogonal_init(shape, gain=1.0):
    """
    Orthogonal initialization using SVD.

    Parameters:
    - shape: (fan_in, fan_out)
    - gain: scaling factor (use sqrt(2) for ReLU networks)
    """
    fan_in, fan_out = shape
    a = np.random.randn(fan_in, fan_out)
    U, _, Vt = np.linalg.svd(a, full_matrices=False)
    # Choose U or Vt depending on shape
    q = U if U.shape == (fan_in, fan_in) else Vt
    if q.shape != shape:
        q = q[:fan_in, :fan_out]
    return gain * q


def get_variance_after_layers(init_func, n_layers=20, layer_size=256):
    """Measure variance across layers in a linear network."""
    weights = [init_func((layer_size, layer_size)) for _ in range(n_layers)]
    x = np.random.randn(1000, layer_size)
    x = x / np.std(x)
    variances = [np.var(x)]
    for W in weights:
        x = x @ W
        variances.append(np.var(x))
    return variances
Out[20]:
Console
Variance preservation (20-layer linear network, no activation):
------------------------------------------------------------

Random normal (std=1.0):
  Initial: 1.0000, Final: 1.5142e+48

He normal:
  Initial: 1.0000, Final: 1.0017e+06

Orthogonal:
  Initial: 1.0000, Final: 1.0000e+00

Orthogonal initialization achieves near-perfect variance preservation in linear networks. This makes it particularly valuable for recurrent neural networks, where the hidden-to-hidden weight matrix is applied at every time step. We'll explore this in more depth when we cover RNNs.

For feedforward networks, the practical advantages of orthogonal initialization over He are less clear-cut, because activation functions break the orthogonality property immediately after the first layer. Xavier and He initialization work well for typical feedforward architectures, while orthogonal initialization is the preferred choice for RNN hidden-to-hidden matrices.

BERT and GPT Initialization Schemes

Modern large language models take a more pragmatic approach to initialization than the theoretically derived schemes above. Both BERT and GPT use a simple, uniform strategy: normal distribution with a fixed small standard deviation, regardless of layer size. This works because layer normalization, applied throughout these architectures, normalizes activations during training and reduces the network.s sensitivity to the exact initialization scale.

The choice of a fixed standard deviation rather than a layer-size-dependent formula is a deliberate engineering decision. In transformer architectures, every attention layer and feed-forward layer is followed by layer normalization, which rescales activations to have unit variance at every step. This means the network continuously self-corrects its activation distribution, making the initialization less critical for preventing vanishing or exploding activations. The initialization instead needs to be "reasonable enough" that the network can start making productive gradient updates from the first iteration.

BERT Initialization

BERT uses N(0,0.02)\mathcal{N}(0, 0.02) for all weight matrices and zeros for all bias vectors. The 0.02 standard deviation was chosen empirically by the original BERT authors based on what produced stable training for their specific architecture. The layer normalization parameters (scale γ\gamma and bias β\beta) are initialized to 1 and 0, respectively, establishing an initial identity transformation.

The choice of 0.02 is smaller than what Xavier or He would prescribe for typical transformer layer dimensions. For a layer with nin=768n_{\text{in}} = 768 (the hidden dimension of BERT-base), He initialization gives σ=2/7680.051\sigma = \sqrt{2/768} \approx 0.051, and Xavier gives σ=2/(768+768)0.036\sigma = \sqrt{2/(768+768)} \approx 0.036. The BERT value of 0.02 is smaller. This conservatism is intentional: smaller initialization means smaller initial activations, and with layer normalization continuously rescaling, the network quickly adapts to useful activation scales regardless of the initialization.

GPT-2 Initialization

GPT-2 uses the same N(0,0.02)\mathcal{N}(0, 0.02) base distribution but introduces a residual scaling factor for the projection matrices at the end of each attention block and each feed-forward block. These weights are scaled by 1/nlayers1/\sqrt{n_{\text{layers}}}:

wprojN ⁣(0,0.02nlayers)w_{\text{proj}} \sim \mathcal{N}\!\left(0, \frac{0.02}{\sqrt{n_{\text{layers}}}}\right)

where:

  • nlayersn_{\text{layers}}: the total number of transformer layers in the model
  • 0.020.02: the base standard deviation shared with BERT
  • 1nlayers\frac{1}{\sqrt{n_{\text{layers}}}}: a residual scaling factor that prevents variance from accumulating across layers

This scaling addresses a specific problem with residual connections. In a residual network, each layer computes x+1=x+F(x)x_{\ell+1} = x_\ell + F(x_\ell), adding the residual branch output F(x)F(x_\ell) to the input. If each residual branch contributes variance σ2\sigma^2, then after LL layers the total variance grows to Lσ2L \sigma^2. Scaling the projection weights by 1/L1/\sqrt{L} reduces each branch's variance contribution to σ2/L\sigma^2/L, keeping the total variance constant at σ2\sigma^2.

This residual scaling trick has been adopted by many subsequent transformer architectures. Larger models like GPT-3 and various open-source LLMs use the same basic approach, sometimes with slightly different base standard deviations tuned to their specific architectures. The key insight transfers directly: when residual branches accumulate, scale the contributing weights to keep variance bounded.

In[21]:
Code
import torch.nn as nn


class BertStyleInit(nn.Module):
    """Demonstrates BERT-style initialization."""

    def __init__(self, d_model, n_layers):
        super().__init__()
        self.layers = nn.ModuleList(
            [nn.Linear(d_model, d_model) for _ in range(n_layers)]
        )
        self.layer_norms = nn.ModuleList(
            [nn.LayerNorm(d_model) for _ in range(n_layers)]
        )
        self._init_weights_bert()

    def _init_weights_bert(self):
        """BERT-style initialization: N(0, 0.02) for weights, 0 for biases."""
        for module in self.modules():
            if isinstance(module, nn.Linear):
                nn.init.normal_(module.weight, mean=0.0, std=0.02)
                if module.bias is not None:
                    nn.init.zeros_(module.bias)
            elif isinstance(module, nn.LayerNorm):
                nn.init.ones_(module.weight)
                nn.init.zeros_(module.bias)

    def forward(self, x):
        for linear, norm in zip(self.layers, self.layer_norms):
            x = x + linear(norm(x))
        return x


class GPT2StyleInit(nn.Module):
    """Demonstrates GPT-2-style initialization with residual scaling."""

    def __init__(self, d_model, n_layers):
        super().__init__()
        self.n_layers = n_layers
        self.projections = nn.ModuleList(
            [nn.Linear(d_model, d_model) for _ in range(n_layers)]
        )
        self.layer_norms = nn.ModuleList(
            [nn.LayerNorm(d_model) for _ in range(n_layers)]
        )
        self._init_weights_gpt2()

    def _init_weights_gpt2(self):
        """GPT-2 initialization: N(0, 0.02) with 1/sqrt(n_layers) residual scaling."""
        for module in self.modules():
            if isinstance(module, nn.Linear):
                nn.init.normal_(module.weight, mean=0.0, std=0.02)
                if module.bias is not None:
                    nn.init.zeros_(module.bias)
            elif isinstance(module, nn.LayerNorm):
                nn.init.ones_(module.weight)
                nn.init.zeros_(module.bias)
        # Scale down output projection weights
        for proj in self.projections:
            proj.weight.data *= 1.0 / (self.n_layers**0.5)

    def forward(self, x):
        for proj, norm in zip(self.projections, self.layer_norms):
            x = x + proj(norm(x))
        return x


d_model = 256
n_layers = 12
Out[22]:
Console
BERT vs GPT-2 initialization comparison:
============================================================

BERT-style first layer weight statistics:
  Mean:  -0.000006
  Std:   0.0200
  Range: [-0.0845, 0.0882]

GPT-2-style first projection weight statistics (scaled by 1/sqrt(12)):
  Mean:  -0.000008
  Std:   0.005759  (expected: 0.02/sqrt(12) = 0.005774)
  Range: [-0.0232, 0.0236]

Output std after 12 layers:
  BERT:  1.5036
  GPT-2: 1.0453
Out[23]:
Visualization
Line plot showing output standard deviation across 12 transformer layers for BERT and GPT-2 initialization schemes.
Output standard deviation across 12 transformer layers for BERT-style and GPT-2-style initialization. BERT initialization causes output variance to grow as residual contributions accumulate across layers. GPT-2's residual scaling (dividing projection weights by the square root of the number of layers) compensates for this accumulation and keeps the output variance stable throughout the network depth.

Empirical Comparison of Initialization Methods

Theory tells us which initialization should work best for a given activation function, but practice sometimes differs from theory. Let's run a controlled empirical comparison to see how the different methods perform during training.

In[24]:
Code
import torch
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset


def create_network_with_init(
    hidden_sizes, activation="relu", init_method="he_normal"
):
    """Create a sequential network with the specified initialization method."""
    layers = []
    for i in range(len(hidden_sizes) - 1):
        linear = nn.Linear(hidden_sizes[i], hidden_sizes[i + 1])

        if init_method == "xavier_uniform":
            nn.init.xavier_uniform_(linear.weight)
        elif init_method == "xavier_normal":
            nn.init.xavier_normal_(linear.weight)
        elif init_method == "he_normal":
            nn.init.kaiming_normal_(linear.weight, nonlinearity="relu")
        elif init_method == "he_uniform":
            nn.init.kaiming_uniform_(linear.weight, nonlinearity="relu")
        elif init_method == "orthogonal":
            nn.init.orthogonal_(linear.weight, gain=np.sqrt(2))
        elif init_method == "zeros":
            nn.init.zeros_(linear.weight)
        elif init_method == "bert":
            nn.init.normal_(linear.weight, mean=0.0, std=0.02)

        nn.init.zeros_(linear.bias)
        layers.append(linear)

        if i < len(hidden_sizes) - 2:
            if activation == "relu":
                layers.append(nn.ReLU())
            elif activation == "tanh":
                layers.append(nn.Tanh())

    return nn.Sequential(*layers)


def train_model(model, train_loader, epochs=60, lr=0.01):
    """Train model and return loss history."""
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=lr)
    losses = []
    for epoch in range(epochs):
        epoch_loss = 0
        for X, y in train_loader:
            optimizer.zero_grad()
            outputs = model(X)
            loss = criterion(outputs, y)
            loss.backward()
            optimizer.step()
            epoch_loss += loss.item()
        losses.append(epoch_loss / len(train_loader))
    return losses


# Create a structured 5-class classification dataset
torch.manual_seed(42)
np.random.seed(42)

n_samples = 2000
n_features = 100
n_classes = 5

# Gaussian clusters with realistic overlap
centers = np.random.randn(n_classes, n_features) * 3
X_np = np.vstack(
    [
        centers[c] + np.random.randn(n_samples // n_classes, n_features) * 1.8
        for c in range(n_classes)
    ]
)
y_np = np.repeat(np.arange(n_classes), n_samples // n_classes)

X = torch.FloatTensor(X_np)
y = torch.LongTensor(y_np)
dataset = TensorDataset(X, y)
train_loader = DataLoader(dataset, batch_size=64, shuffle=True)

hidden_sizes = [100, 128, 128, 128, n_classes]
Out[25]:
Visualization
Line plot showing training loss over 60 epochs for six initialization methods, with zero init flat and all others converging.
Training loss over 60 epochs for six initialization methods on a 4-layer ReLU network. Zero initialization fails completely (flat high-loss curve) because the symmetry problem prevents any learning. Xavier, He, and orthogonal initialization all enable convergence. He initialization shows slightly faster initial descent on this ReLU network, as expected from its derivation. The BERT-style fixed std=0.02 is suboptimal here because the standard deviation is not adapted to the actual layer dimensions.
Out[26]:
Console
Final training loss after 60 epochs:
--------------------------------------------------
  Zero init                : 1.6095
  Xavier normal            : 0.0003
  He normal                : 0.0001
  He uniform               : 0.0001
  Orthogonal               : 0.0001
  BERT (std=0.02)          : 0.0010

The comparison shows a clear hierarchy. Zero initialization fails completely. Xavier performs well but converges slightly more slowly than He on this ReLU network, because it was designed for tanh. He and orthogonal initialization both perform well and converge at similar rates. The BERT-style fixed standard deviation (0.02) underperforms because it is not calibrated to these layer dimensions.

PyTorch Implementation

Modern deep learning frameworks provide built-in initialization functions. PyTorch offers the full suite of initialization methods through the torch.nn.init module. Understanding how to use these functions and when to apply them is an essential practical skill.

In[27]:
Code
# Demonstrate PyTorch's built-in initialization functions
torch.manual_seed(42)

init_examples = {
    "xavier_uniform_": lambda m: nn.init.xavier_uniform_(m.weight),
    "xavier_normal_": lambda m: nn.init.xavier_normal_(m.weight),
    "kaiming_uniform_": lambda m: nn.init.kaiming_uniform_(
        m.weight, nonlinearity="relu"
    ),
    "kaiming_normal_": lambda m: nn.init.kaiming_normal_(
        m.weight, nonlinearity="relu"
    ),
    "orthogonal_": lambda m: nn.init.orthogonal_(m.weight, gain=np.sqrt(2)),
}
Out[28]:
Console
PyTorch built-in initialization statistics (256x512 layer, fan_in=512, fan_out=256):
-----------------------------------------------------------------
  xavier_uniform_       : std = 0.05111, range = [-0.0884, 0.0884]
  xavier_normal_        : std = 0.05110, range = [-0.2352, 0.2104]
  kaiming_uniform_      : std = 0.06252, range = [-0.1083, 0.1082]
  kaiming_normal_       : std = 0.06247, range = [-0.2800, 0.2481]
  orthogonal_           : std = 0.06250, range = [-0.2696, 0.2816]

The key PyTorch parameters for Kaiming (He) initialization are:

  • mode ('fan_in' or 'fan_out'): Controls whether to use the input dimension or output dimension for the variance calculation. Use 'fan_in' (the default) to preserve forward pass variance, or 'fan_out' to preserve backward pass variance.
  • nonlinearity ('relu', 'leaky_relu', 'tanh', etc.): Specifies the activation function so PyTorch computes the correct gain factor automatically.
  • a (for Leaky ReLU): The negative slope parameter. PyTorch computes the gain as 2/(1+a2)\sqrt{2 / (1 + a^2)}, matching our derived formula.

For Xavier initialization, the gain parameter multiplies the standard deviation. PyTorch provides nn.init.calculate_gain('tanh') and similar functions to look up the recommended gain for a given activation.

Applying Initialization to a Custom Network

In practice, you'll often want to apply initialization to an entire model at once. The cleanest pattern is to write an _init_weights method and call self.apply() to walk through all submodules.

In[29]:
Code
class MLP(nn.Module):
    """A multi-layer perceptron with configurable initialization."""

    def __init__(
        self, input_size, hidden_sizes, output_size, activation="relu"
    ):
        super().__init__()
        all_sizes = [input_size] + hidden_sizes + [output_size]
        self.layers = nn.ModuleList(
            [
                nn.Linear(all_sizes[i], all_sizes[i + 1])
                for i in range(len(all_sizes) - 1)
            ]
        )
        self.activation = activation
        self._init_weights()

    def _init_weights(self):
        """Apply appropriate initialization based on activation function."""
        for layer in self.layers:
            if self.activation == "relu":
                nn.init.kaiming_normal_(layer.weight, nonlinearity="relu")
            elif self.activation == "tanh":
                nn.init.xavier_normal_(
                    layer.weight, gain=nn.init.calculate_gain("tanh")
                )
            else:
                nn.init.xavier_normal_(layer.weight)
            nn.init.zeros_(layer.bias)

    def forward(self, x):
        for i, layer in enumerate(self.layers):
            x = layer(x)
            if i < len(self.layers) - 1:
                if self.activation == "relu":
                    x = torch.relu(x)
                elif self.activation == "tanh":
                    x = torch.tanh(x)
        return x
Out[30]:
Console
MLP initialization check:
  ReLU MLP, first layer std:  0.1424
  Tanh MLP, first layer std:  0.1572
  Expected ReLU std (He):     0.1414
  Expected Tanh std (Xavier): 0.0937

The _init_weights pattern scales cleanly from simple MLPs to complex architectures. When building transformer models, you'd typically call self.apply(self._init_weights) to recursively initialize all submodules, then optionally scale specific weights (like GPT-2's projection weights) with a second pass.

Bias Initialization

While we've focused on weight initialization, biases also need consideration. The standard practice is to initialize biases to zero.

In[31]:
Code
import torch.nn as nn


def init_biases(layer, method="zeros"):
    """Initialize biases with specified method."""
    if method == "zeros":
        nn.init.zeros_(layer.bias)
    elif method == "small_positive":
        nn.init.constant_(layer.bias, 0.01)
    elif method == "normal":
        nn.init.normal_(layer.bias, mean=0, std=0.01)

Unlike weights, initializing all biases to the same value (zero) does not cause a symmetry problem. The gradient of the loss with respect to a bias depends on the upstream gradient, not on other neurons' biases. Each neuron receives a distinct upstream gradient because its incoming weights are different (due to random weight initialization), so biases update independently from the start.

This is a subtle but important distinction. The symmetry problem for weights arises because two neurons receiving the same input with the same weights produce the same output and therefore receive the same gradient. Biases do not have this issue because the gradient for a bias in layer \ell depends on the product of all upstream activations and weights from layer \ell to the output. Since those upstream weights differ between neurons, the bias gradients differ too.

For ReLU networks, some practitioners use small positive biases (0.01) to ensure neurons start in an active state. This prevents "dead neurons" where all inputs are negative from the start. However, batch normalization and careful learning rate selection have largely eliminated the need for this adjustment in modern architectures.

There are special cases where bias initialization does matter. The output layer bias can be initialized to the log-odds of the class priors (for classification) or the mean target value (for regression), giving the network a sensible starting point that reduces the number of epochs needed to converge. Gate biases in LSTM cells are sometimes initialized to positive values (commonly 1 or 2) to keep the forget gate initially open, allowing gradients to flow more easily through the gate during early training.

Initialization and Normalization: A Symbiotic Relationship

One of the most important developments in deep learning was the realization that initialization and normalization are complementary tools addressing the same underlying problem: keeping activation distributions healthy throughout a deep network. Understanding their relationship clarifies when each is necessary and when one can substitute for the other.

Batch normalization, introduced by Ioffe and Szegedy in 2015, directly normalizes pre-activation values to have zero mean and unit variance at each layer, then scales and shifts them by learned parameters. This operation continuously corrects the activation distribution during training, reducing the network.s sensitivity to poor initialization. As a result, networks with batch normalization can converge from a much wider range of initializations than those without.

Layer normalization, the variant used in transformers, operates along the feature dimension rather than the batch dimension. It has the same effect of continuously normalizing activations, but it does so per-sample rather than per-batch. This makes it better suited for variable-length sequences and small batch sizes.

The existence of these normalization layers explains a fact that puzzles many practitioners: why does BERT work so well with a simple fixed initialization that theory suggests is suboptimal? The answer is that layer normalization renders the precise initialization scale much less important. Whatever the initial activation distribution, the first layer normalization immediately rescales it. The initialization just needs to avoid catastrophic failure modes (complete collapse or extreme saturation) in the handful of forward passes before the first gradient update.

Without normalization, initialization becomes much more critical. If you design a network that omits batch or layer normalization for any reason, the theoretically derived initializations (Xavier or He, depending on your activation function) are essential for achieving reliable training. This was the regime that the Glorot/Bengio and He et al. papers were addressing, before normalization was ubiquitous.

The practical implication is: if you add batch normalization or layer normalization to your network, you have more flexibility in initialization; if you remove normalization layers for efficiency or other architectural reasons, you should apply principled initialization more carefully.

Impact on Training Stability

Proper initialization has measurable effects on training stability beyond just convergence speed. The connection is to how well-conditioned the early gradient signal is: well-initialized networks start in regions of the loss surface where gradients are informative and directions are well-scaled, enabling optimizers to take productive steps from the first iteration.

Poor initialization forces the optimizer to spend many epochs correcting the initial state before it can begin learning the actual task. In the worst cases, networks with bad initialization never converge at all, because early gradient signals are too weak (vanishing) or too noisy (exploding) to provide useful update directions. The optimizer takes large steps in meaningless directions, or takes steps so small that any progress is undetectable.

The condition number of the weight matrices is another lens through which to understand initialization quality. When weights are initialized appropriately, the early layers of the network apply transformations with condition numbers close to 1, meaning all directions are scaled similarly. This makes the optimization landscape well-conditioned and gradient descent more efficient. Poor initialization can produce extremely ill-conditioned transformations where some directions are amplified by orders of magnitude more than others, making optimization very difficult.

The interactions between initialization and other training components are also important. Batch normalization dramatically reduces sensitivity to initialization by normalizing activations at each layer during training. Layer normalization, used in transformers, plays a similar role. This is why BERT and GPT can use a simple fixed standard deviation rather than layer-size-dependent formulas: the normalization layers continuously correct the activation distribution during training, absorbing initialization errors.

When normalization layers are absent (some architectures avoid them for specific reasons), initialization quality becomes more critical. In these settings, using the theoretically derived initialization for your specific activation function is the safest strategy.

Diagnosing Initialization Problems

When a network fails to train, determining whether initialization is the culprit requires specific diagnostic tools. Simply observing poor performance after many training epochs does not distinguish initialization failure from other problems like inappropriate learning rates, overfitting, or data issues.

The most informative diagnostic is to examine activation statistics in the first few forward passes, before any gradient updates. If activations are nearly zero at the output of every layer, you have a vanishing activation problem that is almost certainly initialization-related. If activations are extremely large or if many neurons have saturated activations (tanh outputs close to ±1\pm 1, sigmoid outputs close to 0 or 1), you have an explosion or saturation problem.

Gradient statistics after the first backward pass provide complementary information. Computing the gradient norm at each layer and plotting it across depth reveals whether gradients are vanishing or exploding as they flow backward through the network. A healthy gradient flow shows gradient norms roughly comparable across all layers. Vanishing gradients appear as norms that decay exponentially with distance from the output; exploding gradients appear as norms that grow exponentially.

In[32]:
Code
def diagnose_initialization(model, input_tensor):
    """
    Analyze activation and gradient statistics at initialization.
    Returns activation statistics per layer.
    """
    model.zero_grad()
    activation_stats = []

    # Forward pass with hooks to capture activations
    hooks = []
    activations_captured = []

    def hook_fn(module, input, output):
        if isinstance(output, torch.Tensor):
            activations_captured.append(
                {
                    "mean": output.detach().mean().item(),
                    "std": output.detach().std().item(),
                    "frac_dead": (output.detach() == 0).float().mean().item(),
                }
            )

    for name, module in model.named_modules():
        if isinstance(module, nn.Linear):
            hooks.append(module.register_forward_hook(hook_fn))

    output = model(input_tensor)
    loss = output.sum()
    loss.backward()

    for hook in hooks:
        hook.remove()

    return activations_captured
Out[33]:
Console
Activation diagnostics at initialization:
============================================================

zeros:
  Layer 1: mean=+0.0000, std=0.0000, dead=100.0%
  Layer 2: mean=+0.0000, std=0.0000, dead=100.0%
  Layer 3: mean=+0.0000, std=0.0000, dead=100.0%
  Layer 4: mean=+0.0000, std=0.0000, dead=100.0%

he_normal:
  Layer 1: mean=+0.0295, std=1.4281, dead=0.0%
  Layer 2: mean=+0.1051, std=1.4485, dead=0.0%
  Layer 3: mean=+0.0879, std=1.6974, dead=0.0%
  Layer 4: mean=-1.0546, std=1.4966, dead=0.0%

xavier_normal:
  Layer 1: mean=-0.0113, std=0.9139, dead=0.0%
  Layer 2: mean=+0.0317, std=0.6534, dead=0.0%
  Layer 3: mean=+0.0206, std=0.4679, dead=0.0%
  Layer 4: mean=-0.0132, std=0.3902, dead=0.0%

This diagnostic output immediately shows the difference between initialization schemes. With zero initialization, standard deviations collapse to zero after the first layer and the dead neuron fraction is 100% from the start. With He initialization, standard deviations remain stable across layers and the dead neuron fraction is around 50% (expected for a ReLU network, since the network is applying ReLU to symmetrically distributed pre-activations). With Xavier on a ReLU network, you see a progressive decay in standard deviation, showing the mismatch between Xavier's tanh-oriented design and the ReLU activation.

Limitations and Practical Guidance

Weight initialization, while important, is not a complete solution to training difficulties.

Initialization is only the starting point. A good initialization establishes favorable conditions for learning, but it cannot compensate for fundamental problems like inappropriate architecture choices, insufficient data, or training hyperparameters that are far off. A network with He initialization will still fail if the learning rate is too large by several orders of magnitude.

Normalization techniques reduce sensitivity. Batch normalization and layer normalization have made precise initialization less critical for many architectures. Modern transformers like BERT and GPT demonstrate that a simple, fixed initialization standard deviation works well when combined with layer normalization. The normalization layers continuously adjust the activation distribution during training, absorbing initialization errors.

Very deep networks remain challenging. For networks with hundreds of layers, even careful initialization may not prevent early training instabilities. Additional techniques like learning rate warmup, gradient clipping, and careful residual branch scaling become necessary. The next chapter covers gradient clipping in detail.

The derivations for Xavier and He initialization make assumptions that do not always hold. They assume weights and inputs are independent, that inputs have zero mean, and that the activation function behaves as modeled (linear for Xavier, variance-halving for He). In practice, these assumptions are approximately but not exactly satisfied, particularly after a few training steps. The initializations work well in practice despite these approximations because the variance analysis captures the dominant first-order behavior.

As a practical decision guide:

  • Use He initialization for ReLU and GELU networks (the current default for feedforward and convolutional architectures). This is nn.init.kaiming_normal_() or nn.init.kaiming_uniform_() in PyTorch.
  • Use Xavier initialization for tanh and sigmoid networks. This is nn.init.xavier_normal_() or nn.init.xavier_uniform_() in PyTorch.
  • Use orthogonal initialization for RNN hidden-to-hidden matrices, where the same matrix is applied many times.
  • Follow the BERT/GPT-2 approach for transformer architectures with layer normalization: use N(0,0.02)\mathcal{N}(0, 0.02) for all weights with residual scaling applied to projection layers.
  • If you are uncertain about which activation a layer feeds, default to He initialization; it is more tolerant of mismatch than Xavier because the 2\sqrt{2} factor provides a buffer against variance decay.

Summary

Weight initialization determines whether a neural network can learn effectively from the start. The core principle is variance preservation: weights should be scaled so that activations and gradients maintain reasonable magnitudes as they propagate through layers.

Key takeaways:

  • Zero initialization causes symmetry collapse: all neurons in a layer learn identically, wasting representational capacity. Any constant initialization has this problem.
  • Random initialization introduces scale sensitivity: too small causes vanishing activations, too large causes explosion or saturation
  • LeCun initialization uses variance 1/nin1/n_{\text{in}} to preserve forward pass variance in linear networks. It is the theoretical foundation that Xavier and He build upon.
  • Xavier initialization uses variance 2/(nin+nout)2/(n_{\text{in}} + n_{\text{out}}), balancing forward and backward variance for tanh and sigmoid activations
  • He initialization uses variance 2/nin2/n_{\text{in}}, adding a factor of 2 to compensate for ReLU's variance-halving effect
  • Orthogonal initialization preserves exact variance in linear transformations, making it valuable for RNNs where spectral radius matters for long-range gradient flow
  • BERT and GPT use a simple N(0,0.02)\mathcal{N}(0, 0.02) approach, relying on layer normalization and residual scaling to maintain stability
  • Normalization layers reduce sensitivity to initialization and explain why modern large models need less precise initialization schemes
  • Diagnostics matter: examining activation statistics and gradient norms at the start of training can pinpoint initialization problems before wasting compute on a doomed training run

The next chapter covers gradient clipping, a complementary technique that handles gradient explosions that can occur during training even when initialization is correct, particularly in recurrent networks processing long sequences.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about weight initialization in neural networks.

Weight Initialization Quiz

Question 1 of 70 of 7 completed
What is the fundamental problem with initializing all weights in a layer to zero?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025weightinitialization, author = {Michael Brenndoerfer}, title = {Weight Initialization: Xavier, He & Variance Preservation}, year = {2025}, url = {https://mbrenndoerfer.com/writing/weight-initialization-neural-networks-xavier-he}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-15} }
APAAcademic
Michael Brenndoerfer (2025). Weight Initialization: Xavier, He & Variance Preservation. Retrieved from https://mbrenndoerfer.com/writing/weight-initialization-neural-networks-xavier-he
MLAAcademic
Michael Brenndoerfer. "Weight Initialization: Xavier, He & Variance Preservation." 2026. Web. September 15, 2026. <https://mbrenndoerfer.com/writing/weight-initialization-neural-networks-xavier-he>.
CHICAGOAcademic
Michael Brenndoerfer. "Weight Initialization: Xavier, He & Variance Preservation." Accessed September 15, 2026. https://mbrenndoerfer.com/writing/weight-initialization-neural-networks-xavier-he.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Weight Initialization: Xavier, He & Variance Preservation'. Available at: https://mbrenndoerfer.com/writing/weight-initialization-neural-networks-xavier-he (Accessed: September 15, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Weight Initialization: Xavier, He & Variance Preservation. https://mbrenndoerfer.com/writing/weight-initialization-neural-networks-xavier-he

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.