动手深度学习-注意力机制

注意力机制

动物需要在复杂的环境下有效的关注值得注意的点

心里学框架:人类根据随意线索和不随意线索来选择注意点

全集、全连接、池化层都只考虑不随意线索

注意力机制的显式则考虑随意线索

  • 随意的线索被称之为查询
  • 每个输入是一个值(value)和随意线索(key)的对
  • 通过注意力池化层来由偏向性的选择某些输入

给定数据$(x_i,y_i)=1,...,n$平均池化是最简单的方案$f(x)=\frac1n\sum_iy_i$

这里的y是训练集的输出

更号的方案是60年代提出来的Nadaraya watson核回归。下式中有角标是训练集的,没有角标是新的。K是kernel,是一个衡量X和$x_i$距离的函数

$$ \begin{aligned} &\bullet\text{ 使用高斯核 }K(u)=\frac1{\sqrt{2\pi}}\exp(-\frac{u^2}2) \\ &·那么 \\ &f(x)&& =\sum_{i=1}^n\frac{\exp\left(-\frac{1}{2}(x-x_i)^2\right)}{\sum_{j=1}^n\exp\left(-\frac{1}{2}(x-x_j)^2\right)}y_i \\ &&&=\sum_{i=1}^n\mathrm{softmax}\left(-\frac12(x-x_i)^2\right)y_i \end{aligned} $$

在之前基础上引入可以学习的w

$$ f(x)=\sum_{i=1}^n\mathrm{softmax}\left(-\frac12((x-x_i)w)^2\right)y_i $$

import torch
from torch import nn
from d2l import torch as d2l

# 生成数据集
n_train = 50  # 训练样本数
x_train, _ = torch.sort(torch.rand(n_train) * 5)  # 排序后的训练样本


def f(x):
    return 2 * torch.sin(x) + x ** 0.8


# 后半部分模拟噪音
y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,))  # 训练样本的输出
x_test = torch.arange(0, 5, 0.1)  # 测试样本
y_truth = f(x_test)  # 测试样本的真实输出
n_test = len(x_test)  # 测试样本数
print(n_test)


# 绘制
def plot_kernel_reg(y_hat):
    d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'],
             xlim=[0, 5], ylim=[-1, 5])
    d2l.plt.plot(x_train, y_train, 'o', alpha=0.5);
    d2l.plt.show()


# 平均汇聚
y_hat = torch.repeat_interleave(y_train.mean(), n_test)
plot_kernel_reg(y_hat)

# 非参数注意力汇聚(池化)

# X_repeat的形状:(n_test,n_train),
# 每一行都包含着相同的测试输入(例如:同样的查询)
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))
# x_train包含着键。attention_weights的形状:(n_test,n_train),
# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重
attention_weights = nn.functional.softmax(-(X_repeat - x_train) ** 2 / 2, dim=1)
# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = torch.matmul(attention_weights, y_train)
plot_kernel_reg(y_hat)

比如我看一张图,人就是Q,图片就是V,K就是图片里的元素

查询值Q,被查询对象V,K就是key是具体的表达

我们回去判断哪些东西对我们而言更重要,哪些对我们而言不重要(计算Q和V里事物的重要度)

计算重要度就是计算相似度

通过点乘(内积)的方法计算Q和K里面每一个事物的相似度,就可以拿到Q和$k_1$的相似度$s_1$,Q和$k_2$的相似度$s_2$

做$softmax(a_1,a_2,...,a_n)$就可以得到概率,进而就知道哪个是最重要的。

我们还得进行一个汇总,当你使用Q查询结束了之后,Q已经失去了使用的价值。我们最终还是要拿到这张图片的,只不过现在的这张图片,它多了一些信息

$$ (a_1,a_2,\cdots,a_n)*(_1,v_2,\cdots,v_n)=(a_1*v_1+a_2*v_2+\cdots+a_n*v_n) $$

这样的话就得到了一个新的V,这个新的V'就包含了哪些更重要那些不重要的信息在里面。然后用V'代替V

一般K=V(在transform中),K也可以不等于V,但是K和V一定有某种联系。这样的QK点乘才能指导V哪些重要那些不重要

但是这里softmax函数有一个问题,就是会计算指数函数,导致a1和a2的差额越大这个概率就越离谱

所以其实是用的

$$ \alpha_{i}=softmax(\frac{f(Q,K_{i})}{k^{\sqrt{d}_k}}) $$

# 批量矩阵乘法 假设俩个张量的形状分别是(n,a,b)和(n,b,c),它们的批量矩阵输出的形状为(n,a,c)

X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
print(torch.bmm(X, Y).shape)

mm(matrix multiplication)

函数用于两个二维矩阵之间的乘法。它接受两个参数:第一个参数是一个 (m x n) 的矩阵,第二个参数是一个 (n x p) 的矩阵,并返回一个 (m x p) 的结果矩阵。

  • A: (m x n) 的矩阵
  • B: (n x p) 的矩阵
  • 返回值: (m x p) 的矩阵

bmm`(batch matrix multiplication)

bmm 函数用于两个三维张量(可以看作是一批矩阵)之间的批量矩阵乘法。它接受两个参数,每个参数都是形状为 (batch_size x m x n)(batch_size x n x p) 的张量,并返回一个 (batch_size x m x p) 的结果张量。

@运算符

@ 运算符是 Python 中用于矩阵乘法的标准运算符,它在 PyTorch 中也支持。这个运算符根据输入张量的维度来选择适当的乘法操作。如果输入的是两个二维张量,则等同于 mm;如果是三维张量,则等同于 bmm

而在注意力机制的背景中我们可以使用小批量乘法来计算小批量数据中的加权平均值


带参数的注意力汇聚

class NWKernelRegression(nn.Module):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.w = nn.Parameter(torch.rand((1,), requires_grad=True))

    def forward(self, queries, keys, values):
        # queries和attention_weights的形状为(查询个数,“键-值”对个数)
        queries = queries.repeat_interleave(keys.shape[1]).reshape((-1, keys.shape[1]))
        #这里的w用来控制核窗口的大小
        self.attention_weights = nn.functional.softmax(-((queries - keys) * self.w)**2 / 2, dim=1)
        # values的形状为(查询个数,“键-值”对个数)
        return torch.bmm(self.attention_weights.unsqueeze(1),values.unsqueeze(-1)).reshape(-1)
       
 #训练
# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入
X_tile = x_train.repeat((n_train, 1))
# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出
Y_tile = y_train.repeat((n_train, 1))
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
#训练带参数的注意力汇聚模型时,使用平方损失函数和随机梯度下降。
net = NWKernelRegression()
loss = nn.MSELoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.5)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])

for epoch in range(5):
    trainer.zero_grad()
    l = loss(net(x_train, keys, values), y_train)
    l.sum().backward()
    trainer.step()
    print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
    animator.add(epoch + 1, float(l.sum()))

注意力分数

左边是注意力权重,右边是注意力分数

$$ f(x)=\sum_i\alpha(x,x_i)y_i=\sum_{i=1}^n\mathrm{softmax}\left(-\frac12(x_0-x_i)^2\right)y_i $$

可学参数:$\mathbf{W}_k\in\mathbb{R}^{h\times k},\mathbf{W}_q\in\mathbb{R}^{h\times q},\mathbf{v}\in\mathbb{R}^h$

$$a(\mathbf{k},\mathbf{q})=\mathbf{v}^T\tanh(\mathbf{W}_k\mathbf{k}+\mathbf{W}_q\mathbf{q})$$

等价于将key和value合并秋来然后放到一个隐藏大小为h输出大小为1的单隐藏层MLP。

·如果query和key都是同样的长度$\mathbf{q},\mathbf{k}_i\in\mathbb{R}^d$,那么可以
$$a(\mathbf{q},\mathbf{k_i})=\langle\mathbf{q},\mathbf{k_i}\rangle/\sqrt{d}$$
· 向量化版本

$\cdot$ $\mathbf{Q} \in \mathbb{R} ^{n\times d}, \mathbf{K} \in \mathbb{R} ^{m\times d}, \mathbf{V} \in \mathbb{R} ^{m\times \nu }$
·注意力分数:$a(\mathbf{Q},\mathbf{K})=\mathbf{Q}\mathbf{K}^T/\sqrt{d}\in\mathbb{R}^{n\times m}$
·注意力池化:$f=$softmax $\left(a(\mathbf{Q},\mathbf{K})\right)\mathbf{V}\in\mathbb{R}^n\times\nu$

注意力分数是query和key的相似度,注意力权重是分数softmax的结果

import math
import torch
from torch import nn
from d2l import torch as d2l


# 可能只有前面几个是有效的
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)


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


# @save
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)

    # 是有多少个键值对是需要的,这里是对每个queries考虑多少个键值对
    def forward(self, queries, keys, values, valid_lens):
        queries, keys = self.W_q(queries), self.W_k(keys)
        # 在维度扩展后,
        # queries的形状:(batch_size,查询的个数,1,num_hidden)
        # key的形状:(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)


# 演示
queries, keys = torch.normal(0, 1, (2, 1, 20)), torch.ones((2, 10, 2))
# values的小批量,两个值矩阵是相同的
values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat(
    2, 1, 1)
valid_lens = torch.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(attention.attention_weights.reshape((1, 1, 2, 10)), xlabel='Keys', ylabel='Queries')


# 不需要学习任何内容
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)


# 缩放点积的演示
queries = torch.normal(0, 1, (2, 1, 2))
attention = DotProductAttention(dropout=0.5)
attention.eval()
attention(queries, keys, values, valid_lens)

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

带注意力的seq2seq(bahdanau)

编码器对每次词的输入作为key和value(key就是value)

解码器RNN对上一个词的输出是query

import torch
from torch import nn
from d2l import torch as d2l


# @save
class AttentionDecoder(d2l.Decoder):
    """带有注意力机制解码器的基本接口"""

    def __init__(self, **kwargs):
        super(AttentionDecoder, self).__init__(**kwargs)

    @property
    def attention_weights(self):
        raise NotImplementedError


class Seq2SeqAttentionDecoder(AttentionDecoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
        # 唯一的变化在这里,使用加性的注意力
        self.attention = d2l.AdditiveAttention(num_hiddens, num_hiddens, num_hiddens, dropout)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)

    # 增加了enc_valid_lens
    def init_state(self, enc_outputs, enc_valid_lens, *args):
        # outputs的形状为(batch_size,num_steps,num_hiddens).
        # hidden_state的形状为(num_layers,batch_size,num_hiddens)
        outputs, hidden_state = enc_outputs
        return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)

    def forward(self, X, state):
        # enc_outputs的形状为(batch_size,num_steps,num_hiddens).
        # hidden_state的形状为(num_layers,batch_size,
        # num_hiddens)
        enc_outputs, hidden_state, enc_valid_lens = state
        # 输出X的形状为(num_steps,batch_size,embed_size)
        X = self.embedding(X).permute(1, 0, 2)
        outputs, self._attention_weights = [], []
        for x in X:
            # query的形状为(batch_size,1,num_hiddens)
            query = torch.unsqueeze(hidden_state[-1], dim=1)  # hidden_state是上一个时间rnn最后一个的输出,然后加一个维度上去
            # context的形状为(batch_size,1,num_hiddens)
            # enc_valid_lens意思是只对固定数量的kv对算权重,核心是在这一行
            context = self.attention(query, enc_outputs, enc_outputs, enc_valid_lens)
            # 在特征维度上连结
            x = torch.cat((context, torch.unsqueeze(x, dim=1)), dim=-1)
            # 将x变形为(1,batch_size,embed_size+num_hiddens)
            out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)
            outputs.append(out)
            self._attention_weights.append(self.attention.attention_weights)
        # 全连接层变换后,outputs的形状为
        # (num_steps,batch_size,vocab_size)
        outputs = self.dense(torch.cat(outputs, dim=0))
        return outputs.permute(1, 0, 2), [enc_outputs, hidden_state, enc_valid_lens]

    @property
    def attention_weights(self):
        return self._attention_weights


# 接下来,使用包含7个时间步的4个序列输入的小批量测试Bahdanau注意力解码器。
encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,
                             num_layers=2)
encoder.eval()
decoder = Seq2SeqAttentionDecoder(vocab_size=10, embed_size=8, num_hiddens=16,
                                  num_layers=2)
decoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)  # (batch_size,num_steps)
state = decoder.init_state(encoder(X), None)
output, state = decoder(X, state)
output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape

# 训练
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 250, d2l.try_gpu()

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = d2l.Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
decoder = Seq2SeqAttentionDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)
net = d2l.EncoderDecoder(encoder, decoder)
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

# 计算bleu分数
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, dec_attention_weight_seq = d2l.predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device, True)
    print(f'{eng} => {translation}, ', f'bleu {d2l.bleu(translation, fra, k=2):.3f}')

自注意力机制 self-attention

给定序列x1-xn

自注意力池化层将 $\mathbf{x}_i$当做key,value, query来对序列抽取特征得到$\mathbf{y}_1,\ldots,\mathbf{y}_n$,这里
$$\mathbf{y}_i=f(\mathbf{x}_i,(\mathbf{x}_1,\mathbf{x}_1),...,(\mathbf{x}_n,\mathbf{x}_n))\in\mathbb{R}^d$$

self-attention的关键点在于不仅仅是K==V而且K==V==Q。这三者是同源于X的

通过三个参数$W_Q,W_K,W_V$

1.QKV的获取

2相乘

3缩放+softmax

4相乘

$z_1$表示的就是thinking的新的向量表示。

对于thinking,初始词向量为$x_1$

现在我们通过thinking machines这句话去查询这句话里的每一个单词和thinking之间的相似度

新的z1依然是thinking的词向量表示,只不过这个词向量表示蕴含了thinking machines这句话对于thinking 而言哪个更重要

注意力机制中没有规定QKV是怎么来的,之规定QKV是怎么做的。而自注意力机制是通过三个矩阵乘法(三次线性变换)从源输入中得到的。

对于一个词向量做的是空间上的对应。只是对X的另外一种表达。

(Q和V不相同就是交叉注意力机制)

跟cnn和rnn不同,自注意力机制没有记录位置信息

位置编码将位置信息注入到输入里

位置编码positional encoding

attention解决了长依赖的问题,并且可以并行

缺点:可以并行也就是说词语次之间不存在顺序关系。

在rnn中隐含着顺序关系,因为每一次输入都是由上一个输出给出的。(打乱一句,这句话里的每个词向量也不会变)

  • i 为序列中的位置索引(例如,句子中词的位置)。
  • j为维度索引,用来确定在多维向量中的具体维度。
  • d为整个向量的维度大小,即向量的长度。

$$ p_{i,2j}=\sin\left(\frac i{10000^{2j/d}}\right),\quad p_{i,2j+1}=\cos\left(\frac i{10000^{2j/d}}\right) $$

得到位置编码之后混合它原本的信息,编码成一个新的向量。

借住上述公式,我们可以得到一个特定位置的$d_{model}$维的位置向量,并借住三角函数的性质

$$\left.\left\{\begin{array}{l}sin(\alpha+\beta)=sin\alpha cos\beta+cos\alpha sin\beta\\cos(\alpha+\beta)=cos\alpha cos\beta-sin\alpha sin\beta\end{array}\right.\right.$$

我们可以得到:

$$PE(pos+k,2i)=PE(pos,2i)\times PE(k,2i+1)+PE(pos,2i+1)\times PE(k,2i)\\PE(pos+k,2i+1)=PE(pos,2i+1)\times PE(k,2i+1)-PE(pos,2i)\times PE(k,2i)$$

可以看出,对于 pos+k 位置的位置向量某一维$2i$或$2i+1$而言,可以表示为,pos 位置与 k位置的位置向量的$2i$与$2i+1$维的线性组合,这样的线性组合意味着位置向量中蕴含了相对位置信息。

掩码自注意力机制

它是在self attention上做了改进

为什么要做这个改进:生成模型

当我们做生成任务的时候,我们也想对生成的这个单词做注意力计算,但是,生成的句子是一个一个单词生成的。

因为自注意力机制明确知道这句话有多少个单词,并且一次性给足,但是掩码在最后一次才能给足所有信息。

多头注意力机制

Z相比较X有了提升,通过Multi-Head Self Attention,得到的Z'相比较有了进一步的提升

一般来说多头的个数用h表示我们通常使用8头自注意力机制。

对于X我们不是直接拿X去得到Z,而是把X分成了8块(8头)z0-z7

然后把z0和z7拼接起来再做一次线性变化。

Last modification:September 3, 2024
如果觉得我的文章对你有用,请随意赞赏