Skip to content

输入关键词开始搜索

注意力机制学习笔记:从 QKV 到缩放点积注意力

本文记录我学习《动手学深度学习》第 10 章注意力机制时的理解过程。刚开始看注意力机制时,我觉得 Query、Key、Value、注意力评分函数、softmax 权重这些概念都比较抽象,尤其是教材中用“自主性提示”和“非自主性提示”来引入注意力机制,一开始并不好理解。

经过这一轮学习之后,我对注意力机制的理解逐渐清晰起来。现在我可以先用一句话概括:

注意力机制就是:根据当前的查询 Query,从一组 Key-Value 中找到最相关的信息,并对 Value 进行加权求和。


1. 从人类注意力理解 Query、Key、Value

Section titled “1. 从人类注意力理解 Query、Key、Value”

教材首先从人类注意力机制讲起。

人类注意力可以分为两类:

  • 非自主性注意力:外界刺激吸引注意力,例如亮光、响声、鲜艳颜色、突然移动的物体等。
  • 自主性注意力:人根据自己的目标主动寻找信息,例如在街上主动寻找奶茶店、在文章里寻找某个关键词。

对应到注意力机制中:

人类注意力概念注意力机制概念我的理解
自主性提示Query我现在想找什么
非自主性提示Key每个信息有什么特征可以被匹配
感官输入Value真正要被取出来的信息

所以我可以这样理解:

Query:我想找什么
Key:每条信息的标签或特征
Value:真正的信息内容

例如,在图书馆找一本深度学习相关的书:

Query = 我想找“深度学习”的书
Key = 每本书的标签,比如机器学习、数据库、操作系统
Value = 每本书真正的内容

注意力机制会用 Query 去和每个 Key 做匹配,哪个 Key 更相关,对应的 Value 权重就更大。


2. 注意力机制和全连接层、池化层的区别

Section titled “2. 注意力机制和全连接层、池化层的区别”

我一开始的问题是:

全连接层也能综合特征,池化层也能汇聚信息,为什么还要注意力机制?

后来理解到,关键区别在于:

是否有 Query,也就是是否根据当前目标动态选择信息。

平均池化是:

所有输入一视同仁,每个输入权重一样

最大池化是:

谁最大就选谁

全连接层虽然有权重,但是训练完成后,它的权重通常是固定的,并不会根据某个具体 Query 动态变化。

而注意力机制是:

给定不同 Query,会得到不同的注意力权重

因此,同一批输入信息,在不同 Query 下会有不同的关注重点。

这一点是我理解注意力机制最关键的一步:

注意力机制的核心不是简单加权,而是根据 Query 动态决定应该关注哪些信息。


注意力权重通常可以用热力图显示。

热力图中:

横轴:Keys
纵轴:Queries
颜色深浅:注意力权重大小

如果某个位置颜色越深,说明这个 Query 对这个 Key 的关注程度越高。

教材中使用了单位矩阵作为例子:

attention_weights = np.eye(10).reshape((1, 1, 10, 10))

单位矩阵的特点是对角线为 1,其他位置为 0。

所以热力图中会出现一条对角线,表示:

Query 0 只关注 Key 0
Query 1 只关注 Key 1
Query 2 只关注 Key 2

Query 9 只关注 Key 9

这个例子本身不是训练出来的真实注意力,而是为了演示如何可视化注意力权重。


4. Nadaraya-Watson 核回归:用回归问题理解注意力

Section titled “4. Nadaraya-Watson 核回归:用回归问题理解注意力”

为了理解注意力汇聚,教材引入了 Nadaraya-Watson 核回归。

这里构造了一个人工数据集:

yi=2sin(xi)+xi0.8+ϵy_i = 2\sin(x_i) + x_i^{0.8} + \epsilon

其中:

2sin(xi)+xi0.82\sin(x_i)+x_i^{0.8} 是真实函数
ϵ\epsilon 是随机噪声

训练数据的构建过程可以理解为:

随机生成 50 个 xx
代入真实函数 f(x)f(x)
再加上随机噪声
得到训练样本 (x_train, y_train)

代码大致如下:

n_train = 50
x_train = np.sort(np.random.rand(n_train) * 5)
def f(x):
return 2 * np.sin(x) + x**0.8
y_train = f(x_train) + np.random.normal(0.0, 0.5, (n_train,))

测试集则是在 [0, 5) 范围内每隔 0.1 取一个点:

x_test = np.arange(0, 5, 0.1)
y_truth = f(x_test)

这里的对应关系是:

注意力机制核回归例子
Query当前要预测的测试点 x
Key训练样本输入 xix_i
Value训练样本输出 yiy_i

也就是说:

要预测某个 xx 的输出,就去训练集中找哪些 xix_i 和它更接近,然后更重视这些 xix_i 对应的 yiy_i


5. 平均汇聚:一个最简单但效果差的基线

Section titled “5. 平均汇聚:一个最简单但效果差的基线”

平均汇聚的思想非常简单:

f(x)=1ni=1nyif(x)=\frac{1}{n}\sum_{i=1}^{n}y_i

也就是:

不管输入 xx 是多少,都直接预测为所有 y_train 的平均值

代码是:

y_hat = y_train.mean().repeat(n_test)

这会导致预测曲线是一条水平线。

它的问题是:

完全没有考虑当前要预测的 xx 在哪里

例如:

预测 x=0.2x=0.2 时,用所有训练样本平均
预测 x=4.8x=4.8 时,还是用所有训练样本平均

所以它不能反映真实函数随 x 变化的趋势。

这一部分让我意识到:

简单平均并不能体现注意力,因为它没有根据 Query 调整权重。


6. 非参数注意力汇聚:距离越近,权重越大

Section titled “6. 非参数注意力汇聚:距离越近,权重越大”

为了改进平均汇聚,Nadaraya-Watson 核回归根据输入位置来分配权重:

f(x)=i=1nα(x,xi)yif(x)=\sum_{i=1}^{n}\alpha(x,x_i)y_i

其中:

α(x,xi)\alpha(x,x_i) 表示 xx 对第 ii 个训练样本的注意力权重

如果使用高斯核,可以得到:

f(x)=i=1nsoftmax(12(xxi)2)yif(x)=\sum_{i=1}^{n}\operatorname{softmax}\left(-\frac{1}{2}(x-x_i)^2\right)y_i

这里的核心逻辑是:

xxxix_i 越近
(xxi)2(x-x_i)^2 越小
12(xxi)2-\frac{1}{2}(x-x_i)^2 越大
softmax 后权重越大

也就是:

距离近 → 得分大 → 权重大
距离远 → 得分小 → 权重小

对应代码:

x_repeat = x_test.repeat(n_train).reshape((-1, n_train))
attention_weights = npx.softmax(-(x_repeat - x_train)**2 / 2)
y_hat = np.dot(attention_weights, y_train)

这时,每个测试点都会根据自己和训练样本的距离,分配不同的注意力权重。

和平均汇聚相比,非参数注意力汇聚的预测曲线已经能够随着真实函数变化而变化。


softmax 的公式是:

αi=esij=1nesj\alpha_i=\frac{e^{s_i}}{\sum_{j=1}^{n}e^{s_j}}

它的作用是把一组任意得分转化成一组权重,并满足:

每个权重大于 0
所有权重加起来等于 1
得分越大,权重越大

例如,给定三个得分:

[-0.281, -0.031, -0.281]

先取指数:

e^-0.281 ≈ 0.755
e^-0.031 ≈ 0.969
e^-0.281 ≈ 0.755

再除以总和:

0.755 / 2.479 ≈ 0.305
0.969 / 2.479 ≈ 0.391
0.755 / 2.479 ≈ 0.305

得到权重:

[0.305, 0.391, 0.305]

所以得分最高的第二个样本获得了最大权重。

我的理解是:

softmax 把“匹配得分”变成了“关注比例”。


8. 带参数注意力汇聚:让模型自己学习关注范围

Section titled “8. 带参数注意力汇聚:让模型自己学习关注范围”

非参数注意力汇聚中的规则是固定的:

距离越近,权重越大

但是这个“近”到底应该多近,是人为设定的。

带参数注意力汇聚在公式中加入了一个可学习参数 ww

