3、自注意力机制(Self-Attention)基础回顾:QKV计算、多头注意力、位置编码、Transformer

好,咱们进入正题。自注意力机制,说白了就是让模型学会「自己看自己」。你想想看,一段话里每个词都不是孤立的,它们之间有关系。自注意力要解决的,就是怎么把这种关系给量化出来。

我记得刚接触这个概念时,总觉得它很玄乎。后来亲手搭了一遍才发现,核心逻辑其实非常清晰。咱们一步步拆开看。

3.1 QKV 计算:三个角色的分工

自注意力的核心,就是三个矩阵:Query(查询)、Key(键)、Value(值)。

你可以这样理解:

  • Query:你在问「谁跟我有关系?」
  • Key:每个词回答「我有什么特征?」
  • Value:真正要传递的信息内容

计算过程其实就三步:

  1. Q 和 K 做点积,得到注意力分数
  2. 用 softmax 归一化,变成权重
  3. 权重乘以 V,得到加权后的输出

公式长这样:

Attention(Q, K, V) = softmax(Q × K^T / √d_k) × V

这里有个小细节:除以 √d_k 是为了防止点积结果太大,导致 softmax 梯度消失。我在项目中遇到过因为忘记加这个缩放因子,训练直接炸掉的情况。嗯,这个坑踩过一次就忘不了了。

核心要点:Q 和 K 的维度必须一致,V 的维度可以不同。实际中为了计算方便,通常让它们相等。

3.2 多头注意力:让模型从不同角度看问题

单头注意力有个问题——它只能捕捉一种关系模式。但现实中,词与词之间的关系是多样的。比如「苹果」这个词,可能跟「吃」有关,也可能跟「手机」有关。

多头注意力就是解决这个问题的。它把 Q、K、V 分别拆成 h 个头,每个头独立计算注意力,最后再拼起来。

我个人的习惯是设 h=8,这是 Transformer 里的经典配置。当然,如果你资源紧张,4 个头也能用,效果会差一些。

# 伪代码示意
def multi_head_attention(Q, K, V, h=8):
    # 拆头
    Q_split = split_heads(Q, h)  # [batch, h, seq_len, d_k]
    K_split = split_heads(K, h)
    V_split = split_heads(V, h)
    
    # 每个头独立计算
    heads = []
    for i in range(h):
        head = attention(Q_split[i], K_split[i], V_split[i])
        heads.append(head)
    
    # 拼接并投影
    concat = concat_heads(heads)
    output = linear_projection(concat)
    return output

小技巧:实际实现时,不要用循环。用 reshape 和 transpose 一次性搞定,速度能快 10 倍以上。

3.3 位置编码:给序列注入「顺序感」

自注意力有个「致命」问题——它不关心词的顺序。你想想看,「我打你」和「你打我」,在自注意力眼里是一样的,因为词袋里都是这三个词。

这显然不行。所以我们需要位置编码,给每个位置一个独特的「身份标识」。

Transformer 用的是正弦余弦编码:

PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

为什么用这个?因为它有几个好性质:

  • 每个位置的编码是唯一的
  • 不同位置之间的相对距离可以计算
  • 可以外推到更长的序列(虽然效果会变差)

我曾经试过用可学习的位置编码替代正弦余弦,在小数据集上效果差不多。但一旦序列长度超过训练时的最大长度,可学习编码就崩了。所以,如果你要处理变长序列,还是老老实实用正弦余弦吧。

注意:位置编码是加在输入 embedding 上的,不是拼接。维度必须一致。

3.4 Transformer 架构:编码器-解码器结构

Transformer 把自注意力用到了极致。它的整体结构分为编码器和解码器两部分。

咱们用一张图来理解:

编码器 (Encoder) 多头自注意力 前馈神经网络 × N 层 解码器 (Decoder) 掩码多头自注意力 交叉注意力 前馈神经网络 × N 层 输入序列 输出序列 每个子层都有残差连接 + 层归一化

编码器这边,输入序列经过多头自注意力和前馈网络,每个子层都带着残差连接和层归一化。解码器那边多了一个交叉注意力,用来关注编码器的输出。

这里有个关键点:解码器的自注意力是「掩码」的。什么意思?就是预测第 t 个词时,不能看到后面的词。我刚开始做翻译任务时,忘了加掩码,结果模型学会了「作弊」——直接复制后面的词,训练损失低得离谱,但推理时完全不能用。

经验之谈:如果你自己实现 Transformer,建议先在小数据集上跑通,验证掩码是否正确。一个简单的方法:检查解码器第 i 个位置的注意力权重,看它是否只关注前 i 个位置。

3.5 实际应用中的注意事项

说了这么多,最后分享几个实战中的坑:

  • 显存爆炸:自注意力的复杂度是 O(n²),序列长度翻倍,显存占用翻 4 倍。我处理过 2048 长度的序列,单层注意力就吃了 8G 显存。解决方案:用稀疏注意力或者分块计算。
  • 初始化要小心:QKV 的权重初始化不当,会导致注意力分数要么太大要么太小。我习惯用 xavier 初始化,然后加一个小的缩放因子。
  • 层数不是越多越好:6 层编码器 + 6 层解码器是经典配置。我试过堆到 12 层,效果提升有限,但训练时间翻倍。

我的建议:刚开始学的时候,先用单头注意力跑通流程,再切到多头。这样调试起来方便很多。

好了,自注意力机制的基础就回顾到这里。这些东西看着简单,但每个细节都值得深挖。下一节咱们聊聊怎么把卷积和自注意力结合起来,那才是真正的重头戏。


可可精品资料库