Weight Initialization › BERT and GPT Initialization Schemes
Language AI HandbookAll chapters Weight Initialization › BERT and GPT Initialization SchemesOne section · Sections in this chapter10 / 19 ↓
Reading tools Definitions & inline links
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
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 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 and bias ) 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 (the hidden dimension of BERT-base), He initialization gives , and Xavier gives . 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 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 :
where:
- : the total number of transformer layers in the model
- : the base standard deviation shared with BERT
- : 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 , adding the residual branch output to the input. If each residual branch contributes variance , then after layers the total variance grows to . Scaling the projection weights by reduces each branch's variance contribution to , keeping the total variance constant at .
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.
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 = 12BERT 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
