注意力评分函数

:label:sec_attention-scoring-functions

在 :numref:sec_nadaraya-watson中, 我们使用高斯核来对查询和键之间的关系建模。 我们可以将 :eqref:eq_nadaraya-watson-gaussian中的 高斯核指数部分视为注意力评分函数(attention scoring function), 简称评分函数(scoring function), 然后把这个函数的输出结果输入到softmax函数中进行运算。 通过上述步骤,我们将得到与键对应的值的概率分布(即注意力权重)。 最后,注意力汇聚的输出就是基于这些注意力权重的值的加权和。

从宏观来看,我们可以使用上述算法来实现 :numref:fig_qkv中的注意力机制框架。 :numref:fig_attention_output说明了 如何将注意力汇聚的输出计算成为值的加权和, 其中$a$表示注意力评分函数。 由于注意力权重是概率分布, 因此加权和其本质上是加权平均值。

计算注意力汇聚的输出为值的加权和 :label:fig_attention_output

用数学语言描述,假设有一个查询 $\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, :eqlabel:eq_attn-pooling

其中查询$\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}. :eqlabel:eq_attn-scoring-alpha

正如我们所看到的,选择不同的注意力评分函数$a$会导致不同的注意力汇聚操作。 在本节中,我们将介绍两个流行的评分函数,稍后将用他们来实现更复杂的注意力机制。

```{.python .input} import math from d2l import mxnet as d2l from mxnet import np, npx from mxnet.gluon import nn npx.set_np()

  1. ```{.python .input}
  2. #@tab pytorch
  3. from d2l import torch as d2l
  4. import math
  5. import torch
  6. from torch import nn

```{.python .input}

@tab tensorflow

from d2l import tensorflow as d2l import tensorflow as tf

  1. ## [**掩蔽softmax操作**]
  2. 正如上面提到的,softmax操作用于输出一个概率分布作为注意力权重。
  3. 在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。
  4. 例如,为了在 :numref:`sec_machine_translation`中高效处理小批量数据集,
  5. 某些文本序列被填充了没有意义的特殊词元。
  6. 为了仅将有意义的词元作为值来获取注意力汇聚,
  7. 我们可以指定一个有效序列长度(即词元的个数),
  8. 以便在计算softmax时过滤掉超出指定范围的位置。
  9. 通过这种方式,我们可以在下面的`masked_softmax`函数中
  10. 实现这样的*掩蔽softmax操作*(masked softmax operation),
  11. 其中任何超出有效长度的位置都被掩蔽并置为0
  12. ```{.python .input}
  13. #@save
  14. def masked_softmax(X, valid_lens):
  15. """通过在最后一个轴上掩蔽元素来执行softmax操作"""
  16. # X:3D张量,valid_lens:1D或2D张量
  17. if valid_lens is None:
  18. return npx.softmax(X)
  19. else:
  20. shape = X.shape
  21. if valid_lens.ndim == 1:
  22. valid_lens = valid_lens.repeat(shape[1])
  23. else:
  24. valid_lens = valid_lens.reshape(-1)
  25. # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
  26. X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, True,
  27. value=-1e6, axis=1)
  28. return npx.softmax(X).reshape(shape)

```{.python .input}

@tab pytorch

@save

def masked_softmax(X, valid_lens): “””通过在最后一个轴上掩蔽元素来执行softmax操作”””

  1. # X:3D张量,valid_lens:1D或2D张量
  2. if valid_lens is None:
  3. return nn.functional.softmax(X, dim=-1)
  4. else:
  5. shape = X.shape
  6. if valid_lens.dim() == 1:
  7. valid_lens = torch.repeat_interleave(valid_lens, shape[1])
  8. else:
  9. valid_lens = valid_lens.reshape(-1)
  10. # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
  11. X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens,
  12. value=-1e6)
  13. return nn.functional.softmax(X.reshape(shape), dim=-1)
  1. ```{.python .input}
  2. #@tab tensorflow
  3. #@save
  4. def masked_softmax(X, valid_lens):
  5. """通过在最后一个轴上掩蔽元素来执行softmax操作"""
  6. # X:3D张量,valid_lens:1D或2D张量
  7. if valid_lens is None:
  8. return tf.nn.softmax(X, axis=-1)
  9. else:
  10. shape = X.shape
  11. if len(valid_lens.shape) == 1:
  12. valid_lens = tf.repeat(valid_lens, repeats=shape[1])
  13. else:
  14. valid_lens = tf.reshape(valid_lens, shape=-1)
  15. # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
  16. X = d2l.sequence_mask(tf.reshape(X, shape=(-1, shape[-1])),
  17. valid_lens, value=-1e6)
  18. return tf.nn.softmax(tf.reshape(X, shape=shape), axis=-1)

