Attention机制最早在视觉领域提出,由 GoogleMind 2014年发表《Recurrent Models of Visual Attention》,使Attention机制流行起来,论文采用了RNN模型,并加入了Attention机制来进行图像的分类。

2005年,Bahdanau等人在论文《Neural Machine Translation by Jointly Learning to Align and Translate》中,将attention机制首次应用在nlp领域,其采用Seq2Seq+Attention模型来进行机器翻译,并且得到了效果的提升,Seq2Seq With Attention中进行了介绍。

2017 年,Google 机器翻译团队在NIPS 2017上发表的《Attention is All You Need》中,完全抛弃了RNN和CNN等网络结构,而仅仅采用Attention机制来进行机器翻译任务,并且取得了很好的效果,注意力机制也成为了大家近期的研究热点。

Transformer 是 Google 的团队在 2017 年基于 Attention机制 提出的一种 NLP 经典模型,现在比较火热的 Bert 也是基于Transformer。Transformer 模型采用 Self-Attention机制,抛弃了RNN顺序结构,使得模型可以并行化训练,而且能够拥有全局信息。

2018年,Google团队又将由 Self-Attention机制 构建的Transformer 迁移到CV领域,发表了《Image Transformer》,至此Transformer开始引爆CV圈。


注意力机制一种是 软注意力(Soft-Attention),另一种是 强注意力(Hard-Attention)

  • 软注意力的关键点在于,这种注意力更关注区域或通道,而且软注意力是确定性的注意力,学习完后可以直接通过网络生成,最重要的一点是,软注意力机制是可微分的,这是非常重要的,可以微分的注意力就可以通过神经网络算出梯度,并且前向传播和反向传播来学习得到的注意力的权重;
  • 强注意力 与软注意力不同点在于,强注意力更加关注点,也就是图像中的每个点都有可能延伸出注意力,同时强注意力是一个随机的预测过程,更强调动态变化,当然,最关键的是强注意力是一个不可微分的注意力,训练过程往往是通过增强学习来完成;

1、NLP Self-Attention

1.1 处理Sequence数据的模型:

Transformer是一个Sequence to Sequence model,特别之处在于它大量用到了self-attention。要处理一个Sequence,最常想到的就是使用RNN,它的输入是一串vector sequence,输出是另一串vector sequence,如下图1左所示。

如果假设是一个single directional的RNN,那当输出 b 4 b_4 b4 时,默认 a 1 , a 2 , a 3 , a 4 a_1,a_2,a_3,a_4 a1,a2,a3,a4 都已经看过了。如果假设是一个bi-directional的RNN,那当输出 b 任 意 b_{任意} b 时,默认 a 1 , a 2 , a 3 , a 4 a_1,a_2,a_3,a_4 a1,a2,a3,a4 都已经看过了。RNN非常擅长于处理input是一个sequence的状况。

那RNN有什么样的问题呢? 它的问题就在于:RNN很不容易并行化 (hard to parallel)。

为什么说RNN很不容易并行化呢?假设在single directional的RNN的情形下,你今天要算出 b 4 b_4 b4 ,就必须要先看 a 1 a_1 a1 再看 a 2 a_2 a2 再看 a 3 a_3 a3 再看 a 4 a_4 a4 ,所以这个过程很难平行处理。所以今天就有人提出把CNN拿来取代RNN,如下图1右所示。其中,橘色的三角形表示一个filter,每次扫过3个向量 a a a ,扫过一轮以后,就输出了一排结果,使用橘色的小圆点表示。这是第一个橘色的filter的过程,还有其他的filter,比如图2中的黄色的filter,它经历着与 橘色的filter 相似的过程,又输出一排结果,使用黄色的小圆点表示。

在这里插入图片描述

在这里插入图片描述

所以,用CNN,你确实也可以做到跟RNN的输入输出类似的关系,也可以做到输入是一个sequence,输出是另外一个sequence。

