FeedForward与Transformer Block:Attention之外的另一半
深入解析 FeedForward 前馈网络,以及如何将 RMSNorm、RoPE、Attention、FeedForward 四大组件组装成完整的 Transformer Block。读完本文,你将彻底掌握 Transformer 的完整架构
本文是 MiniMind 学习系列的第4篇(共4篇),深入解析 FeedForward 前馈网络,以及如何将 RMSNorm、RoPE、Attention、FeedForward 四大组件组装成完整的 Transformer Block。读完本文,你将彻底掌握 Transformer 的完整架构。
关于本系列#
MiniMind ↗ 是一个简洁但完整的大语言模型训练项目,包含从数据处理、模型训练到推理部署的完整流程。我在学习这个项目的过程中,将核心技术点整理成了 minimind-notes ↗ 仓库,并产出了这个4篇系列博客,系统性地讲解 Transformer 的核心组件。
本系列包括:
- 归一化机制 - 为什么需要RMSNorm
- RoPE位置编码 - 如何让模型理解词序
- Attention机制 - Transformer的核心引擎
- FeedForward与完整架构(本篇)- 组件如何协同工作
一、引言#
1.1 被忽视的另一半#
提到 Transformer,大家都会想到:
- ✅ Attention 机制(明星组件)
- ✅ 位置编码(RoPE)
- ❓ FeedForward?那是什么?
事实:
- FeedForward 占 Transformer Block 代码的 40%
- FeedForward 的参数量占总参数的 2/3!
- 没有 FeedForward,只有 Attention 是训练不出好模型的
1.2 本文要解答的问题#
- FeedForward 到底在做什么?
- 为什么要”扩张-压缩”(768→2048→768)?
- SwiGLU 比普通 FFN 强在哪?
- Attention vs FeedForward 的分工是什么?
- 4 个组件如何组装成完整的 Transformer Block?
- 残差连接(Residual Connection)的作用是什么?
1.3 适合读者#
- 学过 Attention 但对 FFN 不清楚
- 想完整理解 Transformer 架构
- 准备从零实现 Transformer
- 想知道为什么 Transformer “work”
二、FeedForward 是什么?#
2.1 核心思想#
“对每个词的向量进行复杂的非线性变换”
关键特点:
- ✅ 每个词独立处理(没有词与词的交互)
- ✅ 输入维度 = 输出维度(768 维)
- ✅ 但内容完全改变(经过非线性变换)
- ✅ 通过高维空间增强表达能力
2.2 典型结构#
输入: [batch, seq_len, 768]
↓
扩张: Linear(768 → 2048)
↓
激活: 非线性函数(ReLU/GELU/SiLU)
↓
压缩: Linear(2048 → 768)
↓
输出: [batch, seq_len, 768]python2.3 简单实现#
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleFeedForward(nn.Module):
def __init__(self, hidden_size=768, intermediate_size=2048):
super().__init__()
self.w1 = nn.Linear(hidden_size, intermediate_size, bias=False)
self.w2 = nn.Linear(intermediate_size, hidden_size, bias=False)
def forward(self, x):
# x: [batch, seq_len, 768]
h = self.w1(x) # 扩张: [batch, seq_len, 2048]
h = F.relu(h) # 激活
output = self.w2(h) # 压缩: [batch, seq_len, 768]
return outputpython2.4 与 Attention 的对比#
# Attention
句子: "我 爱 编程"
# "爱"可以看到"我"和"编程"
# → 词与词交互,融合上下文
# FeedForward
句子: "我 爱 编程"
# "爱"只看自己,独立变换
# → 每个词独立,深度处理python类比:
- Attention = 开会讨论(大家交换信息)
- FeedForward = 各自思考(独立消化信息)
三、为什么要”扩张-压缩”?#
3.1 常见疑问#
“为什么不直接 768 → 768?绕一大圈去 2048 再回来,不是浪费计算吗?”
这是个非常好的问题!
3.2 直觉理解:表达能力#
方案 1:直接变换
# 768 → 768
output = W @ x # W 是 [768, 768] 矩阵
# 这只是线性变换!
# 表达能力 = 一个矩阵乘法python方案 2:扩张-压缩
# 768 → 2048 → 768
h = W1 @ x # [2048, 768] @ [768] = [2048]
h = activation(h) # 非线性!
output = W2 @ h # [768, 2048] @ [2048] = [768]
# 表达能力 = 两个矩阵乘法 + 非线性激活
# 可以拟合更复杂的函数!python3.3 数学本质:高维空间的魔力#
关键洞察:在高维空间中,向量有更多”自由度”
简化理解:
- 直接变换(768 → 768):只能做线性组合,受限于原始维度
- 扩张到高维(768 → 2048):在高维空间有更多自由度
- 非线性激活:引入非线性变换能力
- 压缩回原维(2048 → 768):保留高维空间学到的复杂模式
实验验证:拟合复杂函数(如 y = sin(x) + cos(2x))时:
- 直接变换
Linear(1, 1):只能拟合线性函数,误差大 ❌ - 扩张-压缩
Linear(1, 64) → ReLU → Linear(64, 1):可以拟合非线性函数,误差小 ✅ - 结论:扩张-压缩 = 表达能力++
3.4 类比理解#
类比 1:做菜
直接变换(768 → 768):
生食材 → 装盘
没有加工,味道单一 ❌
扩张-压缩(768 → 2048 → 768):
食材 → 切碎、调味、烹饪(2048 维高维空间)→ 装盘
经过高维空间的"加工",味道丰富 ✅plaintext类比 2:照片处理
直接变换:
原图 → 简单滤镜 → 输出
效果有限 ❌
扩张-压缩:
原图 → 提取特征(更多维度)→ 复杂变换 → 压缩回原尺寸
可以实现降噪、超分辨率等复杂操作 ✅plaintext3.5 为什么是 2048 维(而不是 1024 或 4096)?#
经验规则:intermediate_size ≈ hidden_size × 2.67 ~ 4
| 模型 | hidden_size | intermediate_size | 比例 |
|---|---|---|---|
| BERT-Base | 768 | 3072 | 4.0 |
| GPT-2 | 768 | 3072 | 4.0 |
| Llama-7B | 4096 | 11008 | 2.69 |
| MiniMind | 768 | 2048 | 2.67 |
为什么不是更大?
- 更大 = 更多参数 = 更慢、更耗显存
- 2.67-4 倍是实验验证的最佳平衡点
四、SwiGLU:现代 Transformer 的选择#
4.1 FeedForward 的演进#
第1代(GPT-2/BERT,2018-2019):
h = ReLU(W1 @ x)
output = W2 @ hpython第2代(GPT-3,2020):
h = GELU(W1 @ x) # 更平滑的激活函数
output = W2 @ hpython第3代(Llama/MiniMind,2023):
gate = SiLU(W_gate @ x)
up = W_up @ x
h = gate * up # 门控机制!⭐
output = W_down @ hpython4.2 SwiGLU 详解#
完整名称:Swish-Gated Linear Unit
核心思想:用两个分支,一个控制另一个
import torch
import torch.nn as nn
import torch.nn.functional as F
class SwiGLU(nn.Module):
def __init__(self, dim=768, hidden_dim=2048):
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 = self.gate_proj(x) # [batch, seq, 2048]
up = self.up_proj(x) # [batch, seq, 2048]
# SiLU 激活 + 门控(逐元素相乘)
hidden = F.silu(gate) * up
# 压缩回原维度
output = self.down_proj(hidden)
return outputpython维度变化:
输入: [batch, seq, 768]
↓
gate: [batch, seq, 2048] ← W_gate @ x
up: [batch, seq, 2048] ← W_up @ x
↓
hidden: [batch, seq, 2048] ← SiLU(gate) * up
↓
输出: [batch, seq, 768] ← W_down @ hiddenplaintext4.3 SiLU 激活函数#
# SiLU(x) = x * sigmoid(x)
def silu(x):
return x * torch.sigmoid(x)
# 也叫 Swishpython对比常见激活函数:
| 激活函数 | 公式 | 特点 |
|---|---|---|
| ReLU | max(0, x) | 简单但梯度可能为 0 |
| GELU | x * Φ(x) | 平滑但计算稍慢 |
| SiLU/Swish | x * σ(x) | 平滑且计算快 ✅ |
4.4 门控机制的威力#
直觉:gate 分支控制 up 分支哪些信息通过
# 示例
gate = torch.tensor([0.1, 0.9, 0.5, 0.2]) # 门控值
up = torch.tensor([5.0, 3.0, 2.0, 8.0]) # 上投影值
# 逐元素相乘
hidden = gate * up
# = [0.5, 2.7, 1.0, 1.6]
# 观察:
# gate=0.9 的位置:大部分信息通过(2.7 ≈ 3.0)
# gate=0.1 的位置:少量信息通过(0.5 << 5.0)python类比:
普通 FFN:
所有信息都通过同一个"门"(单一激活函数)
SwiGLU:
gate 分支像"保安",决定 up 分支的哪些信息能进来
动态选择,更灵活!plaintext4.5 普通 FFN vs SwiGLU 对比#
| 特性 | 普通 FFN | SwiGLU |
|---|---|---|
| 分支数 | 1 个 | 2 个(gate + up) |
| 激活函数 | ReLU/GELU | SiLU |
| 门控 | 无 | 有(gate × up) |
| 参数量 | 2 × 768 × 2048 | 3 × 768 × 2048(多 50%) |
| 计算量 | 较少 | 稍多 |
| 效果 | 好 | 更好(实验证明) |
| 使用模型 | GPT-2, BERT | Llama, MiniMind, PaLM |
4.6 为什么参数增加但值得?#
# 参数量对比(以 MiniMind 为例)
普通 FFN 参数:
W1: 768 × 2048 = 1,572,864
W2: 2048 × 768 = 1,572,864
总计: 3,145,728
SwiGLU 参数:
gate_proj: 768 × 2048 = 1,572,864
up_proj: 768 × 2048 = 1,572,864
down_proj: 2048 × 768 = 1,572,864
总计: 4,718,592(多 50%)
# 但是!
# Attention 层参数: 768 × 768 × 4 ≈ 2.4M
# SwiGLU 参数: 4.7M
# FeedForward 占总参数: 4.7M / (2.4M + 4.7M) ≈ 66%
# 提升这部分的效果,对整体模型提升很大!python实验结果(Llama 论文):
- 同等参数量下,SwiGLU 比 GELU 好 5-10%
- 同等效果下,SwiGLU 训练更快(梯度更稳定)
五、Attention vs FeedForward 分工#
5.1 完整对比#
| 特性 | Attention | FeedForward |
|---|---|---|
| 处理方式 | 词与词交互 | 每个词独立 |
| 作用 | 信息交换(开会) | 深度思考(独立消化) |
| 输入 | [seq, 768] | [seq, 768] |
| 中间维度 | [seq, seq](分数矩阵) | [seq, 2048](扩张) |
| 输出 | [seq, 768] | [seq, 768] |
| 位置编码 | 需要(RoPE) | 不需要 |
| 参数量 | 约 33% | 约 67% |
| 计算瓶颈 | seq² (序列长度平方) | batch×seq (线性) |
| 类比 | 查字典、开会 | 做数学题、思考 |
5.2 为什么两者缺一不可?#
只有 Attention:
- ✅ 词之间可以交互
- ❌ 缺少”深度理解”
- 就像只开会讨论,没有独立思考
- 模型学不到复杂的模式
只有 FeedForward:
- ✅ 每个词可以复杂变换
- ❌ 不知道上下文
- 就像闭门造车,不听别人意见
- 模型不知道词与词的关系
两者结合:
Step 1: Attention
→ 让模型知道"哪些词相关"
→ 融合上下文信息
Step 2: FeedForward
→ 让模型知道"如何处理这些信息"
→ 深度非线性变换
Step 3: 重复 N 次
→ 逐层提炼理解plaintext5.3 完整流程示例#
句子: "我爱编程"
# ========== Attention 阶段 ==========
# "爱"从"我"和"编程"收集信息
"爱" ← "我"(29%)+ "爱"(36%)+ "编程"(25%)
# "爱"现在知道:连接"我"和"编程"
# ========== FeedForward 阶段 ==========
# "爱"基于收集的信息进行深度思考
"爱"的表示 [768 维]
↓ 扩张到高维空间
[2048 维]
↓ 门控机制 + 非线性变换
[2048 维] # 在高维空间做复杂推理
↓ 压缩回原维度
[768 维] # 提炼后的理解
# 最终
# "爱"既融合了上下文(Attention),
# 又完成了深度理解(FeedForward)python六、Transformer Block 组装#
6.1 四大核心组件#
回顾我们学过的 4 个组件:
- RMSNorm:稳定数值(归一化)
- Attention:词与词交互(信息交换)
- FeedForward:独立深化(深度思考)
- Residual Connection:保底机制(残差连接)
6.2 完整 Transformer Block 结构#
import torch
import torch.nn as nn
class TransformerBlock(nn.Module):
def __init__(self, config):
super().__init__()
# RMSNorm #1:在 Attention 之前
self.input_layernorm = RMSNorm(config.hidden_size)
# Multi-Head Attention
self.self_attn = Attention(config)
# RMSNorm #2:在 FeedForward 之前
self.post_attention_layernorm = RMSNorm(config.hidden_size)
# FeedForward (SwiGLU)
self.mlp = FeedForward(config)
def forward(self, x, position_embeddings):
# ========== 第一部分:Attention ==========
# 1. 保存输入(残差连接用)
residual = x
# 2. RMSNorm #1(归一化)
x = self.input_layernorm(x)
# 3. Multi-Head Attention
x = self.self_attn(x, position_embeddings)
# 4. 残差连接
x = residual + x
# ========== 第二部分:FeedForward ==========
# 5. 保存当前状态(残差连接用)
residual = x
# 6. RMSNorm #2(归一化)
x = self.post_attention_layernorm(x)
# 7. FeedForward(SwiGLU)
x = self.mlp(x)
# 8. 残差连接
x = residual + x
return xpython6.3 数据流图#
输入 x: [batch, seq_len, 768]
↓
├──────┐ (保存 residual)
↓ │
RMSNorm #1 ← 归一化
↓
Multi-Head Attention (+ RoPE) ← 词与词交互
↓
└──────┘ (加上 residual) ← 残差连接
↓
├──────┐ (保存 residual)
↓ │
RMSNorm #2 ← 归一化
↓
FeedForward (SwiGLU) ← 独立深化
↓
└──────┘ (加上 residual) ← 残差连接
↓
输出 x: [batch, seq_len, 768]plaintext6.4 残差连接(Residual Connection)#
公式:
y = x + F(x)
# 而不是
y = F(x) # 没有残差python三大好处:
好处 1:保底机制#
# 最坏情况:F 学不到东西
y = x + 0 = x # 至少还有输入!
# 没有残差的话
y = F(x) = 噪音 # 完全损坏 ❌python好处 2:增量学习#
# 有残差:只需学"调整量"
y = x + Δx # Δx 是小的调整
# 没有残差:需要学"完整输出"
y = F(x) # F 必须学会从零构建 ypython类比:
没有残差:每次修图完全覆盖原图
原图 → 滤镜 → 新图(丢失原图)
有残差:原图 + 每次的调整
原图 → 原图 + 调整1 → 原图 + 调整1 + 调整2 ...
所有信息都保留!plaintext好处 3:梯度高速公路#
# 反向传播
dy/dx = 1 + dF/dx
# 即使 F 的梯度消失(dF/dx → 0)
dy/dx = 1 # 梯度还能传回去!✅
# 没有残差
dy/dx = dF/dx # 消失就彻底断了 ❌python6.5 Pre-Norm vs Post-Norm#
Post-Norm(原始 Transformer,2017):
# 归一化在子层之后
x = x + Attention(x)
x = Norm(x)
x = x + FeedForward(x)
x = Norm(x)pythonPre-Norm(现代 Transformer,Llama/MiniMind):
# 归一化在子层之前
x = x + Attention(Norm(x))
x = x + FeedForward(Norm(x))pythonPre-Norm 的优势:
| 特性 | Post-Norm | Pre-Norm |
|---|---|---|
| 训练稳定性 | 深层网络困难 | 更稳定 ✅ |
| 梯度传播 | 可能被 Norm 打断 | 残差路径更干净 ✅ |
| 学习率 | 需要 warmup | 可以用更大学习率 ✅ |
现代 LLM 全部使用 Pre-Norm(GPT-3, Llama, MiniMind, Mistral…)
七、MiniMind 完整架构#
7.1 整体结构#
MiniMindForCausalLM
├─ lm_head: 输出层 (hidden_size → vocab_size)
│ 将 768 维向量映射到 6400 个词的概率分布
│
└─ MiniMindModel
├─ embed_tokens: 词嵌入层 (vocab_size → hidden_size)
│ 将 token ID 转换成 768 维向量
│
├─ layers: N 个 TransformerBlock (默认 8 层)
│ └─ TransformerBlock × 8
│ ├─ input_layernorm: RMSNorm
│ ├─ self_attn: Multi-Head Attention
│ ├─ post_attention_layernorm: RMSNorm
│ └─ mlp: FeedForward (SwiGLU)
│
└─ norm: 最终的 RMSNorm
对最后一层输出再次归一化plaintext7.2 前向传播流程#
# 输入
token_ids = [34, 128, 556, 89, ...] # "我爱编程"对应的 ID
# ========== 词嵌入 ==========
x = embed_tokens(token_ids)
# [batch, seq_len] → [batch, seq_len, 768]
# ========== 预计算 RoPE ==========
cos, sin = precompute_freqs_cis(...) # 位置编码
# ========== TransformerBlock #1 ==========
x = block_1(x, position_embeddings=(cos, sin))
# [batch, seq_len, 768] → [batch, seq_len, 768]
# ========== TransformerBlock #2 ==========
x = block_2(x, position_embeddings=(cos, sin))
# ...
# ========== TransformerBlock #8 ==========
x = block_8(x, position_embeddings=(cos, sin))
# ========== 最终归一化 ==========
x = self.norm(x)
# [batch, seq_len, 768]
# ========== 输出层 ==========
logits = lm_head(x)
# [batch, seq_len, 768] → [batch, seq_len, 6400]
# 每个位置预测下一个 token 的概率分布
# ========== 生成 ==========
next_token_id = torch.argmax(logits[:, -1, :], dim=-1)
# 选择概率最高的 tokenpython7.3 参数统计#
# MiniMind2 (104M 参数)
# 词嵌入
embed_tokens: 6400 × 768 = 4,915,200
# 8 个 TransformerBlock
每个 Block:
Attention:
q_proj: 768 × 768 = 589,824
k_proj: 768 × 768 = 589,824
v_proj: 768 × 768 = 589,824
o_proj: 768 × 768 = 589,824
小计: 2,359,296
FeedForward (SwiGLU):
gate_proj: 768 × 2048 = 1,572,864
up_proj: 768 × 2048 = 1,572,864
down_proj: 2048 × 768 = 1,572,864
小计: 4,718,592
每个 Block 总计: 7,077,888
8 个 Block: 8 × 7,077,888 = 56,623,104
# 输出层
lm_head: 768 × 6400 = 4,915,200
# 总计
4.9M + 56.6M + 4.9M ≈ 104M ✅python观察:
- FeedForward 参数(4.7M)是 Attention 参数(2.4M)的 2 倍!
- FeedForward 占每个 Block 的 67% 参数
7.4 配置参数#
# MiniMind 配置(model/model_minimind.py)
class MiniMindConfig:
hidden_size = 768 # 隐藏层维度
num_hidden_layers = 8 # Transformer Block 层数
num_attention_heads = 8 # 注意力头数
num_key_value_heads = 2 # GQA: KV 头数
intermediate_size = 2048 # FFN 中间维度
vocab_size = 6400 # 词汇表大小
max_position_embeddings = 32768 # 最大序列长度
rope_theta = 1000000.0 # RoPE 基础频率
rms_norm_eps = 1e-5 # RMSNorm 的 epsilon
use_moe = False # 是否使用 MoEpython八、动手实验#
想要深入理解 FeedForward,可以尝试以下实验:
- 对比普通 FFN 和 SwiGLU:实现两种架构,对比参数量(SwiGLU 多 50%)和训练效果
- 验证扩张-压缩的必要性:尝试 768→768 直接变换 vs 768→2048→768,观察拟合能力差异
- 测试残差连接:对比有无残差连接的网络训练稳定性和收敛速度
参考 MiniMind 项目的 learning_materials/feedforward_explained.py 获取完整实验代码。
九、总结#
9.1 核心要点#
- ✅ FeedForward 作用:独立深化,对每个词做复杂非线性变换
- ✅ 扩张-压缩必要性:高维空间有更强表达能力,可以拟合复杂函数
- ✅ SwiGLU 优势:门控机制,两个分支,效果比普通 FFN 好 5-10%
- ✅ Attention vs FFN:交互 vs 独立,开会 vs 思考,缺一不可
- ✅ Transformer Block:4 个组件完美组合(Norm + Attn + Norm + FFN + Residual)
- ✅ 残差连接:保底机制 + 增量学习 + 梯度高速公路
- ✅ Pre-Norm:现代深层 Transformer 的标准选择
9.2 Transformer Block 口诀#
Norm → Attention → 残差
Norm → FeedForward → 残差
重复 N 次 → 完整模型!plaintext9.3 系列回顾#
恭喜你完成了 MiniMind 核心架构的学习!
4 篇博客回顾:
- ✅ RMSNorm:归一化原理,为什么比 LayerNorm 快 7.7 倍
- ✅ RoPE:位置编码,多频率机制的深入分析
- ✅ Attention:Q、K、V,Multi-Head 的完整理解
- ✅ FeedForward + 架构:扩张-压缩,完整组装
你现在掌握了:
- ✅ Transformer 的所有核心组件
- ✅ 每个组件的数学原理和代码实现
- ✅ 组件之间如何配合
- ✅ 为什么 Transformer “work”
- ✅ 可以从零实现一个小型 Transformer!
9.4 下一步建议#
1. 动手实践#
# 克隆 MiniMind
git clone https://github.com/jingyaogong/minimind
cd minimind
# 训练一个小模型
cd trainer
python train_pretrain.pybash2. 深入学习#
- GQA (Grouped Query Attention):节省显存
- Flash Attention:优化计算效率
- MoE (Mixture of Experts):提升容量
- KV Cache:加速推理
3. 实现挑战#
# 挑战:从零实现 MiniMind
class MyMiniMind(nn.Module):
def __init__(self):
# 你来实现!
passpython十、参考资料#
论文#
- Attention Is All You Need ↗ - Transformer 原始论文
- GLU Variants Improve Transformer ↗ - SwiGLU 论文
- Deep Residual Learning for Image Recognition ↗ - ResNet/残差连接
代码#
- MiniMind 源码:github.com/jingyaogong/minimind ↗
- 本文学习材料:
learning_materials/feedforward_explained.py - Transformer Block:
model/model_minimind.py:359-380
相关阅读#
本文作者:joye 发布日期:2025-12-30 最后更新:2025-12-30 系列文章:MiniMind 学习笔记(4/4)
如果觉得有帮助,欢迎:
- ⭐ Star 原项目 MiniMind ↗
- ⭐ Star 我的学习笔记 minimind-notes ↗
- 💬 留言讨论你的学习心得
- 🔗 分享给其他学习 LLM 的朋友