大模型底层学习(一)-Transformer 架构总览

0 阅读19分钟

前言

过去半年在 Agent 方向投入了大量精力,从 Agent 记忆架构设计到 Skill 工程化落地,积累了不少实践经验。但回头审视,发现自己一直停留在应用层——知道怎么调、怎么排、怎么蒸馏,但对底层为什么是这样缺乏系统理解。

于是决定给自己开一门"补课",从 Transformer 架构出发,系统学习大模型底层知识。不追求面面俱到,但求每个概念都能讲清楚"为什么"。

核心概念

1.1 CNN vs RNN vs Transformer:三种处理数据的方式

在Transformer出现之前,处理序列数据(如文本)的主流方法是RNN(循环神经网络) 。而更早之前,处理图像数据的主流方法是CNN(卷积神经网络) 。三者代表了不同时代、不同场景下处理数据的典型思路。

CNN vs RNN vs Transformer:核心区别一览表

维度CNN(卷积神经网络)RNN(循环神经网络)Transformer
核心操作卷积核在空间上滑动隐藏状态在时间步上传递注意力机制全局关联
擅长捕捉局部空间模式(边缘→纹理→部件→整体)时间序列依赖(前文→后文)全局依赖关系(任意两个位置直接关联)
参数共享同一个卷积核扫遍整个输入同一组权重在每个时间步复用Q/K/V投影矩阵在序列上复用
并行性✅ 完全并行(每个位置独立卷积)❌ 必须串行(等前一个时间步算完)✅ 完全并行(所有位置同时计算)
感受野局部→通过层层堆叠扩大理论上无限(实际会遗忘)全局(单层即可看到所有位置)
典型输入图像、音频频谱图、网格数据文本、语音、时间序列文本(当前主流)、图像(ViT)、多模态

CNN 的工作方式

输入: 一张图片(像素矩阵)
          │
          ▼
  ┌─────────────────┐
  │ 卷积层1 (3×3核)  │  ← 提取边缘、线条
  │  ReLU激活        │
  │  MaxPooling      │  ← 下采样
  └─────────────────┘
          │
          ▼
  ┌─────────────────┐
  │ 卷积层2 (3×3核)  │  ← 提取纹理、图案
  │  ReLU + Pooling  │
  └─────────────────┘
          │
          ▼
  ┌─────────────────┐
  │ 全连接层         │  ← 综合判断:猫/狗/车
  └─────────────────┘
          │
          ▼
  输出: 分类结果

CNN像一个拿放大镜的鉴定师——从局部特征开始看,逐层组合成整体判断。

CNN 的主场

  • 图像分类:猫狗识别、医学影像筛查 — 卷积核逐层提取"边缘→纹理→器官轮廓"
  • 目标检测:自动驾驶识别行人/车辆 — YOLO、Faster R-CNN
  • 图像分割:把每个像素分类(如肿瘤区域勾画)— U-Net
  • 人脸识别:人脸特征提取比对 — FaceID
  • OCR:文字检测定位 — 识别图片中文字位置

RNN的工作方式

时间步 t=1: 输入"今" → 隐藏状态 h1 → 输出
时间步 t=2: 输入"天" + 隐藏状态h1 → 隐藏状态 h2 → 输出
时间步 t=3: 输入"天" + 隐藏状态h2 → 隐藏状态 h3 → 输出
时间步 t=4: 输入"气" + 隐藏状态h3 → 隐藏状态 h4 → 输出

RNN像一个流水线工人——每次只处理一个物品,把结果传给下一个工人。

优点:能处理任意长度的序列
致命缺点

  1. 无法并行:必须等前一个词处理完才能处理下一个
  2. 长距离依赖问题:处理第100个词时,第1个词的信息可能已经丢失

Transformer的工作方式

输入: ["今", "天", "天", "气"]
          │
          ▼
  ┌───┬───┬───┬───┐
  │   │   │   │   │  ← 所有词同时处理
  │   │   │   │   │  ← Self-Attention:每个词"看到"所有其他词
  │   │   │   │   │  ← FFN:独立处理每个词
  └───┴───┴───┴───┘
          │
          ▼
  输出: ["今天", "天气"]

