3、自注意力机制:Self-Attention原理,QKV矩阵的计算过程,多头注意力的拆分与合并
好,咱们进入正题。自注意力机制,说白了就是让模型学会「自己看自己」。你想想看,在处理一句话时,每个词都需要知道跟其他词的关系有多重要。比如「我喜欢吃苹果」——「苹果」跟「吃」关系近,跟「我」关系就远一点。自注意力就是干这个的。
3.1 从「查字典」理解QKV
我第一次接触QKV时,觉得这名字起得挺玄乎。其实没那么复杂。我习惯用一个比喻:你拿着一个问题(Query),去一堆资料(Key)里找答案,最后拿到结果(Value)。
- Query(Q):当前词在问「谁跟我有关?」
- Key(K):其他词在回答「我跟你有多相关?」
- Value(V):真正要提取的信息内容
嗯,这里要注意:Q、K、V其实都是从同一个输入X通过线性变换得到的。说白了就是三个不同的投影矩阵在干活。
3.2 QKV矩阵的计算过程
假设输入序列长度为n,每个词的维度是d。那么输入X的形状就是(n, d)。
计算过程分三步走:
- 生成Q、K、V:分别乘以三个权重矩阵Wq、Wk、Wv,形状都是(d, d_k)。
- 计算注意力分数:Q乘以K的转置,再除以√d_k做缩放。
- Softmax归一化:得到注意力权重,再乘以V得到输出。
公式长这样:
Attention(Q, K, V) = softmax(Q × K^T / √d_k) × V
为什么要除以√d_k?我在项目中遇到过,如果不做这个缩放,当d_k很大时,点积结果会非常大,softmax会进入梯度饱和区。说白了就是模型学不动了。
核心要点:Q和K的点积衡量的是「相似度」。相似度越高,注意力权重越大。这个权重再乘以V,就是加权后的信息。
3.3 多头注意力:拆分与合并
一个头只能学到一种关系模式。但现实中,词与词之间的关系是多样的。比如「苹果」既跟「吃」有动作关系,又跟「红」有属性关系。一个头搞不定,那就多来几个头。
拆分过程:
- 假设有h个头,每个头的维度是d_k = d / h
- 把Q、K、V分别拆成h份,每份形状是(n, d_k)
- 每个头独立计算自注意力
合并过程:
- 把h个头的输出拼起来,形状变回(n, d)
- 再乘以一个输出投影矩阵Wo,得到最终结果
代码实现起来其实很直观:
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.Wq = nn.Linear(d_model, d_model)
self.Wk = nn.Linear(d_model, d_model)
self.Wv = nn.Linear(d_model, d_model)
self.Wo = nn.Linear(d_model, d_model)
def forward(self, x):
batch_size, seq_len, d_model = x.shape
# 生成QKV并拆分成多头
Q = self.Wq(x).view(batch_size, seq_len, self.num_heads, self.d_k)
K = self.Wk(x).view(batch_size, seq_len, self.num_heads, self.d_k)
V = self.Wv(x).view(batch_size, seq_len, self.num_heads, self.d_k)
# 转置为 (batch, heads, seq, d_k)
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
# 计算注意力
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
attn = torch.softmax(scores, dim=-1)
output = torch.matmul(attn, V)
# 合并多头
output = output.transpose(1, 2).contiguous()
output = output.view(batch_size, seq_len, d_model)
return self.Wo(output)
个人经验:我建议num_heads一般设为8或16。头数太少,表达能力不够;头数太多,每个头的维度太小,学不到啥东西。d_k至少要有32,这是我踩坑踩出来的经验。
3.4 避坑指南
我曾经在做一个文本分类项目时,把num_heads设成了32,d_model才256。结果每个头的维度只有8,模型怎么训都训不上去。后来改成8个头,效果立竿见影。
还有一点:多头注意力的计算量是O(n²·d),序列长度n一长,显存就炸了。我建议在实际应用中,如果序列超过512,可以考虑用稀疏注意力或者窗口注意力来优化。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| num_heads | 8 ~ 16 | 头数适中,平衡表达力和计算量 |
| d_k | ≥ 32 | 每个头的维度不能太小 |
| d_model | 256 ~ 1024 | 根据任务复杂度调整 |
嗯,总结一下:自注意力就是让每个词去「关注」所有词,多头注意力就是让模型从多个角度去「关注」。拆分是为了并行学习不同关系,合并是为了把学到的信息整合起来。这个设计,说实话,真的很巧妙。