f(x)=i=1nsoftmax(12((xxi)w)2)yif(x)=\sum_{i=1}^{n}\operatorname{softmax}\left(-\frac{1}{2}((x-x_i)w)^2\right)y_i

这里的 ww 控制模型对距离的敏感程度。

如果 ww 较大:

距离差异被放大
模型更关注离当前 xx 很近的样本
预测曲线更灵活

如果 ww 较小:

距离差异被缩小
模型参考更大范围的样本
预测曲线更平滑

所以 ww 可以理解为:

注意力范围的调节参数


我一开始的问题是:

这个 w 到底是怎么学出来的?

后来理解到,训练过程就是:

  1. 随机初始化 ww
  2. 用当前 ww 计算注意力权重
  3. 用注意力权重对 y_train 加权求和,得到预测值 y_hat
  4. 计算预测值和真实值之间的 loss
  5. 反向传播计算 loss/w\partial \text{loss}/\partial w
  6. 用梯度下降更新 ww

更新公式为:

wwηLww \leftarrow w - \eta \frac{\partial L}{\partial w}

其中 η\eta 是学习率。

假设某次计算得到:

w=0.5w=0.5
loss/w=0.906\partial \text{loss}/\partial w=-0.906
学习率 η=0.1\eta=0.1

则更新后:

wnew=0.50.1×(0.906)w_{\text{new}} = 0.5 - 0.1 \times (-0.906)
=0.5906=0.5906

因为梯度是负数,所以 ww 会变大。

这说明在当前情况下:

增大 ww 可以让 loss 下降

因此模型会自动把 ww 往增大的方向调整。

这让我对“学习”有了更直观的认识:

参数学习的本质,就是根据 loss 对参数的梯度,判断参数应该往哪个方向调整。


假设现在要预测:

query x=2.5x=2.5
真实 y=3y=3

训练集中有 3 个样本作为 Key 和 Value:

训练样本key xix_ivalue yiy_i距离 xxi\lvert x-x_i\rvert
样本 11.001.5
样本 22.030.5
样本 34.011.5

很明显,x=2.5x=2.5xi=2.0x_i=2.0 最近,而且它的 yi=3y_i=3,正好接近真实值。所以模型应该学会:

更关注样本 2

注意力打分公式是:

si=12((xxi)w)2s_i=-\frac{1}{2}((x-x_i)w)^2

代入 w=0.5w=0.5

样本距离打分softmax 后权重
样本 11.5-0.2810.305
样本 20.5-0.0310.391
样本 31.5-0.2810.305

预测值为:

y^=0.305×0+0.391×3+0.305×1\hat{y}=0.305\times0+0.391\times3+0.305\times1
= 1.477

真实值是:

y=3y=3

损失为:

loss=12×(1.4773)2\text{loss}=\frac{1}{2}\times(1.477-3)^2
1.159\approx1.159

说明 w=0.5 时,模型还不够关注最近的样本 2。

样本距离打分softmax 后权重
样本 11.5-1.1250.212
样本 20.5-0.1250.576
样本 31.5-1.1250.212

预测值为:

y^=0.212×0+0.576×3+0.212×1\hat{y}=0.212\times0+0.576\times3+0.212\times1
= 1.940

损失为:

loss=12×(1.9403)2\text{loss}=\frac{1}{2}\times(1.940-3)^2
0.561\approx0.561

损失从 1.159 降到了 0.561,说明把 ww 变大是有帮助的。

样本距离打分softmax 后权重
样本 11.5-4.5000.018
样本 20.5-0.5000.965
样本 31.5-4.5000.018

预测值为:

y^=0.018×0+0.965×3+0.018×1\hat{y}=0.018\times0+0.965\times3+0.018\times1
= 2.912

损失为:

loss=12×(2.9123)2\text{loss}=\frac{1}{2}\times(2.912-3)^2
0.004\approx0.004

这时预测已经非常接近真实值 3

因此,这个例子说明:

ww 变大 → 模型更关注近处样本 → 预测更接近真实值


上面例子中,当 w=0.5w=0.5 时,我们可以计算梯度。

损失函数为:

L=12(y^y)2L=\frac{1}{2}(\hat{y}-y)^2

所以:

Lw=(y^y)y^w\frac{\partial L}{\partial w} = (\hat{y}-y)\frac{\partial \hat{y}}{\partial w}