Transformer像一个会议主持人——把所有参会者同时叫到一起,每个人都能听到其他人的发言。

关键洞察:Self-Attention让每个词都能直接"看到"序列中的其他所有词,不再需要逐个传递信息。

为什么 CNN 和 RNN 在 NLP 中退场了

RNN 的死因:必须串行计算,GPU 几千个核只能用一个 → 速度慢;长序列梯度消失 → 记不住远处的信息。Transformer 直接用 Self-Attention 替代,完全并行 + 全局视野,RNN 被降维打击。

CNN 在 NLP 中的退场:文本没有"局部空间结构",卷积核的归纳偏置(局部性)在文本里不如 Attention 的全局视野有用。但在视觉领域 CNN 仍然很强,尤其是计算资源受限场景(移动端、边缘设备),因为 CNN 的参数效率和推理速度比 ViT(Vision Transformer)更优。

1.2 Transformer的完整架构

image.png

Transformer由两部分组成:Encoder(编码器)Decoder(解码器) ,拆解一下图中的流程:

输入序列
   │
   ▼
  ┌─────────┐
  │ Embedding │  ← 词嵌入(把词变成向量)
  │ + Pos Enc │  ← 位置编码(告诉模型词的位置)
  └─────────┘
   │
   ▼
  ┌─────────────────────────────┐
  │         Encoder Stack         │
  │  ┌─────────────────────────┐  │
  │  │   Multi-Head Self-Attn   │  │
  │  │   + Add & LayerNorm      │  │
  │  │   + FFN (前馈网络)        │  │
  │  │   + Add & LayerNorm      │  │
  │  └─────────────────────────┘  │
  │  ┌─────────────────────────┐  │
  │  │   Multi-Head Self-Attn   │  │
  │  │   + Add & LayerNorm      │  │
  │  │   + FFN                  │  │
  │  │   + Add & LayerNorm      │  │
  │  └─────────────────────────┘  │
  │  ... (N层重复)              │  │
  └─────────────────────────────┘
   │
   ▼
  ┌─────────────────────────────┐
  │         Decoder Stack         │
  │  ┌─────────────────────────┐  │
  │  │ Masked MHSA (防偷看)      │  │
  │  │ + Add & LayerNorm        │  │
  │  │ + Cross-Attention         │  │
  │  │   (用Encoder输出作为K,V)  │  │
  │  │ + Add & LayerNorm        │  │
  │  │ + FFN                    │  │
  │  │ + Add & LayerNorm        │  │
  │  └─────────────────────────┘  │
  │  ... (N层重复)              │  │
  └─────────────────────────────┘
   │
   ▼
  线性层 + Softmax
   │
   ▼
  输出概率分布

1.3 六大核心组件详解

组件1:词嵌入(Embedding)

作用:把离散的token(整数)变成连续的向量,词表会把每个字映射为一个整数的token_id。

import torch
import torch.nn as nn

# 词嵌入层:vocab_size=30000,模型的词表大小, d_model=768,每个 token 被表示为一个 768 维的浮点数向量,贯穿后续所有的使用。
embed = nn.Embedding(30000, 768)

# 输入: [batch_size, seq_len] = [2, 5]
input_ids = torch.tensor([
    [100, 200, 300, 400, 500],
    [150, 250, 350, 450, 550]
])

# 输出: [batch_size, seq_len, d_model] = [2, 5, 768]
embeddings = embed(input_ids)
print(f"Embedding shape: {embeddings.shape}")

每个token变成一个768维的向量。

组件2:位置编码(Positional Encoding)

为什么需要? Transformer没有RNN的循环结构,不知道词的顺序。

想象一下:如果你把所有词的顺序打乱,Transformer对每个词的处理是独立的(在Self-Attention之前),所以输出会完全一样——因为它不知道哪个词排第几位。

方法:把位置编码向量加到词嵌入向量上:

image.png

其中 i 是词在序列中的位置。

组件3:Self-Attention(自注意力)

Self-Attention的核心思想:每个词同时关注序列中所有其他词,根据相关性加权聚合信息

公式:

image.png

Q(Query) :我在找什么
K(Key) :我能提供什么
V(Value) :我实际包含什么

组件4:FFN(前馈网络)