但是,表面上CNN和RNN可以做到相同的输入和输出,但是CNN只能考虑非常有限的内容。比如在我们右侧的图中CNN的filter只考虑了3个vector,不像RNN可以考虑之前的所有vector。但是CNN也不是没有办法考虑很长时间的dependency的,你只需要堆叠filter,多堆叠几层,上层的filter就可以考虑比较多的资讯,比如,第二层的filter (蓝色的三角形)看了6个vector,所以,只要叠很多层,就能够看很长时间的资讯。

而CNN的一个好处是:
它是可以并行化的 (can parallel),不需要等待红色的filter算完,再算黄色的filter。但是必须要叠很多层filter,才可以看到长时的资讯。所以今天有一个想法:self-attention,如下图3所示,目的是使用self-attention layer取代RNN所做的事情。

在这里插入图片描述

所以重点是:我们有一种新的layer,叫self-attention,它的输入和输出和RNN是一模一样的,输入一个sequence,输出一个sequence,它的每一个输出 b 1 − b 4 b_1 - b_4 b1b4都看过了整个的输入sequence,这一点与bi-directional RNN相同。但是神奇的地方是:它的每一个输出 b 1 − b 4 b_1 - b_4 b1b4可以并行化计算。


Attention

为了理解注意力的计算,我们可以将注意力的计算与数据库世界进行比较。当我们在数据库中进行搜索时,我们提交一个查询(Q),并在可用数据中搜索一个或多个满足查询的键。输出是与查询最相关的键关联的值。

注意力计算的情况非常相似。我们首先把要计算注意力的句子看作一组向量。每个单词,通过一个单词嵌入机制,被编码成一个向量。我们认为这些向量是搜索的关键,关于我们正在搜索的查询,它可以是来自同一个句子(自我注意)或来自另一个句子的单词。在这一点上,我们需要计算查询和每个可用键之间的相似性,通过缩放点积进行数学计算。这个过程将返回一系列实际值,可能彼此非常不同,但是由于我们希望获得0和1之间的权重,其和等于1,因此我们对结果应用SoftMax。一旦获得了权重,我们就必须将每个单词的权重以及它与查询的相关性乘以表示它的向量。我们最终返回这些产品的组合作为注意向量。

为了建立这种机制,我们使用 linear layers,从输入向量开始,通过矩阵乘法生成键、查询和值。键和查询的组合将允许在这两个集合之间获得最正确的匹配,其结果将与值组合以获得最相关的组合。

在这里插入图片描述


1.2 Self-attention:

那么self-attention具体是怎么做的呢?

在这里插入图片描述


首先假设我们的input是图4的 x 1 − x 4 x_1 - x_4 x1x4,是一个sequence,每一个input (vector)先乘上一个矩阵 W W W得到embedding,即向量 a 1 − a 4 a_1 - a_4 a1a4 。接着这个embedding进入self-attention层,每一个向量 a 1 − a 4 a_1 - a_4 a1a4分别乘上3个不同的transformation matrix W q , W k , W v W_q ,W_k ,W_v Wq,Wk,Wv ,以向量 a 1 a_1 a1为例,分别得到3个不同的向量 q 1 , k 1 , v 1 q_1 ,k_1 ,v_1 q1,k1,v1

在这里插入图片描述

接下来使用每个query q q q去对每个key k k k 做attention,attention就是匹配这2个向量有多接近,比如我现在要对 q 1 q^1 q1 k 2 k^2 k2做attention,我就可以把这2个向量做scaled inner product,得到 α 1 , 1 \alpha_{1,1} α1,1。接下来你再拿 q 1 q^1 q1 k 2 k^2 k2做attention,得到 α 1 , 2 \alpha_{1,2} α1,2,你再拿 q 1 q^1 q1 k 3 k^3 k3做attention,得到 α 1 , 3 \alpha_{1,3} α1,3,你再拿 1 1 1^1 11 k 4 k^4 k4做attention,得到 α 1 , 4 \alpha_{1,4} α1,4。那这个scaled inner product具体是怎么计算的呢?

