深度学习之注意力机制(Attention)、自注意力机制(self-attention)、多头注意力机制(MultiHeadAttention)

前言

本篇文章讲解一下注意力机制相关的内容,这几年大火的Transformer都离不开注意力机制这个内容。那么什么是注意力机制,理解起来很容易,比如有很多个物品,我们最选择性的关心重要的特征来发现我们需要的东西。我们之前使用的CNN之类的卷积,并没有这种抓重点特征的功能。


一、self-Attention

自注意力机制的基本思想是,在处理序列数据时,每个元素都可以与序列中的其他元素建立关联,而不仅仅是依赖于相邻位置的元素。它通过计算元素之间的相对重要性来自适应地捕捉元素之间的长程依赖关系。这里就像循环神经网络RNN产生的可以获取之前和之后的信息。

1.self-attention

自注意力是自己注意自己的数据,并没有前验数据。比如这里是个seq2seq的问答问题,这里面的a都是编码后的单词或者汉字。通过不同的矩阵Wq,Wk,Wv的到q(query),k(key),v(value)。每个汉字或单词都对应一个q,k,v。可以理解成q可以用来帮助寻找与自己有关系和有依赖的其他单词汉字,而k代表其他汉字单词的特征,通过q和k确定该汉字和其他汉字的依赖关系的权重,是否有密切的关系,得出注意力权重。通过注意力权重乘value值,就得到了新的特征。
self-attention 在Attention Is All You need里提出,其中多头注意力机制是Transformer的一部分。实现self-attention的方法有很多,我们先说论文里的
Scaled Dot-product Attention。
每个ai对应一个编码后的字,通过矩阵生成q,k,v。因为q和v后面需要进行点积操作,所以要控制大小长度一样,v的大小可以和q,k不同。

在这里插入图片描述
每个qi和句子中所有的kj进下面公司的运算得到注意分数,也就是给vj和ai的相关性打分。
在这里插入图片描述

注意力分数还要经过softmax函数生成注意力权重,其总和为1,控制范围。得出注意力权重之后,对应权重与对应value相乘然后求和,就得到了在原本向量的基础上并包括其他向量关系的新特征。这样的特征训练会有更好的效果。

在这里插入图片描述

在这里插入图片描述

2.自注意力机制优点

1.提高模型对输入序列中不同位置的关注程度。自注意力机制允许模型在处理序列数据时,根据输入序列中不同位置的信息来调整注意力的权重,从而更好地捕捉序列中不同位置的重要信息。
2.实现长距离依赖建模。传统的注意力机制通常只关注输入序列中相邻的位置,而自注意力机制可以通过学习位置之间的关联性,实现对任意两个位置之间的依赖关系建模,从而更好地处理长距离依赖问题。
3.提高模型的泛化能力。自注意力机制能够自动学习输入序列中不同位置的相关性,从而减少了人工设计特征的需要,提高了模型的泛化能力。
4.支持并行计算。自注意力机制可以对输入序列中的不同位置进行并行计算,从而加速模型的训练和推理过程。

3.自注意力机制实现(pytorch)

#slef-attention可以理解成提取上下文关系进行编码,得出了新的特征表示
class SelfAttention(nn.Module):
    def __init__(self,dim,dqk,dv):
        super(SelfAttention,self).__init__()
        self.query = nn.Linear(dim, dqk)
        self.key = nn.Linear(dim, dqk)
        self.value = nn.Linear(dim, dv)

    def forward(self,x):
        q = self.query(x)
        k = self.key(x)
        v = self.value(x)
        # q,k,v 的shape都是(batch_size, nums_steps, dqk或dv)
        attn_weights = torch.matmul(q, k.transpose(1, 2))   #计算注意力分数
        attn_weights = nn.functional.softmax(attn_weights, dim=-1)  # 利用softmax函数得出归一化权重
        attended_values = torch.matmul(attn_weights, v) #求解出最后的特征
        return attended_values  #这里的特征维度是(batch_size,num_steps ,dv)

#得出自注意力特征后就可以进行分类了
class Selfattention_Classfier(nn.Module):
    def __init__(self,dim,dqk,dv,hidden_size,output_size):
        super(Selfattention_Classfier,self).__init__()
        self.attention = SelfAttention(dim,dqk,dv)  #用了上面定义的模块
        self.fc1 = nn.Linear(dv, hidden_size)
        self.fc2 = nn.Linear(hidden_size,output_size)

    def forward(self,x):
        x = self.attention(x)
        x = self.fc1(x)
        x = torch.relu(x)
        x= self.fc2(x)
        return x


二、MutilHead Attention(多头注意力机制)

多头注意力机制就是在注意力机制是进行改进,让其注意多个方面,因此就有了多组q,多组k和v。就如同下面的图一样,当然参数的数量随着着注意的头的数量成倍的增加。
在这里插入图片描述

1.手写代码实现

在多头注意力机制实现的过程中qi和ki的形状都是(batch_size,num_heads,num_seqs,qkdim)
对q和k进行矩阵运算求解注意力分数时,利用torch.matmul(q, k.transpose(-2, -1))和单注意力机制很像,这个矩阵乘法只是对后两维进行矩阵乘法,前面两位一一对应,生成(batch_size,num_heads,num_steps,num_steps)的注意力分数和v(batch_size,heads_num,num_steps,dv)乘法,最终生成(batch_size,heads_num.num_steps,dv)的新特征。