为了[演示此函数是如何工作]的, 考虑由两个$2 \times 4$矩阵表示的样本, 这两个样本的有效长度分别为$2$和$3$。 经过掩蔽softmax操作,超出有效长度的值都被掩蔽为0。

```{.python .input} masked_softmax(np.random.uniform(size=(2, 2, 4)), d2l.tensor([2, 3]))

  1. ```{.python .input}
  2. #@tab pytorch
  3. masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))

```{.python .input}

@tab tensorflow

masked_softmax(tf.random.uniform(shape=(2, 2, 4)), tf.constant([2, 3]))

  1. 同样,我们也可以使用二维张量,为矩阵样本中的每一行指定有效长度。
  2. ```{.python .input}
  3. masked_softmax(np.random.uniform(size=(2, 2, 4)),
  4. d2l.tensor([[1, 3], [2, 4]]))

```{.python .input}

@tab pytorch

masked_softmax(torch.rand(2, 2, 4), d2l.tensor([[1, 3], [2, 4]]))

  1. ```{.python .input}
  2. #@tab tensorflow
  3. masked_softmax(tf.random.uniform(shape=(2, 2, 4)), tf.constant([[1, 3], [2, 4]]))

[加性注意力]

:label:subsec_additive-attention

一般来说,当查询和键是不同长度的矢量时, 我们可以使用加性注意力作为评分函数。 给定查询$\mathbf{q} \in \mathbb{R}^q$和 键$\mathbf{k} \in \mathbb{R}^k$, 加性注意力(additive attention)的评分函数为

a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R}, :eqlabel:eq_additive-attn

其中可学习的参数是$\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}$。 如 :eqref:eq_additive-attn所示, 将查询和键连结起来后输入到一个多层感知机(MLP)中, 感知机包含一个隐藏层,其隐藏单元数是一个超参数$h$。 通过使用$\tanh$作为激活函数,并且禁用偏置项。

下面我们来实现加性注意力。

```{.python .input}

@save

class AdditiveAttention(nn.Block): “””加性注意力””” def init(self, numhiddens, dropout, **kwargs): super(AdditiveAttention, self)._init(**kwargs)

  1. # 使用'flatten=False'只转换最后一个轴,以便其他轴的形状保持不变
  2. self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False)
  3. self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False)
  4. self.w_v = nn.Dense(1, use_bias=False, flatten=False)
  5. self.dropout = nn.Dropout(dropout)
  6. def forward(self, queries, keys, values, valid_lens):
  7. queries, keys = self.W_q(queries), self.W_k(keys)
  8. # 在维度扩展后,
  9. # queries的形状:(batch_size,查询的个数,1,num_hidden)
  10. # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
  11. # 使用广播的方式进行求和
  12. features = np.expand_dims(queries, axis=2) + np.expand_dims(
  13. keys, axis=1)
  14. features = np.tanh(features)
  15. # self.w_v仅有一个输出,因此从形状中移除最后那个维度。
  16. # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
  17. scores = np.squeeze(self.w_v(features), axis=-1)
  18. self.attention_weights = masked_softmax(scores, valid_lens)
  19. # values的形状:(batch_size,“键-值”对的个数,值的维度)
  20. return npx.batch_dot(self.dropout(self.attention_weights), values)
  1. ```{.python .input}
  2. #@tab pytorch
  3. #@save
  4. class AdditiveAttention(nn.Module):
  5. """加性注意力"""
  6. def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
  7. super(AdditiveAttention, self).__init__(**kwargs)
  8. self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
  9. self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
  10. self.w_v = nn.Linear(num_hiddens, 1, bias=False)
  11. self.dropout = nn.Dropout(dropout)
  12. def forward(self, queries, keys, values, valid_lens):
  13. queries, keys = self.W_q(queries), self.W_k(keys)
  14. # 在维度扩展后,
  15. # queries的形状:(batch_size,查询的个数,1,num_hidden)
  16. # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
  17. # 使用广播方式进行求和
  18. features = queries.unsqueeze(2) + keys.unsqueeze(1)
  19. features = torch.tanh(features)
  20. # self.w_v仅有一个输出,因此从形状中移除最后那个维度。
  21. # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
  22. scores = self.w_v(features).squeeze(-1)
  23. self.attention_weights = masked_softmax(scores, valid_lens)
  24. # values的形状:(batch_size,“键-值”对的个数,值的维度)
  25. return torch.bmm(self.dropout(self.attention_weights), values)

