本文总结了当前注意力机制最为经典的三类模型,包括:自注意力机制(包括 多头注意力机制)、通道注意力机制和空间注意力机制,图片源于Prof. Hung-yi Lee的课件和相关原始paper。
1. 注意力机制的产生
深度学习中的注意力机制(Attention Mechanism)是一种模仿人类视觉和认知系统的方法,它允许神经网络在处理输入数据时集中注意力于相关的部分。通过引入注意力机制,神经网络能够自动地学习并选择性地关注输入中的重要信息,提高模型的性能和泛化能力。
以下这张图可以较好地去理解注意力机制,其展示了人类在看到一幅图像时如何高效分配有限注意力资源的,其中红色区域表明视觉系统更加关注的目标,从图中可以看出:人们会把注意力更多的投入到人的脸部。文本的标题以及文章的首句等位置。
注意力示例图
注意力机制从本质上讲和人类的选择性注意力机制类似,核心目标也是从众多信息中选出对当前任务目标更加关键的信息。深度学习中,注意力机制通常应用于序列数据(如文本、语音或图像序列)的处理。其中,最典型的注意力机制包括自注意力机制、空间注意力机制和时间注意力机制。这些注意力机制允许模型对输入序列的不同位置分配不同的权重,以便在处理每个序列元素时专注于最相关的部分。
2. 注意力机制
2.1 原理
本章主要介绍最原始的注意力机制,也被称为注意力分数。
起初的注意力机制将注意力汇聚的输出计算成为值的加权和。通过Query与Key的注意力汇聚(即,给定一个 Query,计算Query与 Key的相关性,然后根据Query与Key的相关性去和对应的Value进行相乘)实现对Value的注意力权重分配,生成最终的输出结果。
这样说可能有点生涩难懂,我们举一个简单的机器翻译的例子,如下图所示。
我们需要将中文的"我"翻译成英文的"me",这就需要"我"和"me"之间的注意力分数相对于"我"和其他英文单词的要高。在这里,我们先解释下 Query, Key,和Value 的含义:
Query:我们就可以将"我"看作成 Query,因为这就是我们当前需要查询的目标,即当前输入的特征表示。
Key:可以将每个单词的重要特征表示看作成 Key。
Value:每个单词本身的特征向量看作为 Value,一般和 Key成对出现,也就是我们常说的"键-值"对。
具体操作如下:
(1)先根据 Query,Key计算两者的相关性,然后再通过 softmax 函数得到 注意力分数,使用 softmax 函数是为了使得所有的注意力分数在 [0,1] 之间,并且和为1。这里的重点在于如何计算 Query,Key的相关性,这也是很多论文一个小的创新点所在。Query,Key的相关性公式一般表示如下:
score(q,ki)=softmax(α(q,ki))=exp(α(q,ki))∑1jexp(α(q,kj))
α(q,ki) 有很多变体,比如:加性注意力、缩放点积注意力等等。
在加性注意力中,主要是将 Query,Key分别乘以对应的可训练矩阵,然后进行相加,具体如下:
α(q,ki)=wvTtanh(Wqq+Wkk)
其中, ,Wq,Wk 分别是是 Query,Key对应的可训练矩阵, wvT 是 Value对应的可训练矩阵,是为了后面方便和 Value 进行相乘。
在缩放点积注意力中,主要是直接将Query,Key进行相乘,具体如下:
α(q,ki)=QKTd
从公式可以看出,这就需要 Query,Key的长度是一样的,都为 d ;为什么要除以 d ?下面我会讲到。
(2)根据注意力分数进行加权求和,得到带注意力分数的 Value,以方便进行下游任务。
Output=score(Q,K)V
在(1)中,我们得到了Query,Key的相关性,如果相关性越大,注意力分数就越高,反之越低;然后将注意力分数乘以对应的 Value,再进行加权求和;就比如:"我"和"me"的相关性较大,注意力分数就会越高;这样可以让下游任务理解"我"和"me"是匹配程度高。
我们可以反过来想,如果直接将不带注意力分数的 V 进行输入到下游任务,下游任务可能会认为所有单词的重要性程度都是一样的,或者随机将不相关的单词与 Query 进行匹配。
2.2 代码
加性注意力
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)
def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# print(queries.shape, keys.shape)
# 在维度扩展后,
# queries的形状:(batch_size,查询的个数,1,num_hidden)
# key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
# 使用广播方式进行求和
# print(queries.unsqueeze(2).shape, keys.unsqueeze(1).shape)
features = queries.unsqueeze(2) + keys.unsqueeze(1)
features = torch.tanh(features)
# print(features.shape)
# self.w_v仅有一个输出,因此从形状中移除最后那个维度。
# scores的形状:(batch_size,查询的个数,“键-值”对的个数)
print(self.w_v(features).shape)
scores = self.w_v(features).squeeze(-1)
print(scores.shape)
self.attention_weights = masked_softmax(scores, valid_lens)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
return torch.bmm(self.dropout(self.attention_weights), values)
3. 自注意力机制(Self-Attention Mechanism)
自注意力机制的基本思想是,在处理序列数据时,每个元素都可以与序列中的其他元素建立关联,而不仅仅是依赖于相邻位置的元素。它通过计算元素之间的相对重要性来自适应地捕捉元素之间的长程依赖关系。
具体而言,对于序列中的每个元素,自注意力机制计算其与其他元素之间的相似度,并将这些相似度归一化为注意力权重。然后,通过将每个元素与对应的注意力权重进行加权求和,可以得到自注意力机制的输出。
假设,我们有一个序列(或者说是一句话):"你好机车",分别使用 x1,x2,x3,x4 表示,如上图所示。
简单地说, 表示你表示好,表示机,表示车x1表示"你",x2表示"好",x3表示"机",x4表示"车" 。
2.1 Embedding 操作
首先,先对"你好机车"这段话进行Embedding操作(可以使用 Word2Vec等方法),得到新的向量, a1,a2,a3,a4 ;如下图所示。
Embedding 操作
其中, W 表示 Embedding 的参数矩阵。
2.2 q, k 操作
Embedding操作后, a1,a2,a3,a4 将会作为注意力机制的 input data。
第一步,每个 a1,a2,a3,a4 都会分别乘以三个矩阵,分别是 q,k,v ;需要注意的是,矩阵 q,k,v 在整个过程中是共享的;公式如下:
qi=Wqai
ki=Wkai
vi=Wvai
q, k, v 矩阵
其中, q (Query) 的含义一般的解释是用来和其他单词进行匹配,更准确地说是用来计算当前单词或字与其他的单词或字之间的关联或者关系; k (Key) 的含义则是被用来和 q 进行匹配,也可理解为单词或者字的关键信息。
如下图所示,若需要计算 a1 和 a2,a3,a4 之间的关系(或关联),则需要用 q1 和 k2,k3,k4 进行匹配计算,计算公式如下:
α1,i=q1⋅ki/d (当然这个公式并不是固定的,可以用各种方法来计算,大家也能在这方面做创新)
其中, d 表示 q 和 k 的矩阵维度,在 Self-Attention 中, q 和 k 的维度是一样的。这里除以 d 的原因是防q 和 k的点乘结果较大。
q, k, v 的计算
经过 q 和 k 的点乘操作后,会得到 α1,1,α1,2,α1,3,α1,4 ;然后,就是对其进行 softmax 操作,得到 α~1,1,α~1,2,α~1,3,α~1,4 。
softmax 操作
2.3 v 操作
v 的含义主要是表示当前单词或字的重要信息表示,也可以理解为单词的重要特征,例如: v1 代表"你"这个字的重要信息。在 v 操作中,会将 q,k 操作后得到的 α~1,1,α~1,2,α~1,3,α~1,4 和 v1,v2,v3,v4 分别相乘,如下图所示,计算公式如下:
b1=∑iα~1,ivi
v 操作
对应的,b2=∑iα~2,ivi ,以此类推。
v 操作
由此可见,自注意力机制通过计算序列中不同位置之间的相关性( q,k 操作),为每个位置分配一个权重,然后对序列进行加权求和( v 操作)。
2.4 代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 定义自注意力模块
class SelfAttention(nn.Module):
def __init__(self, embed_dim):
super(SelfAttention, self).__init__()
self.query = nn.Linear(embed_dim, embed_dim)
self.key = nn.Linear(embed_dim, embed_dim)
self.value = nn.Linear(embed_dim, embed_dim)
def forward(self, x):
q = self.query(x)
k = self.key(x)
v = self.value(x)
attn_weights = torch.matmul(q, k.transpose(1, 2))
attn_weights = nn.functional.softmax(attn_weights, dim=-1)
attended_values = torch.matmul(attn_weights, v)
return attended_values
# 定义自注意力分类器模型
class SelfAttentionClassifier(nn.Module):
def __init__(self, embed_dim, hidden_dim, num_classes):
super(SelfAttentionClassifier, self).__init__()
self.attention = SelfAttention(embed_dim)
self.fc1 = nn.Linear(embed_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
def forward(self, x):
attended_values = self.attention(x)
x = attended_values.mean(dim=1) # 对每个位置的向量求平均
x = self.fc1(x)
x = torch.relu(x)
x = self.fc2(x)
return x
4. 多头自注意力机制(Multi-head Self-Attention Machanism)
多头注意力机制是在自注意力机制的基础上发展起来的,是自注意力机制的变体,旨在增强模型的表达能力和泛化能力。它通过使用多个独立的注意力头,分别计算注意力权重,并将它们的结果进行拼接或加权求和,从而获得更丰富的表示。
在自注意力机制中,每个单词或者字都仅仅只有一个 q, k ,v 与其对应,如下图所示;
q^i = W^qa^i
单头注意力机制
多头注意力机制则是在 ai 乘以一个 q,k,v 后,会再分配多个 q,k,v ,这里以2个 q,k,v 为例,如下图所示;
d操作呢?
多头自注意力
4.1 q, k 操作
在多头注意力机制中, ai 会先乘 q 矩阵, qi=Wqai ;
其次,会为其多分配两个head,以 q 为例,包括: qi,1,qi,2 ;
qi,1=Wq,1qi
qi,2=Wq,2qi
同样地, k 和 v 也是一样的操作。
那么,下面就是 q 和 k 的点乘操作了,在多头注意力机制中,有多个q 和 k,究竟应该选择哪个进行操作呢?
其实很简单,就是看下标,如下图所示。 qi,1 会和 ki,1 和 kj,1 进行点乘,再进行 softmax 操作。
4.2 v 操作
多头注意力机制中 v 操作和自注意力机制操作是类似的,如上图所示;
4.3 代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 定义多头自注意力模块
class MultiHeadSelfAttention(nn.Module):
def __init__(self, embed_dim, num_heads):
super(MultiHeadSelfAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.query = nn.Linear(embed_dim, embed_dim)
self.key = nn.Linear(embed_dim, embed_dim)
self.value = nn.Linear(embed_dim, embed_dim)
self.fc = nn.Linear(embed_dim, embed_dim)
def forward(self, x):
batch_size, seq_len, embed_dim = x.size()
# 将输入向量拆分为多个头
q = self.query(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = self.key(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = self.value(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 计算注意力权重
attn_weights = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float))
attn_weights = torch.softmax(attn_weights, dim=-1)
# 注意力加权求和
attended_values = torch.matmul(attn_weights, v).transpose(1, 2).contiguous().view(batch_size, seq_len, embed_dim)
# 经过线性变换和残差连接
x = self.fc(attended_values) + x
return x
# 定义多头自注意力分类器模型
class MultiHeadSelfAttentionClassifier(nn.Module):
def __init__(self, embed_dim, num_heads, hidden_dim, num_classes):
super(MultiHeadSelfAttentionClassifier, self).__init__()
self.attention = MultiHeadSelfAttention(embed_dim, num_heads)
self.fc1 = nn.Linear(embed_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
def forward(self, x):
x = self.attention(x)
x = x.mean(dim=1) # 对每个位置的向量求平均
x = self.fc1(x)
x = torch.relu(x)
x = self.fc2(x)
return x
5. 通道注意力机制
顾名思义,通道注意力机制是通过计算每个通道channel的重要性程度;因此,常常被用在卷积神经网络里面。目前,比较经典的通道注意力机制方法就是SENet模型,SENet通过学习通道间的关系(每个通道的重要性),提升了网络在特征表示中的表达能力,进而提升了模型的性能。
5.1 SENet 介绍
SENet示例图
如上图所示,数据X经过卷积操作后,得到 U , U 的通道数用 C 表示, H×W 表示一个通道上的长和宽;此后,SENet引入了一个Squeeze模块 Fsq(⋅) 和一个Excitation模块 Fex(⋅,W) 。
Fsq(⋅) 通过全局平均池化操作将每个通道的特征图转化为一个标量值,简单地说,就是用全局平均池化将每个通道上的数据进行压缩,压缩成一个标量值,即得到一个 1×1×C 的矩阵。然后, Fex(⋅,W) 通过激活函数(如sigmoid或ReLU)对1×1×C 的矩阵进行操作, W 表示的就是激活函数,得到带有颜色的1×1×C 的矩阵,用来来学习每个通道的权重。最后,经过 Fscale(⋅,⋅) 将这些权重应用于原始特征图上,将带有颜色的1×1×C 的矩阵和 U进行点乘 ,以得到加权后的特征图。最后,将加权后的特征图输入到后续的卷积层进行分类或检测任务。
值得注意的是,SENet并不是一个单独的网络结构,而是可以与其他卷积神经网络结构(如ResNet、Inception等)相结合,以增强它们的表达能力。通过在现有网络结构中添加SENet模块,可以更容易地将SENet应用于现有的深度学习任务中。说的更加简答粗暴点,可以直接在每个卷积之后都可以添加SENet,当然这样也有可能会带来过拟合的问题。
5.2 代码
import torch
import torch.nn as nn
class SELayer(nn.Module):
def __init__(self, channel, reduction=16):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y
class SENet(nn.Module):
def __init__(self, num_classes=1000, reduction=16):
super(SENet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.se = SELayer(64, reduction=reduction)
self.fc = nn.Linear(64, num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.se(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
6. 空间注意力机制
空间注意力机制和通道注意力机制具有异曲同工之妙,通道注意力机制旨在捕捉通道的重要性的程度,空间注意力机制旨在通过引入注意力模块,使模型能够自适应地学习不同区域的注意力权重。这样,模型可以更加关注重要的图像区域,而忽略不重要的区域。其中,最为典型的是 CBAM(Convolutional Block Attention Module),CBAM 是一种结合了通道注意力和空间注意力的模型,旨在增强卷积神经网络对图像的关注能力。
6.1 CBAM 介绍
CBAM 示意图
从上图可以看出,CBAM模块由两个注意力模块组成:通道注意力模块(Channel Attention Module)和空间注意力模块(Spatial Attention Module)。
CBAM 通道注意力示意图
CBAM中通道注意力这边主要简单说一下,它使用全局平均池化和全局最大池化分别来获取每个通道的全局统计信息(SENet仅使用全局平均池化),并通过两层全连接层来学习通道的权重。然后,会将处理后产生的两个结果进行相加,通过使用Sigmoid函数将权重归一化到0到1之间,对每个通道进行缩放。最后,将缩放后的通道特征与原始特征相乘,以产生具有增强通道重要性的特征。
CBAM 空间注意力示意图
CBAM中空间注意力模块是使用最大池化和平均池化来获取每个空间位置的最大值和平均值。具体地说,由于卷积之后会产生多个通道,CBAM中空间注意力会在每一个特征点的通道上进行最大池化和平均池化操作,得到两个matrix后,将两个matrix进行拼接,并通过一个卷积层和Sigmoid函数来学习每个空间位置的权重。最后,将权重应用于特征图上的每个空间位置,以产生具有增强空间重要性的特征。
6.2 代码
import torch
import torch.nn as nn
class ChannelAttention(nn.Module):
def __init__(self, in_channels, reduction_ratio=16):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(in_channels, in_channels // reduction_ratio),
nn.ReLU(inplace=True),
nn.Linear(in_channels // reduction_ratio, in_channels)
)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * self.sigmoid(y)
class SpatialAttention(nn.Module):
def __init__(self):
super(SpatialAttention, self).__init__()
self.conv = nn.Conv2d(2, 1, kernel_size=7, padding=3)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
max_pool = torch.max(x, dim=1, keepdim=True)[0]
avg_pool = torch.mean(x, dim=1, keepdim=True)
y = torch.cat([max_pool, avg_pool], dim=1)
y = self.conv(y)
return x * self.sigmoid(y)
class CBAM(nn.Module):
def __init__(self, in_channels, reduction_ratio=16):
super(CBAM, self).__init__()
self.channel_attention = ChannelAttention(in_channels, reduction_ratio)
self.spatial_attention = SpatialAttention()
def forward(self, x):
x = self.channel_attention(x)
x = self.spatial_attention(x)
return x
Woo Tzins
24 次咨询
5.0
华东师范大学 计算机科学与技术学院博士在读
9337 次赞同
去咨询
参考文献:
[1] Vaswani A, Shazeer N, Parmar N, et al. Attention is all you need[J]. Advances in neural information processing systems, 2017, 30.
[2] Hu J, Shen L, Sun G. Squeeze-and-excitation networks[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2018: 7132-7141.
[3] Woo S, Park J, Lee J Y, et al. Cbam: Convolutional block attention module[C]//Proceedings of the European conference on computer vision (ECCV). 2018: 3-19.
[4] 李沐, et al. ,动手深度学习.