代码如下(示例):

class MultiHeadSelfAttention(nn.Module):
    def __init__(self,input_size, dqk, dv, num_heads):   #num_heads 是注意力多头的数目
        super(MultiHeadSelfAttention, self).__init__()
        self.num_heads = num_heads
        self.dqk = dqk
        self.dv = dv  #每个头对应那么多个参数q,k,v.
        #这里的q,k,v都是求解的多个头的q k v 后面要进行拆分
        self.query = nn.Linear(input_size, self.dqk * num_heads)
        self.key = nn.Linear(input_size,self.dqk * num_heads)
        self.value = nn.Linear(input_size, self.dv *num_heads)
        #self.fc = nn.Linear(embed_dim, embed_dim)

    def forward(self, x):
        batch_size, seq_len, total_dim = x.size()

        # 将输入向量拆分为多个头
        #经过下面几行代码 q k v 的形状都变成了 (batch_size , num_heads , seq_len , dqk/dv)
        q = self.query(x).view(batch_size, seq_len, self.num_heads, self.dqk).transpose(1, 2)
        k = self.key(x).view(batch_size, seq_len, self.num_heads, self.dqk).transpose(1, 2)
        v = self.value(x).view(batch_size, seq_len, self.num_heads, self.dv).transpose(1, 2)

        # 计算注意力分数和注意力权重
        # 这里先进行矩阵乘法
        # q的形状是 (batch_size , num_heads , seq_len , dqk)
        # k的形状是 (batch_size , num_heads , seq_len , dqk) 但是后两维会transpose
        # attn_scores的形状是 (batch_size , num_heads ,seq_len, seq_len)
        attn_scores = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.dqk, dtype=torch.float))
        attn_weights = torch.softmax(attn_scores, dim=-1)

        # 注意力加权求和
        # 这里注意力权重的形状是 (batch_size , num_heads , seq_len , seq_len )
        # v的形状是 (batch_size , num_heads , seq_len , dv)
        attended_values = torch.matmul(attn_weights, v).transpose(1, 2).contiguous().view(batch_size, seq_len, self.dv * self.num_heads) #这里把多头的注意力特征展开成一个整体进行运算

        # # 经过线性变换和残差连接
        # x = self.fc(attended_values) + x

        return attended_values
        
# 定义多头自注意力分类器模型
class MultiHeadSelfAttentionClassifier(nn.Module):

    def __init__(self, input_size, num_heads, hidden_dim ,dqk, dv,num_classes):
        super(MultiHeadSelfAttentionClassifier, self).__init__()
        self.attention = MultiHeadSelfAttention(input_size, dqk, dv, num_heads)
        self.flattern = nn.Flatten()        #展开成(batch_size, num_heads * seq_len *dv )
        self.fc1 = nn.Linear(num_heads * 250 * dv , hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, num_classes)

    def forward(self, x):

        x = self.attention(x)
        # 传进来的 attention_values 是 (batch_size , num_heads , seq_len , dv)
        x = self.flattern(x)
        x = self.fc1(x)
        x = torch.relu(x)
        x = self.fc2(x)
        return x

2.利用pytorch包装方法代码实现

接下来讲解一下使用torch.nn.MultilheadAttention()的方法,直接调包实现。
先说一下这个方法的参数有哪些
在这里插入图片描述
返回的就更简单了,返回我们生成的注意力特征和注意力权重。

class multiheadattention_calssfier(nn.Module):
    def __init__(self,input_size, dqk, dv, num_heads):  #这里的input_size是所有头的总和,dv,dqk也是如此,而且dqk和qv默认为input_size
        super(multiheadattention_calssfier,self).__init__()
        #batch_first 为 True 默认输出维度为 (batch_size , seq_len ,dim_featuere)
        self.mutil_layer = nn.MultiheadAttention(embed_dim = input_size , num_heads = num_heads,vdim = dv , kdim = dqk ,batch_first = True)
        self.query = nn.Linear(input_size, dqk)
        self.key = nn.Linear(input_size, dqk)
        self.value = nn.Linear(input_size, dv)

    def forward(self,x):
        q = self.query(x)
        k = self.key(x)
        v = self.value(x)
        output, attention_weights = self.mutil_layer(q,k,v) #这样就生成我们需要的注意力分数和注意力权重了
        return output


二、通道注意力机制SENet

SENet 是应用在卷积中的一种注意力机制。S和E分别是Squeeze 和Excitation。就比如正常的RGB图片是三通道的,每个通道有不同的意义,比如看一个整体偏红色的图片,显然红色通道更为重要,于是想着像前面的注意力机制一样,给每个通道算出权重来运算。
在这里插入图片描述
如图所示,数据X经过卷积操作得到了U,然后先进行squeeze模块操作,这里的squeeze是通过全局平均池化实现的,可以把(C,H,W)的数据压缩成(C,1,1),也就是每一个通道对应一个标量。然后经过FC全连接降低数据量,进行池化,最后再经过一个全连接层把其大小再变成C个,每个对应一个通道,即为对每个通道的权重分数。

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值