前面已经算出:

y^=1.4775\hat{y}=1.4775
y=3y=3
y^y=1.5225\hat{y}-y=-1.5225

打分函数为:

si=12((xxi)w)2s_i=-\frac{1}{2}((x-x_i)w)^2

ww 求导:

siw=(xxi)2w\frac{\partial s_i}{\partial w}=-(x-x_i)^2w

代入 w=0.5w=0.5

s1/w=2.25×0.5=1.125\partial s_1/\partial w=-2.25\times0.5=-1.125
s2/w=0.25×0.5=0.125\partial s_2/\partial w=-0.25\times0.5=-0.125
s3/w=2.25×0.5=1.125\partial s_3/\partial w=-2.25\times0.5=-1.125

softmax 权重对 ww 的导数为:

aiw=ai(siwjajsjw)\frac{\partial a_i}{\partial w} = a_i \left( \frac{\partial s_i}{\partial w} - \sum_j a_j\frac{\partial s_j}{\partial w} \right)

计算得到:

a/w[0.1191, 0.2381, 0.1191]\partial a/\partial w \approx [-0.1191,\ 0.2381,\ -0.1191]

预测值为:

y^=a1y1+a2y2+a3y3\hat{y}=a_1y_1+a_2y_2+a_3y_3

所以:

y^/w\partial \hat{y}/\partial w
=0×(0.1191)+3×0.2381+1×(0.1191)=0\times(-0.1191)+3\times0.2381+1\times(-0.1191)
=0.5952=0.5952

最后:

L/w\partial L/\partial w
=(1.47753)×0.5952=(1.4775-3)\times0.5952
0.906\approx -0.906

因此:

loss/w0.906\partial \text{loss}/\partial w \approx -0.906

这个负号说明:

增大 ww 会让 loss 下降

所以梯度下降会把 ww 往增大的方向更新。


模型训练的目标是让 loss 变小。

如果把 loss 看成参数 ww 的函数:

loss=L(w)\text{loss}=L(w)

那么梯度表示:

当 w 稍微变化时,loss 会怎么变化

如果:

loss/w>0\partial \text{loss}/\partial w > 0

说明 ww 增大时 loss 会增大,所以应该减小 ww

如果:

loss/w<0\partial \text{loss}/\partial w < 0

说明 ww 增大时 loss 会减小,所以应该增大 ww

因此梯度下降使用负梯度方向更新参数:

wwηLww \leftarrow w - \eta \frac{\partial L}{\partial w}

我的理解是:

梯度告诉模型“往哪里走 loss 会下降”,所以它可以指导参数更新。

这比暴力尝试每个参数要高效得多。因为神经网络里通常有大量参数,不可能手动枚举所有参数组合。


前面 Nadaraya-Watson 核回归中,注意力权重是根据距离计算的。

而在更一般的注意力机制中,需要一个函数衡量 Query 和 Key 的匹配程度,这个函数叫:

注意力评分函数 attention scoring function

一般写作:

a(q,ki)a(q,k_i)

其中:

qq 是 Query
kik_i 是第 ii 个 Key
a(q,ki)a(q,k_i) 是匹配得分

然后使用 softmax 将得分转化为注意力权重:

α(q,ki)=exp(a(q,ki))j=1mexp(a(q,kj))\alpha(q,k_i)=\frac{\exp(a(q,k_i))}{\sum_{j=1}^{m}\exp(a(q,k_j))}

最后输出为:

f(q,(k1,v1),...,(km,vm))=i=1mα(q,ki)vif(q,(k_1,v_1),...,(k_m,v_m))=\sum_{i=1}^{m}\alpha(q,k_i)v_i

整体流程是:

Query + Keys

注意力评分函数

softmax

注意力权重

对 Values 加权求和

输出


在 NLP 任务中,一个 batch 里的句子长度可能不同。

例如:

句子 1:我 爱 你
句子 2:我 喜欢 深度 学习

为了组成 batch,需要补齐:

句子 1:我 爱 你 <pad>
句子 2:我 喜欢 深度 学习

但是 <pad> 不是有效信息,不应该被注意力机制关注。

因此需要使用 masked softmax。

