2、自注意力计算原理:QKV矩阵的数学推导与直观理解
好,咱们进入正题。自注意力机制,说白了就是让模型学会「关注该关注的」。我刚开始接触这个概念时,总觉得QKV这三个矩阵神神秘秘的。后来在项目中调了无数次参数,才真正理解它们各自扮演的角色。
今天我就带你一步步拆解,从数学推导到直观理解,把QKV彻底讲透。
2.1 从「查字典」说起
想象一下你在查字典。你想找「注意力」这个词的解释。你会怎么做?
- Query(查询):就是你想查的词,比如「注意力」
- Key(键):字典里每个词条的标题
- Value(值):每个词条对应的详细解释
自注意力机制干的也是类似的事。每个输入 token 都会生成三个向量:Q、K、V。然后通过 Q 和 K 的匹配程度,来决定从 V 中提取多少信息。
核心思想:Q 问「谁跟我相关?」,K 回答「我跟你有多相关」,V 则提供「相关的具体内容」。
2.2 数学推导:QKV 是怎么来的?
假设我们有一个输入序列,每个 token 的嵌入向量为 x。那么 Q、K、V 就是通过三个不同的权重矩阵做线性变换得到的:
Q = X · W_Q
K = X · W_K
V = X · W_V
其中:
- X 的形状是 (seq_len, d_model)
- W_Q、W_K、W_V 的形状都是 (d_model, d_k)
- d_k 通常等于 d_model / num_heads
我个人习惯把 d_k 设为 64 或 128。太小了表达能力不够,太大了计算量吃不消。
2.3 注意力分数的计算
有了 Q 和 K,下一步就是计算注意力分数。公式长这样:
Attention(Q, K, V) = softmax(Q · K^T / √d_k) · V
这里有个细节:为什么要除以 √d_k?
嗯,这个问题我在面试时被问过好几次。原因是这样的——当 d_k 比较大时,Q 和 K 的点积结果会变得很大,导致 softmax 后的梯度极小,模型学不动。除以 √d_k 相当于把方差拉回到 1 左右,让梯度更稳定。
小技巧:如果你发现模型训练不稳定,可以检查一下 d_k 的设置。我曾经在一个项目中把 d_k 设成了 512,结果 loss 死活降不下去。后来改成 64,问题就解决了。
2.4 直观理解:QKV 在做什么?
咱们用个具体的例子来说明。假设句子是「我 爱 吃 苹果」。
| Token | Q 的作用 | K 的作用 | V 的作用 |
|---|---|---|---|
| 我 | 查询「谁跟我搭配?」 | 提供「我是主语」的信息 | 提供「我」的语义特征 |
| 爱 | 查询「我爱的对象是?」 | 提供「我是动词」的信息 | 提供「爱」的语义特征 |
| 吃 | 查询「谁在吃?吃什么?」 | 提供「我是动作」的信息 | 提供「吃」的语义特征 |
| 苹果 | 查询「谁吃我?」 | 提供「我是宾语」的信息 | 提供「苹果」的语义特征 |
你看,每个 token 的 Q 都在「主动询问」相关信息,K 在「被动展示」自身属性,V 则负责「贡献」具体内容。三者配合,模型就能知道「吃」和「苹果」关系更近,而「我」和「爱」关系更近。
2.5 多头注意力:让模型从不同角度观察
单头注意力有个问题——它只能学习一种注意力模式。但现实中,一句话可能有多种理解角度。
比如「他打碎了花瓶」:
- 一个头关注「谁打的?」→ 他
- 一个头关注「打了什么?」→ 花瓶
- 一个头关注「结果如何?」→ 碎了
多头注意力就是让模型同时学习多个这样的「关注角度」。每个头有自己的 W_Q、W_K、W_V,最后把所有头的输出拼接起来。
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)
避坑指南:我曾经在调参时把头的数量设得太多(比如 32 个头),结果每个头学到的模式高度重复,白白浪费计算资源。一般来说 8 到 16 个头就够用了。
2.6 代码实现:从零搭建自注意力
光说不练假把式。咱们用 PyTorch 实现一个简化版的自注意力层:
import torch
import torch.nn as nn
import torch.nn.functional as F
class SelfAttention(nn.Module):
def __init__(self, d_model, d_k):
super().__init__()
self.W_Q = nn.Linear(d_model, d_k)
self.W_K = nn.Linear(d_model, d_k)
self.W_V = nn.Linear(d_model, d_k)
self.scale = d_k ** 0.5
def forward(self, x):
# x: (batch, seq_len, d_model)
Q = self.W_Q(x) # (batch, seq_len, d_k)
K = self.W_K(x)
V = self.W_V(x)
# 计算注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale
attn_weights = F.softmax(scores, dim=-1)
# 加权求和
output = torch.matmul(attn_weights, V)
return output, attn_weights
这段代码虽然简单,但核心逻辑都在了。你想想看,整个自注意力机制,本质上就是三个线性变换加一个 softmax。
2.7 总结:QKV 的关键要点
- Q 和 K 决定「关注谁」:通过点积计算相似度,决定每个 token 对其他 token 的关注程度
- V 决定「关注什么」:根据注意力权重,从 V 中提取对应信息
- 除以 √d_k 是必要的:防止 softmax 进入饱和区,保证梯度正常
- 多头注意力提升表达能力:让模型从多个子空间学习不同的关注模式
说实话,QKV 的设计非常优雅。它把「查询-匹配-提取」这个人类认知的基本模式,用数学公式完美地表达了出来。理解了这一点,你再看 Transformer 的其他部分,就会觉得豁然开朗。
下一章咱们聊聊 Dropout 率怎么影响自注意力的表现。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。