```{.python .input}

@tab tensorflow

@save

class AdditiveAttention(tf.keras.layers.Layer): “””Additiveattention.””” def init(self, keysize, querysize, num_hiddens, dropout, **kwargs): super().__init(**kwargs) self.W_k = tf.keras.layers.Dense(num_hiddens, use_bias=False) self.W_q = tf.keras.layers.Dense(num_hiddens, use_bias=False) self.w_v = tf.keras.layers.Dense(1, use_bias=False) self.dropout = tf.keras.layers.Dropout(dropout)

  1. def call(self, queries, keys, values, valid_lens, **kwargs):
  2. queries, keys = self.W_q(queries), self.W_k(keys)
  3. # 在维度扩展后,
  4. # queries的形状:(batch_size,查询的个数,1,num_hidden)
  5. # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
  6. # 使用广播方式进行求和
  7. features = tf.expand_dims(queries, axis=2) + tf.expand_dims(
  8. keys, axis=1)
  9. features = tf.nn.tanh(features)
  10. # self.w_v仅有一个输出,因此从形状中移除最后那个维度。
  11. # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
  12. scores = tf.squeeze(self.w_v(features), axis=-1)
  13. self.attention_weights = masked_softmax(scores, valid_lens)
  14. # values的形状:(batch_size,“键-值”对的个数,值的维度)
  15. return tf.matmul(self.dropout(
  16. self.attention_weights, **kwargs), values)
  1. 我们用一个小例子来[**演示上面的`AdditiveAttention`类**],
  2. 其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小),
  3. 实际输出为$(2,1,20)$$(2,10,2)$$(2,10,4)$
  4. 注意力汇聚输出的形状为(批量大小,查询的步数,值的维度)。
  5. ```{.python .input}
  6. queries, keys = d2l.normal(0, 1, (2, 1, 20)), d2l.ones((2, 10, 2))
  7. # values的小批量数据集中,两个值矩阵是相同的
  8. values = np.arange(40).reshape(1, 10, 4).repeat(2, axis=0)
  9. valid_lens = d2l.tensor([2, 6])
  10. attention = AdditiveAttention(num_hiddens=8, dropout=0.1)
  11. attention.initialize()
  12. attention(queries, keys, values, valid_lens)

```{.python .input}

@tab pytorch

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)

  1. ```{.python .input}
  2. #@tab tensorflow
  3. queries, keys = tf.random.normal(shape=(2, 1, 20)), tf.ones((2, 10, 2))
  4. # values的小批量,两个值矩阵是相同的
  5. values = tf.repeat(tf.reshape(
  6. tf.range(40, dtype=tf.float32), shape=(1, 10, 4)), repeats=2, axis=0)
  7. valid_lens = tf.constant([2, 6])
  8. attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8,
  9. dropout=0.1)
  10. attention(queries, keys, values, valid_lens, training=False)

尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的, 所以[注意力权重]是均匀的,由指定的有效长度决定。

```{.python .input}

@tab all

d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), xlabel=’Keys’, ylabel=’Queries’)

  1. ## [**缩放点积注意力**]
  2. 使用点积可以得到计算效率更高的评分函数,
  3. 但是点积操作要求查询和键具有相同的长度$d$
  4. 假设查询和键的所有元素都是独立的随机变量,
  5. 并且都满足零均值和单位方差,
  6. 那么两个向量的点积的均值为$0$,方差为$d$
  7. 为确保无论向量长度如何,
  8. 点积的方差在不考虑向量长度的情况下仍然是$1$
  9. 我们将点积除以$\sqrt{d}$
  10. 则*缩放点积注意力*(scaled dot-product attention)评分函数为:
  11. $$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}.$$
  12. 在实践中,我们通常从小批量的角度来考虑提高效率,
  13. 例如基于$n$个查询和$m$个键-值对计算注意力,
  14. 其中查询和键的长度为$d$,值的长度为$v$
  15. 查询$\mathbf Q\in\mathbb R^{n\times d}$
  16. $\mathbf K\in\mathbb R^{m\times d}$
  17. $\mathbf V\in\mathbb R^{m\times v}$的缩放点积注意力是:
  18. $$ \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}.$$
  19. :eqlabel:`eq_softmax_QK_V`
  20. 在下面的缩放点积注意力的实现中,我们使用了暂退法进行模型正则化。
  21. ```{.python .input}
  22. #@save
  23. class DotProductAttention(nn.Block):
  24. """缩放点积注意力"""
  25. def __init__(self, dropout, **kwargs):
  26. super(DotProductAttention, self).__init__(**kwargs)
  27. self.dropout = nn.Dropout(dropout)
  28. # queries的形状:(batch_size,查询的个数,d)
  29. # keys的形状:(batch_size,“键-值”对的个数,d)
  30. # values的形状:(batch_size,“键-值”对的个数,值的维度)
  31. # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
  32. def forward(self, queries, keys, values, valid_lens=None):
  33. d = queries.shape[-1]
  34. # 设置transpose_b=True为了交换keys的最后两个维度
  35. scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d)
  36. self.attention_weights = masked_softmax(scores, valid_lens)
  37. return npx.batch_dot(self.dropout(self.attention_weights), values)