α a , i = q 1 ⋅ k i / d (1) \alpha_{a,i} = q^1\cdot k^i / \sqrt d \tag1 αa,i=q1ki/d (1)

式中, d d d q q q k k k的维度。因为 q ⋅ k q\cdot k qk的数值会随着dimension的增大而增大,所以要除以 d i m e n s i o n \sqrt {dimension} dimension 的值,相当于归一化的效果。

接下来要做的事如图6所示,把计算得到的所有 α 1 , i \alpha_{1,i} α1,i 值取 softmax 操作。

在这里插入图片描述

取完 softmax 操作以后,我们得到了 α ^ 1 , i \widehat{\alpha}_{1,i} α 1,i ,我们用它和所有的 v i v^i vi 值进行相乘。具体来讲,把 α ^ 1 , 1 \widehat{\alpha}_{1,1} α 1,1乘上 v 1 v^1 v1,把 α ^ 1 , 2 \widehat{\alpha}_{1,2} α 1,2乘上 v 2 v^2 v2,把 α ^ 1 , 3 \widehat{\alpha}_{1,3} α 1,3乘上 v 3 v^3 v3 ,把 α ^ 1 , 4 \widehat{\alpha}_{1,4} α 1,4乘上 v 4 v^4 v4,把结果通通加起来得到 b 1 b^1 b1,所以,今天在产生 b 1 b^1 b1 的过程中用了整个sequence的资讯 (Considering the whole sequence)。

  • 如果要考虑local的information,则只需要学习出相应的 α ^ 1 , i = 0 \widehat{\alpha}_{1,i}=0 α 1,i=0 b 1 b^1 b1 就不再带有那个对应分支的信息了;
  • 如果要考虑global的information,则只需要学习出相应的 α ^ 1 , i ≠ 0 \widehat{\alpha}_{1,i} \neq 0 α 1,i=0 b 1 b^1 b1 就带有全部的对应分支的信息了。

在这里插入图片描述

同样的方法,也可以计算出 b 2 , b 3 , b 4 b^2,b^3,b^4 b2,b3,b4,如下图8所示, b 2 b^2 b2 就是拿query q 2 q^2 q2去对其他的 k k k 做attention,得到 α ^ 2 , i \widehat{\alpha}_{2,i} α 2,i,再与value值 v i v^i vi 相乘取weighted sum得到的。

在这里插入图片描述
经过了以上一连串计算,self-attention layer做的事情跟RNN是一样的,只是它可以并行的得到layer输出的结果,如图9所示。现在我们要用矩阵表示上述的计算过程。

在这里插入图片描述

首先输入的embedding是 I = [ a 1 , a 2 , a 3 , a 4 ] I=[a^1,a^2,a^3,a^4] I=[a1,a2,a3,a4] ,然后用 I I I 乘以transformation matrix W q W^q Wq 得到 Q = [ q 1 , q 2 , q 3 , q 4 ] Q=[q^1,q^2,q^3,q^4] Q=[q1,q2,q3,q4] ,它的每一列代表着一个vector q q q。同理,用 I I I 乘以 transformation matrix W k W^k Wk 得到 K = [ k 1 , k 2 , k 3 , k 4 ] K=[k^1,k^2,k^3,k^4] K=[k1,k2,k3,k4] ,它的每一列代表着一个vector k k k。用 I I I 乘以transformation matrix W v W^v Wv得到 Q = [ v 1 , v 2 , v 3 , v 4 ] Q=[v^1,v^2,v^3,v^4] Q=[v1,v2,v3,v4],它的每一列代表着一个vector v v v

在这里插入图片描述

接下来是 k k k q q q 的attention过程,我们可以把vector k k k 横过来变成行向量,与列向量 q q q 做内积,这里省略了 d \sqrt d d 。这样, α \alpha α 就成为了 4 × 4 4\times4 4×4 的矩阵,它由4个行向量拼成的矩阵和4个列向量拼成的矩阵做内积得到,如图11所示。

