Residual Connections › Practical Implementation

Michael BrenndoerferUpdated June 8, 20254 min read
Language AI HandbookAll chapters
Part 15 of 67Transformer Blocks
Chapter 103Residual Connections › Practical Implementation
Reading section 8 of 12 · Practical Implementation · 4 minSection 8 of 12 · 4 min

Book contents

Section 8 of 12 · 4 min read

Residual Connections › Practical ImplementationOne section · Sections in this chapter8 / 12 ↓
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

Having covered the theory, we now put it into practice. The following code creates a flexible residual block that supports all the variants we've discussed: pre-norm or post-norm configuration, fixed or learned scaling, and ReZero initialization.

In[27]:
Code
class FlexibleResidualBlock(nn.Module):
    """
    Flexible residual block supporting:
    - Pre-norm or post-norm
    - Optional residual scaling
    - ReZero initialization
    """

    def __init__(self, dim, norm_position="pre", scale=1.0, rezero=False):
        super().__init__()
        self.norm_position = norm_position
        self.rezero = rezero

        # Scaling factor
        if rezero:
            self.scale = nn.Parameter(torch.zeros(1))
        else:
            self.scale = scale

        # Sub-layer
        self.linear1 = nn.Linear(dim, dim * 4)
        self.activation = nn.GELU()
        self.linear2 = nn.Linear(dim * 4, dim)

        # Normalization
        self.norm = nn.LayerNorm(dim)

    def forward(self, x):
        if self.norm_position == "pre":
            # Pre-norm: normalize input, clean residual
            normalized = self.norm(x)
            residual = self.linear1(normalized)
            residual = self.activation(residual)
            residual = self.linear2(residual)
            return x + self.scale * residual
        else:
            # Post-norm: normalize after residual addition
            residual = self.linear1(x)
            residual = self.activation(residual)
            residual = self.linear2(residual)
            return self.norm(x + self.scale * residual)


# Compare configurations
dim = 256
prenorm = FlexibleResidualBlock(dim, norm_position="pre")
postnorm = FlexibleResidualBlock(dim, norm_position="post")
rezero = FlexibleResidualBlock(dim, norm_position="pre", rezero=True)
Out[28]:
Console
Flexible Residual Block Configurations
--------------------------------------------------
Pre-norm:
  Scale: 1.0000
  Input mean: 0.0062, std: 1.0020
  Output mean: 0.0065, std: 1.0227

Post-norm:
  Scale: 1.0000
  Input mean: 0.0062, std: 1.0020
  Output mean: 0.0000, std: 1.0000

ReZero:
  Scale: 0.0000
  Input mean: 0.0062, std: 1.0020
  Output mean: 0.0062, std: 1.0020

The output statistics reveal each configuration's character. Pre-norm and post-norm, both with scale 1.0, transform the input substantially. Notice how the output statistics differ from input. The key difference between them appears in the output distribution: post-norm produces output with statistics closer to zero mean and unit variance (the normalization's effect on the final output), while pre-norm allows more drift.

ReZero behaves distinctly: with scale 0.0, input and output are identical. The residual function computes something, but the zero multiplier suppresses it completely. This is exactly the "perfect identity initialization" property: at the start of training, before alpha increases, ReZero blocks pass information through unchanged.

Deep Network Test

The theoretical benefits of residual connections should manifest in concrete, measurable ways when we build deep networks. Let's construct 30-layer networks with and without residual connections and compare their gradient flow characteristics:

In[29]:
Code
def build_deep_network(n_layers, dim, use_residuals=True, norm="pre"):
    """Build a deep network with or without residual connections."""
    layers = []
    for _ in range(n_layers):
        if use_residuals:
            layers.append(FlexibleResidualBlock(dim, norm_position=norm))
        else:
            # Plain block without residual
            layers.append(
                nn.Sequential(
                    nn.LayerNorm(dim) if norm == "pre" else nn.Identity(),
                    nn.Linear(dim, dim * 4),
                    nn.GELU(),
                    nn.Linear(dim * 4, dim),
                    nn.LayerNorm(dim) if norm == "post" else nn.Identity(),
                )
            )
    return nn.Sequential(*layers)


def test_gradient_flow(network, dim):
    """Test if gradients flow through the network."""
    x = torch.randn(4, 32, dim, requires_grad=True)
    y = network(x)
    loss = y.sum()
    loss.backward()

    return {
        "output_norm": y.norm().item(),
        "gradient_norm": x.grad.norm().item(),
        "output_mean": y.mean().item(),
        "output_std": y.std().item(),
    }


# Compare 30-layer networks
dim = 128
n_layers = 30

torch.manual_seed(42)
residual_net = build_deep_network(n_layers, dim, use_residuals=True)
torch.manual_seed(42)
plain_net = build_deep_network(n_layers, dim, use_residuals=False)

residual_stats = test_gradient_flow(residual_net, dim)
plain_stats = test_gradient_flow(plain_net, dim)
Out[30]:
Console
Deep Network Comparison (30 layers)
--------------------------------------------------
Metric               |   Plain Network | Residual Network
--------------------------------------------------
output_norm          |         22.9232 |         189.8797
gradient_norm        |         63.5118 |         202.0742
output_mean          |         -0.0082 |           0.0353
output_std           |          0.1789 |           1.4831

The comparison reveals why residual connections have become ubiquitous in deep learning. At 30 layers of depth, the residual network maintains healthy gradient and output norms. The training signal reaches early parameters with sufficient magnitude to drive meaningful updates. The plain network shows different behavior in its gradient magnitudes, a sign of the compounding effects we analyzed earlier.

This gap widens dramatically with depth. At 50 layers, plain networks become nearly untrainable. At 100 layers, they're hopeless. Residual networks, in contrast, scale gracefully: models with 96, 128, or even 1000+ layers train successfully when equipped with skip connections. This scalability is why every major deep learning architecture since 2015, from ResNets to transformers to diffusion models, incorporates residual connections as a foundational element.