每个Transformer层里,Self-Attention之后还有一个全连接前馈网络

FFN(x) = SwiGLU(xW_1 + b_1)W_2 + b_2

为什么需要FFN? 因为Self-Attention只处理了"词之间的关系",但没有做"词本身的非线性变换"。

类比:Self-Attention像是开会讨论(词之间互相交流),FFN像是每个人回去独自思考(对收到的信息做非线性处理)。

具体结构

  1. 先经过一个线性变换,维度从 d_model4*d_model(扩大4倍)
  2. 经过SwiGLU激活函数
  3. 再经过一个线性变换,维度从 4*d_modeld_model(恢复原尺寸)

参数量:FFN部分通常是大模型中参数量最大的部分

例如,一个4096维的模型(如LLaMA-2 70B):

  • Self-Attention:4096×4096×4(Q/K/V+输出投影)= 约6700万参数
  • FFN:4096×16384 + 16384×4096 = 约1.34亿参数

FFN的参数量是Self-Attention的约2倍!

Self-Attention 的本质局限

Self-Attention 只做线性加权(加权平均),不管怎么组合权重,本质上都是把已有信息混合在一起,不会产生新的信息、新的特征。FFN 就是来解决这个问题的——对每个词的向量做非线性变换,提炼出更深层的模式。

FFN 内部的详细过程
  1. 第一步:线性变换d_model4*d_model(扩大4倍)。类比"回工位展开笔记本,把讨论内容拆解到多个角度"
  2. 第二步:激活函数(SwiGLU/GELU)。这一步是关键——没有它,线性层叠再多也等价于一个线性层。引入非线性,让模型能学到复杂的模式
  3. 第三步:线性变换4*d_modeld_model(压缩回来)。类比"写成一页总结,提炼出关键结论"
具体例子:"苹果"的语义转换

以"苹果"为例,Self-Attention 后"苹果"的向量已经融合了上下文(如前面有"吃了"),但此时向量里"吃了"和"苹果"的信息只是混在一起。FFN 要做的是从"吃了+苹果"的简单叠加,变成"吃了苹果"这个组合语义——一个全新的特征。

FFN 为什么扩大 4 倍

高维空间更容易线性可分。把 768 维映射到 3072 维,就像把揉成一团的纸展开——在更高维度的空间里,不同语义模式的边界更清晰。

FFN 存储知识的证据

有研究表明,FFN 的第一层相当于一个"键值记忆库",不同的神经元存储了不同的知识和模式。FFN 占了每层 2/3 的参数,模型的"知识"主要存在 FFN 的权重矩阵里。

没有 FFN 会怎样

如果去掉 FFN,只剩 Self-Attention 堆叠,模型只能做信息的重新分配和混合,无法学到非线性特征,表达能力大幅下降。

一句话总结

Self-Attention 让词之间交换信息("听"),FFN 让每个词把交换来的信息消化成新的理解("想"),缺一不可。

组件5:残差连接(Residual Connection)

核心思想:不是直接传递输出,而是"原始输入 + 处理结果"。

output = LayerNorm(x + SubLayer(x))

类比:调节水温时不是"重新调一杯",而是在原基础上加减几度

为什么需要?

  1. 缓解梯度消失:深层网络中,梯度从最后一层传回第一层时会指数级缩小。残差连接提供了梯度高速公路——梯度可以直接从深层跳到浅层。
  2. 学习恒等映射:如果某个层不需要改变输入,残差连接可以让它学习"什么都不做"(输出为0)。
def residual_block(x, sub_layer):
    """残差连接的基本结构"""
    sub_output = sub_layer(x)    # 子层处理
    return x + sub_output        # 原始输入 + 处理结果
梯度反向传播的详细解释

x + SubLayer(x) 求导:∂输出/∂x = 1 + ∂SubLayer(x)/∂x

关键是那个 1——它不依赖任何权重,不会变小,不会消失。

反向传播时梯度有两条路径:

  1. 路径1:经过 SubLayer 的权重矩阵 → 可能被反复乘,指数级缩小(梯度消失)
  2. 路径2:直接加1,原样传过去 → 梯度不变,畅通无阻

没有谁来"决定"走哪条路,两条路同时存在,梯度是两条路的和。

对比没有残差连接的情况

