14 KiB
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}}
# 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)
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
# 每个专家就是一个 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