Feed-Forward Networks › The Position-Wise Feed-Forward Network

Michael BrenndoerferUpdated June 10, 20259 min read
Language AI HandbookAll chapters
Part 15 of 67Transformer Blocks
Chapter 107Feed-Forward Networks › The Position-Wise Feed-Forward Network
Reading section 2 of 13 · The Position-Wise Feed-Forward Network · 9 minSection 2 of 13 · 9 min

Book contents

Section 2 of 13 · 9 min read

Feed-Forward Networks › The Position-Wise Feed-Forward NetworkOne section · Sections in this chapter2 / 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

To understand why transformers need the feed-forward network, consider what attention alone provides. Self-attention computes weighted averages of value vectors, where the weights come from query-key similarities. This is powerful for gathering contextual information: a token can attend to any position in the sequence and assemble a representation that reflects its surrounding context. But no matter how sophisticated the attention patterns, the operation remains a convex combination of value vectors. Each output is literally a weighted sum of inputs. This is a linear function.

Linear functions have severe limitations for language understanding. They can only rotate and scale the input space, then translate it. They cannot learn the curved decision boundaries that distinguish "bank" (financial institution) from "bank" (riverbank). They cannot capture the complex feature interactions that tell you whether "not bad" is negative or positive in context. They cannot represent the conditional relationships that make "the cat sat on the mat" grammatically fine but "the cat sat on the cat" slightly odd. For a model to approximate arbitrary functions of language, it needs the ability to carve up representation space in nonlinear ways.

The key insight is that any sufficiently wide network with a nonlinear activation function can approximate arbitrary continuous functions. This is the universal approximation theorem, a foundational result in neural network theory. The FFN uses this result by introducing a nonlinear activation between two linear layers. The first layer projects the input into a higher-dimensional space. The activation function applies a nonlinearity. The second layer projects back. The result is a function that can approximate far more complex relationships than any linear operation could.

There is a critical design choice embedded in how the FFN is applied: instead of mixing information across positions like attention does, the FFN processes each position independently. If your sequence has 100 tokens, the FFN applies the same transformation to each of those 100 representations separately, using identical weights for all positions. This is called a position-wise or position-independent transformation, and it fundamentally shapes the FFN's role in the transformer.

Position-Wise Transformation

A position-wise operation applies the same function to each position in a sequence independently. The function's parameters are shared across positions, but the inputs and outputs at each position don't interact with each other.

This division of labor is elegant. Attention handles inter-position communication: it routes information between tokens, allowing the model to build representations that depend on context. The FFN handles intra-position computation: it transforms each position's representation using a learned nonlinear function. By separating these concerns, the transformer achieves both contextual awareness (from attention) and expressive power (from the FFN).

Why share the same network across all positions? Two reasons justify this choice. First, language exhibits translation invariance in a deep sense: the grammatical patterns that help understand "the cat" are equally useful whether those words appear at the beginning, middle, or end of a sentence. A transformation that converts a noun phrase's representation into something more useful should work wherever that noun phrase appears. Second, parameter sharing dramatically reduces model size. Instead of learning separate networks for each of the potentially thousands of positions in a sequence, we learn one network that generalizes across all positions. This is a major efficiency win.

Think of the position-wise FFN as a shared "dictionary of transformations." After attention has looked up what each position means in context, the FFN looks up how to transform that meaning into a richer representation, using the same dictionary regardless of where the position appears in the sequence.

The Two-Layer Architecture

With this motivation in place, let's examine what the FFN computes. The architecture is surprisingly simple: two linear transformations with a nonlinear activation function sandwiched between them. But this simplicity is deceptive. The expand-transform-contract structure enables remarkably expressive transformations, and the specific choice of expansion factor shapes the model's capacity in ways that affect both learning and efficiency.

Consider a single token's representation: a vector xx with dmodeld_{\text{model}} dimensions. This vector encodes everything the model currently knows about that token in its context, having passed through however many transformer layers preceded this one. The FFN transforms this vector in three stages:

  1. Expand: Project xx into a higher-dimensional space
  2. Transform: Apply nonlinearity to enable learning of curved boundaries
  3. Contract: Project back to the original dimension

The complete formula captures all three stages in one expression. Given an input vector x∈Rdmodelx \in \mathbb{R}^{d_{\text{model}}}, the FFN computes:

FFN(x)=σ(xW1+b1)W2+b2\text{FFN}(x) = \sigma(xW_1 + b_1)W_2 + b_2