普通网络 96 层:0.9 × 0.9 × ... × 0.9(96次)= 0.00003,梯度几乎为0

有残差连接:最坏情况 = 0 + 0 + ... + 1 = 1,仍然有梯度

谁决定梯度是否需要过某一层

没有机制来"决定"。

是训练过程中权重自动调整的结果:

  • 如果某层有用 → 反向传播时梯度自然倾向于走该层 → 权重被强化
  • 如果某层没用 → 权重趋近于0 → 梯度主要通过残差路径跳过 → 该层自动变成"透传"

这就是论文标题"Deep Residual Learning"的核心:学习的是"残差"(SubLayer(x)),不是完整变换。如果某层不需要变换,学一个全零输出即可。

一句话总结

没有任何机制来"决定"——残差连接通过 +1 的数学性质,无条件保证每层都能收到梯度。至于每层学到了什么、是否被"跳过",是训练过程中权重自动调整的结果。

组件6:Layer Normalization

作用:对每个样本的特征维度做归一化,使训练更稳定。

LayerNorm(x) = γ · (x - μ) / √(σ² + ε) + β

其中:

  • μ是该样本所有特征的均值
  • σ是该样本所有特征的标准差
  • γβ是可学习的缩放和平移参数
  • ε是极小值,防止除以0

为什么不用BatchNorm?

BatchNorm是对批次维度做归一化,但文本序列长度可变,不同样本的特征维度不同。LayerNorm对每个样本独立做归一化,不受批次大小和序列长度影响。

类比:BatchNorm像是"全班同学统一打分"(依赖批次),LayerNorm像是"每个人自己调整分数"(独立处理)。

为什么深层网络需要归一化

深层网络中,每一层的输出都会放大或缩小上一层的输出。假设每层平均放大 1.05 倍:

  • 10层后:1.05^10 ≈ 1.63(还好)
  • 50层后:1.05^50 ≈ 11.47(开始失控)
  • 96层后:1.05^96 ≈ 125倍(GPT-3 的层数,完全爆炸)

反之如果每层平均缩小 0.95 倍:

  • 96层后:0.95^96 ≈ 0.007(梯度几乎为0)

这就是为什么没有归一化的深层网络根本无法训练。

LayerNorm 具体在做什么(逐步拆解)

用一个具体例子说明。假设某个 token 的向量是 [2.0, -1.0, 0.5, 3.0](简化为4维,实际768维):

第一步:算均值
μ = (2.0 + (-1.0) + 0.5 + 3.0) / 4 = 1.125

第二步:算方差
σ² = ((2.0-1.125)² + (-1.0-1.125)² + (0.5-1.125)² + (3.0-1.125)²) / 4 = 2.422

第三步:标准化
x_norm = (x - μ) / √(σ² + ε) = (x - 1.125) / 1.557
→ [0.563, -1.366, -0.401, 1.205]
→ 均值变成0,方差变成1

第四步:缩放和平移
output = γ × x_norm + β
→ 假设 γ=2, β=0.5 → [1.626, -2.232, -0.302, 2.910]
→ 恢复了表达能力

为什么要加 γ 和 β:如果只做标准化(第三步),所有 token 的向量都被压缩到相同的分布,模型丢失了"这个特征应该大、那个特征应该小"的信息。γ 和 β 是可学习参数,让模型自己决定每个维度"应该"是什么分布——需要大的维度 γ 调大,需要小的维度 γ 调小,需要偏移的维度 β 调正或调负。

为什么不用 BatchNorm

假设一个 batch 有 3 个句子:

  • 句子A: [今天, 天气, 真, 好](4个token)
  • 句子B: [我, 喜欢, 苹果](3个token)
  • 句子C: [深度, 学习, 很, 有, 意思](5个token)

BatchNorm 的问题

  • 它沿 batch 方向归一化,但每个句子长度不同,无法对齐
  • batch_size=1(推理时)没有统计意义
  • 不同句子之间的统计量差异大,归一化不稳定

LayerNorm 的优势

  • 每个 token 独立归一化,不受 batch 中其他句子影响
  • 不依赖 batch_size,训练和推理行为一致
  • 对变长序列天然友好
