共计 2270 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要自注意力机制
在传统的 RNN 和 LSTM 中,序列处理是逐步进行的,这导致两个主要问题:

- 难以捕捉长距离依赖关系
- 无法并行计算,训练速度慢
Transformer 通过自注意力机制完美解决了这些问题。它的核心思想是:让序列中的每个元素都能直接 ” 看到 ” 其他所有元素,并通过学习到的权重来决定关注哪些部分。
2D 多头自注意力计算全流程
1. 输入表示
假设我们有一个序列输入 $X \in \mathbb{R}^{n \times d_{model}}$,其中 n 是序列长度,$d_{model}$ 是嵌入维度。
2. 计算 Q,K,V
通过三个不同的线性变换得到查询 (Query)、键(Key) 和值(Value):
$$
Q = XW^Q, \quad K = XW^K, \quad V = XW^V
$$
其中 $W^Q, W^K, W^V \in \mathbb{R}^{d_{model} \times d_k}$
3. 多头拆分
使用 einops 库将 Q,K,V 拆分为 h 个头:
from einops import rearrange
# 假设 h 是头数,d_k 是每个头的维度
q = rearrange(q, 'b n (h d) -> b h n d', h=h)
k = rearrange(k, 'b n (h d) -> b h n d', h=h)
v = rearrange(v, 'b n (h d) -> b h n d', h=h)
4. 缩放点积注意力
计算注意力权重并应用 softmax:
$$
\text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V
$$
Python 实现:
attn = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
attn = attn.softmax(dim=-1)
out = attn @ v
5. 多头拼接
将多个头的输出拼接起来:
out = rearrange(out, 'b h n d -> b n (h d)')
完整 PyTorch 实现
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
import math
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, h, dropout=0.1):
super().__init__()
assert d_model % h == 0
self.d_k = d_model // h
self.h = h
self.q_linear = nn.Linear(d_model, d_model)
self.k_linear = nn.Linear(d_model, d_model)
self.v_linear = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
self.out = nn.Linear(d_model, d_model)
def forward(self, q, k, v, mask=None):
bs = q.size(0)
# 线性变换
q = self.q_linear(q)
k = self.k_linear(k)
v = self.v_linear(v)
# 拆分为多头
q = rearrange(q, 'b n (h d) -> b h n d', h=self.h)
k = rearrange(k, 'b n (h d) -> b h n d', h=self.h)
v = rearrange(v, 'b n (h d) -> b h n d', h=self.h)
# 计算注意力
scores = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(self.d_k))
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn = F.softmax(scores, dim=-1)
attn = self.dropout(attn)
# 应用注意力权重
out = attn @ v
# 拼接多头
out = rearrange(out, 'b h n d -> b n (h d)')
# 最终线性变换
return self.out(out)
性能优化与避坑指南
计算复杂度分析
- 自注意力的计算复杂度是 $O(n^2d)$,其中 n 是序列长度,d 是嵌入维度
- 当序列长度增加时,显存占用会平方级增长
显存优化技巧
- 使用梯度检查点(gradient checkpointing)
- 混合精度训练
- 适当减少头数(h),例如从 8 头改为 4 头
常见问题解决
- 梯度爆炸:确保除以 $\sqrt{d_k}$ 进行缩放
- 变长序列处理:
# 创建 mask 矩阵 mask = torch.ones(bs, seq_len) for i in range(bs): mask[i, lengths[i]:] = 0 mask = mask.unsqueeze(1).unsqueeze(1) # 形状变为(b,1,1,seq_len) - 数值稳定性:在 softmax 前对 masked 位置填充一个很大的负数(-1e9)
思考与扩展
本文的实现虽然完整,但在处理超长序列时仍然面临计算效率问题。Flash Attention 通过以下方式进行了优化:
- 分块计算注意力,减少显存访问
- 融合多个操作,减少中间结果存储
- 利用 GPU 内存层次结构优化
读者可以思考:如何在本代码基础上集成 Flash Attention?如何处理超过 GPU 显存容量的超长序列?
希望这篇图解 + 代码的实现方式能帮助初学者更好地理解 Transformer 的核心机制。在实际应用中,还需要考虑批处理效率、混合精度训练等工程细节,这些我们可以在后续文章中继续探讨。
