Joye Personal Blog

Back

本文是 MiniMind 学习系列的第4篇(共4篇),深入解析 FeedForward 前馈网络,以及如何将 RMSNorm、RoPE、Attention、FeedForward 四大组件组装成完整的 Transformer Block。读完本文,你将彻底掌握 Transformer 的完整架构。

关于本系列#

MiniMind 是一个简洁但完整的大语言模型训练项目,包含从数据处理、模型训练到推理部署的完整流程。我在学习这个项目的过程中,将核心技术点整理成了 minimind-notes 仓库,并产出了这个4篇系列博客,系统性地讲解 Transformer 的核心组件。

本系列包括:

  1. 归一化机制 - 为什么需要RMSNorm
  2. RoPE位置编码 - 如何让模型理解词序
  3. Attention机制 - Transformer的核心引擎
  4. 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(7682048)

激活: 非线性函数(ReLU/GELU/SiLU)

压缩: Linear(2048768)

输出: [batch, seq_len, 768]
python

2.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 output
python

2.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]

# 表达能力 = 两个矩阵乘法 + 非线性激活
# 可以拟合更复杂的函数!
python

3.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:照片处理

直接变换:
  原图 → 简单滤镜 → 输出
  效果有限 ❌

扩张-压缩:
  原图 → 提取特征(更多维度)→ 复杂变换 → 压缩回原尺寸
  可以实现降噪、超分辨率等复杂操作 ✅
plaintext

3.5 为什么是 2048 维(而不是 1024 或 4096)?#

经验规则:intermediate_size ≈ hidden_size × 2.67 ~ 4

模型hidden_sizeintermediate_size比例
BERT-Base76830724.0
GPT-276830724.0
Llama-7B4096110082.69
MiniMind76820482.67

为什么不是更大?

  • 更大 = 更多参数 = 更慢、更耗显存
  • 2.67-4 倍是实验验证的最佳平衡点

四、SwiGLU:现代 Transformer 的选择#

4.1 FeedForward 的演进#

第1代(GPT-2/BERT,2018-2019)

h = ReLU(W1 @ x)
output = W2 @ h
python

第2代(GPT-3,2020)

h = GELU(W1 @ x)  # 更平滑的激活函数
output = W2 @ h
python

第3代(Llama/MiniMind,2023)

gate = SiLU(W_gate @ x)
up = W_up @ x
h = gate * up  # 门控机制!⭐
output = W_down @ h
python

4.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 output
python

维度变化