对比维度BatchNormLayerNorm
归一化方向沿 batch 维度(跨样本)沿特征维度(单样本内)
依赖 batch_size是,batch_size=1 时失效否,每个样本独立
变长序列不兼容(长度不一致)兼容(每个 token 独立处理)
训练/推理差异有(推理用移动平均统计量)无(行为一致)
适用场景图像(固定尺寸)文本(变长序列)

1.4 一个完整的Transformer Layer

输入 x
  │
  │── Self-Attention层
  │   Q = xW_Q, K = xW_K, V = xW_V
  │   Attention = softmax(QK^T/√d_k)V
  │   output1 = LayerNorm(x + Attention)  ← 残差连接
  │
  │── FFN层
  │   hidden = SwiGLU(output1 × W_1)
  │   output2 = LayerNorm(output1 + hidden)  ← 残差连接
  │
  ▼
  输出 output2

关键设计模式:每个子层(Self-Attention和FFN)都被包裹在"残差连接 + LayerNorm"的结构中。这就是所谓的Post-LayerNorm(Post-LN)结构。

注意:原始Transformer用的是Post-LN,但GPT-2和LLaMA等后来的模型改成了Pre-LayerNorm(把LayerNorm放在残差连接之前)。Pre-LN能更好地稳定训练,是大模型的标准配置。


技术细节

2.1 完整的Transformer Block代码

import torch
import torch.nn as nn
import torch.nn.functional as F

class SwiGLU(nn.Module):
    """SwiGLU激活函数(常用于大模型,如LLaMA)"""
    def __init__(self, dim_in, dim_out):
        super().__init__()
        self.w1 = nn.Linear(dim_in, dim_out)
        self.w2 = nn.Linear(dim_in, dim_out)
        self.w3 = nn.Linear(dim_out, dim_out)
    
    def forward(self, x):
        # SwiGLU(x) = SiLU(w1(x)) * w2(x)
        return F.silu(self.w1(x)) * self.w2(x)

class TransformerBlock(nn.Module):
    """完整Transformer Block(Pre-LayerNorm版本)"""
    def __init__(self, d_model=4096, n_heads=32, d_ff=16384, dropout=0.1):
        super().__init__()
        
        # 参数说明(以LLaMA-2 70B为例)
        # d_model = 4096: 每个token的向量维度
        # n_heads = 32: 注意力头数,每头维度=4096/32=128
        # d_ff = 16384: FFN中间层维度(= d_model × 4)
        
        # Self-Attention
        self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True, dropout=dropout)
        
        # Layer Normalization(Pre-LN:放在残差连接之前)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        
        # FFN(前馈网络)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),           # 高斯误差线性单元(大模型常用)
            nn.Linear(d_ff, d_model)
        )
        
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        """前向传播"""
        
        # === 第一步:Self-Attention ===
        # Pre-LN:先归一化,再做注意力
        attn_input = self.norm1(x)
        attn_out, _ = self.attn(attn_input, attn_input, attn_input, attn_mask=mask)
        # 残差连接:原始输入 + 注意力输出
        x = x + self.dropout(attn_out)
        
        # === 第二步:FFN ===
        # Pre-LN:先归一化,再做FFN
        ffn_input = self.norm2(x)
        ffn_out = self.ffn(ffn_input)
        # 残差连接:原始输入 + FFN输出
        x = x + self.dropout(ffn_out)
        
        return x

# 使用示例
model = TransformerBlock(d_model=768, n_heads=12, d_ff=3072)

# 模拟输入:batch_size=2, seq_len=10, d_model=768
x = torch.randn(2, 10, 768)
output = model(x)

print(f"输入 shape: {x.shape}")
print(f"输出 shape: {output.shape}")  # 和输入一样!
print(f"参数量: {sum(p.numel() for p in model.parameters()):,}")

每个Transformer Block的参数量

组件参数量(LLaMA-2 70B为例)说明
Q/K/V投影4096×4096 × 3 = 5033万每个头128维,32个头
输出投影4096×4096 = 1678万拼接所有头的输出
FFN内部4096×16384 × 2 = 1.34亿两个线性层
LayerNorm4096 × 2 = 8192两个归一化层
总计约2.01亿每个Block

