Transformer在谷歌2017年的论文 [1706.03762] Attention Is All You Need 中首次被提出,主要用于NLP(Natuarl Language Processing,自然语言处理)的各项任务。
后来在CV领域,研究者们基于Transformer架构开展了一些工作,证明了其在三个基本CV任务:分类,检测和分割,以及多传感器数据:图像和点云数据上的有效性。与CNN相比,视觉Transformer在多个benchmark中都取得了非常优秀的性能,
Attention Is All You Need
Transformer就是这篇论文提出的一种网络结构,只基于attention机制来描述输入和输出之间的全局依赖关系,完全避免使用卷积和递归。
递归模型(Recurrent Structure)通过输入和输出序列的符号位置进行因子计算,将位置与计算时间中的步骤对齐,会生成一系列隐藏状态h_t,作为前一个隐藏状态h_{t-1}和位置t的输入。这种固有序列的性质使训练并行化变得困难,因为VRAM限制了Batch Size。虽然目前有些工作通过因式分解技巧和条件计算提升了计算效率和模型性能,但是VRAM的限制依然存在。
Recurrent Structure 专门用来处理序列数据,序列数据包括文本和时间序列数据等。Recurrent Structure 结构的特点是在处理序列的每个元素时都会使用相同的神经网络模块,并且这些模块的输出,会作为下一个时刻的输入。RNN,也就是递归神经网络的基本单元会接收当前时间步的输入和前一时间步的隐藏状态,然后输出当前时间步的隐藏状态和输出[10]。
Recurrent Structure 结构的核心是循环单元,通常包含以下结构:
输入门:决定当前输入中有哪些信息需要更新到隐藏状态。
遗忘门:决定哪些信息需要从当前隐藏状态中丢弃。
输出门:决定当前隐藏状态中有哪些信息需要输出。
细胞状态:一个线性单元,负责在时间步之间传递信息
在一个经典RNN结构中,隐藏状态h_t在时间步t计算方式为:
其中:h_t是时间步t的隐藏状态,h_{t-1}是时间步t-1的隐藏状态,x_t是时间步t的输入,W_h和W_x是权重矩阵,b是bias,f是激活函数,如ReLU。
Architecture
序列转换模型大多使用了Encoder-decoder结构。Encoder将符号表示的输入序列(x_1, x_2,...,x_n)映射为一个连续表示的序列(z_1, z_2,...,z_n)。对于一个给定的z,Decoder以每次生成一个字符的方式输出序列(y_1, y_2,...,y_n)。这里的每一步,模型都是自回归的,也就是每次都会将之前生成的符号作为附加输入。Transformer就整体上依照这种结构,为Encoder和Decoder都使用堆叠的 Self-Attention 层以及逐点连接的 Fully-Connected 层。下图为 Transformer的结构,左侧和右侧分别代表Encoder和Decoder。
Encoder由6个完全相同的Layer堆叠而成,每层有2个Sublayer。第一层是 Multi-Head Self-Attention 机制,也就是上图左侧Multi-Head Attention的部分;第二层是一个简单的逐点全连接的前馈网络,也就是上图左侧Feed Forward的部分。两个Sublayer之后加入了残差连接,然后进行层归一化,即Add&Norm。每个Sublayer的输出为 LayerNorm[x + Sublayer(x)]。所有Sublayer都会产生维度为d_{model}=512的输出。
Decoder也由6个完全相同的Layer堆叠。除了每个Encoder中存在的两个Sublayer外,Decoder中还加入了一个Sublayer,用于对Encoder Stacks的输出执行 Multi-Head Attention操作。每个Sublayer之后也使用残差连接+归一化的操作。通过为Decoder中的Self-Attention加入Mask并结合输出偏置Offset,确保位置i只能依赖小于i的位置的已知输出。
残差连接(Residual Connection),也称跳跃连接(Skip Connection)或快捷连接(shortcut connection),通过在网络的某些层之间添加直接连接,旨在解决深层网络训练过程中的梯度消失或爆炸问题,使网络能够更深、更有效地训练。具体来说,残差连接将网络某一层的输出直接加到后面几层(通常是两层)的输出上。这样,在网络的反向传播过程中,梯度可以直接通过这些额外的连接路径流动,而不必只通过层与层之间的权重[10]。
统计学中的残差和误差是非常易混淆的两个概念。误差是衡量观测值和真实值之间的差距,残差是指预测值和观测值之间的差距。
Attention
注意力机制可以描述为将一个query和一组key-value映射到一个输出,其中query,key,value三者和输出均为向量。输出是values的加权求和。每个value的权重通过query和对应key的兼容性函数来计算。
Scaled Dot-Product Attention
论文中提及的注意力机制被称为 Scaled Dot-Product Attention ,意为缩放点积注意力机制。输入由Query,d_k维的Key和d_v维的Value组成,一般假设Q和K的维度相等。计算所有Query和Key的点积,再除以\sqrt{d_k},再通过softmax函数获取value的权重。实际Attention函数中,Query被转换为一个矩阵Q,Key和Value被转换为矩阵K和V。按照如下方式计算输出矩阵:
两个最常用的注意力函数是加法注意力和点积注意力函数。点积Attention在Transformer中的特点是增加了Key的维度——\sqrt{d_k}缩放系数。加法Attention使用一个具有单隐藏层的前馈神经网络来计算兼容性函数。两个Attention时间复杂度相似。但是由于点积注意力可以使用高度优化(如CUDA)的矩阵乘法实现,其速度更快而且空间效率更高。
当\sqrt{d_k}较小时,两个Attention表现相近,当\sqrt{d_k}较大时,加法Attention表现优于点积Attention。作者认为大的\sqrt{d_k}会将让QK^t函数呈数量级增长,从而将softmax函数推向具有极小梯度的区域。为了抵消这种影响,他们为点积乘上了一个\cfrac{1}{\sqrt{d_k}}系数。
关于点积变大的原因,假定q和k的组成都是独立且随机,均值为0且方差为1。两者的点积q\cdot k = \sum_{i=1}^{d_k}q_ik_i具有0均值和方差\sqrt{d_k}[1]。作者并未在后续给出更细节的证明,因此我阅读了其他文章。先回顾Var方差和Cov协方差的公式[12]并进行方差为\sqrt{d_k}的推导。原文见 proof.pdf
对于随机变量X和Y,两者乘积的均值可以写作E[XY]=E[E[XY|Y]]=E[Y\cdot E[X|Y]]。当X和Y互相独立时,E[X|Y] = E[X],前面公式可以简化为E[XY]=E[X]\cdot E[Y]
两者乘积的方差可以写作var[XY]=E[X^2Y^2]-E[XY]^2,根据公式1和2,可以改写为E[X^2Y^2]=cov[X^2,Y^2]+E[X^2]E[Y^2]=cov[X^2,Y^2]+(E[X]^2+var[X])\cdot (E[Y]^2+var[Y])
同时E[XY]^2=(cov[X,Y]+E[X]E[Y])^2,可以得到var[XY]=E[X]^2var[Y]+E[Y]^2var[X]+var[X]var[Y],由于是0均值的两个变量,因此最终得到var[XY]=var[X]var[Y]
在这里,对均值为0和方差为1的Query q_i和Key k_i:
当Key维度升高时,使得注意力的整体方差,也就是上面求的var[q \cdot k]会变大,进而出现极大值,让softmax梯度消失。因为当为softmax输入一个非常大的值时,该值softmax输出接近1,其他值就会接近0。这种情况下,softmax对大部分输入的结果会接近0,这种现象也就是梯度消失。在维度升高时,\sqrt{d_k}可以有效维持方差大小。至于为什么选择使用\sqrt{d_k}?由var[aX]=a^2var[X]可知,在softmax之前除以\sqrt{d_k}可以将点积的方差维持在1附近。
Multi-Head Attention(MHA)
作者发现,相比使用d_{model}维度的Query和Key-Value(以后用QKV统称三者),使用不同的、学习的线性投影把QKV投影到d_k, d_k, d_v维h次是有益的。在QKV的每个映射版本上,并行地执行Attention函数,生成d_v维的输出。然后将输出拼接起来并再次映射,生成一个最终值,如上图右所示。
MHA允许模型把不同位置的子序列的表征都整合到一个信息中。如果只有一个Attention Head,使用平均值的方式会削弱这些信息。
其中,映射是参数矩阵,W_i^Q \in \mathbb{R}^{d_{model} \times d_k},W_i^K \in \mathbb{R}^{d_{model} \times d_k},W_i^V \in \mathbb{R}^{d_{model} \times d_v},W^O \in \mathbb{R}^{hd_{v} \times d_{model}}。
论文中采用h=8个并行Attention。对于每个head,使用d_k=d_v=d_{model}/h=64。由于每个head尺寸减小,计算成本和具有全部维度的单个Attention head相似。
Attention In Transformer
在Transformer中,MHA有三种使用方式
在Encoder->Decoder层中,Query来自前面的Decoder层,Key和Value来自Encoder的输出。这使得Decoder中的每个位置都能关注到输入序列的所有位置。这是模仿序列到序列模型中典型的Encoder-Decoder的Attention机制。
Encoder包含Self-Attention层。在Self-Attention中,所有QKV来自同一个地方,即Encoder前一层的输出。Encoder中的每个位置都可以关注到Encoder上一层的所有位置。
Decoder中的Self-Attention层允许Decoder中的每个位置都关注Decoder层中当前位置及其之前的所有位置。为了保持Decoder的自回归特性(对于每个时间步t,Decoder仅利用:从开始到时间步t-1历史信息和Encoder输出,在一定程度上自己预测自己),需要防止Decoder信息向左流动。在scaled dot-product attention内部屏蔽softmax输入中的所有非法连接值,实际操作中设置为-\infin。
为了避免在t时间步看到之后时间的东西。假设Query和Key等长,且在时间上对应。对于第t时刻的Q_t,只算K_1 \rarr K_{t-1},不应看到到K_t及其之后的信息,因为此时没有K_t。但在Attention中,能够看到所有时间步的信息,Q_t会和所有K,即K_1 \rarr K_{t}做计算。通过加入Mask,将K_t及其后面的值全部换成很大的负数,使它们进入softmax中做指数项的时候变成0。加了Mask之后,后面的部分在经过softmax函数之后,只有前面部分出效果,后面变成0。因此,在计算输出时,只用了V对应的V1 \rarr V_{t-1},后面的忽略。
Position-Wise Feed-Forward Network(FFN)
Encoder和Decoder中的每个层还包含一个全连接的前馈网络。这个网络包含两个线性变换,中间有一个ReLU激活函数。
原始ReLU函数为ReLU=max(0,x) 。
线性变换在不同层使用不同的参数。其另一种描述方式是两个Kernel大小为1的卷积。输入和输出的维度d_{model}=512,内部层维度d_{ff}=2048。
Embedding & Softmax
和其它Seq2Seq模型类似,Transformer使用学习到的嵌入词向量(Embedding),将输入和输出转换为d_{model}=512的向量。还使用普通的线性变换和softmax函数将Decoder输出转换为预测的下一个词的概率。在Transformer中,两个嵌入层之间和pre-softmax线性变换共享相同的权重矩阵。在嵌入层中,把权重乘以\sqrt{d_{model}}。
Positional Encoding(PE)
位置编码。由于Transformer不包含循环和卷积,为了让模型利用Seq的顺序信息,必须加入Seq中关于字符相对或绝对位置的一些信息。因此在Encoder和Decoder Stack的底部的Input Embedding中。位置编码和Embedding具有相同维度d_{model},因此二者可以直接相加。位置编码有多种选择,比如学习的,和固定的。
在Transformer中,使用了不同频率的sin和cos:
此处的pos为位置,i是维度。也就是说PE的每个维度对应一个正弦曲线。波长为2\pi到10000。之所以选择这个函数是假设它允许模型学习,通过相对位置进行关注。因为对于任何固定的偏移量k,PE_{pos+k}都可以被表示为PE_{pos}的线性函数。
Why Self-Attention
考虑到三个方面。
每层计算的总复杂度。
可以并行化的计算量,以所需最小Seq操作数衡量。
网络中长距离依赖关系间的路径长度。在很多Seq转换任务中,如何学习长距离依赖性是一个很大的挑战。影响这种情况下学习的因素之一是网络中向前和向后的信号必须经过这个路径的长度。输入和输出序列中任意位置组合之间的路径越短,越容易学习长距离依赖。
下面表格展示了每层的计算时间复杂度,序列操作的时间复杂度,以及最大路径长度。
知识补充
关于输入[2]
Input Embedding:可以通过Word2vec等模型预训练得到,可以在Transformer中加入Embedding层。
Positional Encoding:用于表示单词出现在句子中的位置。Transformer不采用RNN结构,而是使用全局信息,不能利用单词的顺序信息,而这部分信息对于NLP来说非常重要。所以Transformer中使用Positional Encoding保存单词在序列中的相对或绝对位置。PE可以通过训练也可以通过公式计算得到。Transformer中使用的公式已在上方列出。
Q/K/V的理解
Q
,K
,V
是由输入的词向量x
经过线性变换得到的,其中各个矩阵W
可以经过学习得到, 这种变换可以提升模型的拟合能力, 得到的Q
,K
,V
可以理解为Q
: 要查询的信息K
: 被查询的向量V
: 查询得到的值
首先Q
、K
、V
都源于输入特征本身。文章[7]举了一个和形象的例子帮助理解:
有一个渣男,有N个备胎,想从备胎中选择最符合自己期望的一个。
Q
表示渣男对备胎的要求K
表示备胎眼中渣男的条件,备胎也会看渣男是否满足自己要求V
表示匹配的结果
回到QKV上,就是说渣男的Q和每个备胎的K之间的相似度越高越好。dot-product就是干这个的。
QK^t就是在计算Q和每一行K点积的结果。最终softmax得到归一化权重\alpha。
然后利用权重对V进行加权。
渣男就知道该对谁付出更多注意力了。
另一篇文章[16]也提出了一种理解方式:
假设目前有个新问题Q:今天是否会下雨?
在所有K中寻找最接近Q的K,就是计算相似度的过程。训练的过程就是在训练Q和K 的相似度矩阵。
关于QK矩阵乘法
可以理解为两者的相似度,也可以理解为两者的注意力分数,除以\sqrt{d_k}并softmax后归一化的注意力分数便是V的权重。
当V与权重矩阵相乘的时,实际上就是对值矩阵以对应的权重系数进行特征提取和融合的过程[4]。下面这张图说明了Attention的计算过程:
矩阵乘法的本质可以看一下这篇公众号文章 5分钟搞懂矩阵乘法的本质 ,说得很直观。
关于Attention[10]
核心逻辑就是「从关注全部到关注重点」。
注意力机制能够为输入序列的不同部分分配不同的权重,这样模型就可以更加关注对当前任务更重要的信息。
在处理序列的每个元素时,注意力机制会考虑当前元素与之前处理过的元素之间的相关性,从而动态地调整关注点。
组成部分:
查询(Query):当前要生成的目标元素或需要关注的部分。
键(Key):用于与查询进行比较的输入序列中的元素。
值(Value):输入序列中的元素,其信息将被用来生成输出。
计算过程:
相似度计算:计算Q和K之间的相似度或相关性。
权重计算:将相似度通过softmax函数转换为概率分布,这些概率分布就是分配给每个值的权重。
加权求和:将权重与对应的Value相乘并求和,得到最终注意力输出,这个输出是输入序列的加权和,反应模型当前关注的重点。
注意力机制类型:
软注意力(Soft Attention):可学习的注意力机制,可以为每个输入元素分配不同的权重,并且是可微分的,因此可以通过梯度下降进行训练。
硬注意力(Hard Attention):选择性地关注输入序列中的某个部分,但通常是不可微分的,因此需要使用如强化学习这样的技术来训练。
自注意力(Self-Attention):一种特殊的注意力机制,其中查询、键和值都来自同一个输入序列,这在Transformer模型中得到了广泛应用。
关于Scaled Dot-Product Attention
基本思想是,对于查询(Query)和一系列键值对(Key-Value)的集合,通过计算Q与每个K的点积,并利用softmax函数转换这些点积为概率分布,以此来确定每个值的重要性,最终加权求和得到输出。
缩放点积注意力的理论支撑基于以下几点[15]:
信息检索: 点积可以视为衡量两个向量相似度的一种方式,值越大表示相关性越高。
注意力分配: Softmax函数确保了加权系数之和为1,实现了注意力的归一化分配。
维度缩放: 缩放因子的引入是基于对数线性关系的考虑,使得注意力分数更加平滑且易于优化。
MHA代码实现[15]
from math import sqrt
import torch
import torch.nn as nn
class MultiHeadSelfAttention(nn.Module):
dim_in: int # input dimension
dim_k: int # key and query dimension
dim_v: int # value dimension
num_heads: int # number of heads, for each head, dim_* = dim_* // num_heads
def __init__(self, dim_in, dim_k, dim_v, num_heads=8):
super(MultiHeadSelfAttention, self).__init__()
#维度必须能被num_head 整除
assert dim_k % num_heads == 0 and dim_v % num_heads == 0, "dim_k and dim_v must be multiple of num_heads"
self.dim_in = dim_in
self.dim_k = dim_k
self.dim_v = dim_v
self.num_heads = num_heads
#定义线性变换矩阵
self.linear_q = nn.Linear(dim_in, dim_k, bias=False)
self.linear_k = nn.Linear(dim_in, dim_k, bias=False)
self.linear_v = nn.Linear(dim_in, dim_v, bias=False)
self._norm_fact = 1 / sqrt(dim_k // num_heads)
def forward(self, x):
# x: tensor of shape (batch, n, dim_in)
batch, n, dim_in = x.shape
assert dim_in == self.dim_in
nh = self.num_heads
dk = self.dim_k // nh # dim_k of each head
dv = self.dim_v // nh # dim_v of each head
q = self.linear_q(x).reshape(batch, n, nh, dk).transpose(1, 2) # (batch, nh, n, dk)
k = self.linear_k(x).reshape(batch, n, nh, dk).transpose(1, 2) # (batch, nh, n, dk)
v = self.linear_v(x).reshape(batch, n, nh, dv).transpose(1, 2) # (batch, nh, n, dv)
dist = torch.matmul(q, k.transpose(2, 3)) * self._norm_fact # batch, nh, n, n
dist = torch.softmax(dist, dim=-1) # batch, nh, n, n
att = torch.matmul(dist, v) # batch, nh, n, dv
att = att.transpose(1, 2).reshape(batch, n, self.dim_v) # batch, n, dim_v
return att
代码分析
class MultiHeadSelfAttention(nn.Module):
dim_in: int # input dimension
dim_k: int # key and query dimension
dim_v: int # value dimension
num_heads: int # number of heads, for each head, dim_* = dim_* // num_heads
继承Pytorch的nn基类,该基类是所有神经网络模块的基类。并创建一个名为MultiHeadSelfAttention
的类。
声明数据输入的维度,Q-K和V的维度,以及Attention头的数量。对于K和V的维度,应为dim_v/num_heads
和dim_k/num_heads
。这些变量都应为int
型。
def __init__(self, dim_in, dim_k, dim_v, num_heads=8):
super(MultiHeadSelfAttention, self).__init__()
调用父类nn的__init__
方法来初始化基类。
#维度必须能被num_head 整除
assert dim_k % num_heads == 0 and dim_v % num_heads == 0, "dim_k and dim_v must be multiple of num_heads"
assert
语句确保Q-K和V的维度能够被num_heads
整除。每个head处理相同大小的向量。
self.dim_in = dim_in
self.dim_k = dim_k
self.dim_v = dim_v
self.num_heads = num_heads
输入参数保存为实例的变量。
#定义线性变换矩阵
self.linear_q = nn.Linear(dim_in, dim_k, bias=False)
self.linear_k = nn.Linear(dim_in, dim_k, bias=False)
self.linear_v = nn.Linear(dim_in, dim_v, bias=False)
self._norm_fact = 1 / sqrt(dim_k // num_heads)
定义了3个线性层,用于将输入序列转换为Q, K, V向量。bias
表示不加入偏置。nn.Linear
函数用于构建线性层,能够实现y=Ax+b
线性变换。A
是权重矩阵,b
是偏置,当然这里没有b
。所以这个函数创建了一个dim_in * dim_k
的权重矩阵。把输入x
映射到输出空间。
然后定义了一个缩放因子。_norm_fact
也就是论文中的\sqrt{d_k}。
def forward(self, x):
# x: tensor of shape (batch, n, dim_in)
batch, n, dim_in = x.shape
assert dim_in == self.dim_in
forward
函数接收一个tensor x
,形状为(batch, n, dim_in)
其中batch
是批大小,n
是序列长度,dim_in
是输入维度。
nh = self.num_heads
dk = self.dim_k // nh # dim_k of each head
dv = self.dim_v // nh # dim_v of each head
q = self.linear_q(x).reshape(batch, n, nh, dk).transpose(1, 2) # (batch, nh, n, dk)
k = self.linear_k(x).reshape(batch, n, nh, dk).transpose(1, 2) # (batch, nh, n, dk)
v = self.linear_v(x).reshape(batch, n, nh, dv).transpose(1, 2) # (batch, nh, n, dv)
nh
是head的数量,dk
是dim_k/num_head
,dv是dim_v/num_head
。
然后输入x
通过三个线性层分别对Q, K, V向量进行转换,每个线性层的输出都被reshape
为(batch, n, nh, dk)
的尺寸,也就是将3维tensort重新排列为4维tensort,把x
分配给了nh
个heads。然后通过transpose
方法交换了第1和第2个维度(最前面是0维),让新的tensort形状变成了(batch, nh, n, dk)
。此处的目的是确保每个head的q
和k
可以并行的进行矩阵乘操作。
dist = torch.matmul(q, k.transpose(2, 3)) * self._norm_fact # batch, nh, n, n
dist = torch.softmax(dist, dim=-1) # batch, nh, n, n
计算Q和K的点积,通过softmax归一化后得到Attention权重。
att = torch.matmul(dist, v) # batch, nh, n, dv
att = att.transpose(1, 2).reshape(batch, n, self.dim_v) # batch, n, dim_v
return att
使用Attention权重,通过matmul
函数对V进行加权求和(矩阵乘)。得到每个head的输出。最后将多头输出的att
第2和第3维度进行转置并reshape为(batch, n, self.dim_v)
,得到最终Attention输出。
参考文章
[1] [1706.03762] Attention Is All You Need
[2] 一文了解Transformer全貌(图解Transformer)
[3] 详解Transformer中Self-Attention以及Multi-Head Attention_transformer multi head-CSDN博客
[4] 《Attention is all you need》通俗解读,彻底理解版:注意力机制的运算-CSDN博客
[5] 论文解读:Attention is All you need - 知乎
[6] Transformer(一)--论文翻译:Attention Is All You Need 中文版-CSDN博客
[7] transformer中QKV的通俗理解(渣男与备胎的故事)-CSDN博客
[8] 最新综述!一文详解视觉Transformer在CV中的现状、趋势和未来方向(分类/检测/分割/多传感器融合) - 知乎
[9] [2010.11929] An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale
[10] 论文精读与复现:大名鼎鼎的Attention is all you need(上)_in this work, we presented the transformer, the fi-CSDN博客
[11] transformer中的attention为什么scaled? - 知乎
[13]5分钟搞懂矩阵乘法的本质
[14] 【深度学习】Multi-Head Attention 原理与代码实现_multihead attention代码-CSDN博客
[15] 深度探索:机器学习中的缩放点积注意力(Scaled Dot-Product Attention)原理及应用-CSDN博客
[16]