# 3.4 前馈网络：Transformer 的“记忆层”

Transformer 每层中除了注意力子层外，还有一个**逐位置前馈网络**（Position-wise Feed-Forward Network，FFN）。这个组件经常被忽视，但它在 Transformer 中扮演着不可或缺的角色。

## 3.4.1 结构与计算

前馈网络的结构非常简洁——两层全连接网络，中间夹一个非线性激活函数：

$$\text{FFN}(x) = \sigma(x W\_1 + b\_1) W\_2 + b\_2$$

这里把单个位置的表示 $$x$$ 视为行向量，$$W\_1 \in \mathbb{R}^{d\_{\text{model}} \times d\_{ff}}$$，$$W\_2 \in \mathbb{R}^{d\_{ff} \times d\_{\text{model}}}$$。原始 Transformer base 模型使用 ReLU 激活函数 $$\sigma(x) = \max(0, x)$$，中间层维度 $$d\_{ff} = 2048$$，是 $$d\_{\text{model}} = 512$$ 的 4 倍（big 模型则为 $$d\_{ff} = 4096$$）。

“逐位置”意味着**同一个 FFN 独立地应用于序列中的每个位置**，各位置之间不共享信息。这与注意力层形成互补：注意力层负责位置之间的信息交互，FFN 负责对每个位置的信息进行独立变换。

下图展示了 FFN 的沙漏结构——先升维以扩展表达空间，经过非线性变换后再降回原始维度：

