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 的关键要点

  1. Q 和 K 决定「关注谁」:通过点积计算相似度,决定每个 token 对其他 token 的关注程度
  2. V 决定「关注什么」:根据注意力权重,从 V 中提取对应信息
  3. 除以 √d_k 是必要的:防止 softmax 进入饱和区,保证梯度正常
  4. 多头注意力提升表达能力:让模型从多个子空间学习不同的关注模式

说实话,QKV 的设计非常优雅。它把「查询-匹配-提取」这个人类认知的基本模式,用数学公式完美地表达了出来。理解了这一点,你再看 Transformer 的其他部分,就会觉得豁然开朗。

下一章咱们聊聊 Dropout 率怎么影响自注意力的表现。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。