where:

  • x∈Rdmodelx \in \mathbb{R}^{d_{\text{model}}}: the input vector for a single position (the token's representation from the previous layer)
  • W1∈Rdmodel×dffW_1 \in \mathbb{R}^{d_{\text{model}} \times d_{ff}}: the first projection matrix, which expands the dimension from dmodeld_{\text{model}} to dffd_{ff}
  • b1∈Rdffb_1 \in \mathbb{R}^{d_{ff}}: the first bias vector, added after the first linear transformation
  • σ\sigma: a nonlinear activation function (e.g., ReLU, GELU) applied element-wise to the hidden representation
  • W2∈Rdff×dmodelW_2 \in \mathbb{R}^{d_{ff} \times d_{\text{model}}}: the second projection matrix, which contracts the dimension back from dffd_{ff} to dmodeld_{\text{model}}
  • b2∈Rdmodelb_2 \in \mathbb{R}^{d_{\text{model}}}: the second bias vector, added to produce the final output
  • dffd_{ff}: the hidden (or intermediate) dimension, typically set to 4×dmodel4 \times d_{\text{model}}

To understand this formula, let's read it from the inside out, following the order of operations.

Stage 1: The Expansion (xW1+b1xW_1 + b_1)

The input vector xx is multiplied by a weight matrix W1W_1 and shifted by a bias vector b1b_1. This is a standard linear transformation, but with a twist: the output dimension is larger than the input dimension. If xx has 512 dimensions, the result might have 2048 dimensions. We're projecting into a higher-dimensional space where the data is easier to manipulate. In this expanded space, the model has more "room" to organize features before applying nonlinearity.

Why does projecting to higher dimensions help? Think of it this way: in the original dmodeld_{\text{model}}-dimensional space, the token's representation may conflate many different features in a compressed form. By expanding to dffd_{ff} dimensions, the first layer can disentangle these features, spreading them across a larger space where they are more separable. The nonlinearity can then act on these individual features independently, creating the curved boundaries that linear operations cannot.

Stage 2: The Nonlinearity (σ(⋅)\sigma(\cdot))

The activation function σ\sigma is applied element-wise to the expanded representation. Common choices include ReLU (which zeros out negative values) and GELU (a smoother alternative that we'll examine in detail in the next chapter). This is where the magic happens: the nonlinearity allows the network to learn curved decision boundaries that would be impossible with linear transformations alone.

The element-wise nature of the activation matters. Because it operates on each hidden dimension independently, it creates a kind of gating: some hidden dimensions activate strongly and contribute to the output, while others are suppressed to near zero. Different input patterns activate different subsets of hidden dimensions, which is exactly how the network implements different transformations for different types of inputs.

Stage 3: The Contraction ((⋅)W2+b2(\cdot)W_2 + b_2)

Finally, the transformed representation is projected back to the original dimension via weight matrix W2W_2 and bias b2b_2. The output has the same dimensionality as the input, which is essential for the residual connection that adds the FFN output back to its input. The second linear layer combines the activated hidden dimensions into a compressed dmodeld_{\text{model}}-dimensional output, essentially summarizing the computation done in the expanded space.

Why does this formula make sense? Notice that without the nonlinearity σ\sigma, the composition of two linear maps is itself a linear map: xW1W2+constx W_1 W_2 + \text{const} is just another linear function of xx. The nonlinearity is the only thing that prevents the two layers from collapsing into a single linear layer. The biases also allow the network to shift the activation function's operating range, making it easier to learn the right threshold for each hidden dimension.

The expansion ratio dff/dmodeld_{ff} / d_{\text{model}} is a key hyperparameter. The original transformer used a 4x expansion: for dmodel=512d_{\text{model}} = 512, the hidden dimension was dff=2048d_{ff} = 2048. This ratio has become a de facto standard, though modern architectures sometimes use different values, especially when combined with gated variants, as we'll explore later.

Implementation

With the formula understood, translating it to code reveals its simplicity. The entire FFN is just two matrix multiplications with a nonlinearity in between:

In[3]:
Code
import numpy as np


def relu(x):
    """ReLU activation function."""
    return np.maximum(0, x)


def ffn(x, W1, b1, W2, b2, activation=relu):
    """
    Position-wise feed-forward network.

    Args:
        x: Input tensor, shape (n, d_model) or (d_model,)
        W1: First layer weights, shape (d_model, d_ff)
        b1: First layer bias, shape (d_ff,)
        W2: Second layer weights, shape (d_ff, d_model)
        b2: Second layer bias, shape (d_model,)
        activation: Nonlinear activation function

    Returns:
        Output tensor, same shape as x
    """
    hidden = activation(x @ W1 + b1)
    output = hidden @ W2 + b2
    return output


# Example dimensions (from original transformer)
d_model = 512  # Model dimension
d_ff = 2048  # Hidden dimension (4x expansion)

# Initialize weights with Xavier/Glorot initialization
W1 = np.random.randn(d_model, d_ff) * np.sqrt(2.0 / (d_model + d_ff))
b1 = np.zeros(d_ff)
W2 = np.random.randn(d_ff, d_model) * np.sqrt(2.0 / (d_ff + d_model))
b2 = np.zeros(d_model)

# Process a single position
x_single = np.random.randn(d_model)
y_single = ffn(x_single, W1, b1, W2, b2)
Out[4]:
Console
Single position FFN:
  Input shape:  (512,)
  Output shape: (512,)
  Input norm:   22.2545
  Output norm:  13.1094

The FFN preserves the dimensionality of its input: a 512-dimensional vector goes in, and a 512-dimensional vector comes out. This is essential for the residual connection that adds the FFN output back to its input. The residual connection (which adds the original input xx to the FFN output) requires both tensors to have the same shape, so the FFN's dimension-preserving property is architecturally necessary rather than merely convenient.

Notice that we initialize the weights using Xavier/Glorot initialization, scaling by 2/(din+dout)\sqrt{2 / (d_{in} + d_{out})}. This initialization keeps the variance of activations stable across layers, preventing the exploding or vanishing gradients that plagued deep networks before careful initialization became standard practice. The biases are initialized to zero, which is standard; the weight matrices carry all the initial structure.