输入:  [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 @ hidden
plaintext

4.3 SiLU 激活函数#

# SiLU(x) = x * sigmoid(x)
def silu(x):
    return x * torch.sigmoid(x)

# 也叫 Swish
python

对比常见激活函数

激活函数公式特点
ReLUmax(0, x)简单但梯度可能为 0
GELUx * Φ(x)平滑但计算稍慢
SiLU/Swishx * σ(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 分支的哪些信息能进来
  动态选择,更灵活!
plaintext

4.5 普通 FFN vs SwiGLU 对比#

特性普通 FFNSwiGLU
分支数1 个2 个(gate + up)
激活函数ReLU/GELUSiLU
门控有(gate × up)
参数量2 × 768 × 20483 × 768 × 2048(多 50%
计算量较少稍多
效果更好(实验证明)
使用模型GPT-2, BERTLlama, 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 完整对比#

特性AttentionFeedForward
处理方式词与词交互每个词独立
作用信息交换(开会)深度思考(独立消化)
输入[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 次
  → 逐层提炼理解
plaintext

5.3 完整流程示例#

句子: "我爱编程"

# ========== Attention 阶段 ==========
# "爱"从"我"和"编程"收集信息
"爱""我"29%+ "爱"36%+ "编程"25%
# "爱"现在知道:连接"我"和"编程"

# ========== FeedForward 阶段 ==========
# "爱"基于收集的信息进行深度思考
"爱"的表示 [768 维]
  ↓ 扩张到高维空间
[2048 维]
  ↓ 门控机制 + 非线性变换
[2048 维]  # 在高维空间做复杂推理
  ↓ 压缩回原维度
[768 维]  # 提炼后的理解

# 最终
# "爱"既融合了上下文(Attention),
# 又完成了深度理解(FeedForward)
python

六、Transformer Block 组装#

6.1 四大核心组件#

回顾我们学过的 4 个组件:

  1. RMSNorm:稳定数值(归一化)
  2. Attention:词与词交互(信息交换)
  3. FeedForward:独立深化(深度思考)
  4. 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 x
python

6.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]
plaintext

6.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 必须学会从零构建 y
python

类比

没有残差:每次修图完全覆盖原图
  原图 → 滤镜 → 新图(丢失原图)

有残差:原图 + 每次的调整
  原图 → 原图 + 调整1 → 原图 + 调整1 + 调整2 ...
  所有信息都保留!
plaintext

好处 3:梯度高速公路#

# 反向传播
dy/dx = 1 + dF/dx

# 即使 F 的梯度消失(dF/dx → 0)
dy/dx = 1  # 梯度还能传回去!✅

# 没有残差
dy/dx = dF/dx  # 消失就彻底断了 ❌
python

6.5 Pre-Norm vs Post-Norm#

Post-Norm(原始 Transformer,2017)

# 归一化在子层之后
x = x + Attention(x)
x = Norm(x)
x = x + FeedForward(x)
x = Norm(x)
python

Pre-Norm(现代 Transformer,Llama/MiniMind)

# 归一化在子层之前
x = x + Attention(Norm(x))
x = x + FeedForward(Norm(x))
python

Pre-Norm 的优势

特性Post-NormPre-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
        对最后一层输出再次归一化
plaintext

7.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)
# 选择概率最高的 token
python

7.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.9M104M
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             # 是否使用 MoE
python

八、动手实验#

想要深入理解 FeedForward,可以尝试以下实验:

  1. 对比普通 FFN 和 SwiGLU:实现两种架构,对比参数量(SwiGLU 多 50%)和训练效果
  2. 验证扩张-压缩的必要性:尝试 768→768 直接变换 vs 768→2048→768,观察拟合能力差异
  3. 测试残差连接:对比有无残差连接的网络训练稳定性和收敛速度

参考 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 次 → 完整模型!
plaintext

9.3 系列回顾#

恭喜你完成了 MiniMind 核心架构的学习!

4 篇博客回顾

  1. RMSNorm:归一化原理,为什么比 LayerNorm 快 7.7 倍
  2. RoPE:位置编码,多频率机制的深入分析
  3. Attention:Q、K、V,Multi-Head 的完整理解
  4. FeedForward + 架构:扩张-压缩,完整组装

你现在掌握了

  • ✅ Transformer 的所有核心组件
  • ✅ 每个组件的数学原理和代码实现
  • ✅ 组件之间如何配合
  • ✅ 为什么 Transformer “work”
  • ✅ 可以从零实现一个小型 Transformer!

9.4 下一步建议#

1. 动手实践#

# 克隆 MiniMind
git clone https://github.com/jingyaogong/minimind
cd minimind

# 训练一个小模型
cd trainer
python train_pretrain.py
bash

2. 深入学习#

  • GQA (Grouped Query Attention):节省显存
  • Flash Attention:优化计算效率
  • MoE (Mixture of Experts):提升容量
  • KV Cache:加速推理

3. 实现挑战#

# 挑战:从零实现 MiniMind
class MyMiniMind(nn.Module):
    def __init__(self):
        # 你来实现!
        pass
python

十、参考资料#

论文#

代码#

相关阅读#


本文作者:joye 发布日期:2025-12-30 最后更新:2025-12-30 系列文章:MiniMind 学习笔记(4/4)

如果觉得有帮助,欢迎:

  • ⭐ Star 原项目 MiniMind
  • ⭐ Star 我的学习笔记 minimind-notes
  • 💬 留言讨论你的学习心得
  • 🔗 分享给其他学习 LLM 的朋友
FeedForward与Transformer Block:Attention之外的另一半
https://astro-pure.js.org/blog/20251219---feedforward-transformer-block/post
Author Joye
Published at 2025年12月19日
Comment seems to stuck. Try to refresh?✨