Encoder Architecture › Building an Encoder Layer
Language AI HandbookAll chapters Encoder Architecture › Building an Encoder LayerOne section · Sections in this chapter5 / 13 ↓
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 understood the attention mechanism, we can now assemble a complete encoder layer. But attention alone isn't enough. Deep networks need architectural support to train successfully, and encoder layers combine several components that work together to enable learning.
An encoder layer has two major sublayers, each solving a different problem:
-
Multi-head self-attention enables tokens to gather information from each other. This is where bidirectional context mixing happens.
-
Feed-forward network (FFN) processes each token independently through a nonlinear transformation. This adds capacity that pure attention lacks.
Both sublayers are wrapped with two additional mechanisms that make deep stacking possible:
-
Residual connections add the input directly to the output of each sublayer. This creates "gradient highways" that allow learning signals to flow through many layers without vanishing.
-
Layer normalization stabilizes the distribution of activations, preventing them from exploding or collapsing as they pass through many transformations.
The modern pre-norm configuration places normalization before each sublayer rather than after. This ordering provides cleaner gradient flow and has become the standard for deep transformers. The complete computation unfolds in two stages:
Stage 1: Attention sublayer
First, we normalize the input . Then attention computes context-aware representations. Finally, we add the original input back (the residual connection). The result blends the original representation with information gathered from other positions.
Stage 2: Feed-forward sublayer
The same pattern repeats: normalize, transform, add residual. The FFN applies the same transformation independently to each token position, adding nonlinearity and additional learnable capacity.
Together, these stages form the encoder layer's output:
where:
- : input to the encoder layer, containing token representations
- : normalizes activations to have zero mean and unit variance per token
- : bidirectional multi-head self-attention (no causal mask)
- : position-wise feed-forward network with nonlinear activation
- : intermediate representation after the attention sublayer
- : final output of the encoder layer, same shape as input
Notice that the output has the same shape as the input . This dimensional consistency is what allows us to stack encoder layers: the output of one layer becomes the input to the next, with each layer progressively refining the representations.
Let's implement each component, starting with the building blocks:
class LayerNorm:
"""Layer Normalization."""
def __init__(self, dim, eps=1e-6):
self.eps = eps
self.gamma = np.ones(dim)
self.beta = np.zeros(dim)
def __call__(self, x):
mean = np.mean(x, axis=-1, keepdims=True)
var = np.var(x, axis=-1, keepdims=True)
x_norm = (x - mean) / np.sqrt(var + self.eps)
return self.gamma * x_norm + self.beta
class MultiHeadAttention:
"""Multi-head self-attention for encoder (bidirectional)."""
def __init__(self, d_model, n_heads):
self.d_model = d_model
self.n_heads = n_heads
self.d_head = d_model // n_heads
# Initialize projections
scale = np.sqrt(2.0 / (d_model + d_model))
self.W_q = np.random.randn(d_model, d_model) * scale
self.W_k = np.random.randn(d_model, d_model) * scale
self.W_v = np.random.randn(d_model, d_model) * scale
self.W_o = np.random.randn(d_model, d_model) * scale
def __call__(self, x, mask=None):
"""
Apply multi-head attention.
Args:
x: Input of shape (seq_len, d_model)
mask: Optional padding mask
Returns:
Output of shape (seq_len, d_model)
"""
seq_len = x.shape[0]
# Project to Q, K, V
Q = x @ self.W_q
K = x @ self.W_k
V = x @ self.W_v
# Reshape for multi-head: (seq_len, n_heads, d_head)
Q = Q.reshape(seq_len, self.n_heads, self.d_head)
K = K.reshape(seq_len, self.n_heads, self.d_head)
V = V.reshape(seq_len, self.n_heads, self.d_head)
# Transpose for attention: (n_heads, seq_len, d_head)
Q = Q.transpose(1, 0, 2)
K = K.transpose(1, 0, 2)
V = V.transpose(1, 0, 2)
# Compute attention scores: (n_heads, seq_len, seq_len)
scores = np.matmul(Q, K.transpose(0, 2, 1)) / np.sqrt(self.d_head)
# Apply mask if provided (for padding)
if mask is not None:
scores = scores + mask
# Note: No causal mask! This is bidirectional attention
weights = softmax(scores, axis=-1)
# Apply attention: (n_heads, seq_len, d_head)
attended = np.matmul(weights, V)
# Reshape back: (seq_len, d_model)
attended = attended.transpose(1, 0, 2).reshape(seq_len, self.d_model)
# Output projection
output = attended @ self.W_o
return output, weights
class FeedForward:
"""Position-wise feed-forward network."""
def __init__(self, d_model, d_ff):
scale1 = np.sqrt(2.0 / (d_model + d_ff))
scale2 = np.sqrt(2.0 / (d_ff + d_model))
self.W1 = np.random.randn(d_model, d_ff) * scale1
self.b1 = np.zeros(d_ff)
self.W2 = np.random.randn(d_ff, d_model) * scale2
self.b2 = np.zeros(d_model)
def __call__(self, x):
"""Apply two-layer FFN with GELU activation."""
hidden = x @ self.W1 + self.b1
# GELU activation
hidden = (
0.5
* hidden
* (
1
+ np.tanh(np.sqrt(2 / np.pi) * (hidden + 0.044715 * hidden**3))
)
)
return hidden @ self.W2 + self.b2The feed-forward network uses GELU (Gaussian Error Linear Unit) activation, which has become the standard for transformer models. Unlike ReLU which hard-thresholds at zero, GELU provides smooth gating that depends on the input's magnitude:

Let's visualize how layer normalization transforms the activation distributions:


Now we assemble these components into a complete encoder layer:
class EncoderLayer:
"""
Single transformer encoder layer.
Uses pre-norm configuration with bidirectional self-attention.
"""
def __init__(self, d_model, n_heads, d_ff):
"""
Args:
d_model: Model dimension
n_heads: Number of attention heads
d_ff: Feed-forward hidden dimension
"""
self.d_model = d_model
# Sublayer 1: Multi-head self-attention
self.norm1 = LayerNorm(d_model)
self.attention = MultiHeadAttention(d_model, n_heads)
# Sublayer 2: Feed-forward network
self.norm2 = LayerNorm(d_model)
self.ffn = FeedForward(d_model, d_ff)
def __call__(self, x, mask=None):
"""
Forward pass through encoder layer.
Args:
x: Input of shape (seq_len, d_model)
mask: Optional padding mask
Returns:
Output of shape (seq_len, d_model)
"""
# Sublayer 1: Attention with residual
normed = self.norm1(x)
attended, attn_weights = self.attention(normed, mask)
x = x + attended # Residual connection
# Sublayer 2: FFN with residual
normed = self.norm2(x)
ffn_out = self.ffn(normed)
x = x + ffn_out # Residual connection
return x, attn_weights
# Test the encoder layer
d_model = 256
n_heads = 8
d_ff = 1024
seq_len = 10
layer = EncoderLayer(d_model, n_heads, d_ff)
x = np.random.randn(seq_len, d_model)
output, attn_weights = layer(x)Encoder Layer Test ---------------------------------------- Configuration: Model dimension (d_model): 256 Attention heads: 8 Head dimension: 32 FFN dimension: 1024 Input shape: (10, 256) Output shape: (10, 256) Attention weights shape: (8, 10, 10) Input norm: 51.3758 Output norm: 62.4308
The encoder layer preserves the input shape: a sequence of tokens with -dimensional representations goes in, and an identically shaped sequence comes out. The attention weights have shape (n_heads, seq_len, seq_len), showing that each head computes its own full attention matrix over all position pairs.
Visualizing Multi-Head Attention Patterns
Different attention heads learn to focus on different aspects of the input. Some might attend to adjacent tokens (capturing local syntax), while others span long distances (capturing semantic relationships). Let's visualize how the 8 attention heads in our encoder layer distribute their attention differently:








The variation across heads illustrates why multi-head attention is so powerful. No single head needs to capture all relationships. The ensemble of heads provides coverage across local patterns, position-specific attention, and long-range dependencies. During training, each head specializes for patterns that help the overall task.