Layer Normalization › Visualizing Layer Normalization's Effect

Michael BrenndoerferUpdated June 11, 20253 min read
Language AI HandbookAll chapters
Part 15 of 67Transformer Blocks
Chapter 104Layer Normalization › Visualizing Layer Normalization's Effect
Reading section 9 of 16 · Visualizing Layer Normalization's Effect · 3 minSection 9 of 16 · 3 min

Book contents

Section 9 of 16 · 3 min read

Layer Normalization › Visualizing Layer Normalization's EffectOne section · Sections in this chapter9 / 16 ↓
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

Let's visualize how layer normalization transforms the activation distribution during a forward pass through multiple transformer blocks.

In[23]:
Code
class StackedTransformerBlocks(nn.Module):
    """Stack of transformer blocks for visualization."""

    def __init__(self, d_model, n_heads, d_ff, n_layers, use_layernorm=True):
        super().__init__()
        self.use_layernorm = use_layernorm
        self.layers = nn.ModuleList(
            [
                PreNormTransformerBlock(d_model, n_heads, d_ff)
                for _ in range(n_layers)
            ]
        )
        if not use_layernorm:
            # Replace LayerNorm with identity
            for layer in self.layers:
                layer.norm1 = nn.Identity()
                layer.norm2 = nn.Identity()

    def forward_with_activations(self, x):
        """Return activations after each layer."""
        activations = [x.detach().clone()]
        for layer in self.layers:
            x = layer(x)
            activations.append(x.detach().clone())
        return activations


# Create models with and without layer normalization
d_model, n_heads, d_ff, n_layers = 64, 4, 256, 10

model_with_ln = StackedTransformerBlocks(
    d_model, n_heads, d_ff, n_layers, use_layernorm=True
)
model_without_ln = StackedTransformerBlocks(
    d_model, n_heads, d_ff, n_layers, use_layernorm=False
)

# Forward pass with larger initial variance to amplify divergence without LayerNorm
x = torch.randn(4, 16, d_model) * 2.0
acts_with_ln = model_with_ln.forward_with_activations(x)
acts_without_ln = model_without_ln.forward_with_activations(x)
Out[24]:
Visualization
Line plot showing stable activation mean near zero and std near one across 10 layers with LayerNorm.
Activation statistics with layer normalization across 10 transformer layers. The mean (blue) stays near zero and the standard deviation (red) remains stable around 1. This shows the consistent normalization that enables reliable training in deep networks.
Line plot showing activation mean and std that drift away from normalized values across 10 layers without LayerNorm.
Activation statistics without layer normalization across 10 transformer layers. Without normalization, activations may drift in mean and variance as they propagate through layers, a behavior that becomes more pronounced with larger networks or more training steps.

With layer normalization, activations maintain stable statistics throughout the network. Without it, activations can drift, though the effect depends heavily on initialization. During training, weight updates can shift activation statistics dramatically, so this stability helps keep them anchored.

The stability plots also reveal an important practical consequence of pre-norm design: the residual accumulation means the standard deviation at the output of the model can grow slightly over layers, even with layer normalization in place. This is because each layer adds its output to the residual stream without normalizing the stream itself; normalization only happens on the input to each sublayer. Well-designed pre-norm models account for this by including a final layer normalization before the output head. This ensures the last representation is in a well-defined scale before the projection to vocabulary logits.