在得到 A ^ \widehat A A 以后,如上文所述,要得到 b 1 b^1 b1 , 就要使用 α ^ 1 , i \widehat{\alpha}_{1,i} α 1,i 分别与 b i b^i bi 相乘再求和得到,所以 A ^ \widehat A A 要再左乘 V V V 矩阵。

在这里插入图片描述

到这里你会发现这个过程可以被表示为,如图12所示:输入矩阵 I ∈ R ( d , N ) I \in R(d,N) IR(d,N) 分别乘上3个不同的矩阵 W q , W k , W v ∈ R ( d , d ) W_q,W_k,W_v \in R(d,d) Wq,Wk,WvR(d,d) 得到3个中间矩阵 Q , K , V ∈ R ( d , N ) Q,K,V \in R(d,N) Q,K,VR(d,N)。它们的维度是相同的。把 K K K转置之后与 Q Q Q 相乘得到Attention矩阵 A ∈ R ( N , N ) A \in R(N,N) AR(N,N) ,代表每一个位置两两之间的attention。再将它取 softmax 操作得到 A ^ ∈ R ( N , N ) \widehat{A} \in R(N,N) A R(N,N),最后将它乘以 V V V 矩阵得到输出vector O ∈ R ( d , N ) O \in R(d,N) OR(d,N)

A ^ = s o f t m a x ( A ) = K T ⋅ Q (2) \widehat{A} = softmax(A)=K^T \cdot Q \tag{2} A =softmax(A)=KTQ(2)
O = V ⋅ A ^ (3) O=V \cdot \widehat{A} \tag{3} O=VA (3)

在这里插入图片描述


1.3 Multi-head Self-attention:

但是,如果我们想把注意力集中在一个单词上,这个机制就足够了,但是如果我们想从几个角度看这个句子,然后并行计算几次注意力,会怎么样?我们使用所谓的多头注意力 Multi - Head Attention。

以2个head的情况为例:由 a i a^i ai 生成的 q i q^i qi 进一步乘以2个转移矩阵变为 q i , 1 q^{i,1} qi,1 q i , 2 q^{i,2} qi,2 ,同理由 a i a^i ai 生成的 k i k^i ki 进一步乘以2个转移矩阵变为 k i , 1 k^{i,1} ki,1 k i , 2 k^{i,2} ki,2 ,由 a i a^i ai生成的 v i v^i vi 进一步乘以2个转移矩阵变为 v i , 1 v^{i,1} vi,1 v i , 2 v^{i,2} vi,2 。接下来 q i , 1 q^{i,1} qi,1 再与 k i , 1 k^{i,1} ki,1 做attention,得到weighted sum的权重 α \alpha α ,再与 v i , 1 v^{i,1} vi,1 做weighted sum得到最终的 b i , 1 ( i = 1 , 2... , N ) b^{i,1}(i=1,2...,N) bi,1(i=1,2...,N) 。同理得到 b i , 2 ( i = 1 , 2... , N ) b^{i,2}(i=1,2...,N) bi,2(i=1,2...,N) 。现在我们有了 b i , 1 ( i = 1 , 2... , N ) ∈ R ( d , 1 ) b^{i,1}(i=1,2...,N) \in R(d,1) bi,1(i=1,2...,N)R(d,1) b i , 2 ( i = 1 , 2... , N ) ∈ R ( d , 1 ) b^{i,2}(i=1,2...,N) \in R(d,1) bi,2(i=1,2...,N)R(d,1) ,可以把它们concat起来,再通过一个transformation matrix调整维度,使之与刚才的 b i ( i = 1 , 2... , N ) ∈ R ( d , 1 ) b^i (i=1,2...,N) \in R(d,1) bi(i=1,2...,N)R(d,1) 维度一致(这步如图13所示)。