它的作用是:

把无效位置的权重变成 0
只在有效位置上做 softmax

实现时通常把无效位置替换成一个非常小的数,例如 -1e6

X = npx.sequence_mask(X, valid_lens, True, value=-1e6)

因为:

exp(106)0\exp(-10^6)\approx0

所以这些位置经过 softmax 后权重接近 0。

我的理解是:

masked softmax 是为了防止模型把注意力分配给 padding 等无效位置。


加性注意力是一种具体的注意力评分函数。

公式为:

a(q,k)=wvTtanh(Wqq+Wkk)a(q,k)=w_v^T\tanh(W_q q + W_k k)

它的计算过程是:

Query qq 经过 WqW_q 映射
Key kk 经过 WkW_k 映射
两者相加
经过 tanh
再经过 wvw_v 得到一个标量得分

之所以叫“加性注意力”,是因为它通过:

Wqq+WkkW_q q + W_k k

来融合 Query 和 Key。

加性注意力的一个重要优点是:

Query 和 Key 的原始维度可以不同

原因是:

WqW_q 可以把 Query 映射到隐藏维度 hh
WkW_k 可以把 Key 映射到隐藏维度 hh

例如:

qq 原来是 3 维
kk 原来是 5 维

可以设置:

WqW_q: 3 维 → 8 维
WkW_k: 5 维 → 8 维

这样:

WqqW_q qWkkW_k k 都是 8 维

于是就可以相加。

本质上,这就是全连接层或线性层在做维度变换。


假设:

q=[q1,q2,q3]q=[q_1,q_2,q_3]

要把 3 维的 q 变成 8 维,可以让它乘一个 8×38 \times 3 的矩阵 WqW_q

hq=Wqq+bqh_q = W_q q + b_q

其中:

WqR8×3W_q\in\mathbb{R}^{8\times3}
qR3q\in\mathbb{R}^{3}
hqR8h_q\in\mathbb{R}^{8}

每一个输出维度都是原始特征的加权组合:

h1=w11q1+w12q2+w13q3h_1=w_{11}q_1+w_{12}q_2+w_{13}q_3
h2=w21q1+w22q2+w23q3h_2=w_{21}q_1+w_{22}q_2+w_{23}q_3

h8=w81q1+w82q2+w83q3h_8=w_{81}q_1+w_{82}q_2+w_{83}q_3

所以维度变换不是凭空增加信息,而是把原始特征重新组合成新的表示。

PyTorch 中可以这样写:

import torch
import torch.nn as nn
W_q = nn.Linear(3, 8)
W_k = nn.Linear(5, 8)
q = torch.randn(3)
k = torch.randn(5)
h_q = W_q(q)
h_k = W_k(k)
print(h_q.shape) # torch.Size([8])
print(h_k.shape) # torch.Size([8])

这里的 nn.Linear(3, 8) 内部包含:

权重矩阵:8 × 3
偏置向量:8

这些参数会在训练过程中自动学习。


加性注意力的模型可以大致写成:

class AdditiveAttention(nn.Block):
def __init__(self, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False)
self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False)
self.w_v = nn.Dense(1, use_bias=False, flatten=False)
self.dropout = nn.Dropout(dropout)
def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
features = np.expand_dims(queries, axis=2) + np.expand_dims(keys, axis=1)
features = np.tanh(features)
scores = np.squeeze(self.w_v(features), axis=-1)
self.attention_weights = masked_softmax(scores, valid_lens)
return npx.batch_dot(self.dropout(self.attention_weights), values)

其中:

WqW_q:把 Query 映射到隐藏空间
WkW_k:把 Key 映射到隐藏空间
wvw_v:把隐藏向量变成注意力得分

假设:

queries: (2, 1, 20)
keys: (2, 10, 2)
values: (2, 10, 4)

经过 WqW_qWkW_k 之后:

queries: (2, 1, 8)
keys: (2, 10, 8)

然后扩展维度:

queries → (2, 1, 1, 8)
keys → (2, 1, 10, 8)

广播相加得到:

features: (2, 1, 10, 8)

含义是:

2 个样本
每个样本 1 个 query
每个 query 和 10 个 key 分别组合
每个组合是 8 维隐藏特征

