1. 自注意力机制基础:注意力从何而来

自注意力(Self-Attention)这个概念,最早是从机器翻译领域的注意力机制演变过来的。说白了,就是让模型自己学会“看哪里更重要”。你想想看,当我们在读一句话的时候,眼睛会不自觉地聚焦在某些关键词上。自注意力做的,就是把这个过程数学化。

我个人习惯把自注意力理解为“内部关系挖掘”。它不是关注外部上下文,而是关注序列内部元素之间的相互依赖。嗯,这里有个关键点:自注意力没有距离的概念。不管两个词隔多远,都能直接建立联系。这也是它能解决长距离依赖问题的根本原因。

1.1 Q、K、V 三件套

Q(Query)、K(Key)、V(Value)是自注意力的三驾马车。我第一次接触时也被绕晕过,其实用一个比喻就清楚了:

  • Query(查询):好比你在搜索引擎里输入的关键词
  • Key(键):好比网页的标题和摘要
  • Value(值):好比网页的真正内容

搜索引擎会先匹配 Query 和 Key,找到相关性最高的网页,然后把对应的 Value(内容)取出来。自注意力也是一样。只不过在自注意力中,Q、K、V 都是从同一个输入序列通过线性变换得到的。

具体来说,输入序列 X 会乘以三个不同的权重矩阵:

Q = X * W_Q
K = X * W_K
V = X * W_V

这里 W_Q、W_K、W_V 都是可学习的参数。我在项目中遇到过输入序列特别长的情况,这时候 Q、K、V 的维度设计就很关键。一般我们会把维度设成 d_k,也就是每个头的维度。

我的经验是:不要把 Q、K、V 的维度设得太大。太大的维度会让 softmax 后的分布过于平滑,反而抓不住关键信息。

1.2 注意力分数的计算

有了 Q、K、V,下一步就是计算注意力分数。这一步其实就一句话:Query 和 Key 做点积。

对于一个位置 i 的 Query,它会和所有位置(包括自己)的 Key 做点积:

score_ij = Q_i · K_j

这个分数表示位置 i 对位置 j 的关注程度。分数越高,就说明 i 越“在意” j。

但直接点积有个问题。维度 d_k 越大,点积的结果方差就越大,softmax 后的梯度就越容易消失。为了解决这个问题,我们加上一个缩放因子:

score_ij = (Q_i · K_j) / sqrt(d_k)

我曾经见过有人忘记加这个缩放因子,结果模型训练初期 loss 直接崩掉。这个 sqrt(d_k) 看起来不起眼,但真的能救命。

1.3 注意力权重与加权求和

计算出所有分数后,接下来就是经典的 softmax 操作。目的很简单:把分数变成概率分布,和为 1。

alpha_ij = softmax(score_ij) = exp(score_ij) / sum_j(exp(score_ij))

这些 alpha_ij 就是注意力权重。权重越大,说明位置 i 在聚合信息时,越应该参考位置 j 的信息。

最后一步,用权重去加权求和 Value:

output_i = sum_j(alpha_ij * V_j)
自注意力计算流程图 输入 X Q K V 注意力分数 = Q·K^T Softmax → 注意力权重 输出 = Σ(权重 × V)

从这张图可以看得很清楚:整个自注意力其实就是“查询-匹配-聚合”三部曲。Q 去匹配 K,得到注意力分数,然后用 softmax 归一化成权重,最后用权重去加权求和 V。

1.4 核心概念对比

概念 含义 维度 我的理解
Query 查询向量 batch × seq_len × d_k “我想找什么”
Key 被匹配的键 batch × seq_len × d_k “我这里有什么”
Value 实际提取的信息 batch × seq_len × d_v “我能提供什么”
注意力分数 Q 与 K 的点积结果 batch × seq_len × seq_len “匹配度有多高”
注意力权重 softmax 归一化后的分数 batch × seq_len × seq_len “应该看多少”
输出 加权求和后的上下文向量 batch × seq_len × d_v “聚合后的信息”

核心公式总结

自注意力的完整计算公式就是:

Attention(Q, K, V) = softmax(Q · K^T / sqrt(d_k)) · V

这个公式你看到的越多,就越能理解为什么自注意力能成为 Transformer 的基石。

1.5 一个直观的代码示例

为了让你更直观地理解这个过程,我给出一个 Python 示例。这不是实际框架代码,只是为了展示计算逻辑:

import numpy as np

def self_attention(X, W_Q, W_K, W_V):
    # X: (seq_len, d_model)
    # W_Q, W_K, W_V: (d_model, d_k)
    
    # Step 1: 计算 Q, K, V
    Q = X @ W_Q  # (seq_len, d_k)
    K = X @ W_K  # (seq_len, d_k)
    V = X @ W_V  # (seq_len, d_v)
    
    # Step 2: 计算注意力分数
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)  # (seq_len, seq_len)
    
    # Step 3: softmax 归一化
    weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
    
    # Step 4: 加权求和
    output = weights @ V  # (seq_len, d_v)
    
    return output, weights

注意:这个示例为了可读性牺牲了效率。实际工程中你不会用 for 循环或这种方式。但我建议你亲手跑一下,用一个小矩阵验证每个步骤的输出形状。嗯,我就是这么过来的。

1.6 为什么自注意力有效?

我个人觉得,自注意力最厉害的地方在于:每个位置都能直接看到所有位置。对比一下 RNN,信息要一步步传递,距离越远越容易丢失。CNN 呢,只能看到固定大小的窗口。

自注意力打破了距离的限制。它让模型可以自由地建立任意两个位置之间的依赖关系。这种“直接对话”的能力,在图谱、序列、甚至图像任务中都非常有价值。

举个例子:在分子属性预测中,一个原子的行为可能受到远端某个官能团的影响。自注意力可以轻松捕捉这种长程相互作用,而传统方法需要深层叠加才能做到。

好了,这一章的内容就到这里。自注意力本质并不复杂,但它是后续一切复杂结构的基础。理解了 QKV 和注意力权重,你就掌握了进入 Transformer 世界的第一把钥匙。