在这里插入图片描述

在这里插入图片描述

从下图14可以看到 Multi-Head Attention 包含多个 Self-Attention 层,首先将输入 X X X 分别传递到 2个不同的 Self-Attention 中,计算得到 2 个输出结果。得到2个输出矩阵之后,Multi-Head Attention 将它们拼接在一起 (Concat),然后传入一个Linear层,得到 Multi-Head Attention 最终的输出 Z Z Z。可以看到 Multi-Head Attention 输出的矩阵 Z Z Z 与其输入的矩阵 X X X 的维度是一样的。

在这里插入图片描述

这里有一组Multi-head Self-attention的解果,其中绿色部分是一组query和key,红色部分是另外一组query和key,可以发现绿色部分其实更关注global的信息,而红色部分其实更关注local的信息。

在这里插入图片描述


1.4 Positional Encoding:

以上是multi-head self-attention的原理,但是还有一个问题是:现在的self-attention中没有位置的信息,一个单词向量的“近在咫尺”位置的单词向量和“远在天涯”位置的单词向量效果是一样的,没有表示位置的信息(No position information in self attention)。所以你输入"A打了B"或者"B打了A"的效果其实是一样的,因为并没有考虑位置的信息。所以在self-attention原来的paper中,作者为了解决这个问题所做的事情是如下图16所示:

在这里插入图片描述

具体的做法是:给每一个位置规定一个表示位置信息的向量 e i e^i ei ,让它与 a i a^i ai 加在一起之后作为新的 a i a^i ai 参与后面的运算过程,但是这个向量 e i e^i ei 是由人工设定的,而不是神经网络学习出来的。每一个位置都有一个不同的 e i e^i ei

那到这里一个自然而然的问题是:
为什么是 e i e^i ei a i a^i ai 相加?为什么不是concatenate?加起来以后,原来表示位置的资讯不就混到 a i a^i ai里面去了吗?不就很难被找到了吗?

这里提供一种解答这个问题的思路:

如图15所示,我们先给每一个位置的 x i ∈ R ( d , 1 ) x^i \in R(d,1) xiR(d,1) append一个one-hot编码的向量 p i ∈ R ( d , 1 ) p^i \in R(d,1) piR(d,1),得到一个新的输入向量 x p i ∈ R ( d + N , 1 ) x^i_p \in R(d+N,1) xpiR(d+N,1) ,这个向量作为新的输入,乘以一个transformation matrix W = [ W I , W P ] ∈ R ( d , d + N ) W=[W^I,W^P] \in R(d,d+N) W=[WI,WP]R(d,d+N)。那么:

W ⋅ x p i = W I , W P ⋅ [ x i p i ] = W I ⋅ x i + W P ⋅ p i = a i + e i (4) W \cdot x^i_p = {W^I,W^P} \cdot \begin{bmatrix} x^i \\ p^i \end{bmatrix} = W^I \cdot x^i + W^P \cdot p^i = a^i + e^i \tag4 Wxpi=WI,WP[xipi]=WIxi+WPpi=ai+ei(4)

所以, e i e^i ei a i a^i ai 相加就等同于把原来的输入 x i x^i xi concat一个表示位置的独热编码 p i p^i pi,再做transformation。

这个与位置编码乘起来的矩阵 W P W^P WP 是手工设计的,如图17所示。

在这里插入图片描述

Transformer 中除了单词的 Embedding,还需要使用位置 Embedding 表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息,不能利用单词的顺序信息,而这部分信息对于 NLP 来说非常重要。所以 Transformer 中使用位置 Embedding 保存单词在序列中的相对或绝对位置。

位置 Embedding 用 PE表示,PE 的维度与单词 Embedding 是一样的。PE 可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,计算公式如下:

