2. 自注意力机制:Query、Key、Value的三角关系,数学公式与直观理解
好,咱们进入正题。自注意力机制,说白了就是让模型学会「关注哪里」。你想想看,人眼看一张图,也不是每个像素都平均用力对吧?我们会盯着关键区域。自注意力做的就是这件事——让模型自己决定哪些位置更重要。
2.1 从「检索」说起:Query、Key、Value的直觉
我第一次接触Q、K、V时,觉得这名字起得挺玄乎。后来发现,它其实就是个「检索系统」。
- Query(查询):你想找什么?比如你在图书馆问「有没有Transformer相关的书?」
- Key(键):每本书的标签。比如《Attention Is All You Need》的标签是「Transformer、注意力」。
- Value(值):书本身的内容。
流程很简单:Query 去匹配每个 Key,算出相似度(注意力分数),然后用这个分数去加权求和对应的 Value。分数高的,说明这本书是你想要的,内容就多取一些。
核心思想:自注意力就是让序列中的每个元素,去「关注」序列中所有其他元素,然后根据关注程度聚合信息。
2.2 数学公式:从点积到Softmax
公式其实就一个,但背后有故事。标准的缩放点积注意力:
Attention(Q, K, V) = softmax(Q × K^T / √d_k) × V
咱们拆开看:
- Q × K^T:计算相似度矩阵。每个 Query 和所有 Key 做点积。点积越大,说明越相似。
- 除以 √d_k:缩放因子。d_k 是 Key 的维度。为什么要除?我踩过这个坑——如果不缩放,当 d_k 很大时,点积结果会非常大,Softmax 的梯度会变得极小,模型学不动。除以 √d_k 就是把方差拉回到 1 左右。
- Softmax:把分数变成概率分布,和为 1。
- 乘以 V:用概率加权求和,得到最终的输出。
我的经验:在实际项目中,我习惯把 d_k 设为 64 或 128。太小了表达能力不够,太大了训练不稳定。这个经验值在 ViT 和 DETR 里都验证过。
2.3 直观理解:一张图说清楚
假设输入是 4 个 token,每个 token 有 3 维特征。Q、K、V 都是通过线性变换从输入得到的。
| 步骤 | 操作 | 形状变化 |
|---|---|---|
| 1. 输入 | 原始特征 | [4, 3] |
| 2. 生成 Q/K/V | 乘以权重矩阵 W_q, W_k, W_v | [4, d_k] |
| 3. 计算注意力分数 | Q × K^T | [4, 4] |
| 4. 缩放 + Softmax | 除以 √d_k,再 Softmax | [4, 4](每行和为1) |
| 5. 加权求和 | 分数 × V | [4, d_v] |
你看,输出和输入长度一样(都是4),但每个位置都融合了全局信息。这就是自注意力的魔力——感受野直接拉满。
2.4 多头注意力:为什么一个头不够?
我刚开始做视觉 Transformer 时,只用单头注意力,效果一般。后来换成多头,性能直接起飞。为什么?
单头注意力只能学到一种「关注模式」。比如有的头关注全局轮廓,有的头关注局部纹理。多个头并行,模型就能学到不同的特征子空间。
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) × W_o
其中 head_i = Attention(Q × W_q_i, K × W_k_i, V × W_v_i)
每个头有自己的 W_q, W_k, W_v,相当于让模型从不同角度「看」数据。我习惯用 8 个头,每个头维度 64,这样总维度和单头 512 一致,参数量不变但效果更好。
避坑指南:我曾经把头的数量设得太多(比如 32 个),结果每个头分到的维度太少(才 16 维),表达能力反而下降。一般来说,每个头的维度在 64 左右比较稳妥。
2.5 自注意力 vs 卷积:本质区别
很多人问我:「自注意力是不是能替代卷积?」我的回答是:各有千秋。
- 卷积:局部操作,参数共享,平移等变。适合提取低级特征。
- 自注意力:全局操作,动态权重,排列不变。适合建模长距离依赖。
举个例子:你要识别一张图里「左边有只猫,右边有只狗」。卷积需要堆很多层才能让信息传过去,自注意力一步到位。但反过来,如果只是检测边缘,卷积效率高得多。
嗯,这里要注意:自注意力的计算复杂度是 O(n²),n 是序列长度。对于高分辨率图像,直接做自注意力显存会爆炸。所以后来有了 Swin Transformer 这种窗口注意力,算是折中方案。
2.6 代码实现:从零写一个自注意力
光说不练假把式。我带你手写一个简化版的自注意力,去掉 batch 维度,方便理解。
import torch
import torch.nn.functional as F
def scaled_dot_product_attention(Q, K, V):
"""
Q, K, V: [seq_len, d_k]
"""
d_k = Q.size(-1)
# 1. 计算相似度
scores = torch.matmul(Q, K.transpose(-2, -1)) # [seq_len, seq_len]
# 2. 缩放
scores = scores / (d_k ** 0.5)
# 3. Softmax
attn_weights = F.softmax(scores, dim=-1)
# 4. 加权求和
output = torch.matmul(attn_weights, V)
return output, attn_weights
# 测试
seq_len, d_k = 4, 64
Q = torch.randn(seq_len, d_k)
K = torch.randn(seq_len, d_k)
V = torch.randn(seq_len, d_k)
out, weights = scaled_dot_product_attention(Q, K, V)
print(f"输出形状: {out.shape}") # [4, 64]
print(f"注意力矩阵形状: {weights.shape}") # [4, 4]
你看,核心代码就几行。但实际项目中,我会加上 Dropout、残差连接和 LayerNorm。这些细节在后续章节会展开。
2.7 小结:记住这三句话
- Q 问,K 答,V 给内容——这是自注意力的灵魂。
- 缩放点积 + Softmax + 加权求和——这是自注意力的骨架。
- 多头并行,各司其职——这是自注意力强大的原因。
我个人习惯把自注意力理解为「动态卷积」。卷积的权重是固定的,自注意力的权重是输入相关的。这种动态性让 Transformer 在视觉任务上有了更强的表达能力。下一章,咱们聊聊位置编码——没有它,自注意力就是「一团浆糊」。