PyTorch 2.0 实现 Self-Attention:从 QKV 矩阵到 6 行核心代码解析

发布时间:2026/7/7 19:44:24
PyTorch 2.0 实现 Self-Attention:从 QKV 矩阵到 6 行核心代码解析 PyTorch 2.0 实现 Self-Attention从 QKV 矩阵到 6 行核心代码解析在自然语言处理和计算机视觉领域自注意力机制Self-Attention已成为现代深度学习模型的核心组件。本文将带你从零开始使用 PyTorch 2.0 实现一个完整的 Self-Attention 模块并深入解析其中的 QKV 矩阵计算过程。我们将重点关注工程实现细节而非单纯的理论公式推导。1. Self-Attention 机制概述自注意力机制的核心思想是让序列中的每个元素都能关注到序列中的所有其他元素并根据相关性动态调整权重。这种机制相比传统的 RNN 或 CNN 具有以下优势并行计算不再受限于序列的时序依赖长距离依赖直接建模任意距离的元素关系动态权重根据输入内容自适应调整关注点在 PyTorch 中实现 Self-Attention 主要涉及以下关键步骤通过线性变换生成 Q(Query)、K(Key)、V(Value) 矩阵计算注意力分数Q 与 K 的点积缩放并应用 softmax 归一化使用注意力权重对 V 进行加权求和2. QKV 矩阵的生成与维度变换让我们首先实现 QKV 矩阵的生成部分。假设输入是一个形状为(batch_size, seq_len, embed_dim)的张量import torch import torch.nn as nn import math class SelfAttention(nn.Module): def __init__(self, embed_dim): super().__init__() self.embed_dim embed_dim # 定义三个线性变换层 self.query nn.Linear(embed_dim, embed_dim) self.key nn.Linear(embed_dim, embed_dim) self.value nn.Linear(embed_dim, embed_dim)这里的关键点在于理解 QKV 矩阵的维度变化。假设我们有以下参数batch_size32seq_len10(序列长度)embed_dim512(嵌入维度)那么输入张量的形状是(32, 10, 512)。经过线性变换后矩阵变换前形状变换后形状Q(32,10,512)(32,10,512)K(32,10,512)(32,10,512)V(32,10,512)(32,10,512)实际上线性变换保持了张量的原始形状但内部的值已经根据学习的权重进行了转换。3. 注意力分数计算的核心步骤接下来我们实现前向传播的核心计算过程。这是 Self-Attention 最关键的数学运算def forward(self, x): Q self.query(x) # (batch_size, seq_len, embed_dim) K self.key(x) # (batch_size, seq_len, embed_dim) V self.value(x) # (batch_size, seq_len, embed_dim) # 计算注意力分数 scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.embed_dim) attn_weights torch.softmax(scores, dim-1) output torch.matmul(attn_weights, V) return output让我们分解这 6 行核心代码的计算过程矩阵乘法Q K^T计算查询和键的相似度Q 形状: (batch_size, seq_len, embed_dim)K^T 形状: (batch_size, embed_dim, seq_len)结果形状: (batch_size, seq_len, seq_len)缩放操作除以sqrt(embed_dim)防止点积结果过大导致 softmax 梯度消失经验值来自 Transformer 原始论文Softmax 归一化将分数转换为概率分布每行的和为 1表示对序列中所有元素的关注权重加权求和用注意力权重对 V 进行加权结果形状: (batch_size, seq_len, embed_dim)4. 维度变换与多头注意力基础理解维度变换对实现 Self-Attention 至关重要。让我们通过一个具体例子说明假设输入句子深度学习很有趣分词后得到 4 个 token嵌入维度为 8输入矩阵 x: (1, 4, 8) # (batch_size1, seq_len4, embed_dim8)经过 QKV 线性变换后我们得到Q: (1,4,8) K: (1,4,8) V: (1,4,8)计算注意力分数时scores Q K.transpose(-2,-1) # (1,4,4)得到的 4x4 矩阵表示每个 token 与其他 token 的关联强度。例如深度学习很有趣深度0.80.60.10.05学习0.70.90.20.1很0.10.20.950.8有趣0.050.10.80.9经过 softmax 后每行的和变为 1形成注意力权重。最后与 V 相乘得到输出。5. 完整实现与优化技巧现在我们将前面的代码整合成一个完整的 SelfAttention 类并添加一些实用功能class SelfAttention(nn.Module): def __init__(self, embed_dim, dropout0.1): super().__init__() self.embed_dim embed_dim self.query nn.Linear(embed_dim, embed_dim) self.key nn.Linear(embed_dim, embed_dim) self.value nn.Linear(embed_dim, embed_dim) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): batch_size, seq_len, _ x.shape Q self.query(x) K self.key(x) V self.value(x) # 计算缩放点积注意力 scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.embed_dim) # 应用mask如果有 if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn_weights torch.softmax(scores, dim-1) attn_weights self.dropout(attn_weights) output torch.matmul(attn_weights, V) return output, attn_weights这个实现增加了两个重要功能Dropout在注意力权重上应用 dropout防止过拟合Mask 机制支持处理变长序列如 NLP 中的 padding使用方法示例# 初始化 attn SelfAttention(embed_dim512) # 随机输入 x torch.randn(32, 10, 512) # (batch_size, seq_len, embed_dim) # 前向传播 output, weights attn(x) print(output.shape) # torch.Size([32, 10, 512]) print(weights.shape) # torch.Size([32, 10, 10])6. 实际应用中的注意事项在实际项目中实现 Self-Attention 时有几个关键点需要注意数值稳定性使用math.sqrt(d_k)进行缩放至关重要可以添加微小值防止除零错误内存消耗注意力矩阵的大小为(seq_len, seq_len)长序列会导致内存爆炸性增长解决方案分块计算、稀疏注意力并行计算优化利用 PyTorch 的运算符自动并行化矩阵乘法对于大 batch考虑使用torch.bmm梯度流动softmax 可能导致梯度消失可以尝试log_softmax或自定义梯度以下是一个优化后的实现加入了这些考虑class OptimizedSelfAttention(nn.Module): def __init__(self, embed_dim, dropout0.1): super().__init__() self.embed_dim embed_dim # 使用单个线性层同时计算QKV提高效率 self.qkv nn.Linear(embed_dim, embed_dim * 3) self.dropout nn.Dropout(dropout) self.scale 1.0 / math.sqrt(embed_dim) def forward(self, x, maskNone): batch_size, seq_len, _ x.shape # 一次性计算QKV qkv self.qkv(x).chunk(3, dim-1) Q, K, V map(lambda t: t.view(batch_size, seq_len, -1), qkv) # 更高效的矩阵乘法 scores torch.bmm(Q, K.transpose(1, 2)) * self.scale if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) attn self.dropout(attn) output torch.bmm(attn, V) return output, attn这个优化版本使用单个线性层同时计算 QKV减少内存访问采用torch.bmm进行批量矩阵乘法预先计算缩放因子避免重复计算更高效的内存视图操作7. 扩展到多头注意力真正的 Transformer 使用的是多头注意力Multi-Head Attention这是 Self-Attention 的扩展。虽然本文聚焦于单头实现但理解其基础对实现多头版本至关重要。多头注意力的核心思想是将 QKV 分割成多个头每个头独立计算注意力将结果拼接后通过线性变换以下是简化的多头注意力结构class MultiHeadAttention(nn.Module): def __init__(self, embed_dim, num_heads8, dropout0.1): super().__init__() assert embed_dim % num_heads 0, embed_dim必须能被num_heads整除 self.embed_dim embed_dim self.num_heads num_heads self.head_dim embed_dim // num_heads self.qkv nn.Linear(embed_dim, embed_dim * 3) self.proj nn.Linear(embed_dim, embed_dim) self.dropout nn.Dropout(dropout) self.scale 1.0 / math.sqrt(self.head_dim) def forward(self, x, maskNone): batch_size, seq_len, _ x.shape # 计算QKV并分割成多头 qkv self.qkv(x).chunk(3, dim-1) Q, K, V map(lambda t: t.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2), qkv) # 计算注意力分数 scores torch.matmul(Q, K.transpose(-2, -1)) * self.scale if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) attn self.dropout(attn) # 应用注意力权重并合并多头 output torch.matmul(attn, V) output output.transpose(1, 2).contiguous().view(batch_size, seq_len, -1) output self.proj(output) return output, attn这个实现展示了如何从单头注意力扩展到多头注意力这是构建完整 Transformer 模型的基础。