§3.4Feed-Forward, Layer Normalization & Positional Embeddings

Part I NLPT pp. 14–18 · ~8 min read

  • feed-forward layer
  • gelu
  • layer normalization
  • skip connection
  • post-layer normalization
  • pre-layer normalization
  • positional embedding
  • permutation equivariant

Attention is only half an encoder layer. This section adds the other half — the feed-forward network — plus the two tricks that let us stack layers deep (layer normalization + skip connections) and the fix for attention’s order-blindness (positional embeddings).

The feed-forward layer

The second sublayer is a plain two-layer network — but a position-wise one: it processes each token embedding independently, the same MLP applied at every position. (Some call it a 1-D convolution with kernel size 1.)

class FeedForward(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.linear_1 = nn.Linear(config.hidden_size, config.intermediate_size)
        self.linear_2 = nn.Linear(config.intermediate_size, config.hidden_size)
        self.gelu = nn.GELU()
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

    def forward(self, x):
        x = self.linear_1(x)
        x = self.gelu(x)
        x = self.linear_2(x)
        x = self.dropout(x)
        return x

A rule of thumb is to make the hidden size of the first layer four times the embedding size, with a GELU activation in between:

attn output[1,5,768]
[1,5,3072]
[1,5,3072]
[1,5,768]

Click an operation to see what it does. A highlighted dimension is one that just changed.

The position-wise feed-forward network: expand 4×, non-linearity, project back.

Note — why “position-wise” matters

An nn.Linear acts on the last dimension and treats every earlier dimension independently. So passing shape [batch, seq_len, hidden] applies the same network to every token separately — no mixing across positions. That’s the division of labour in a transformer layer: attention mixes across tokens; the feed-forward layer transforms each token on its own. This sublayer holds most of the parameters and is what gets scaled up most aggressively (Ch. 11).

Layer normalization and skip connections

To train a deep stack we need two standard tricks. Layer normalization rescales each input to zero mean and unit variance. A skip connection (residual) passes a tensor to the next layer unchanged and adds it to the processed tensor, giving gradients a clean path backward.

The only real choice is where to put the norm. Step through the two options:

Figure 3-6 (recreated). Two places to put layer normalization. Step between them.
Multi-headattention+LayerNormPosition-wiseFF NN+LayerNormnorm comes AFTER each add

1Post layer normalization

The original Transformer arrangement: the sublayer runs, its output is added to the skip connection, and THEN layer norm is applied. Powerful, but the gradients can diverge early in training — so you often need learning-rate warm-up.

step 1 / 2
  • Post-LN (original paper) puts the norm between the skip connections; gradients can diverge, so it usually needs learning-rate warm-up .
  • Pre-LN puts the norm inside the skip span; it’s much more stable and is what we’ll use.

With that decided, a full encoder layer is just these blocks wired together:

class TransformerEncoderLayer(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.layer_norm_1 = nn.LayerNorm(config.hidden_size)
        self.layer_norm_2 = nn.LayerNorm(config.hidden_size)
        self.attention = MultiHeadAttention(config)
        self.feed_forward = FeedForward(config)

    def forward(self, x):
        hidden_state = self.layer_norm_1(x)
        x = x + self.attention(hidden_state)                 # skip connection
        x = x + self.feed_forward(self.layer_norm_2(x))      # skip connection
        return x

Click through the assembled layer — note that its output shape equals its input shape, which is exactly what lets us stack N of them:

encoder block× 6
x (same shape)
x (embeddings)

Click a sublayer to see what it does. Data flows bottom → top; the output has the same shape as the input.

The assembled pre-LN encoder layer. Output shape = input shape, so N of these stack.
encoder_layer = TransformerEncoderLayer(config)
inputs_embeds.shape, encoder_layer(inputs_embeds).size()
# (torch.Size([1, 5, 768]), torch.Size([1, 5, 768]))

Positional embeddings

There’s a catch: as built, the encoder layer is totally invariant to token order — attention and the feed-forward layer are permutation equivariant , so “dog bites man” and “man bites dog” would come out identical. We fix this by adding a positional embedding — a position-dependent vector — to each token embedding.

The simplest, most common approach is a learnable table, indexed by position instead of token id, summed with the token embeddings:

class Embeddings(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.token_embeddings = nn.Embedding(config.vocab_size, config.hidden_size)
        self.position_embeddings = nn.Embedding(config.max_position_embeddings,
                                                config.hidden_size)
        self.layer_norm = nn.LayerNorm(config.hidden_size, eps=1e-12)
        self.dropout = nn.Dropout()

    def forward(self, input_ids):
        seq_length = input_ids.size(1)
        position_ids = torch.arange(seq_length, dtype=torch.long).unsqueeze(0)
        token_embeddings = self.token_embeddings(input_ids)
        position_embeddings = self.position_embeddings(position_ids)
        embeddings = token_embeddings + position_embeddings   # just add them
        embeddings = self.layer_norm(embeddings)
        return self.dropout(embeddings)

An alternative uses fixed sinusoidal patterns — each dimension is a sine or cosine of a different frequency, so every position gets a unique, smooth fingerprint (and no parameters to learn):

embedding dimension →position →pos 0, dim 0: 0.00pos 0, dim 1: 1.00pos 0, dim 2: 0.00pos 0, dim 3: 1.00pos 0, dim 4: 0.00pos 0, dim 5: 1.00pos 0, dim 6: 0.00pos 0, dim 7: 1.00pos 0, dim 8: 0.00pos 0, dim 9: 1.00pos 0, dim 10: 0.00pos 0, dim 11: 1.00pos 0, dim 12: 0.00pos 0, dim 13: 1.00pos 0, dim 14: 0.00pos 0, dim 15: 1.00pos 1, dim 0: 0.84pos 1, dim 1: 0.54pos 1, dim 2: 0.31pos 1, dim 3: 0.95pos 1, dim 4: 0.10pos 1, dim 5: 1.00pos 1, dim 6: 0.03pos 1, dim 7: 1.00pos 1, dim 8: 0.01pos 1, dim 9: 1.00pos 1, dim 10: 0.00pos 1, dim 11: 1.00pos 1, dim 12: 0.00pos 1, dim 13: 1.00pos 1, dim 14: 0.00pos 1, dim 15: 1.00pos 2, dim 0: 0.91pos 2, dim 1: -0.42pos 2, dim 2: 0.59pos 2, dim 3: 0.81pos 2, dim 4: 0.20pos 2, dim 5: 0.98pos 2, dim 6: 0.06pos 2, dim 7: 1.00pos 2, dim 8: 0.02pos 2, dim 9: 1.00pos 2, dim 10: 0.01pos 2, dim 11: 1.00pos 2, dim 12: 0.00pos 2, dim 13: 1.00pos 2, dim 14: 0.00pos 2, dim 15: 1.00pos 3, dim 0: 0.14pos 3, dim 1: -0.99pos 3, dim 2: 0.81pos 3, dim 3: 0.58pos 3, dim 4: 0.30pos 3, dim 5: 0.96pos 3, dim 6: 0.09pos 3, dim 7: 1.00pos 3, dim 8: 0.03pos 3, dim 9: 1.00pos 3, dim 10: 0.01pos 3, dim 11: 1.00pos 3, dim 12: 0.00pos 3, dim 13: 1.00pos 3, dim 14: 0.00pos 3, dim 15: 1.00pos 4, dim 0: -0.76pos 4, dim 1: -0.65pos 4, dim 2: 0.95pos 4, dim 3: 0.30pos 4, dim 4: 0.39pos 4, dim 5: 0.92pos 4, dim 6: 0.13pos 4, dim 7: 0.99pos 4, dim 8: 0.04pos 4, dim 9: 1.00pos 4, dim 10: 0.01pos 4, dim 11: 1.00pos 4, dim 12: 0.00pos 4, dim 13: 1.00pos 4, dim 14: 0.00pos 4, dim 15: 1.00pos 5, dim 0: -0.96pos 5, dim 1: 0.28pos 5, dim 2: 1.00pos 5, dim 3: -0.01pos 5, dim 4: 0.48pos 5, dim 5: 0.88pos 5, dim 6: 0.16pos 5, dim 7: 0.99pos 5, dim 8: 0.05pos 5, dim 9: 1.00pos 5, dim 10: 0.02pos 5, dim 11: 1.00pos 5, dim 12: 0.00pos 5, dim 13: 1.00pos 5, dim 14: 0.00pos 5, dim 15: 1.00pos 6, dim 0: -0.28pos 6, dim 1: 0.96pos 6, dim 2: 0.95pos 6, dim 3: -0.32pos 6, dim 4: 0.56pos 6, dim 5: 0.83pos 6, dim 6: 0.19pos 6, dim 7: 0.98pos 6, dim 8: 0.06pos 6, dim 9: 1.00pos 6, dim 10: 0.02pos 6, dim 11: 1.00pos 6, dim 12: 0.01pos 6, dim 13: 1.00pos 6, dim 14: 0.00pos 6, dim 15: 1.00pos 7, dim 0: 0.66pos 7, dim 1: 0.75pos 7, dim 2: 0.80pos 7, dim 3: -0.60pos 7, dim 4: 0.64pos 7, dim 5: 0.76pos 7, dim 6: 0.22pos 7, dim 7: 0.98pos 7, dim 8: 0.07pos 7, dim 9: 1.00pos 7, dim 10: 0.02pos 7, dim 11: 1.00pos 7, dim 12: 0.01pos 7, dim 13: 1.00pos 7, dim 14: 0.00pos 7, dim 15: 1.00pos 8, dim 0: 0.99pos 8, dim 1: -0.15pos 8, dim 2: 0.57pos 8, dim 3: -0.82pos 8, dim 4: 0.72pos 8, dim 5: 0.70pos 8, dim 6: 0.25pos 8, dim 7: 0.97pos 8, dim 8: 0.08pos 8, dim 9: 1.00pos 8, dim 10: 0.03pos 8, dim 11: 1.00pos 8, dim 12: 0.01pos 8, dim 13: 1.00pos 8, dim 14: 0.00pos 8, dim 15: 1.00pos 9, dim 0: 0.41pos 9, dim 1: -0.91pos 9, dim 2: 0.29pos 9, dim 3: -0.96pos 9, dim 4: 0.78pos 9, dim 5: 0.62pos 9, dim 6: 0.28pos 9, dim 7: 0.96pos 9, dim 8: 0.09pos 9, dim 9: 1.00pos 9, dim 10: 0.03pos 9, dim 11: 1.00pos 9, dim 12: 0.01pos 9, dim 13: 1.00pos 9, dim 14: 0.00pos 9, dim 15: 1.00blue = +1orange = −1
Sinusoidal positional encoding for 10 positions × 16 dimensions. Left-hand dimensions cycle quickly, right-hand ones slowly — together they name each position uniquely.

The three main families:

Three ways to encode position. Click a cell for detail.
LearnableAbsolute (sinusoidal)Relative
HowAn embedding table indexed by positionFixed sine/cosine patternsEncode pairwise distances in attention
Best whenthe pretraining corpus is largedata is limitedrelative position matters most
ExampleBERT, GPTthe original TransformerDeBERTa
Dotted-underlined cells have explanations — click one.

Putting the embeddings and the encoder layers together gives the full encoder:

class TransformerEncoder(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.embeddings = Embeddings(config)
        self.layers = nn.ModuleList([TransformerEncoderLayer(config)
                                     for _ in range(config.num_hidden_layers)])

    def forward(self, x):
        x = self.embeddings(x)
        for layer in self.layers:
            x = layer(x)
        return x
# encoder(inputs.input_ids).size()  ->  torch.Size([1, 5, 768])

The encoder emits one hidden state per token — a flexible output we can adapt for masked-language-modeling, question answering (Ch. 7), or, next, sequence classification. Let’s attach a head and meet the decoder.

Check yourself: feed-forward, norm, and positions

1.What makes the feed-forward sublayer “position-wise”?

2.In the feed-forward layer, what is the typical hidden (intermediate) size, and why does it matter?

3.What is the difference between post-layer-norm and pre-layer-norm?

4.Why must positional embeddings be added at all?

5.How are token embeddings and positional embeddings combined in the standard (learnable) approach?

6.When are fixed sinusoidal positional encodings especially attractive over learnable ones?

6 questions