P E p o s , 2 i = s i n ( p o s / 1000 0 2 i / d m o d e l ) (5.1) PE_{pos,2i} = sin(pos/10000^{2i/d_model}) \tag{5.1} PEpos,2i=sin(pos/100002i/dmodel)(5.1)
P E p o s , 2 i + 1 = c o s ( p o s / 1000 0 2 i / d m o d e l ) (5.2) PE_{pos,2i+1} = cos(pos/10000^{2i/d_model}) \tag{5.2} PEpos,2i+1=cos(pos/100002i/dmodel)(5.2)

式中, p o s pos pos 表示token在sequence中的位置,例如第一个token “我” 的 p o s = 0 pos=0 pos=0

# 式(5)的位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, d_hid, n_position=200):
        super(PositionalEncoding, self).__init__()
        self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))     # Not a parameter

    def _get_sinusoid_encoding_table(self, n_position, d_hid):
        ''' Sinusoid position encoding table
        TODO: make it with torch instead of numpy
        '''

        def get_position_angle_vec(position):
            return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]

        sinusoid_table          = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
        sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  # dim 2i
        sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])  # dim 2i+1
        return torch.FloatTensor(sinusoid_table).unsqueeze(0)

    def forward(self, x):
        return x + self.pos_table[:, :x.size(1)].clone().detach()

i i i,或者准确意义上是 2 i 2i 2i 2 i + 1 2i+1 2i+1 表示了Positional Encoding的维度, i i i 的取值范围是: [ 0 , . . . , d m o d e l / 2 ] [0, ... , d_{model}/2] [0,...,dmodel/2] 。所以当 p o s pos pos 为1时,对应的Positional Encoding可以写成:

P E ( 1 ) = [ s i n ( 1 / 1000 0 0 / 512 ) , c o s ( 1 / 1000 0 0 / 512 ) , s i n ( 1 / 1000 0 2 / 512 ) , s i n ( 1 / 1000 0 2 / 512 ) , . . . ] PE(1)=[sin(1/10000^{0/512}) ,cos(1/10000^{0/512}) ,sin(1/10000^{2/512}) ,sin(1/10000^{2/512}) , ... ] PE(1)=[sin(1/100000/512),cos(1/100000/512),sin(1/100002/512),sin(1/100002/512),...]

式中, d m o d e l = 512 d_{model}=512 dmodel=512 。底数是10000。为什么要使用10000呢,这个就类似于玄学了,原论文中完全没有提啊,这里不得不说说论文的readability的问题,即便是很多高引的文章,最基本的内容都讨论不清楚,所以才出现像上面提问里的讨论,说实话这些论文还远远没有做到easy to follow。这里我给出一个假想: 1000 0 1 / 512 10000^{1/512} 100001/512 是一个比较接近1的数(1.018),如果用100000,则是1.023。这里只是猜想一下,其实大家应该完全可以使用另一个底数。

这个式子的好处是:

  • 每个位置有一个唯一的positional encoding。
  • 使 P E PE PE 能够适应比训练集里面所有句子更长的句子,假设训练集里面最长的句子是有 20 个单词,突然来了一个长度为 21 的句子,则使用公式计算的方法可以计算出第 21 位的 Embedding。
  • 可以让模型容易地计算出相对位置,对于固定长度的间距 k k k,任意位置的 P E p o s + k {PE}_{pos+k} PEpos+k 都可以被 P E p o s PE_{pos} PEpos 的线性函数表示,因为三角函数特性:

c o s ( α + β ) = c o s ( α ) c o s ( β ) − s i n ( α ) s i n ( β ) s i n ( α + β ) = s i n ( α ) c o s ( β ) + c o s ( α ) s i n ( β ) cos(\alpha + \beta) = cos(\alpha)cos(\beta)-sin(\alpha)sin(\beta) \\ sin(\alpha + \beta) = sin(\alpha)cos(\beta)+cos(\alpha)sin(\beta) cos(α+β)=cos(α)cos(β)sin(α)sin(β)sin(α+β)=sin(α)cos(β)+cos(α)sin(β)

