在前面的章节中,我们初步了解了注意力机制和Transformer架构。我们知道,注意力机制允许模型在处理序列时动态关注输入的不同部分,而Transformer完全基于注意力机制构建,摒弃了传统的循环和卷积结构。
自注意力机制(Self-Attention)是Transformer的核心组件,它使得模型能够捕获序列中任意两个位置之间的依赖关系,无论它们距离多远。本节将深入揭秘自注意力机制的工作原理,通过数学推导和代码实现,让你彻底掌握这一现代深度学习的核心技术。
自注意力机制的直观理解
在传统的RNN中,信息只能按顺序从前一个时间步传递到下一个时间步,这限制了模型并行化的能力,并且在处理长序列时容易出现梯度消失问题。自注意力机制通过允许序列中的每个位置直接关注其他所有位置,解决了这些问题。
graph TD
A[RNN信息流] --> B[顺序传递<br/>难以并行化]
C[自注意力机制] --> D[全局连接<br/>高度并行化]
style A fill:#e63946,stroke:#333
style B fill:#e63946,stroke:#333
style C fill:#2a9d8f,stroke:#333
style D fill:#2a9d8f,stroke:#333
自注意力机制详解
核心思想
自注意力机制的核心思想是:对于序列中的每个元素,计算它与其他所有元素的相关性,然后根据这些相关性对所有元素进行加权求和,得到该元素的表示。
数学表达
对于输入序列 ,其中 :
-
线性变换:
其中 是可学习的参数矩阵。
-
计算注意力分数:
关键组件解释
查询(Query)、键(Key)、值(Value)
这三个概念来源于信息检索领域:
- Query(查询):表示当前我们关注的位置
- Key(键):表示序列中每个位置的特征
- Value(值):表示序列中每个位置的实际内容
通过计算Query和Key的相似度,确定对Value的注意力权重。
缩放因子
在计算注意力分数时,除以 是为了防止点积值过大导致softmax函数梯度消失:
- 当 较小时,点积的方差较小
- 当 较大时,点积的方差较大,可能进入softmax的饱和区域
通过除以 ,可以将方差稳定在1左右。
自注意力机制的计算流程
graph LR
A[输入序列] --> B[线性变换]
B --> C[Q, K, V矩阵]
C --> D[计算注意力分数]
D --> E[Softmax归一化]
E --> F[加权求和]
F --> G[输出序列]
style A fill:#a8dadc
style B fill:#457b9d
style C fill:#457b9d
style D fill:#f4a261
style E fill:#f4a261
style F fill:#e76f51
style G fill:#e63946
多头注意力机制
单头注意力可能限制了模型关注不同信息的能力,多头注意力机制通过并行计算多个注意力头来增强模型的表达能力。
计算过程
其中每个头计算为:
其中 ,,,。
动手实现自注意力机制
让我们用Python和PyTorch从零开始实现自注意力机制:
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
# 检查CUDA是否可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
class SelfAttention(nn.Module):
def __init__(self, d_model, d_k, d_v, n_heads=1):
"""
初始化自注意力机制
Args:
d_model: 模型维度
d_k: Key向量维度
d_v: Value向量维度
n_heads: 注意力头数
"""
super(SelfAttention, self).__init__()
self.d_model = d_model
self.d_k = d_k
self.d_v = d_v
self.n_heads = n_heads
# 线性变换矩阵
self.W_Q = nn.Linear(d_model, d_k * n_heads)
self.W_K = nn.Linear(d_model, d_k * n_heads)
self.W_V = nn.Linear(d_model, d_v * n_heads)
self.W_O = nn.Linear(d_v * n_heads, d_model)
def forward(self, Q, K, V, mask=None):
"""
前向传播
Args:
Q: 查询矩阵 [batch_size, seq_len, d_model]
K: 键矩阵 [batch_size, seq_len, d_model]
V: 值矩阵 [batch_size, seq_len, d_model]
mask: 掩码矩阵 [batch_size, seq_len, seq_len]
Returns:
output: 输出矩阵 [batch_size, seq_len, d_model]
attention: 注意力权重 [batch_size, n_heads, seq_len, seq_len]
"""
batch_size = Q.size(0)
# 线性变换并重塑为多头形式
Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_v).transpose(1, 2)
# 计算注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) / np.sqrt(self.d_k)
# 应用掩码(如果有)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 计算注意力权重
attention = F.softmax(scores, dim=-1)
# 加权求和
context = torch.matmul(attention, V)
# 合并多头并线性变换
context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.n_heads * self.d_v)
output = self.W_O(context)
return output, attention
# 示例:在简单序列上测试自注意力机制
def test_self_attention():
# 设置随机种子以确保结果可重现
torch.manual_seed(42)
# 超参数
d_model = 64
d_k = 32
d_v = 32
n_heads = 4
seq_len = 10
batch_size = 2
# 创建自注意力层
attention_layer = SelfAttention(d_model, d_k, d_v, n_heads).to(device)
# 创建随机输入
X = torch.randn(batch_size, seq_len, d_model).to(device)
# 前向传播
output, attention_weights = attention_layer(X, X, X)
print(f"Input shape: {X.shape}")
print(f"Output shape: {output.shape}")
print(f"Attention weights shape: {attention_weights.shape}")
# 可视化第一个样本的第一个注意力头
attention_head_0 = attention_weights[0, 0].cpu().detach().numpy()
plt.figure(figsize=(10, 8))
plt.imshow(attention_head_0, cmap='Blues', aspect='auto')
plt.colorbar()
plt.title("Self-Attention Weights (Head 0, Sample 0)")
plt.xlabel("Key Positions")
plt.ylabel("Query Positions")
# 添加数值标注
for i in range(min(10, seq_len)):
for j in range(min(10, seq_len)):
plt.text(j, i, f'{attention_head_0[i, j]:.2f}',
ha='center', va='center', fontsize=8)
plt.tight_layout()
plt.show()
return attention_layer, X, output, attention_weights
# 运行测试
attention_layer, input_tensor, output_tensor, attention_weights = test_self_attention()
位置编码
由于Transformer不包含循环或卷积结构,需要添加位置信息来表示序列顺序。Transformer使用正弦和余弦函数来生成位置编码:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
"""
初始化位置编码
Args:
d_model: 模型维度
max_len: 最大序列长度
"""
super(PositionalEncoding, self).__init__()
# 创建位置编码矩阵
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() *
(-np.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0).transpose(0, 1)
# 注册为缓冲区,不参与梯度更新
self.register_buffer('pe', pe)
def forward(self, x):
"""
前向传播
Args:
x: 输入张量 [seq_len, batch_size, d_model]
Returns:
x + positional_encoding
"""
x = x + self.pe[:x.size(0), :]
return x
# 测试位置编码
def test_positional_encoding():
d_model = 512
max_len = 100
# 创建位置编码层
pos_encoding = PositionalEncoding(d_model, max_len)
# 创建示例输入
x = torch.zeros(max_len, 1, d_model)
# 应用位置编码
x_with_pos = pos_encoding(x)
# 可视化位置编码
plt.figure(figsize=(15, 5))
plt.imshow(x_with_pos[:, 0, :50].cpu().numpy(), cmap='RdBu', aspect='auto')
plt.colorbar()
plt.title("Positional Encoding")
plt.xlabel("Dimension")
plt.ylabel("Position")
plt.tight_layout()
plt.show()
return pos_encoding
# 运行位置编码测试
pos_encoding = test_positional_encoding()
完整的Transformer编码器层
结合自注意力机制和位置编码,我们可以构建一个完整的Transformer编码器层:
class TransformerEncoderLayer(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
"""
初始化Transformer编码器层
Args:
d_model: 模型维度
n_heads: 注意力头数
d_ff: 前馈网络隐藏层维度
dropout: Dropout概率
"""
super(TransformerEncoderLayer, self).__init__()
# 多头自注意力
self.self_attn = SelfAttention(d_model, d_model // n_heads, d_model // n_heads, n_heads)
# 位置前馈网络
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model)
)
# 层归一化
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
# Dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
"""
前向传播
Args:
x: 输入张量 [batch_size, seq_len, d_model]
mask: 掩码张量 [batch_size, seq_len, seq_len]
Returns:
output: 输出张量 [batch_size, seq_len, d_model]
"""
# 多头自注意力子层
attn_out, _ = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_out))
# 位置前馈网络子层
ffn_out = self.ffn(x)
x = self.norm2(x + self.dropout(ffn_out))
return x
# 测试Transformer编码器层
def test_transformer_encoder():
# 超参数
d_model = 512
n_heads = 8
d_ff = 2048
seq_len = 20
batch_size = 4
# 创建编码器层
encoder_layer = TransformerEncoderLayer(d_model, n_heads, d_ff).to(device)
# 创建随机输入
x = torch.randn(batch_size, seq_len, d_model).to(device)
# 前向传播
output = encoder_layer(x)
print(f"Input shape: {x.shape}")
print(f"Output shape: {output.shape}")
return encoder_layer, x, output
# 运行测试
encoder_layer, input_tensor, output_tensor = test_transformer_encoder()
自注意力机制的优势
1. 长距离依赖建模
自注意力机制能够直接建模序列中任意两个位置之间的依赖关系,不受距离限制:
graph TD
A[位置1] --> B[位置100]
C[位置50] --> D[位置75]
style A fill:#a8dadc
style B fill:#e63946
style C fill:#a8dadc
style D fill:#e63946
classDef position fill:#a8dadc,stroke:#333;
classDef attention fill:#e63946,stroke:#333;
2. 高度并行化
与RNN不同,自注意力机制的计算可以高度并行化,大大提高了训练效率。
3. 可解释性
注意力权重提供了模型决策过程的可解释性,我们可以可视化模型关注了输入的哪些部分。
自注意力机制的局限性
1. 计算复杂度
自注意力机制的计算复杂度为 ,其中 是序列长度,对于很长的序列计算开销较大。
2. 内存消耗
需要存储注意力权重矩阵,内存消耗较大。
总结
自注意力机制是Transformer架构的核心,它通过允许序列中的每个位置直接关注其他所有位置,解决了传统序列模型的诸多限制。本节我们:
- 深入理解了自注意力机制的原理和数学表达
- 学习了多头注意力机制的设计思想
- 掌握了位置编码的重要性及实现方法
- 动手实现了自注意力机制及其在Transformer编码器中的应用
- 了解了自注意力机制的优势和局限性
自注意力机制的提出彻底改变了深度学习领域,特别是在自然语言处理方面。掌握这一技术对于理解现代大语言模型至关重要。
在下一节中,我们将学习BERT和GPT等预训练语言模型,它们基于Transformer架构,在各种NLP任务中取得了突破性成果。
练习题
- 实现带掩码的自注意力机制,用于解码器
- 尝试不同的位置编码方法(如可学习的位置编码)并比较效果
- 在实际文本数据上测试自注意力机制,观察注意力权重的分布
- 研究稀疏注意力机制(如Longformer、BigBird)以处理长序列