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)
从这张图可以看得很清楚:整个自注意力其实就是“查询-匹配-聚合”三部曲。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 世界的第一把钥匙。