接下来我们看看self-attention在 sequence2sequence model 里面是怎么使用的,我们可以把Encoder-Decoder中的 RNN 用self-attention取代掉。

在这里插入图片描述


2、Vision Self-Attention

由于卷积核作用的感受野是局部的,需要经过累积多层之后才能把图像不同部分的区域关联起来,所以在 CVPR2018 Hu J等人提出了 SENet,从特征通道层面上统计图像的全局信息。

Self-Attention 是从 NLP 中借鉴而来的思想,因此保留了 Query、key 和 Value等名称;
Feature maps 是由基本的深度卷积网络得到的特征图,如ResNet、VGG等,通常将 backbone 最后两个下采样层去除,使获得的特征图是输入图像的 1/8 大小。

在这里插入图片描述

Self-Attention 结构分为三个分支,分别是 query、key、value,计算分为三步:

  1. 将query和每个key进行相似度计算得到权重,常用的相似度计算有:点积、拼接、感受机等;
  2. 使用softmax函数对权重进行归一化;
  3. 将权重和相应的 value进行加权求和得到最后的 Attention
class Self_Attn(nn.Module):
    """ Self attention Layer"""

    def __init__(self, in_dim, activation):
        super(Self_Attn, self).__init__()
        self.chanel_in 	= in_dim
        self.activation = activation

        self.q_conv 	= nn.Conv2d(in_channels=in_dim, out_channels=in_dim // 8, kernel_size=(1,1))
        self.k_conv 	= nn.Conv2d(in_channels=in_dim, out_channels=in_dim // 8, kernel_size=(1,1))
        self.v_conv 	= nn.Conv2d(in_channels=in_dim, out_channels=in_dim     , kernel_size=(1,1))
        self.gamma      = nn.Parameter(torch.zeros(1))

        self.softmax    = nn.Softmax(dim=-1)

    def forward(self, x):
        """
            inputs :
                x : input feature maps( B x C x W x H)
            returns :
                out : self attention value + input feature
                attention: B x N x N (N is Width*Height)
        """
        m_batchsize, C, width, height = x.size()
        proj_query  = self.q_conv(x).view(m_batchsize, -1 , width * height).permute(0, 2, 1)     # B X C X (N)
        proj_key    = self.k_conv(x).view(m_batchsize, -1 , width * height)                      # B X C x (W*H)
        energy      = torch.bmm(proj_query, proj_key)       # transpose check
        attention   = self.softmax(energy)                  # B X (N) X (N)
        proj_value  = self.v_conv(x).view(m_batchsize, -1 , width * height)     # B X C X N

        out         = torch.bmm(proj_value, attention.permute(0, 2, 1))
        out         = out.view(m_batchsize, C, width, height)
        out         = self.gamma * out + x

        return out, attention

假设 feature maps 的大小为: B、C、W、H

在初始化函数中,定义了三个 1x1 卷积,分别是 query_conv、key_conv、value_conv

  • 在query_conv卷积中,输入为 (B、C、W、H) 输出为 (B、C/8、W、H)
  • 在key_conv卷积中,输入为 (B、C、W、H) 输出为 (B、C/8、W、H)
  • 在value_conv卷积中, 输入为(B、C、W、H) 输出为 (B、C、W、H)

步骤一

proj_query = self.query_conv(x).view(m_batchsize,-1,width*height).permute(0,2,1)

proj_query 本质上就是卷积,只不过加入了 reshape 操作;

  • 先对输入 feature maps 进行query_conv 卷积,输出为 (B、C/8、WH);
  • view函数调整输出的维度,
    就单张 feature map而言,就是将 WxH 大小拉直,变成 1x(WxH);
    就 batchsize 而言,输出就是 B、C/8、WH ;
  • permute函数对 第二维、第三维进行倒置,输出为 B、WH、C/8;

proj_query 中的第 i 行表示 第 i 个像素位置上所有通道的值;

proj_key =  self.key_conv(x).view(m_batchsize,-1,width*height)

proj_key 与 proj_query 相似,只是没有最后一步倒置,输出为 B、C/8、WH,proj_key 中的第 j 行表示第 j 个像素位置上所有通道的值;


步骤二

energy =  torch.bmm(proj_query,proj_key)

将 batch_size 中每一对 proj_query 和 proj_key 分别进行矩阵相乘,输出为 B、WH、WH ,Energy中的 第(i,j) 是将 proj_query 中的 第 i 行与 proj_key 中的第 j 行点乘而得。
意义在于: energy 中的 第 (i,j) 位置的元素是指输入特征图第 j 个元素对第 i 个元素的影响,从而实现全局上下文任意两个元素的依赖关系。


步骤三

attention = self.softmax(energy)

将 energy 进行 softmax 归一化,是对行归一化;归一化后每行的和为1,对于 (i,j) 位置即可理解为第 j 位置对 i 位置的权重,所有的 j 对 i 位置的权重之和为 1,此时得到 attention_map;

proj_value = self.value_conv(x).view(m_batchsize,-1,width*height)

proj_value 和 proj_query 与 proj_key 一样,只是输入为 B、C、W、H,输出为 B、C、WH,从 self-attention 结构图中可以知道 proj_value 是与 attention_map 进行矩阵相乘,即:

out = torch.bmm(proj_value,attention.permute(0,2,1) )
out = out.view(m_batchsize,C,width,height)

在对 proj_value 与attention_map 点乘之前,先对attention进行转置。这是由于attention中每一行的权重之和为1,是原特征图第j个位置对第i个位置的权重,将其转置之后,每一列之和为1;
proj_value的每一行与attention中的每一列点乘,将权重施加于proj_value上,输出为B×C×(W×H)。


步骤四

out = self.gamma*out + x

这一步是对 attention 之后的out进行加权,x是原始的特征图,将其叠加在原始特征图上。Gamma是经过学习得到的,初始gamma为0,输出即原始特征图,随着学习的深入,在原始特征图上增加了加权的attention,得到特征图中任意两个位置的全局依赖关系。


3、缺陷

在所有这些很好的解释之后,你可能会认为transformer是完美的,没有任何缺陷。很显然,它不是这样的,它的优点之一也是它的缺点,计算注意!

为了计算每个单词相对于所有其他单词的注意力,必须执行N²计算,即使部分可并行,仍然非常昂贵。有了这样的复杂性,试想一下,在一段几百字的文字上,多次计算注意力意味着什么。

从图形上你可以想象一个矩阵,它必须填充每个单词相对于其他单词的注意力值,这显然是有昂贵的成本。必须指出的是,通常在解码器上,可以计算隐藏的注意,避免计算查询词和所有后续词之间的注意。

在这里插入图片描述

有些人可能会争论,但如果transformer带来的许多好处都与注意力机制有关,那么我们真的需要上面提到的所有结构吗?但2017年的第一份谷歌大脑论文不是说“注意力就是你所需要的一切”吗?当然是合法的,但在2021年3月,谷歌研究人员再次发表了一篇题为“注意力不是你所需要的全部”的论文。那是什么意思?研究人员进行了实验,分析了在没有transformer任何其他组件的情况下进行的自我注意机制的行为,发现它以双指数速率收敛到秩1矩阵。这意味着这种机制本身实际上是无用的。那么为什么transformer如此强大呢?

这是由于减少矩阵秩的自我注意机制与transformer的另外两个组成部分跳跃连接和MLP之间的拉锯战。

在这里插入图片描述

第一种方法允许路径的分布多样化,避免获得所有相同的路径,这大大降低了矩阵被降为秩1的概率。MLP由于其非线性,因此能够提高生成矩阵的秩。相反,有人表明,规范化在避免这种自我注意机制的行为方面没有作用。因此,注意力不是你所需要的全部,而是transformer架构设法利用它的优势来取得令人印象深刻的结果。

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