LLaMA-2 70B有80个Transformer Block,加上词嵌入和输出层,总共约700亿参数。

2.2 计算复杂度分析

架构时间复杂度空间复杂度并行性
CNNO(n·k·d)O(k·d)✅ 完全并行
RNNO(n·d²)O(d)❌ 串行
Self-AttentionO(n²·d)O(n²)✅ 完全并行

关键洞察

  1. CNN最轻量:O(n·k·d),卷积核大小k固定,效率最高,但对文本缺乏全局视野
  2. RNN是串行的:必须按顺序处理n个token,每个token需要O(d²)计算,总时间O(n·d²)
  3. Self-Attention可以完全并行:所有token之间的相关性可以同时计算
  4. 瓶颈是n²:序列长度的平方决定了计算量和显存占用

为什么这对大模型训练至关重要?

因为GPU的优势就是并行计算。RNN必须串行,GPU只能用一个核心,和CPU差不多快。Self-Attention可以完全并行,GPU的几千个核心可以同时工作,计算速度提升几十到几百倍。

但是,Self-Attention也有代价:显存占用是O(n²)。一个4096长度的序列需要存储 个注意力分数,占用约64MB显存(FP16)。对于128K上下文,需要 个分数,占用约61.5GB显存!

实际案例

GPT-3的架构细节

GPT-3基于Transformer Decoder-only架构,具体配置:

参数含义
层数96层递进分析,每层都在之前的基础上进行学习和总结
每层头数96头并行分析,每一头代表一个维度:关系、类别...
每头维度128
模型维度12288(96×128)
总参数量1750亿
词表大小50257(Byte Pair Encoding)
上下文窗口2048 tokens

GPT-3的创新不在于架构(和GPT-2几乎一样),而在于规模——1750亿参数,3000亿tokens的训练数据。 ——GPT系列(GPT-3、GPT-4等)只用了Decoder部分!为什么?

为什么GPT是Decoder-only

因为GPT的目标是自回归生成(看到前面的词,预测下一个词)。Decoder的Masked Self-Attention天然适合这种"只能看前面"的任务。

而BERT(双向语言模型)只用了Encoder部分,因为它需要同时利用前后文信息来理解词义。

BERT架构现在最大的活路就是做Embedding模型。 因为双向注意力天然适合把整句编码成一个向量,而生成任务已经被GPT路线统治了。各大RAG框架里用的embedding模型,底层基本都是Encoder-only架构。

LLaMA-2的架构改进

LLaMA-2基于GPT-3的架构,做了以下改进:

  1. SwiGLU激活:用SwiGLU替代GPT-3的ReLU,提升了非线性表达能力
  2. RMSNorm替代LayerNorm:更简单高效的归一化方法
  3. RoPE旋转位置编码:替代了原始Transformer的正弦位置编码
  4. GQA(分组查询注意力) :将K和V头数从32减少到4,显存占用大幅降低

常见误区

误区1:Transformer只能做生成任务

事实:Transformer的Encoder部分(如BERT)非常适合文本理解任务。GPT系列只做生成是因为它们只用了Decoder部分。

误区2:Self-Attention的计算量很小

事实:Self-Attention的时间复杂度是O(n²·d)。当序列长度n很大时(如128K),计算量是 128000^2 * d,非常恐怖。这也是为什么长上下文模型需要大量显存。

误区3:RNN/CNN已经完全被淘汰了

事实:RNN在处理非常长的序列时仍然有优势(O(n)显存 vs O(n²)显存);CNN在视觉领域和边缘计算场景中仍然很强(参数效率、推理速度优于ViT)。Mamba等新的State Space Model(SSM)就是受RNN启发,结合了Transformer和RNN的优点。

误区4:LayerNorm和BatchNorm没区别

事实:BatchNorm对批次维度归一化,LayerNorm对特征维度归一化。在NLP中,因为序列长度可变,BatchNorm会导致归一化不稳定。LayerNorm对每个样本独立处理,更稳定。

最后

感谢你能看到这里,本文简单介绍了【Transformer的架构】,希望对你有用,更多Agent、前端、node、性能相关的知识文章,更多 Agent、前端、Node、性能相关的技术文章和实践总结,可以查看我的代码花园:

📦 github.com/AdolescentJ…