一、论文介绍
1.1 简介
卷积神经网络建立在卷积运算的基础上,通过在局部接受域内融合空间信息和通道信息来提取信息特征。在SeNet工作中,其专注于通道关系,并提出了一种新的架构单元,我们称之为“挤压-激励”(SE)块,该单元通过明确建模通道之间的相互依赖性,自适应地重新校准通道智能特征响应。挤压,模块可以看作是全连接层的通道压缩,激励模块可以看作是全连接的通道放缩。
现在一些SeNet的一些改进取消了全连接层的使用,改用一维卷积进行替代,从运算速度与计算效益上确实大有提高。
1.2 模型图
1.3 方法介绍
先看输入特征数据
其中代表卷积运算后输出
通道的特征数据,
代表
通道的输入数据,
代表
个输入通道为
的卷积核。
由于输出是通过所有通道的求和产生的,因此通道依赖关系隐式嵌入在中,但这些依赖关系与过滤器捕获的空间相关性纠缠在一起。全局注意力可以对整张特征图的每个像素点的关系进行建模,那么同样特征图通道之间也存在联系。SeNet通过显式建模通道相互依赖性来实现这一目标,以便在将卷积核响应馈送到下一个变换之前,分两个步骤(挤压和激励)重新校准滤卷积核响应。
压缩与激励模块
首先经过特征图的全局平均池化将特征图的空间维度进行压缩,。
从该公式中可看出主要的部分就是两个全连接层,进行通道的压缩,
进行通道的扩充,
代表激活函数ReLU,σ代表的是sigmoid函数,以下是sigmoid函数的图:
sigmoid的值域在[0-1]之间,你可以将之前的操作理解为:特征值较大的其概率值也比较大,也就是其相对比较重要。
sigmoid得出的概率值后又与原来的的特征值进行相乘,这一步的目的就是为了引导模型的训练。难道卷积就不能计算出哪里的特征值比较重要?当然可以,不过挤压与激励模块能引导模型的训练方向,不单单局限于特征图的局部信息,更加兼顾通道上重要信息。
二、代码
SeNet相对来说还是比较简单的,通过代码可以很好的理解,废话不多说上代码。
2.1 SEBlock
from torch import 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, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c) # 先pooling获取通道维度,也就是把H,w=>1
y = self.fc(y).view(b, c, 1, 1) # 全连接层对通道系数进行调整
# tensor_1.expand_as(tensor_2) :把tensor_1扩展成和tensor_2一样的形状
return x * y.expand_as(x) # 原值乘以对应系数
改代码与上面讲的一样,先全局池化,再经过全连接的压缩与扩充,主要的是Sigmoid()函数取概率值,最后与原特征值相乘即可。
2.2 SE_ResNet
import torch
import torch.nn as nn
from torch.hub import load_state_dict_from_url
from torchvision.models import ResNet
# 论文核心 SE Block, 这里称为 SE layer
class SELayer(nn.Module):
def __init__(self, channel, reduction=16):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1) # 全局平均池化,输入BCHW -> 输出 B*C*1*1
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False), # 可以看到channel得被reduction整除,否则可能出问题
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c) # 得到B*C*1*1,然后转成B*C,才能送入到FC层中。
y = self.fc(y).view(b, c, 1, 1) # 得到B*C的向量,C个值就表示C个通道的权重。把B*C变为B*C*1*1是为了与四维的x运算。
return x * y.expand_as(x) # 先把B*C*1*1变成B*C*H*W大小,其中每个通道上的H*W个值都相等。*表示对应位置相乘。
def conv3x3(in_planes, out_planes, stride=1):
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False)
class SEBasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None,
*, reduction=16):
# 参数列表里的 * 星号,标志着位置参数的就此终结,之后的那些参数,都只能以关键字形式来指定。
super(SEBasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes, 1)
self.bn2 = nn.BatchNorm2d(planes)
self.se = SELayer(planes, reduction)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.se(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class SEBottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None,
*, reduction=16):
# 参数列表里的 * 星号,标志着位置参数的就此终结,之后的那些参数,都只能以关键字形式来指定。
super(SEBottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.se = SELayer(planes * 4, reduction)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out = self.se(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
def se_resnet18(num_classes=1_000):
model = ResNet(SEBasicBlock, [2, 2, 2, 2], num_classes=num_classes)
model.avgpool = nn.AdaptiveAvgPool2d(1)
return model
def se_resnet34(num_classes=1_000):
model = ResNet(SEBasicBlock, [3, 4, 6, 3], num_classes=num_classes)
model.avgpool = nn.AdaptiveAvgPool2d(1)
return model
def se_resnet50(num_classes=1_000, pretrained=False):
model = ResNet(SEBottleneck, [3, 4, 6, 3], num_classes=num_classes)
model.avgpool = nn.AdaptiveAvgPool2d(1)
if pretrained:
model.load_state_dict(load_state_dict_from_url(
"https://github.com/moskomule/senet.pytorch/releases/download/archive/seresnet50-60a8950a85b2b.pkl"))
return model
def se_resnet101(num_classes=1_000):
model = ResNet(SEBottleneck, [3, 4, 23, 3], num_classes=num_classes)
model.avgpool = nn.AdaptiveAvgPool2d(1)
return model
def se_resnet152(num_classes=1_000):
model = ResNet(SEBottleneck, [3, 8, 36, 3], num_classes=num_classes)
model.avgpool = nn.AdaptiveAvgPool2d(1)
return model
if __name__ == "__main__":
inputs = torch.randn(2, 3, 224, 224)
model = se_resnet50(pretrained=False)
outputs = model(inputs)
print(outputs.size())
Se_Resnet的版本都封装好了,需要的自己训练测试下结果,以上是我的介绍,如有问题请指正。