```{.python .input}

@tab pytorch

@save

class DotProductAttention(nn.Module): “””缩放点积注意力””” def init(self, dropout, kwargs): super(DotProductAttention, self).init(kwargs) self.dropout = nn.Dropout(dropout)

  1. # queries的形状:(batch_size,查询的个数,d)
  2. # keys的形状:(batch_size,“键-值”对的个数,d)
  3. # values的形状:(batch_size,“键-值”对的个数,值的维度)
  4. # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
  5. def forward(self, queries, keys, values, valid_lens=None):
  6. d = queries.shape[-1]
  7. # 设置transpose_b=True为了交换keys的最后两个维度
  8. scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
  9. self.attention_weights = masked_softmax(scores, valid_lens)
  10. return torch.bmm(self.dropout(self.attention_weights), values)
  1. ```{.python .input}
  2. #@tab tensorflow
  3. #@save
  4. class DotProductAttention(tf.keras.layers.Layer):
  5. """Scaleddotproductattention."""
  6. def __init__(self, dropout, **kwargs):
  7. super().__init__(**kwargs)
  8. self.dropout = tf.keras.layers.Dropout(dropout)
  9. # queries的形状:(batch_size,查询的个数,d)
  10. # keys的形状:(batch_size,“键-值”对的个数,d)
  11. # values的形状:(batch_size,“键-值”对的个数,值的维度)
  12. # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
  13. def call(self, queries, keys, values, valid_lens, **kwargs):
  14. d = queries.shape[-1]
  15. scores = tf.matmul(queries, keys, transpose_b=True)/tf.math.sqrt(
  16. tf.cast(d, dtype=tf.float32))
  17. self.attention_weights = masked_softmax(scores, valid_lens)
  18. return tf.matmul(self.dropout(self.attention_weights, **kwargs), values)

为了[演示上述的DotProductAttention], 我们使用与先前加性注意力例子中相同的键、值和有效长度。 对于点积操作,我们令查询的特征维度与键的特征维度大小相同。

```{.python .input} queries = d2l.normal(0, 1, (2, 1, 2)) attention = DotProductAttention(dropout=0.5) attention.initialize() attention(queries, keys, values, valid_lens)

  1. ```{.python .input}
  2. #@tab pytorch
  3. queries = d2l.normal(0, 1, (2, 1, 2))
  4. attention = DotProductAttention(dropout=0.5)
  5. attention.eval()
  6. attention(queries, keys, values, valid_lens)

```{.python .input}

@tab tensorflow

queries = tf.random.normal(shape=(2, 1, 2)) attention = DotProductAttention(dropout=0.5) attention(queries, keys, values, valid_lens, training=False)

  1. 与加性注意力演示相同,由于键包含的是相同的元素,
  2. 而这些元素无法通过任何查询进行区分,因此获得了[**均匀的注意力权重**]。
  3. ```{.python .input}
  4. #@tab all
  5. d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)),
  6. xlabel='Keys', ylabel='Queries')

小结

  • 将注意力汇聚的输出计算可以作为值的加权平均,选择不同的注意力评分函数会带来不同的注意力汇聚操作。
  • 当查询和键是不同长度的矢量时,可以使用可加性注意力评分函数。当它们的长度相同时,使用缩放的“点-积”注意力评分函数的计算效率更高。

练习

  1. 修改小例子中的键,并且可视化注意力权重。可加性注意力和缩放的“点-积”注意力是否仍然产生相同的结果?为什么?
  2. 只使用矩阵乘法,你能否为具有不同矢量长度的查询和键设计新的评分函数?
  3. 当查询和键具有相同的矢量长度时,矢量求和作为评分函数是否比“点-积”更好?为什么?

:begin_tab:mxnet Discussions :end_tab:

:begin_tab:pytorch Discussions :end_tab: