目录

一. 注意力机制(attention)

1.1注意力汇聚

1.2 注意力评分函数 

(1)  [加性注意力]

(2)  [缩放点积注意力]

二.加入注意力机制

2.1 将Bahdanau 注意力加入seq2seq

2.2自注意力(self-attention)

2.3位置编码

三. Transformer

3.1 多头注意力(multi-head attention)

 3.2基于位置的前馈网络

3.3层规范化(LayerNorm)


        并非所有感官的输入信息都是一样的。人类从海量信息中注意到感兴趣的一小部分信息是一种选择,心理学上分为非自主性提示和自主性提示两类,来区分引导注意力的方式。那么如何用神经网络来设计注意力机制的框架?

一. 注意力机制(attention)

        注意力机制与全连接层或者汇聚层的区别源于增加的自主提示。

        在注意力机制中,感官输入被称为值(value),非自主性提示被称为键(key),自主性提示被称为查询(query),就是用注意力汇聚层(attention pooling)与key进行匹配,引导出最匹配的value(感官输入(sensory inputs))。

        ps*:注意力机制的设计有许多替代方案。 例如可以设计一个不可微的注意力模型, 该模型可以使用强化学习方法 :cite:Mnih.Heess.Graves.ea.2014进行训练。 

       一个习惯是调用show_heatmaps函数来显示注意力权重,下图所示,仅当query=key,注意力权重=1,否则0。

1.1注意力汇聚

        (1)非参注意力汇聚层 (不需要学习参数)

                如,最简单的方案:平均池化。

                下图用y_train 的均值预测x_test,与y_test对比。               

        很明显平均汇聚忽略了x_test中x_{i},于是,60年代 Nadaraya-Waston核回归(Nadaraya-Watson kernel regression)提出,根据输入的位置对y_{i}进行加权,其中K是核(kernel):

        根据这个公式生成更为通用的注意力汇聚公式:

        x是查询, (x_{i},y_{i})为键值对,将查询xx_{i}建模为注意力权重(attention weight)\alpha (x,x_{i}),并分配给每一个对应的y_{i}值。

        对于任何查询x,模型在所有键值对(x_{i},y_{i})注意力权重都是一个有效的概率分布: 非负的,并且总和为1。

        定义[高斯核](Gaussian kernel),代入上式:

        在上式中,x_{i}越接近x,那么分配给这个x_{i}对应的y_{i}的权重就越大,即“获得了更多的注意力”。

        用这种方法预测的结果比平均汇聚的预测更接近真实,且是平滑的。

        在上图中,x_test(图中黄色⭕️表示)作为query, x_train作为key,query与key越靠近,注意力权重越高。

        (2)带参注意力汇聚层(学习参数w 

        将查询x与键x_{i}之间的距离乘以可学习参数w

         在注意力机制中,采用[批量矩阵乘法]计算小批量数据中的加权平均值。

  • [批量矩阵乘法]:假设一个小批量数据包含n个矩阵X_i,\cdots,X_n,形状为a\times b,另一个小批量数据包含n个矩阵Y_i,\cdots,Y_n,形状为b\times c,批量矩阵乘法得到n个矩阵X_1Y_1,\cdots,X_nY_n,形状为a\times c。即,假定两个张量形状分别是n\times a\times bn\times b\times c,批量矩阵乘法输出的形状为n\times a\times c

        可以发现: 在尝试拟合带噪声的训练数据时, [预测结果绘制]的线不如之前非参数模型的平滑。 与非参数的注意力汇聚模型相比, 带参数的模型加入可学习的参数后, [曲线在注意力权重较大的区域变得更不平滑]。 

1.2 注意力评分函数 

        本节将介绍两个流行的评分函数,稍后将用来实现更复杂的注意力机制。

  •  [注意力评分函数\alpha]:选择不同的注意力评分函数\alpha会导致不同的注意力汇聚操作。注意力权重是分数的softmax结果。

        (1)当query和key长度不一致时,[将query和key合并,进入一个单输出单隐藏层的MLP]-[加性注意力]

        (2)当query和key长度一致时,[直接将query和key做内积]-[点积注意力]

  • [掩蔽softmax操作-masked_softmax]:softmax操作用于输出一个概率分布作为注意力权重。 在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。 例如,为了高效处理小批量数据集, 某些文本序列被填充了没有意义的特殊词元。 为了仅将有意义的词元作为值来获取注意力汇聚, 可以指定一个有效序列长度(即词元的个数), 以便在计算softmax时过滤掉超出指定范围的位置。
(1)  [加性注意力]

        

         

(2)  [缩放点积注意力]

        另一种是缩放点积注意力,当查询和键具有相同的长度d,实现起来简单,不需要学习参数,唯一的超参数是dropout。

         假设查询和键的所有元素都是独立的随机变量, 并且都满足零均值和单位方差, 那么两个向量的点积的均值为0,方差为d。 为确保无论向量长度如何, 点积的方差在不考虑向量长度的情况下仍然是1, 我们再将点积除以\sqrt{d}, 则缩放点积注意力(scaled dot-product attention)评分函数为:

         

        小结:不同的net,key、query的设置可以不一样。 

二.加入注意力机制

        回顾seq2seq:

        (快捷查看深度学习入门:【《动手学深度学习》之day5】从文本序列开始01自回归 02循环神经网络RNN 03门控循环单元GRU 04长短期记忆网络LSTM 05-CSDN博客链接中第七.编码器解码器和八.seq2seq)

 
        编码器将长度可变的输入序列转换成形状固定的上下文变量c,且只用了编码器最后一个输出作为解码器的输入。

​​

        加入注意力后,变化的主要是:

        上下文变量c在任何解码时间步都会被替换c_{t^{'}}。 假设输入序列中有T个词元, 解码时间步t^{'}的上下文变量是注意力集中的输出 :

        其中,时间步t^{'}-1时的解码器隐状态s_{t^{'}-1}是查询(query), 编码器隐状态\mathbf{h}_t既是键(key),也是值(value), 注意力权重使用加性注意力打分函数。 

  • 编码器对每次词的输出作为key和value
  • 解码器RNN对上一个词的输出是query
  • 注意力的输出和下一个词的词嵌入合并送入解码器
#[带有注意力机制解码器的基本接口]
class AttentionDecoder(d2l.Decoder):
    """带有注意力机制解码器的基本接口"""
    def __init__(self, **kwargs):
        super(AttentionDecoder, self).__init__(**kwargs)

    @property
    def attention_weights(self):
        raise NotImplementedError

2.1 将Bahdanau 注意力加入seq2seq

        [Bahdanau注意力的架构]:

        [实现带有Bahdanau注意力的循环神经网络解码器]

        首先,初始化解码器的状态,需要下面的输入:

  1. 编码器在所有时间步的最终层隐状态,将作为注意力的键和值;
  2. 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态;
  3. 编码器有效长度(排除在注意力池中填充词元)。

        在每个解码时间步骤中,解码器上一个时间步的最终层隐状态将用作查询。 因此,注意力输出和输入嵌入都连结为循环神经网络解码器的输入。

        具体来说,实现代码时,修改的是class Seq2SeqDecoder(d2l.Decoder)--->class Seq2SeqAttentionDecoder(AttentionDecoder):

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)

    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)
            # context的形状为(batch_size,1,num_hiddens)
            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

      seq2seq结果:

                   loss 0.019, 3138.2 tokens/sec on mps
                   go . => va <unk> !, bleu 0.000
                   i lost . => j'ai perdu emporté ., bleu 0.658
                   he's calm . => il est bon ., bleu 0.658
                   i'm home . => je suis ici ici !, bleu 0.447

       加入注意力机制后,attention s2s结果,可以看到效果确实有进步:

                loss 0.019, 1831.8 tokens/sec on mps
                go . => va !, bleu 1.000
                i lost . => j'ai perdu ., bleu 1.000
                he's calm . => il est bon ., bleu 0.658
                i'm home . => je suis chez moi ., bleu 1.000

2.2自注意力(self-attention)

       每个查询都会关注所有的键-值对并生成一个注意力输出。 由于查询、键和值来自同一组输入,因此被称为 自注意力(self-attention)。

        [论文出处]:Lin.Feng.Santos.ea.2017,Vaswani.Shazeer.Parmar.ea.2017

       比较了卷积神经网络(CNN)、循环神经网络(RNN)和自注意力(self-attention)。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,使用自注意力来设计深度架构是很有吸引力的。

2.3位置编码

        在处理词元序列时,循环神经网络是逐个的重复地处理词元的, 而自注意力则因为并行计算而放弃了顺序操作。注意力机制(如 Transformer)本身不关心序列中元素的顺序,它只看每个元素之间的相对关系(通过 query、key、value 计算)。但在自然语言处理、时间序列等任务中,顺序信息非常重要。

        位置编码(Positional Encoding)就是为了让模型“知道”每个元素在序列中的位置。位置编码会生成一个和输入特征同维度的向量,加到每个输入的特征向量上,这样每个位置的输入都带有唯一的“位置信息”。这样,注意力机制在处理时,既能看到内容信息,也能看到顺序信息。

        位置编码可以通过学习得到也可以直接固定得到。 接下来描述的是基于正弦函数和余弦函数的固定位置编码 :cite:Vaswani.Shazeer.Parmar.ea.2017

        位置编码的常见方式:

  • 正弦-余弦位置编码(Transformer原论文):用不同频率的正弦和余弦函数编码每个位置。
  • 可学习的位置编码:把每个位置当成一个参数,和词向量一样训练。

三. Transformer

        Transformer是编码器-解码器架构的一个实例,其整体架构图:

        Transformer模型纯基于自注意力机制的架构,没有任何卷积层或循环神经网络层。

        尽管Transformer最初是应用于在文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。 

3.1 多头注意力(multi-head attention)

        当给定相同的查询、键和值的集合时, 我们希望模型可以基于相同的注意力机制学习到不同的行为, 然后将不同的行为作为知识组合起来, 捕获序列内各种范围的依赖关系 (例如,短距离依赖和长距离依赖关系)。 因此,允许注意力机制组合使用查询、键和值的不同 子空间表示(representation subspaces)可能是有益的。

        与其只使用单独一个注意力汇聚, 我们可以用独立学习得到的h组不同的线性投影(linear projections)来变换查询、键和值。 然后,这组变换后的查询、键和值将并行地送到注意力汇聚中。 最后,将这h个注意力汇聚的输出拼接在一起, 并且通过另一个可以学习的线性投影进行变换, 以产生最终输出。 对于h个注意力汇聚输出,每一个注意力汇聚都被称作一个(head)。这种设计被称为多头注意力(multihead attention) 。

 3.2基于位置的前馈网络

        就是一个全连接层

3.3层规范化(LayerNorm)

        对比BN,不会受序列长度变化影响。还记得讲BN时说到,有很多N的方式。

  • 批量归一化对每个特征/通道里元素进行归一
  • 层归一化对每个样本里的元素进行归

        Transformer在编码器和解码器之间信息传递时,用的是一个正常的attentino,不是自注意力。编码器和解码器都有n个Transformer。

        接下去就可以去看Transformer代码是怎么组合的了。数据用的seq2seq。此处代码略。

        

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