Lesson 11
Residual connections, normalization, and feed-forward layers
Introduction
A residual branch should modify an existing feature rather than erase it. If the old feature is 3 and the branch contributes 0.4, the residual result is 3.4 before later processing.
- Learning goal
Trace one complete transformer block through normalization, attention, residual paths, and position-wise feed-forward layers.
- Before you start
Attention, tensor shapes, linear layers, activation functions, averages, and basic normalization.
Lesson plan
- Name the four block parts and explain the job each one performs.
- Run one encoder layer while tracking sequence, batch, and feature axes.
- Add token and position information, then compare bidirectional and causal masking.
Why combine these parts?
Attention lets one token position collect information from other positions. That is only half of a transformer block. After information moves between positions, a feed-forward network transforms each position independently. Residual connections preserve an easier path for old information and gradients. Layer normalization keeps each token's feature values on a controlled scale. Stacking blocks repeats the pattern: communicate, transform, and stabilize.
The four parts
D.With batch-first layout, a block accepts and returns shape (B,T,D). Keeping this shape unchanged makes blocks easy to stack. The model dimension must divide evenly across the number of attention heads. For D=8 and two heads, each head works with four features.
Worked example: one encoder layer
Predict the shape: a residual addition combines a block input with the block result. Both must have shape (B,T,D). Attention may mix information across T, but it must return the same width D so element-by-element addition is defined.
nn.TransformerEncoderLayer provides the complete block. The word “encoder” here means it is bidirectional by default: every position may attend to every other position. We set dropout to zero so repeated evaluation is stable and easy to inspect.
import torch
from torch import nn
torch.manual_seed(11)
layer = nn.TransformerEncoderLayer(
d_model=8,
nhead=2,
dim_feedforward=16,
dropout=0.0,
batch_first=True,
)
x = torch.randn(2, 4, 8) # B=2, T=4, D=8
output = layer(x)
print(tuple(x.shape))
print(tuple(output.shape))
print(torch.isfinite(output).all().item())
# Expected:
# (2, 4, 8)
# (2, 4, 8)
# True
The block changes the values but preserves the shape. Internally, self-attention mixes positions, while the feed-forward network expands from 8 to 16 features and projects back to 8. Residual additions require matching shapes, which is why the external width remains 8.
Add token meaning and position
For one scalar feature, a residual step can look like 3 + 0.4 = 3.4. Removing the 3 forces the next layer to reconstruct the old signal from 0.4. If a residual addition fails, print both shapes before changing the model width.
Attention alone does not know that token 0 came before token 1. A transformer therefore adds positional information to token embeddings. The example below uses a learned position table. Its token vectors and position vectors both have shape (B,T,D) after broadcasting.
vocabulary_size = 6
maximum_length = 4
token_embedding = nn.Embedding(vocabulary_size, 8)
position_embedding = nn.Embedding(maximum_length, 8)
token_ids = torch.tensor([[0, 1, 2, 3], [3, 2, 1, 0]])
positions = torch.arange(token_ids.shape[1])
x = token_embedding(token_ids) + position_embedding(positions)
output = layer(x)
print(tuple(output.shape)) # Expected: (2, 4, 8)
The one-dimensional position tensor has shape (T,). Its embedding has shape (T,D), and PyTorch broadcasts it across the batch. Every sequence receives positions 0 through 3.
Bidirectional or causal?
An encoder-style task such as masked-token prediction can use both earlier and later context. A GPT-style next-token model must hide future positions with a causal mask. Without the mask, training becomes invalid because a position can read the answer from the target side.
causal_mask = nn.Transformer.generate_square_subsequent_mask(4)
causal_output = layer(x, src_mask=causal_mask)
print(tuple(causal_mask.shape)) # Expected: (4, 4)
print(tuple(causal_output.shape)) # Expected: (2, 4, 8)
A padding mask solves a different problem: it hides storage positions added to make variable-length sequences rectangular. Do not confuse future masking with padding masking.
Common pitfalls
d_modelmust be divisible bynhead.- Token embeddings need positional information; otherwise order is not represented directly.
- A GPT training block needs a causal mask. An encoder task usually does not.
batch_first=Truemeans(B,T,D); the default in some APIs may differ.- Dropout behaves differently in training and evaluation, so call
model.eval()for inference.
Try it
Create a layer with model dimension 12, three heads, and feed-forward width 24. Pass five-token sequences for a batch of four. What are the input, output, and causal-mask shapes?
Reveal the worked answer
layer = nn.TransformerEncoderLayer(
d_model=12, nhead=3, dim_feedforward=24,
dropout=0.0, batch_first=True,
)
x = torch.zeros(4, 5, 12)
mask = nn.Transformer.generate_square_subsequent_mask(5)
y = layer(x, src_mask=mask)
print(tuple(x.shape)) # Expected: (4, 5, 12)
print(tuple(y.shape)) # Expected: (4, 5, 12)
print(tuple(mask.shape)) # Expected: (5, 5)
Three heads are valid because 12 divides evenly by 3. The feed-forward width is internal and does not change the returned model dimension.
Recap
A transformer block combines self-attention, a position-wise MLP, residual paths, and layer normalization. Its external shape stays (B,T,D), which allows stacking. Token and position embeddings provide content and order. Choose masking based on the task: bidirectional context for encoder-style understanding, causal context for next-token generation.
Reference: PyTorch TransformerEncoderLayer documentation.