paper地址: https://arxiv.org/abs/1911.11907
pytorch:https://github.com/iamhankai/ghostnet.pytorch
如果想看原文翻译,请跳转:https://blog.csdn.net/qq_38316300/article/details/104602071
这篇博客主要是对2020CVPR论文GhostNet: More Features from Cheap Operations的源码部分进行赏析,了解GhostNet网络的构建思路,并且使用PyTorch构建整体的网络架构。
目录
前沿知识:
创建GhostNet网络
第一部分:构建网络的第一层
第二部分:构建inverted residual blocks模块
2.1 GhostModule模块
2.2 GhostBottleneck模块
第三部分:构建squeeze层
第四部分:构建分类器
GhostNet网络主要由四个部分组成:
网络的第一层主要是由一个卷积核尺寸为3*3,步长为2的下采样层构成。我们把第一层放在layers元组中,layers能够存储第一层以及inverted residual blocks层。将layers通过变换之后能够变成一个有顺序的Sequential网络层。
def __init__(self, cfgs, num_classes=1000, width_mult=1.):
super(GhostNet, self).__init__()
# setting of inverted residual blocks
self.cfgs = cfgs
# building first layer
output_channel = _make_divisible(16 * width_mult, 4)
layers = [nn.Sequential(
nn.Conv2d(3, output_channel, 3, 2, 1, bias=False),
nn.BatchNorm2d(output_channel),
nn.ReLU(inplace=True)
)]
input_channel = output_channel
在这一模块中,我们的核心是要构建一个GhostBottleneck模块,因为inverted residual blocks模块的主要组成部门就是GhostBottleneck模块。
# building inverted residual blocks
block = GhostBottleneck
for k, exp_size, c, use_se, s in self.cfgs:
output_channel = _make_divisible(c * width_mult, 4)
hidden_channel = _make_divisible(exp_size * width_mult, 4)
layers.append(block(input_channel, hidden_channel, output_channel, k, s,
use_se))
input_channel = output_channel
self.features = nn.Sequential(*layers)
其中,_make_divisible函数能够保证输出通道的数目能够被4整除,代码具体如下:
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v:
new_v += divisor
return new_v
下面,我们构建核心模块GhostBottleneck模块,它主要由四个子模块构成,其分别是:
GhostModule主要有两步操作:原始的卷积操作,生成一定量m个特征图;廉价线性变换得到一定量s个冗余特诊图。
在开始,我们需要得到m和n的具体数值,在GhostModule类中加上下面代码行:
def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
super(GhostModule, self).__init__()
self.oup = oup
init_channels = math.ceil(oup / ratio)
new_channels = init_channels*(ratio-1)
GhostModule类传入的主要参数有输入通道数inp,输出通道数oup以及relu参数。说是主要参数是因为GhostModule中无论是原始卷积还是线性运算所使用的卷积核大小以及步长都是确定的,唯一不定的就是输入输出通道数目以及是否使用激活函数。
确定了两者的输入输出通道数之后,我们在__init__函数中加入原始卷积核以及廉价线性操作的代码
self.primary_conv = nn.Sequential(nn.Conv2d(inp, init_channels, kernel_size, stride,
kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential())
self.cheap_operation = nn.Sequential(nn.Conv2d(init_channels, new_channels, dw_size, 1,
dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential())
从中我们可以看到,Sequential中既包括了卷积操作/线性变化,也包括了BN层以及ReLU层。这是因为在PyTorch,更偏向于将它们当做是一个网络层进行统一处理。在源码中,原始卷积的卷积核尺寸为1,步长为1;廉价线性变换的卷积核尺寸为3,步长为1,只不过增加了groups=inp这一个属性来表示线性变化。
编写了网络层之后,我们需要指定数据在网络层中传递的顺序,在forwad函数中添加下面的代码:
def forward(self, x):
x1 = self.primary_conv(x)
x2 = self.cheap_operation(x1)
out = torch.cat([x1,x2], dim=1)
return out[:,:self.oup,:,:]
下面,我们编写一下depthwise卷积操作,在PyTorch中,这个卷积操作主要在Conv2d上添加了groups属性:
def depthwise_conv(inp, oup, kernel_size=3, stride=1, relu=False):
return nn.Sequential(nn.Conv2d(inp, oup, kernel_size, stride, kernel_size//2,
groups=inp, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU(inplace=True) if relu else nn.Sequential())
下面,我们编写一下SEModule操作,该操作主要就是先进行池化操作,然后再进行全连接操作。
class SELayer(nn.Module):
def __init__(self, channel, reduction=4):
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), )
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)
y = torch.clamp(y, 0, 1)
return x * y
需要注意一下,在池化层到全连接层转换过程中需要需要对数据的尺寸进行转换,将四维数据转换成二维数据,这是因为全连接层只能接收二维数据。全连接之后,需要将权重数据转换成原来四维数据,对原始特征图进行加权处理。
在GhostBottleneck模块中,子模块的主要顺序就是 GhostModule --> depthwise --> SEModule --> GhostModule -->Identity mapping
第一个GhostModul是增加特征图通道数,在该模块中包含更多的信息;第二个GhostModul模块让特征图的通道数恢复到期望的通道数。当特征图的尺寸改变的时候,我们需要对捷径层也进行相应的操作,使得两个连接的特征图尺寸和通道数都要相同。
class GhostBottleneck(nn.Module):
def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se):
super(GhostBottleneck, self).__init__()
assert stride in [1, 2]
self.conv = nn.Sequential(
# pw
GhostModule(inp, hidden_dim, kernel_size=1, relu=True),
# dw
depthwise_conv(hidden_dim, hidden_dim, kernel_size, stride, relu=False)
if stride==2 else nn.Sequential(),
# Squeeze-and-Excite
SELayer(hidden_dim) if use_se else nn.Sequential(),
# pw-linear
GhostModule(hidden_dim, oup, kernel_size=1, relu=False),
)
if stride == 1 and inp == oup:
self.shortcut = nn.Sequential()
else:
self.shortcut = nn.Sequential(
depthwise_conv(inp, inp, 3, stride, relu=True),
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
return self.conv(x) + self.shortcut(x)
GhostBottleneck模块构建之后,我们就可以构建整个GhostNet的网络架构了,首先创建一个GhostNet类,然后依次构建第一部分、inverted residual blocks、构建squeeze模块以及classifier模块。
# building last several layers
output_channel = _make_divisible(exp_size * width_mult, 4)
self.squeeze = nn.Sequential(
nn.Conv2d(input_channel, output_channel, 1, 1, 0, bias=False),
nn.BatchNorm2d(output_channel),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d((1, 1)))
input_channel = output_channel
output_channel = 1280
self.classifier = nn.Sequential(
nn.Linear(input_channel, output_channel, bias=False),
nn.BatchNorm1d(output_channel),
nn.ReLU(inplace=True),
nn.Dropout(0.2),
nn.Linear(output_channel, num_classes))
最后指定数据在GhostNet网络层之间的传输顺序
def forward(self, x):
x = self.features(x)
x = self.squeeze(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x
这样一个GhostNet网络就已经构建完毕了,我们可以给网络加载预训练模型参数,在__init__函数的最后加上
self._initialize_weights()
然后实例化这个函数:
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
我们可以创建一个函数,让这个函数实现GhostNet的一个对象
def ghost_net(**kwargs):
"""
Constructs a MobileNetV3-Large model
"""
cfgs = [
# k, t, c, SE, s
[3, 16, 16, 0, 1],
[3, 48, 24, 0, 2],
[3, 72, 24, 0, 1],
[5, 72, 40, 1, 2],
[5, 120, 40, 1, 1],
[3, 240, 80, 0, 2],
[3, 200, 80, 0, 1],
[3, 184, 80, 0, 1],
[3, 184, 80, 0, 1],
[3, 480, 112, 1, 1],
[3, 672, 112, 1, 1],
[5, 672, 160, 1, 2],
[5, 960, 160, 0, 1],
[5, 960, 160, 1, 1],
[5, 960, 160, 0, 1],
[5, 960, 160, 1, 1]
]
return GhostNet(cfgs, **kwargs)
这样一个GhostNet就完全构建好了,我们可以稍微测试一下:
if __name__=='__main__':
model = ghost_net()
model.eval()
print(model)
input = torch.randn(32,3,224,224)
y = model(input)
print(y)
欢迎和我交流~