之后 w_v 会把最后的 8 维隐藏向量压成 1 个分数:

features: (2, 1, 10, 8)
scores: (2, 1, 10)

也就是说:

每个 query 对 10 个 key 都有一个匹配分数

再经过 masked softmax,就得到了注意力权重。


缩放点积注意力是 Transformer 中常用的注意力形式。

单个 Query 和 Key 的评分函数是:

a(q,k)=qTkda(q,k)=\frac{q^T k}{\sqrt d}

其中 dd 是 Query 和 Key 的维度。

点积 qTkq^T k 表示 Query 和 Key 的相似程度:

越相似,点积越大
越不相似,点积越小

但是如果维度 dd 很大,点积结果可能会变得很大。

如果分数过大,softmax 会变得非常极端,例如几乎把所有权重都分给一个位置,这会影响训练稳定性。

所以要除以 d\sqrt d

控制点积分数的大小
避免 softmax 过于极端
让训练更稳定

多个 Query 和 Key 的矩阵形式为:

softmax(QKTd)V\operatorname{softmax}\left(\frac{QK^T}{\sqrt d}\right)V

流程是:

  1. QKTQK^T 计算每个 Query 和每个 Key 的相似度
  2. 除以 d\sqrt d 进行缩放
  3. softmax 得到注意力权重
  4. 乘以 VV,对 Value 加权求和

代码大致如下:

scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d)
attention_weights = masked_softmax(scores, valid_lens)
output = npx.batch_dot(attention_weights, values)

19. 加性注意力和缩放点积注意力的区别

Section titled “19. 加性注意力和缩放点积注意力的区别”
方法评分函数特点
加性注意力wvTtanh(Wqq+Wkk)w_v^T\tanh(W_q q + W_k k)Query 和 Key 维度可以不同,计算较灵活
缩放点积注意力qTk/dq^T k / \sqrt d计算效率高,但 Query 和 Key 维度需要相同

我的理解是:

加性注意力更像是用一个小网络学习 Query 和 Key 的匹配方式;
缩放点积注意力更直接,用向量点积衡量相似度。

Transformer 中大量使用缩放点积注意力,是因为它可以高效地进行矩阵并行计算。


经过这一部分学习,我对注意力机制的理解从一开始的抽象概念,逐渐变成了一个清晰的计算过程。

最开始我不理解:

什么是自主性提示?
Query、Key、Value 到底是什么?
为什么平均汇聚不算真正的注意力?
softmax 为什么能变成权重?
w 是怎么学习的?
为什么梯度可以调整权重?
加性注意力为什么支持 Query 和 Key 维度不同?
为什么点积注意力要除以 d\sqrt d

现在我的理解是:

  1. Query 表示当前目标。
  2. Key 表示每个信息用于匹配的特征。
  3. Value 表示真正被汇聚的信息。
  4. 注意力评分函数计算 Query 和 Key 的匹配程度。
  5. softmax 将匹配得分转化为权重。
  6. 注意力输出是 Value 的加权求和。
  7. masked softmax 用来屏蔽 padding 等无效位置。
  8. 加性注意力通过线性层统一 Query 和 Key 的维度。
  9. 缩放点积注意力通过 QKTQK^T 高效计算相似度,并用 d\sqrt d 稳定 softmax。

用一句话总结我目前对注意力机制的理解:

注意力机制不是简单地把所有信息平均起来,而是根据当前 Query 动态判断哪些 Key 更重要,并把对应的 Value 按权重汇聚起来。

这也是 Transformer 能够建模序列中不同位置依赖关系的基础。


这一节主要解决了注意力机制的基本计算过程。后面继续学习时,我准备重点关注以下问题:

  1. 多头注意力为什么要分多个头?
  2. 自注意力和普通注意力的区别是什么?
  3. Transformer 中 Q、K、V 是如何由输入序列生成的?
  4. 位置编码为什么是必要的?
  5. Transformer 编码器和解码器分别如何使用注意力机制?

通过这一章的学习,我感觉注意力机制并不是一个单独的公式,而是一种非常通用的信息选择框架。只要问题可以抽象成“根据当前目标,从一组候选信息中选择重要内容”,就可以使用注意力机制。