- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
导读
在《动手学深度学习》(d2l-zh)的注意力机制章节中,注意力评分函数(attention scoring function)是连接"查询(query)与键(key)"和"注意力权重"的核心桥梁。本文基于 chapter_attention-mechanisms/attention-scoring-functions_origin.md 展开,从高斯核出发推导评分函数的数学定义,系统讲解掩蔽 softmax 操作、加性注意力(additive attention)与缩放点积注意力(scaled dot-product attention)两种主流评分函数,并结合仓库d2l工具包(如 d2l/torch.py)中的源码实现,给出可直接运行的 PyTorch 代码示例。读完本文,你将掌握注意力汇聚的加权和本质、如何在批量处理中掩蔽填充词元,以及如何根据查询与键的向量长度选择合适的评分函数,为理解 Transformer、多头注意力等更复杂的注意力机制打下基础。
从高斯核到注意力评分函数
在 Nadaraya-Watson 核回归 一节中,我们使用高斯核来对查询和键之间的关系建模。高斯核的指数部分实际上就是一个注意力评分函数(attention scoring function),简称评分函数。将该函数的输出送入 softmax 运算后,我们得到与键对应的值的概率分布,即注意力权重;最后,注意力汇聚的输出就是基于这些注意力权重的值的加权和。
从宏观来看,上述算法可以实例化注意力机制的通用框架:记注意力评分函数为 $a$,注意力汇聚的输出可以计算为值的加权和。由于注意力权重本身是一个概率分布,因此加权和本质上是加权平均值。
数学定义与通用框架
用数学语言描述:假设有一个查询 $\mathbf{q} \in \mathbb{R}^q$ 和 $m$ 个"键-值"对 $(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)$,其中 $\mathbf{k}_i \in \mathbb{R}^k$、$\mathbf{v}_i \in \mathbb{R}^v$。注意力汇聚函数 $f$ 被表示成值的加权和:
$$f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}m)) = \sum{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i \in \mathbb{R}^v$$
其中,查询 $\mathbf{q}$ 和键 $\mathbf{k}_i$ 的注意力权重(标量)通过注意力评分函数 $a$ 将两个向量映射成标量,再经过 softmax 运算得到:
$$\alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(a(\mathbf{q}, \mathbf{k}i))}{\sum{j=1}^m \exp(a(\mathbf{q}, \mathbf{k}_j))} \in \mathbb{R}$$
由此可见,选择不同的评分函数 $a$ 会直接导致不同的注意力汇聚行为。本节将介绍两个主流评分函数——加性注意力与缩放点积注意力,它们稍后将被用于实现 Bahdanau 注意力(chapter_attention-mechanisms/bahdanau-attention.md)、多头注意力与 Transformer 等更复杂的注意力机制。
掩蔽 Softmax 操作
softmax 操作用于输出一个概率分布作为注意力权重。但在某些场景下,并非所有值都应该被纳入注意力汇聚。典型例子是机器翻译中的小批量处理(见 chapter_recurrent-modern/machine-translation-and-dataset.md):为了对齐不同长度的句子,一些文本序列被填充了没有意义的特殊词元。为了仅对有意义的词元进行注意力汇聚,需要指定一个有效序列长度(有效词元个数),在计算 softmax 时过滤掉超出该范围的位置。
下面的masked_softmax函数实现了这一掩蔽 softmax 操作(masked softmax operation):任何超出有效长度的位置都会被掩蔽并置为 0。以下是 PyTorch 版本的核心实现(与 chapter_attention-mechanisms/attention-scoring-functions.md 中#@tab pytorch的代码一致):
def masked_softmax(X, valid_lens): """通过在最后一个轴上掩蔽元素来执行softmax操作""" # X:3D张量,valid_lens:1D或2D张量 if valid_lens is None: return nn.functional.softmax(X, dim=-1) else: shape = X.shape if valid_lens.dim() == 1: valid_lens = torch.repeat_interleave(valid_lens, shape[1]) else: valid_lens = valid_lens.reshape(-1) # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, value=-1e6) return nn.functional.softmax(X.reshape(shape), dim=-1)实现的关键思路:
valid_lens为None:直接对最后一个轴执行普通 softmax,不进行任何掩蔽。- 一维
valid_lens:长度为批量大小,表示每个样本(矩阵)统一的有效长度。此时通过torch.repeat_interleave(valid_lens, shape[1])将每个长度重复shape[1](每个样本中的矩阵行数)次,以便与展平后的每一行一一对应。 - 二维
valid_lens:为每个矩阵中的每一行单独指定有效长度,先reshape(-1)展平。 - 掩蔽手段:将超出有效长度的元素替换为非常大的负值(
-1e6),其指数结果趋近于 0,从而在 softmax 输出中被完全屏蔽。底层依赖d2l.sequence_mask,其 PyTorch 实现位于 d2l/torch.py:通过torch.arange(maxlen) < valid_len[:, None]生成布尔掩码,再将掩码为False的位置赋值为value。
为了演示该函数的工作方式,考虑一个小批量,包含两个 $2 \times 4$ 的矩阵样本,两个样本的有效长度分别为 2 和 3。经过掩蔽 softmax 操作,超出有效长度的值都被掩蔽为 0:
masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))同样,也可以使用二维张量为矩阵样本中的每一行指定有效长度:
masked_softmax(torch.rand(2, 2, 4), d2l.tensor([[1, 3], [2, 4]]))掩蔽 softmax 不仅在注意力评分函数中使用,在 d2l/torch.py 的MaskedSoftmaxCELoss(带掩蔽的交叉熵损失)中也被复用——sequence_mask会先对标签权重矩阵做同样的掩蔽,这正是本仓库中"一次实现、多处复用"的设计体现。
加性注意力
当查询和键是不同长度的向量时,可以使用加性注意力(additive attention)作为评分函数。给定查询 $\mathbf{q} \in \mathbb{R}^q$ 和键 $\mathbf{k} \in \mathbb{R}^k$,加性注意力的评分函数为:
$$a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R}$$
其中可学习参数为 $\mathbf W_q\in\mathbb R^{h\times q}$、$\mathbf W_k\in\mathbb R^{h\times k}$ 和 $\mathbf w_v\in\mathbb R^{h}$。从结构上看,这等价于将查询和键拼接后输入一个只含单个隐藏层的多层感知机(MLP),隐藏单元数 $h$ 是一个超参数;激活函数使用 $\tanh$,并禁用偏置项。
PyTorch 实现
PyTorch 版本的AdditiveAttention类实现如下(同时保存在 d2l/torch.py 中):
class AdditiveAttention(nn.Module): """加性注意力""" def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): super(AdditiveAttention, self).__init__(**kwargs) self.W_k = nn.Linear(key_size, num_hiddens, bias=False) self.W_q = nn.Linear(query_size, num_hiddens, bias=False) self.w_v = nn.Linear(num_hiddens, 1, bias=False) self.dropout = nn.Dropout(dropout) def forward(self, queries, keys, values, valid_lens): queries, keys = self.W_q(queries), self.W_k(keys) # 在维度扩展后, # queries的形状:(batch_size,查询的个数,1,num_hidden) # keys的形状:(batch_size,1,“键-值”对的个数,num_hiddens) # 使用广播方式进行求和 features = queries.unsqueeze(2) + keys.unsqueeze(1) features = torch.tanh(features) # self.w_v仅有一个输出,因此从形状中移除最后那个维度。 # scores的形状:(batch_size,查询的个数,“键-值”对的个数) scores = self.w_v(features).squeeze(-1) self.attention_weights = masked_softmax(scores, valid_lens) # values的形状:(batch_size,“键-值”对的个数,值的维度) return torch.bmm(self.dropout(self.attention_weights), values)前向传播可分为四个阶段:
- 线性投影:分别用
W_q和W_k将查询、键映射到统一维度num_hiddens。 - 广播求和:
queries.unsqueeze(2)得到形状(batch_size, 查询数, 1, num_hiddens),keys.unsqueeze(1)得到(batch_size, 1, 键-值对数, num_hiddens),两者相加后每个查询与每个键一一组合,形成所有 (查询, 键) 对的融合特征。 - 激活与标量映射:经过
tanh后,w_v(输出维度为 1)将其映射为标量得分scores,形状为(batch_size, 查询数, 键-值对数)。 - 掩蔽 + 归一化 + 加权求和:
masked_softmax(scores, valid_lens)生成注意力权重(同时记录到self.attention_weights供可视化),经 dropout 正则化后,通过批量矩阵乘法torch.bmm与values相乘,得到注意力汇聚输出。
玩具示例
用一个玩具示例演示AdditiveAttention。查询、键、值的形状(批量大小、步数或词元序列长度、特征大小)分别为 $(2, 1, 20)$、$(2, 10, 2)$ 和 $(2, 10, 4)$;注意力汇聚输出的形状为(批量大小,查询的步数,值的维度):
queries, keys = d2l.normal(0, 1, (2, 1, 20)), d2l.ones((2, 10, 2)) # values的小批量数据集中,两个值矩阵是相同的 values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat( 2, 1, 1) valid_lens = d2l.tensor([2, 6]) attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8, dropout=0.1) attention.eval() attention(queries, keys, values, valid_lens)尽管加性注意力包含可学习参数,但由于本例中每个键都是相同的(无法被任何查询区分),得到的注意力权重是均匀的,由指定的有效长度决定。用热力图可以直观地看到这一点:
d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), xlabel='Keys', ylabel='Queries')show_heatmaps的实现位于 d2l/torch.py,它以imshow绘制矩阵热图,并支持设置 x/y 轴标签与子图标题,是本书中可视化注意力权重的通用工具。
缩放点积注意力
使用点积可以得到计算效率更高的评分函数,但点积要求查询和键具有相同的长度 $d$。假设查询和键的所有元素都是独立的随机变量,且均满足零均值和单位方差,那么两个向量点积的均值为 0、方差为 $d$。为确保无论向量长度如何,点积的方差都保持为 1,我们将点积除以 $\sqrt{d}$,得到缩放点积注意力(scaled dot-product attention)评分函数:
$$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}$$
在实践中,通常从小批量的角度考虑效率:对 $n$ 个查询和 $m$ 个键-值对计算注意力,其中查询和键的长度为 $d$,值的长度为 $v$。查询 $\mathbf Q\in\mathbb R^{n\times d}$、键 $\mathbf K\in\mathbb R^{m\times d}$ 和值 $\mathbf V\in\mathbb R^{m\times v}$ 的缩放点积注意力为:
$$\mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}$$
PyTorch 实现
PyTorch 版本的DotProductAttention类如下(同时保存在 d2l/torch.py 中),其中使用 dropout 进行模型正则化:
class DotProductAttention(nn.Module): """缩放点积注意力""" def __init__(self, dropout, **kwargs): super(DotProductAttention, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) # queries的形状:(batch_size,查询的个数,d) # keys的形状:(batch_size,“键-值”对的个数,d) # values的形状:(batch_size,“键-值”对的个数,值的维度) # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] # 设置transpose_b=True为了交换keys的最后两个维度 scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d) self.attention_weights = masked_softmax(scores, valid_lens) return torch.bmm(self.dropout(self.attention_weights), values)与加性注意力相比,该实现更简洁,且没有任何可学习参数,只包含一次批量矩阵乘法(torch.bmm计算 $\mathbf Q \mathbf K^\top$,其中对keys转置以交换后两个维度)、一次缩放、一次掩蔽 softmax 与一次与values的批量矩阵乘法。valid_lens可以省略(None),此时不进行任何掩蔽。
与加性注意力使用相同数据演示
为了对比,使用与加性注意力示例相同的键、值和有效长度;对于点积操作,令查询的特征维度与键的特征维度相同(均为 2):
queries = d2l.normal(0, 1, (2, 1, 2)) attention = DotProductAttention(dropout=0.5) attention.eval() attention(queries, keys, values, valid_lens)与加性注意力演示相同,由于键包含的是相同的元素,这些元素无法通过任何查询进行区分,因此获得均匀的注意力权重:
d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), xlabel='Keys', ylabel='Queries')两种评分函数的对比与选型
| 对比维度 | 加性注意力(AdditiveAttention) | 缩放点积注意力(DotProductAttention) |
|---|---|---|
| 适用条件 | 查询与键向量长度不同($q \neq k$) | 查询与键向量长度相同($q = k = d$) |
| 评分公式 | $\mathbf w_v^\top \tanh(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k)$ | $\mathbf q^\top \mathbf k / \sqrt{d}$ |
| 可学习参数 | 有($\mathbf W_q, \mathbf W_k, \mathbf w_v$) | 无 |
| 计算开销 | 需要 MLP 前向,相对较高 | 仅有矩阵乘法与缩放,计算效率更高 |
| 方差控制 | 不适用 | 除以 $\sqrt{d}$ 使点积方差保持为 1 |
选择建议:当查询和键向量长度相同时,优先使用缩放点积注意力,因为它计算效率更高,且恰好是 Transformer 中自注意力与多头注意力(见 chapter_attention-mechanisms/multihead-attention.md、chapter_attention-mechanisms/transformer.md)的核心运算;当查询与键长度不同(例如 RNN 编码器-解码器中解码器状态与编码器隐状态维度不一致)时,则使用加性注意力,Bahdanau 注意力正是它的直接应用(见 chapter_attention-mechanisms/bahdanau-attention.md)。
小结与练习
小结
- 注意力汇聚的输出可以计算为值的加权平均;选择不同的注意力评分函数会带来不同的注意力汇聚行为。
- 当查询和键是不同长度的向量时,可以使用加性注意力评分函数;当它们长度相同时,缩放点积注意力评分函数的计算效率更高。
练习
- 修改小例子中的键,并可视化注意力权重。加性注意力和缩放点积注意力是否仍然产生相同的结果?为什么?
- 只使用矩阵乘法,能否为具有不同向量长度的查询和键设计新的评分函数?
- 当查询和键具有相同的向量长度时,向量求和作为评分函数是否比点积更好?为什么?
进一步阅读
- 评分函数的两大实现均已收录在
d2l工具包中,可直接导入使用:d2l/torch.py(含masked_softmax、AdditiveAttention、DotProductAttention),其他框架的实现见 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py。 - 评分函数的下游应用:Bahdanau 注意力 chapter_attention-mechanisms/bahdanau-attention.md、多头注意力 chapter_attention-mechanisms/multihead-attention.md、Transformer chapter_attention-mechanisms/transformer.md。
- 掩蔽思想的延伸:带掩蔽的交叉熵损失
MaskedSoftmaxCELoss位于 d2l/torch.py,用于序列到序列学习的训练。
- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
相关推荐
【深度学习实战】注意力机制增强:线性偏置注意力库教程
【深度学习实战】注意力机制增强:线性偏置注意力库教程 项目介绍 注意力与线性偏差(Attention with Linear Biases) 是一个由 Ofir
双向交叉注意力:革新深度学习注意力机制的新范式
双向交叉注意力:革新深度学习注意力机制的新范式 在深度学习和自然语言处理领域,注意力机制一直是推动技术进步的核心动力。双向交叉注意力(Bidirectional
深度学习注意力机制入门指南:从南瓜书第5章神经网络基础到自注意力公式详解
深度学习注意力机制入门指南:从南瓜书第5章神经网络基础到自注意力公式详解 想要理解深度学习中的 注意力机制 和 自注意力公式 吗?Datawhale团队的《南瓜
教程机器学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考