{% @mermaid/diagram content="flowchart LR
A\["输入<br/>d\_model = 512"] --> B\["W₁: 升维<br/>+ 激活函数 σ"]
B --> C\["中间层<br/>d\_ff = 2048<br/>(4 倍扩展)"]
C --> D\["W₂: 降维"]
D --> E\["输出<br/>d\_model = 512"]" %}

图 3-3：FFN 的沙漏结构——先升维再降维，中间层提供更大的变换容量

## 3.4.2 为什么需要前馈网络

一个自然的问题是：**注意力机制已经能够捕捉全局依赖了，为什么还需要 FFN？**

我们可以从**直觉分工**和**数学原理**两个维度来回答这个问题。

**1. 直觉层面的分工：信息收集（计算“怎么联系”） vs. 知识加工（计算“这是什么”）**

注意力机制其实是在 **整理输入信息**。它负责在不同的 Token 之间建立联系，计算每个位置需要关注上下文中哪些部分，并将这些分散的全局信息汇聚到当前 Token 的向量表示中。这是一种 **全局的信息路由与组合**。

然而，仅仅把信息“搬运”和“混合”在一起是不够的。当当前 Token 收集到了充足的全文上下文后，它还需要对这些信息进行 **深度的内部加工转化**，并与模型内部记忆的事实知识进行碰撞。FFN 扮演的就是这个“内部加工者”的角色。它不再去看其它位置，而是只盯着当前这个已经满载全文信息的 Token，结合模型内部的知识进行 **非线性推理**。

打个比方：

* **注意力层** 就像是“开会交流”，每个单词都在环顾四周，听取其他单词的意见，把相关的语境收集到自己身上。
* **前馈网络（FFN）** 就像是“闭门思考”，每个单词回到自己的工位，根据刚才收集到的全盘信息，结合大脑里原本记忆的知识，独立进行深加工，得出新的结论（更新自身的表示，为最终预测下一个 Token 做准备）。

**2. 数学层面的必要性：逐位置非线性与通道混合**

从数学计算上看，自注意力的输出是值向量（Value）的加权和；但注意力权重由 $$QK^T$$ 经过 Softmax 得到，本身包含非线性和输入依赖。因此，不能简单把只有自注意力的多层网络说成会等价塌缩为单层线性变换。

FFN 的关键作用是在每个位置上提供额外的**非线性变换**和**通道混合**：$$W\_1$$ 将表示投影到更宽的中间维度，激活函数进行选择性响应，$$W\_2$$ 再把这些响应组合回模型维度。它与注意力层的跨位置信息路由互补，共同提升 Transformer 的表达能力。

## 3.4.3 FFN 作为“记忆层”的直觉

近年来的研究揭示了 FFN 的另一重要角色：**它是 Transformer 存储事实知识的主要位置。**

Geva 等人（2021 年）的研究表明，FFN 的第一层（$$W\_1$$）可以被视为一组“键”，在上述右乘约定下每一列对应一种激活模式；第二层（$$W\_2$$）对应这些模式关联的“值”——即具体的知识内容。这种视角下，FFN 的工作方式类似于一个**键值存储**：

1. 第一层通过矩阵乘法和激活函数选择性地“激活”某些模式
2. 第二层将激活的模式映射为特定的输出

实验证据支持了这一解释。当研究者编辑 FFN 中的特定参数时，能够精确地修改模型存储的事实知识（如“埃菲尔铁塔在巴黎”→“埃菲尔铁塔在伦敦”）。

## 3.4.4 中间层维度为什么是 4 倍

原始 Transformer 中，FFN 中间层维度 $$d\_{ff}$$ 是 $$d\_{\text{model}}$$ 的 4 倍（$$d\_{ff} = 4 \times d\_{\text{model}}$$）。这个比例的选择基于以下考量：

**扩展再压缩的设计模式**：先将表示投影到更高维度的空间（$$d\_{\text{model}} \rightarrow d\_{ff}$$），在高维空间中进行非线性变换和特征选择，然后压缩回原始维度（$$d\_{ff} \rightarrow d\_{\text{model}}$$）。高维空间提供了更多的“容量”来存储和处理信息。

**参数效率的权衡**：FFN 的参数量为 $$2 \times d\_{\text{model}} \times d\_{ff}$$。4 倍扩展在增加计算能力的同时，参数量仍然可控。实验表明，进一步增大 $$d\_{ff}$$ 的收益递减。

现代大语言模型通常保持了这个约 4 倍的比例，但激活函数从 ReLU 演化为 GELU（Gaussian Error Linear Unit）或 SiLU/Swish——这些更平滑的激活函数在实践中表现更好。此外，Llama 等模型采用了 **SwiGLU**（Gated Linear Unit 的变体），它将 FFN 扩展为三个矩阵，在同等参数量下获得更强的表达能力。

以下代码对比了三种激活函数在相同输入下的行为差异，帮助直观理解从 ReLU 到 SwiGLU 的演进：

```python
import torch
import torch.nn as nn
import torch.nn.functional as F

d_model, d_ff = 8, 32
torch.manual_seed(42)
x = torch.randn(1, d_model)  # 单个位置的输入

# ---- ReLU FFN（原始 Transformer）----
W1 = torch.randn(d_model, d_ff)
W2 = torch.randn(d_ff, d_model)
relu_out = F.relu(x @ W1) @ W2
print("ReLU FFN 输出:", torch.round(relu_out * 1000) / 1000)
# ReLU 将所有负值"硬截断"为 0
print("ReLU 激活后零值比例:", (F.relu(x @ W1) == 0).float().mean().item())

# ---- GELU FFN（GPT-2 等模型）----
gelu_out = F.gelu(x @ W1) @ W2
print("GELU FFN 输出:", torch.round(gelu_out * 1000) / 1000)
# GELU 平滑过渡，负值不完全为 0
print("GELU 激活后零值比例:", (F.gelu(x @ W1) == 0).float().mean().item())

# ---- SwiGLU FFN（Llama 等模型）----
# SwiGLU 使用两个投影 W1 和 W_gate，并用 SiLU 门控
W_gate = torch.randn(d_model, d_ff)
swiglu_hidden = F.silu(x @ W_gate) * (x @ W1)  # 门控机制
swiglu_out = swiglu_hidden @ W2
print("SwiGLU FFN 输出:", torch.round(swiglu_out * 1000) / 1000)
```

上述代码的输出结果如下：

```
ReLU FFN 输出: tensor([[ 18.9740, -18.8040,   4.1560,   8.7130,   7.6620,  -8.1090, -16.5200,
           8.1550]])
ReLU 激活后零值比例: 0.46875
GELU FFN 输出: tensor([[ 18.7710, -17.7720,   4.2050,   8.9820,   7.5910,  -8.2340, -15.5350,
           8.2760]])
GELU 激活后零值比例: 0.03125
SwiGLU FFN 输出: tensor([[ -9.5410, -21.0820,   9.3630,  39.1050,  -8.3680, -12.2070, -66.7940,
          -8.8370]])
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yeasy.gitbook.io/llm_internals/di-yi-bu-fen-ji-chu-pian/03_components/3.4_feedforward.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
