第二章 Transformer架构解析(下)
在进入最关键的Attention之前,我们回顾一下上一个章节的内容:
| 学到的概念 | 核心作用 |
|---|---|
| Tokenization | 文字 → Token |
| Embedding | Token → 向量 |
| Positional Encoding | 给向量添加位置信息 |
| LayerNorm + Softmax | 层归一化缩放 + 数字变概率分布 |
| 神经网络层(FFN) | 非线性变换,理解消化信息 |
2.4 Attention注意力机制
2.4.1 线性变换
在理解 Attention 之前,我们需要先复习一下关于线性变换的知识。这部分看似数学,其实非常直观,也是整个 Attention 机制的 “起点”。
你可以把线性变换理解成一句话:
把一组向量,通过一个可学习的矩阵,投影到一个新的空间里,让它更适合后续计算。
2.4.1.1 矩阵的乘法
矩阵的乘法规则:
[A, B] × [B, C] = [A, C]两个矩阵相乘,第一个矩阵的列必须等于第二个矩阵的行才能相乘,相乘之后的矩阵大小为第一个矩阵的行X第二个矩阵的列,示例如下:
2.4.1.2 矩阵的线性变换
矩阵的变换满足两个条件,可以称其为线性变换:
对任意向量 x、y 和任意数 k:
- 加完再变 = 变完再加
T(x+y)=T(x)+T(y)
乘完再变 = 变完再乘
T(kx)=kT(x)
满足这两条,就是线性变换。
[A,B] × [B,1] = [A,1]跟 Attention 有啥关系?
在 Transformer 里:
Q = 输入 @ Wq K = 输入 @ Wk V = 输入 @ Wv这些全都是线性变换:
- 向量被矩阵拉伸、旋转、投影
- 但不会弯曲、不会平移
- 所以最后算出来的 Q、K、V 还保持着原来的线性结构,方便后续做点积算相似度
2.4.2 Attention的意义
我们先来看一下Attention的结构:
2.4.2.1 Attention的内部结构
input X ↓ 生成 Q, K, V(Linear层,通过 Wq, Wk, Wv 三个权重矩阵) ↓ MatMul(Q @ K^T,计算点积) ↓ Scale(除以 √d_key,缩放点积) ↓ Mask(掩码,防止看到下一个Token) ↓ Softmax(转换为概率分布) ↓ MatMul(与 V 相乘,加权求和) ↓ Concatenate(多头合并) ↓ Wo(Linear层,输出投影) ↓ output完整公式:
Attention(Q,K,V)=softmax(QK⊤dk)V \operatorname{Attention}(Q, K, V) = \operatorname{softmax}\left( \frac{Q K^\top}{\sqrt{d_k}} \right) VAttention(Q,K,V)=softmax(dkQK⊤)V
在第二章开头,我们已经解释了Attention对于传统RNN和CNN的优势
我们先熟悉一下整体流程,然后再来讲解每一步的操作原理。
2.4.2.2 Q、K、V是什么?
在实际训练中,往往会同时处理多个多个句子
1.股海沉浮需理性分析策略静待良机稳健获利
2.编程架构严谨逻辑高效运维安全稳定性能卓越
3.每日坚持读书学习积累知识拓宽眼界提升自我价值
4.乘风破浪勇往直前努力奋斗梦想实现未来光明可期
输入矩阵:
X: [batch_size, context_length, d_model]- batch_size = 4:同时处理 4 个句子(批次大小)
- context_length= 16:每个句子有 16 个 token(上下文长度)
- d_model = 512:每个 token 用 512 维向量表示(模型维度)
上面示例的输入形状为就是**[4,16,512]**
X=输入的Token向量,Q、K、V都是从同一个输入X乘以对应权重矩阵得到:
Q = X @ Wq K = X @ Wk V = X @ Wv三个权重矩阵(Wq、Wk、Wv)是可学习的参数,在训练过程中不断调整。
我们以生成Q为例:
X: [4, 16, 512] (batch_size, context_length, d_model) Wq: [512, 512] (d_model, d_model) Q: [4, 16, 512] (batch_size, context_length, d_model)前面我们已经介绍了矩阵乘法的规则:[…, A, B] @ [B, C] = […, A, C]
所以:[4, 16, 512] @ [512, 512] = [4, 16, 512]
Q、K、V 的形状和输入 X 完全相同
为什么需要分成三份?
既然形状都一样,为什么还需要三个不同的矩阵呢?
因为Q、K、V 承担不同的角色。
- Q(Query,查询):代表"我在找什么信息"
- K(Key,键):代表"我有什么信息可以被找到"
- V(Value,值):代表"如果被找到,我提供什么内容"
在通过不同的权重矩阵Wq、Wk、Wv,模型可以学会:
- 把同一个词转换成不同的"角色"
- 在不同的"语义空间"中进行匹配
做一个比喻:
| 角色 | 类比 | 作用 |
|---|---|---|
| Query (Q) | 读者的搜索词 | “我想找关于深度学习的书” |
| Key (K) | 每本书的索引标签 | “机器学习, Python入门,深度学习” |
| Value (V) | 书的实际内容 | 深度学习这本书的内容 |
什么是Q@K^T
我们要让 Q 的每一行和 K 的每一行做点积去计算每个Token的相似度,根据矩阵乘法规则,则需要将K转置
Q[4,16,,128] @ K^T[4,128,16] = [4, 16, 16]这里的 d_model = 128 是因为在 Multi-Head Attention 中,d_model 会被分成多个头。每个头的维度是 d_model = d_model / num_heads = 512 / 4 = 128。这个我们后续会再提。
结果矩阵的形状是[4, 16, 16]:
- 4 个句子
- 每个句子有一个 16×16 的"注意力矩阵"
- 位置 (i, j) 表示:第 i 个 token 对第 j 个 token 的关注程度
我 爱 学 习 我 [ ] 爱 [ ] 学 [ ] 习 [ ]每个位置的值 = 对应 Q 行向量和 K 列向量的点积。
下一步Scale,缩放操作
Q @ K^T 的结果需要缩放:
Attention Scores = (Q @ K^T) / √d_key缩放的作用:当d_model很大时,点积的结果也会很大
点积 = Σ(q_i × k_i) # 128 个数相乘再相加数值过大的后果:导致softmax后概率变得极端
Softmax([5, 3, 2]) ≈ [0.844, 0.114, 0.042] # 极端分布 假设d_model=64,缩放因子=√64=0.125 Softmax([0.625, 0.375, 0.25]) ≈ [0.405, 0.316, 0.279] # 平滑分布一句话:除以 √d_model 就是把结果数值压小,让 Softmax 不那么极端,梯度更稳定,模型更容易训练。
Mask:掩码
Mask 前的注意力分数矩阵:
| 我 | 爱 | 学 | 习 | |
|---|---|---|---|---|
| 我 | 0.9 | 0.3 | 0.1 | 0.05 |
| 爱 | 0.4 | 0.8 | 0.2 | 0.1 |
| 学 | 0.2 | 0.3 | 0.7 | 0.25 |
| 习 | 0.15 | 0.2 | 0.3 | 0.85 |
Mask 后的注意力分数矩阵(下三角掩码):
| 我 | 爱 | 学 | 习 | |
|---|---|---|---|---|
| 我 | 0.9 | -inf | -inf | -inf |
| 爱 | 0.4 | 0.8 | -inf | -inf |
| 学 | 0.2 | 0.3 | 0.7 | -inf |
| 习 | 0.15 | 0.2 | 0.3 | 0.85 |
在Transformer的自回归生成中,预测下一个Token时,不能提前看到下一个Token是什么,比如在预测"我爱学___",模型不能看到下一个Token。但是在Q @ K^T 时,会计算所有位置的相似度,因此我们需要在矩阵中遮住下一个Token。
解决方法:把右上角(未来的位置)设为负无穷(-inf),这样在softmax后,其概率会变为0,模型就不知道未来的信息了。
Softmax([0.9, -inf,-inf,-inf]) = [1.0, 0.0, 0.0, 0.0]下一步,在Mask后,对输出应用 Softmax,得到的结果就是注意力权重矩阵(每个位置应该分配多少注意力给其他位置)。
根据注意力公式,接下来需要将得到的注意力权重矩阵乘以V
Output = Attention_Weights @ V维度变化:
Attention_Weights: [4, 16, 16] (batch, context_len, context_len) V: [4, 16, 128] (batch, context_len, d_model) Output: [4, 16, 128] (batch, context_len, d_model)得到的output是什么?
output[i] = Σ(attention_weight[i,j] × V[j])每个位置的输出 = 所有位置的 V 的加权平均,权重由注意力分数决定。
output=根据注意力加权组合后的向量。
得到的新向量就包含了句子的语义信息,在Attention之前,每个Token只包含自己的语义信息和位置信息,而在Attention之后,每个Token还包含了该Token与其余Token的语义关系。
代码实现:
importtorchimporttorch.nnasnnimporttorch.nn.functionalasFclassScaledDotProductAttention(nn.Module):"""缩放点积注意力 + Mask 支持"""def__init__(self,dropout=0.1):super().__init__()self.dropout=nn.Dropout(dropout)defforward(self,Q,K,V,mask=None):""" Q: [batch_size, n_heads, seq_len, d_k] K: [batch_size, n_heads, seq_len, d_k] V: [batch_size, n_heads, seq_len, d_v] mask: [batch_size, 1, seq_len, seq_len] # 掩码矩阵 """# 1. 计算 Q · K^Td_k=Q.size(-1)attn_scores=torch.matmul(Q,K.transpose(-2,-1))# [B, H, L, L]# 2. 缩放 1/√d_kattn_scores=attn_scores/torch.sqrt(torch.tensor(d_k,dtype=torch.float32))# 3. Mask(填充-inf 屏蔽未来token)ifmaskisnotNone:attn_scores=attn_scores.masked_fill(mask==0,float('- inf'))# 4. Softmaxattn_weights=F.softmax(attn_scores,dim=-1)# [B, H, L, L]# 5. 乘以 Voutput=torch.matmul(attn_weights,V)# [B, H, L, d_v]returnoutput,attn_weights小结:
Q、K、V 是 Attention 关键。Q 代表"查询",K 代表"键",V 代表"值"。通过 Q @ K^T 得到注意力权重,用注意力权重对 V 加权求和,得到融合了上下文信息的新矩阵。这就是 Attention 让模型"理解"语言的方式。
完整流程:
X → [Wq, Wk, Wv] → Q, K, V ↓ Q @ K^T (相似度) ↓ 除以 √d_key (缩放) ↓ Mask (遮挡未来) ↓ Softmax (归一化) ↓ @ V (加权求和) ↓ Output2.4.3 多头注意力(Multi-Head Attention)
2.4.3.1 为什么需要多头注意力?
单头注意力只能从一个 “视角” 捕捉 Token 间的语义关联,而多头注意力让模型可以同时从多个不同的语义子空间(视角)学习 Token 间的关系,能更全面、更精细地捕捉文本中的复杂语义信息。
举个直观的例子:
对于句子 “苹果发布了新款手机,它的价格很亲民”,
- 头 1 可能关注 “它” 和 “新款手机” 的指代关系;
- 头 2 可能关注 “苹果” 和 “发布” 的动作关联;
- 头 3 可能关注 “价格” 和 “亲民” 的属性关联。
多个头的注意力互补,最终融合后的结果能让模型对语义的理解更完整。
2.4.3.2 多头注意力的核心逻辑
多头注意力的本质是:将 Q、K、V 拆分成多个子空间,在每个子空间独立计算注意力,最后将所有头的结果拼接并投影,得到最终输出。
核心步骤:
- 维度拆分:将原始的 Q、K、V(维度为
d_model)拆分为num_heads个独立的子 Q、子 K、子 V,每个子空间维度为d_k = d_model / num_heads(需保证d_model能被num_heads整除); - 独立计算:对每个子 Q、子 K、子 V,单独执行 “缩放点积注意力” 计算;
- 结果拼接:将所有头的注意力输出拼接,恢复到
d_model维度; - 线性投影:通过一个可学习的权重矩阵
W_o对拼接结果做线性变换,得到最终输出。
2.4.3.3 维度变化详解
以经典的d_model=512、num_heads=8为例:
- 原始 Q/K/V:
[batch_size, seq_len, 512] - 拆分后每个头的 Q/K/V:
[batch_size, seq_len, 64](512/8=64) - 为了并行计算,通常会调整维度顺序为:
[batch_size, num_heads, seq_len, d_k](即[4,8,16,64],对应前文batch_size=4、seq_len=16); - 每个头独立计算注意力后,输出为:
[batch_size, 8, 16, 64]; - 拼接后:
[batch_size, 16, 8×64=512]; - 最后通过
W_o(维度[512,512])投影,输出仍为[batch_size, 16, 512]。
2.4.3.4 多头的优势
- 多视角捕捉关联:不同头聚焦不同类型的语义关系(指代、动作、属性等),比单头更全面;
- 提升表达能力:拆分到子空间后,模型能学习更细粒度的语义模式,避免单一空间的信息混淆;
- 保持维度兼容:拼接 + 投影后输出维度与输入一致,可无缝接入后续的 FFN 层,符合 Transformer 的整体架构设计。
2.4.3.5 多头的优势
| 维度 | 单头注意力 | 多头注意力 |
|---|---|---|
| 语义视角 | 单一视角 | 多视角并行学习 |
| 表达能力 | 有限,易丢失细粒度信息 | 更强,能捕捉复杂语义关联 |
| 计算复杂度 | O(dmodel⋅L2) | O(dmodel⋅L2)(拆分后每个头计算量降低,整体与单头相当) |
| 训练稳定性 | 梯度易极端 | 多子空间分散梯度,更稳定 |
一句话总结:
把 QKV 分成 h 个头,分头算注意力,再拼回去投影,让模型看得更细、更全、更准。
2.4.4 残差链接(Residual Connection)
2.4.4.1 为什么需要残差链接?
Transformer 的 Encoder/Decoder 层堆叠了很多层(比如 BERT-base 有 12 层,GPT-3 有 96 层)。
层数越深,模型越容易出现两个致命问题:
- 梯度消失/爆炸:反向传播时,梯度在多层矩阵乘法中不断衰减或放大,导致底层无法有效更新。
- 模型退化(Degradation):层数过多时,深层网络可能出现“越学越差”的情况,甚至不如浅层网络效果好。
残差链接就是为了解决这两个问题而设计的。
2.4.4.2 残差链接的核心公式
假设某一层的输入为 x,该层的变换函数为 F(x)(例如多头注意力或前馈网络),则残差链接的输出为:
$$
\text{Output} = x + F(x)
$$
在 Transformer 中,它和层归一化(Layer Normalization)一起组成标准范式:
LayerNorm(x+F(x)) \text{LayerNorm}(x + F(x))LayerNorm(x+F(x))
2.4.4.3 Transformer 中的具体应用位置
残差链接贯穿整个 Transformer 架构,每一个子层都遵循这一结构:
Encoder 层
- 多头自注意力层:
LayerNorm(x + Attention(x)) - 前馈网络层:
LayerNorm(x + FeedForward(x))
- 多头自注意力层:
Decoder 层
- 掩码多头自注意力层:
LayerNorm(x + MaskedAttention(x)) - Encoder-Decoder 交叉注意力层:
LayerNorm(x + CrossAttention(x, EncoderOutput)) - 前馈网络层:
LayerNorm(x + FeedForward(x))
- 掩码多头自注意力层:
2.4.4.4 残差链接的作用详解
缓解梯度消失,让深层网络可训练
残差链接提供了一条“梯度高速通道”。在反向传播时,梯度可以直接通过x这条路径无损地传回底层,避免了在深层网络中逐层衰减,让模型可以稳定训练几十甚至上百层。解决模型退化问题
残差链接允许网络在学习复杂特征的同时,保留原始输入信息。如果某一层的变换F(x)效果不佳,网络可以选择让F(x)趋近于 0,直接通过x传递信息,保证至少不会比浅层网络差。信息传递更完整
直接将原始输入x加到变换后的结果上,相当于给了模型一个“参考系”,避免了信息在多层变换中丢失,让后续层可以同时利用原始信息和新学到的特征。
2.4.4.5 残差链接的 PyTorch 代码实现
以多头注意力层为例,残差链接的实现非常简洁:
importtorchimporttorch.nnasnn# 以之前的 MultiHeadAttention 类为基础classTransformerEncoderLayer(nn.Module):def__init__(self,d_model,num_heads,d_ff,dropout=0.1):super().__init__()self.attn=MultiHeadAttention(d_model,num_heads,dropout)self.ffn=nn.Sequential(nn.Linear(d_model,d_ff),nn.ReLU(),nn.Linear(d_ff,d_model),nn.Dropout(dropout))self.norm1=nn.LayerNorm(d_model)self.norm2=nn.LayerNorm(d_model)self.dropout=nn.Dropout(dropout)#随机丢弃,防止过拟合defforward(self,x,mask=None):# 1. 多头注意力 + 残差链接 + 层归一化residual=x attn_out,_=self.attn(x,x,x,mask)x=residual+self.dropout(attn_out)x=self.norm1(x)# 2. 前馈网络 + 残差链接 + 层归一化residual=x ffn_out=self.ffn(x)x=residual+self.dropout(ffn_out)x=self.norm2(x)returnx2.4.4.6 Dropout
在代码中我们可以发现,在LayerNorm之后接了一个Dropout。
在神经网络有一个常见问题:过拟合。
过拟合是指模型在训练数据上表现很好,但在新数据上表现很差。就像一个学生:
- 把所有考试题都背下来了(训练数据)
- 但遇到新题就不会做(测试数据)
模型"记住"了训练数据的细节,而不是学到了通用的规律。
而Dropout的解决方法:在训练时随机丢弃一些神经元
核心作用:
- 防止过拟合,提升泛化能力
每次训练都随机关闭不同的神经元,迫使模型不依赖任何单个神经元/特征,学习到更鲁棒、更通用的模式,而不是死记训练数据。 - 相当于训练了多个“子模型”的集成
每次随机失活的网络结构都不一样,相当于同时训练了很多不同的小网络,推理时用的是所有子模型的平均效果,天然带有“模型集成”的优势。 - 降低神经元间的协同依赖
防止某些神经元“过度合作”,避免它们为了拟合训练数据互相形成依赖关系,让每个神经元都能学到独立有用的特征。
训练 vs 推理的 Dropout 差异:
| 阶段 | Dropout 状态 | 行为 |
|---|---|---|
| 训练 | 启用 | 随机失活神经元,按比例缩放剩余输出 |
| 推理 | 关闭 | 所有神经元都正常工作,输出无缩放 |
一句话总结:
残差链接打通梯度通道、避免网络退化,Dropout 随机失活防过拟合,两者配合让深层 Transformer 训练稳定又能学到通用特征。
2.5 向前传播
2.5.1 GTP-1 与 GTP-2架构对比
这张图对比了 GPT-2 和 GPT-1 的架构。它们都是Decoder-Only架构,主要区别是 LayerNorm 的位置和新增了Dropout:
- 训练稳定性大幅提升(最关键的优势)
- Post-Norm 的痛点:残差相加后再做归一化,梯度在反向传播时必须穿过归一化层。在深层网络中,这会导致梯度被不断缩放,容易出现梯度消失 / 爆炸,训练时需要配合复杂的学习率预热(Warmup)、梯度裁剪等技巧,否则很难收敛。
- Pre-Norm 的优势:归一化仅作用于进入子层的输入,残差连接的 “主干道” 完全不受归一化影响,梯度可以 “无阻碍” 地从深层直接传回浅层。即使是上百层的大模型,也能稳定训练,对学习率的鲁棒性更强,很多时候甚至不需要复杂的预热策略。
- 深层模型更容易收敛
- 随着模型层数增加,Post-Norm 的梯度问题会被放大,训练难度呈指数级上升。
- Pre-Norm 通过约束子层的输入分布(均值为 0、方差为 1),让注意力、前馈网络的计算更稳定,即使堆叠几十上百层,也能保证梯度正常流动,这也是 GPT-3、LLaMA 等现代大模型都采用 Pre-Norm 的核心原因。
3.对超参数调优更友好
- Post-Norm 对学习率、优化器的设置非常敏感,需要大量调参才能找到合适的配置。
- Pre-Norm 的训练过程更 “皮实”,降低了对超参数的依赖,减少了训练过程中 “调参炼丹” 的成本,更适合大规模模型的工程实现。
2.5.2 完整的向前传播
已GTP-2的架构为例:
input:"小猫爱吃" ↓ Step 1: Tokenization(分词) ↓ Step 2: Word Embeddings(词嵌入) ↓ Step 3: Positional Encoding(位置编码) ↓ Step 4-6: N × Transformer Block(LayerNorm → Attention → Residual → LayerNorm → FFN → Residual) (推理过程) ↓ Step 7: Final Layer Norm ↓ Step 8: Linear(映射词表) → Softmax → Output Probability ↓ output:"鱼"(概率最高的词)本章总结:
Transformer 的前向传播是一个优雅的流水线:输入的文字被转换成向量,经过多层处理(每层都包含 Attention 理解上下文、FFN 提取特征),最后映射到词表得到概率分布。整个过程中,维度在 Block 中保持不变(都是 d_model),只在最后映射时从 d_model 变成 vocab_size。
:“小猫爱吃”
↓
Step 1: Tokenization(分词)
↓
Step 2: Word Embeddings(词嵌入)
↓
Step 3: Positional Encoding(位置编码)
↓
Step 4-6: N × Transformer Block(LayerNorm → Attention → Residual → LayerNorm → FFN → Residual) (推理过程)
↓
Step 7: Final Layer Norm
↓
Step 8: Linear(映射词表) → Softmax → Output Probability
↓
output:“鱼”(概率最高的词)
------ **本章总结:** > **Transformer 的前向传播是一个优雅的流水线:输入的文字被转换成向量,经过多层处理(每层都包含 Attention 理解上下文、FFN 提取特征),最后映射到词表得到概率分布。整个过程中,维度在 Block 中保持不变(都是 d_model),只在最后映射时从 d_model 变成 vocab_size。** ------ 下一章我们将用代码完整实现Transformer的传播流程。