411 lines
14 KiB
Markdown
411 lines
14 KiB
Markdown
# FFN 前馈网络详解
|
||
|
||
> Feed-Forward Network — Transformer 中参数量最大(~2/3)、负责知识存储和特征变换的核心组件
|
||
|
||
---
|
||
|
||
## 一、FFN 在 Transformer 中的角色
|
||
|
||
```
|
||
Transformer Block:
|
||
|
||
注意力层(信息混合) FFN 层(特征变换)
|
||
┌─────────────────┐ ┌─────────────────────┐
|
||
│ Token 之间交互 │ │ 每个 Token 独立处理 │
|
||
│ "谁跟谁有关?" │ → │ "这个 Token 是什么意思?"│
|
||
│ 参数量 ~1/3 │ │ 参数量 ~2/3 │
|
||
└─────────────────┘ └─────────────────────┘
|
||
```
|
||
|
||
| 组件 | 参数量占比 | 功能 |
|
||
|---|---|---|
|
||
| **注意力层** | ~1/3 | Token 间的信息路由与混合("连接信息") |
|
||
| **FFN 层** | ~2/3 | Token 内部的特征变换与知识存储("存储知识") |
|
||
|
||
> **直觉**:注意力层决定"从哪读信息",FFN 层存储"读到的信息怎么理解"。研究表明 FFN 的权重矩阵充当了**联想记忆(Associative Memory)**——W₁ 的每一行是一个"模式检测器",W₂ 的对应列是"该模式的输出"。
|
||
|
||
---
|
||
|
||
## 二、基础公式
|
||
|
||
### 标准 FFN(原始 Transformer)
|
||
|
||
```
|
||
FFN(x) = W₂ · ReLU(W₁ · x + b₁) + b₂
|
||
```
|
||
|
||
```
|
||
输入 x(d_model 维度)
|
||
│
|
||
▼
|
||
W₁: d_model → d_ff ← 升维(通常 d_ff = 4 × d_model)
|
||
│
|
||
▼
|
||
激活函数(ReLU/GELU/...) ← 引入非线性
|
||
│
|
||
▼
|
||
W₂: d_ff → d_model ← 降维回原维度
|
||
│
|
||
▼
|
||
输出(d_model 维度)
|
||
```
|
||
|
||
### 参数量计算
|
||
|
||
```
|
||
标准 FFN(无偏置):
|
||
params = d_model × d_ff + d_ff × d_model
|
||
= 2 × d_model × d_ff
|
||
= 2 × d_model × 4d_model (通常 d_ff = 4d)
|
||
= 8 × d_model²
|
||
```
|
||
|
||
| d_model | d_ff (4×) | FFN 参数量 | 一层总参数(含注意力) |
|
||
|---|---|---|---|
|
||
| 512 | 2048 | 2.1M | ~3M |
|
||
| 4096 (LLaMA 7B) | 11008 (约2.7×) | ~90M | ~135M |
|
||
| 8192 (LLaMA 70B) | 22016 | ~360M | ~540M |
|
||
|
||
> 在 LLaMA 等 SwiGLU 模型中,`d_ff` 通常设为约 `8/3 × d_model`(而非 4×),以补偿 GLU 变体引入的额外参数。
|
||
|
||
---
|
||
|
||
## 三、激活函数演进路线
|
||
|
||
```
|
||
2017 ─→ 2019 ────→ 2020 ──────→ 2025
|
||
ReLU GELU SwiGLU PolyGLU / 动态路由
|
||
│ │ │ │
|
||
│ │ │ └─ 每个神经元学自己的激活函数
|
||
│ │ └─ 门控线性单元,当前生产标准
|
||
│ └─ 平滑版 ReLU,不再死神经元
|
||
└─ 简单粗暴,负值直接清零
|
||
```
|
||
|
||
### 3.1 ReLU(原始 Transformer)
|
||
|
||
$$\text{ReLU}(x) = \max(0, x)$$
|
||
|
||
```
|
||
│
|
||
2 ┤ ╱
|
||
1 ┤ ╱
|
||
0 ┼════●──────── ← 负半轴完全为零
|
||
│ ╱
|
||
-1 ┤ ╱
|
||
└────────────
|
||
```
|
||
|
||
| 优点 | 缺点 |
|
||
|---|---|
|
||
| 计算极快,硬件原生支持 | **死神经元**:一旦输出为零,梯度为零,永不恢复 |
|
||
| 稀疏激活(约 50%) | 负值信息完全丢失 |
|
||
|
||
> **死神经元问题**:如果某个神经元的输出始终 ≤ 0(因为权重初始化或学习偏差),ReLU 的梯度永远为 0,这个神经元就"死了"——再也不会更新。
|
||
|
||
### 3.2 GELU(GPT-2 / BERT 时代)
|
||
|
||
$$\text{GELU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}\left[1 + \text{erf}\left(\frac{x}{\sqrt{2}}\right)\right]$$
|
||
|
||
```
|
||
│
|
||
2 ┤ ╱
|
||
1 ┤ ╱╱
|
||
0 ┼────●───────── ← 负半轴有小的非零输出
|
||
-1 ┤╱╱
|
||
└────────────
|
||
```
|
||
|
||
| 优点 | 缺点 |
|
||
|---|---|
|
||
| 平滑可微,梯度流更好 | 计算比 ReLU 慢(erf 函数) |
|
||
| 负值有概率性"门控"而非直接清零 | 所有输入用同一个函数处理 |
|
||
| 缓解死神经元问题 | |
|
||
|
||
> **GELU 的直觉**:把输入值 `x` 乘以它"来自正半轴的概率"——如果 x 很可能是负的,输出很小但不为零。
|
||
|
||
### 3.3 SiLU / Swish
|
||
|
||
$$\text{SiLU}(x) = x \cdot \sigma(x) = \frac{x}{1 + e^{-x}}$$
|
||
|
||
```
|
||
│
|
||
2 ┤ ╱
|
||
1 ┤ ╱╱
|
||
0 ┼─────●───────── ← 负半轴有"负沟"(non-monotonic)
|
||
-0.3┤ ╱ ╲ ← 这个沟是 SiLU 的独特特征
|
||
└────────────
|
||
```
|
||
|
||
| 关键特性 | 说明 |
|
||
|---|---|
|
||
| **自门控** | 每个值决定自己通过多少:`x · σ(x)` |
|
||
| **非单调** | 负半轴先下降再回升,能捕获复杂模式 |
|
||
| **无界正值** | 正半轴可以无限大(不像 sigmoid 有上限) |
|
||
| **平滑** | 处处可微,梯度流极好 |
|
||
|
||
> SiLU 和 Swish 是**同一个函数**的两个名字。Google 叫它 Swish,PyTorch 叫它 SiLU。
|
||
|
||
---
|
||
|
||
## 四、GLU 门控线性单元:范式转变
|
||
|
||
### 4.1 从"单路激活"到"双路门控"
|
||
|
||
```
|
||
标准 FFN: GLU FFN:
|
||
|
||
x x
|
||
│ ┌────┴────┐
|
||
W₁ W_gate W_up
|
||
│ │ │
|
||
Act σ │
|
||
│ │ │
|
||
W₂ └────┬────┘
|
||
│ ⊗ (逐元素相乘)
|
||
输出 │
|
||
W_down
|
||
│
|
||
输出
|
||
```
|
||
|
||
### 4.2 为什么乘法比加法强?
|
||
|
||
> 门控 = 逐元素乘法 = 引入**二阶交互项**
|
||
|
||
```
|
||
GLU(x) 包含 xᵢ · xⱼ 项(不同维度之间产生交互)
|
||
FFN(x) 只有 σ(Wx) 项(每个维度独立处理)
|
||
|
||
类比:
|
||
FFN = 每个特征独立打分
|
||
GLU = 特征之间可以"对话"——"我是形容词,如果前面是名词就激活我"
|
||
```
|
||
|
||
### 4.3 SwiGLU(2020→至今的生产标准)
|
||
|
||
$$\text{SwiGLU}(x) = \text{SiLU}(x W_{\text{gate}}) \otimes (x W_{\text{up}}) \cdot W_{\text{down}}$$
|
||
|
||
```python
|
||
# SwiGLU 的实际代码(也是 LLaMA、DeepSeek、Mistral 等主流模型的实现)
|
||
|
||
class SwiGLU(nn.Module):
|
||
def __init__(self, dim, hidden_dim):
|
||
super().__init__()
|
||
# 三个权重矩阵(不是标准的两个)
|
||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False) # 门控通路
|
||
self.up_proj = nn.Linear(dim, hidden_dim, bias=False) # 值通路
|
||
self.down_proj = nn.Linear(hidden_dim, dim, bias=False) # 降维
|
||
|
||
def forward(self, x):
|
||
# gate: 门控信号(SiLU 激活)
|
||
# up: 值信号(无激活,直接通过)
|
||
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
||
```
|
||
|
||
### 4.4 参数量补偿:为什么 d_ff 变小了
|
||
|
||
| 方案 | 矩阵数 | 参数量 | d_ff(等参) |
|
||
|---|---|---|---|
|
||
| 标准 FFN | W₁, W₂ (2个) | 2 × d × d_ff | 4d |
|
||
| SwiGLU | W_gate, W_up, W_down (3个) | 3 × d × d_ff | **8d/3 ≈ 2.67d** |
|
||
|
||
```
|
||
等参数量下:
|
||
标准 FFN:d_ff = 4d → 2 × d × 4d = 8d²
|
||
SwiGLU:d_ff = 8d/3 → 3 × d × 8d/3 = 8d²
|
||
↑
|
||
参数总量相同,但 SwiGLU 表达能力更强
|
||
```
|
||
|
||
> 实际中,即使参数相当,SwiGLU 的困惑度(Perplexity)也比 GELU 低约 **8.3%**(文本任务)——这是"免费的性能提升"。
|
||
|
||
---
|
||
|
||
## 五、GLU 变体对比
|
||
|
||
| 变体 | 门控激活函数 | 公式 | 特点 |
|
||
|---|---|---|---|
|
||
| **GLU(原始)** | Sigmoid | `σ(xW_g) ⊗ (xW_u)` | 门控有上限(0,1),表达受限 |
|
||
| **ReGLU** | ReLU | `ReLU(xW_g) ⊗ (xW_u)` | 最快,但仍可能死神经元 |
|
||
| **GeGLU** | GELU | `GELU(xW_g) ⊗ (xW_u)` | 视觉/多模态任务稍好 |
|
||
| **SwiGLU** | SiLU/Swish | `SiLU(xW_g) ⊗ (xW_u)` | **文本生成 SOTA,生产标准** |
|
||
|
||
### 选择指南
|
||
|
||
| 场景 | 推荐 | 原因 |
|
||
|---|---|---|
|
||
| LLM 文本生成 | **SwiGLU** | 困惑度最低,LLaMA/DeepSeek/Mistral 全部使用 |
|
||
| 视觉编码器 | GeGLU 或 GELU | 分类任务上略优(~2% Top-1 提升) |
|
||
| 延迟敏感 | ReGLU | Relu 计算成本最低,硬件原生支持 |
|
||
| 混合模态 | 编码器 GELU + 解码器 SwiGLU | 各取所长,推理可提速 ~15% |
|
||
|
||
---
|
||
|
||
## 六、FFN = 知识的"键值记忆"
|
||
|
||
> 这个视角将 FFN 从"无聊的全连接层"升级为"模型的知识存储系统"
|
||
|
||
### 联想记忆假说
|
||
|
||
```
|
||
FFN(x) = W₂ · Act(W₁ · x)
|
||
|
||
W₁ 的每一行 = 一个"键"(Key),检测特定模式
|
||
W₂ 的对应列 = 一个"值"(Value),存储该模式对应的输出
|
||
|
||
过程:
|
||
1. W₁·x → 将输入与所有"键"匹配(内积 = 相似度)
|
||
2. Act → 过滤:只保留匹配度高的模式
|
||
3. W₂· ... → 激活的模式"写出"对应的值
|
||
```
|
||
|
||
### 实验证据
|
||
|
||
| 发现 | 说明 |
|
||
|---|---|
|
||
| **知识定位** | FFN 的特定神经元可以对应具体的事实(如"埃菲尔铁塔在巴黎") |
|
||
| **可编辑性** | 修改 FFN 权重可以精确修改模型存储的事实(模型编辑) |
|
||
| **稀疏激活** | 对每个 Token,FFN 中只有少数神经元真正激活(和 MoE 思路一致) |
|
||
| **层间分工** | 浅层 FFN → 语法/局部模式、深层 FFN → 语义/全局知识 |
|
||
|
||
---
|
||
|
||
## 七、2025 前沿:激活函数本身也"可学习"
|
||
|
||
### 7.1 PolyGLU(2025年3月)
|
||
|
||
> **不再固定用一种激活函数,而是让每个神经元学习自己最适合的激活函数**
|
||
|
||
```
|
||
PolyGLU(x) = [ Σ g_k · σ_k(x·W_gate) ] ⊗ (x·W_up)
|
||
|
||
其中 σ_k 从候选池中选择:
|
||
{ ReLU, Tanh, SiLU, GELU }
|
||
|
||
g_k = 路由权重(每个神经元不同),训练中自动收敛
|
||
```
|
||
|
||
**训练后的层间分布(19K步训练)**:
|
||
|
||
```
|
||
层 0-2(浅层): GELU 35-40% | Tanh 15-25% | SiLU 15-20% | ReLU ~5%
|
||
层 3-14(中层): GELU 为主,逐步过渡
|
||
层 15-27(深层): Tanh 50-65% ← 深层网络偏好对称有界激活!
|
||
```
|
||
|
||
> 关键发现:深层网络的神经元**自发偏好 Tanh**(对称、有界),浅层偏好 GELU(概率性门控)。这暗示不同深度的层确实需要不同"类型"的非线性变换。
|
||
|
||
### 7.2 动态激活选择(LLaMA 4)
|
||
|
||
LLaMA 4 实验在 SwiGLU 和 GELU 之间做**输入级动态切换**:
|
||
- 用轻量门控网络根据输入内容选择激活函数
|
||
- 复杂推理任务 → 倾向于 SwiGLU(更强的特征交互)
|
||
- 简单模式匹配 → 倾向于 GELU(更快的计算)
|
||
|
||
---
|
||
|
||
## 八、DeepSeek 风格的实际实现
|
||
|
||
### 标准 SwiGLU(如 LLaMA)
|
||
|
||
```python
|
||
class LlamaMLP(nn.Module):
|
||
def __init__(self, config):
|
||
super().__init__()
|
||
self.gate_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
||
self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
||
self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
|
||
|
||
def forward(self, x):
|
||
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
||
```
|
||
|
||
### DeepSeek-V3 的 MoE + SwiGLU
|
||
|
||
```python
|
||
# 每个专家就是一个 SwiGLU FFN
|
||
class DeepSeekExpert(nn.Module):
|
||
def __init__(self, dim, moe_intermediate_size):
|
||
super().__init__()
|
||
self.gate_proj = nn.Linear(dim, moe_intermediate_size, bias=False)
|
||
self.up_proj = nn.Linear(dim, moe_intermediate_size, bias=False)
|
||
self.down_proj = nn.Linear(moe_intermediate_size, dim, bias=False)
|
||
|
||
def forward(self, x):
|
||
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
||
|
||
# MoE 层:N 个专家 + 路由器
|
||
class DeepSeekMoE(nn.Module):
|
||
def __init__(self, dim, n_routed_experts=256, n_shared_experts=2, top_k=8):
|
||
super().__init__()
|
||
self.shared_experts = nn.ModuleList([
|
||
DeepSeekExpert(dim, dim * 2) for _ in range(n_shared_experts)
|
||
])
|
||
self.routed_experts = nn.ModuleList([
|
||
DeepSeekExpert(dim, dim * 2) for _ in range(n_routed_experts)
|
||
])
|
||
self.gate = nn.Linear(dim, n_routed_experts, bias=False)
|
||
|
||
def forward(self, x):
|
||
# 共享专家(始终激活)
|
||
shared_out = sum(e(x) for e in self.shared_experts)
|
||
# 路由专家(稀疏激活 Top-K)
|
||
router_logits = self.gate(x)
|
||
topk_weights, topk_ids = torch.topk(router_logits.softmax(-1), self.top_k)
|
||
routed_out = 0
|
||
for i, expert_id in enumerate(topk_ids):
|
||
routed_out += topk_weights[i] * self.routed_experts[expert_id](x)
|
||
return shared_out + routed_out
|
||
```
|
||
|
||
> DeepSeek-V3 有 256 个路由专家 + 2 个共享专家,每个专家内部都是 SwiGLU FFN。详见 [[MoE 混合专家架构]]。
|
||
|
||
---
|
||
|
||
## 九、进化总览
|
||
|
||
```
|
||
标准 FFN GLU 系列
|
||
──────── ────────
|
||
非线性来源: 激活函数 门控乘法
|
||
通路数: 单路 双路(值 + 门控)
|
||
交互阶数: 一阶 二阶(xᵢ·xⱼ)
|
||
参数效率: 基准 显著更优
|
||
参数量: 2 × d × d_ff 3 × d × d_ff
|
||
生产标准: 已过时 SwiGLU(2025年)
|
||
|
||
未来方向
|
||
────────
|
||
激活函数选择: 可学习、每个神经元不同
|
||
实现: PolyGLU、动态路由
|
||
目标: 让模型自主发现最优非线性
|
||
```
|
||
|
||
---
|
||
|
||
## 十、一句话总结
|
||
|
||
```
|
||
FFN 不是"无聊的全连接层"——
|
||
它是 Transformer 中参数最密集、负责存储世界知识的核心组件。
|
||
|
||
ReLU → GELU → SiLU → SwiGLU → PolyGLU 的进化线,
|
||
本质上是"从固定非线性到可学习、动态、输入感知的门控机制"的演进。
|
||
|
||
你正在用的 LLaMA、DeepSeek、Mistral、Qwen 的每一层中,
|
||
SwiGLU 正在用乘法门控默默地做着模型 2/3 的"思考"。
|
||
```
|
||
|
||
---
|
||
|
||
## 相关链接
|
||
|
||
- [[Transformer 架构详解]] — FFN 在完整架构中的位置
|
||
- [[大模型架构基础]] — 各组件全景概览
|
||
- [[MoE 混合专家架构]] — FFN 的稀疏化扩展:用 N 个 SwiGLU 替代 1 个
|
||
|
||
---
|
||
|
||
*最后更新:2026-08-05*
|