构建自己的GhostNet网络(PyTorch)

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层

第四部分:构建分类器


前沿知识:

  • 你已经读过了论文,了解了Ghost模块提出的必要性和重要性
  • 能够使用PyTorch构建简单的卷积神经网络

创建GhostNet网络

GhostNet网络主要由四个部分组成:

  • 第一部分:使用3*3的卷积核,对原始图像进行下采样。
  • 第二部分:使用16个GhostBottleneck模块,代替卷积核操作,进行图像的高维特征提取
  • 第三部分:使用全局平均池化,对特征图进行压缩得到更强的表征信息
  • 第四部分:构建网络的分类器,进行图像分类

第一部分:构建网络的第一层

网络的第一层主要是由一个卷积核尺寸为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

第二部分:构建inverted residual blocks模块

在这一模块中,我们的核心是要构建一个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:该模块只能够增加特征图的通道数,不能够改变其次尺寸
  • depthwise convolution:能够改变特征图的尺寸,不改变通道数(这两个模块实现了一个卷积核的改变尺寸和通道数的作用)
  • SEModule:能够从全局感受野出发,增加重要的特征图的权重,削弱不重要的特征图的权重
  • Identity mapping:类似残差网络中的恒等映射,能够保留更多的信息,使得更深的网络可以比较容易的训练

2.1 GhostModule模块

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

需要注意一下,在池化层到全连接层转换过程中需要需要对数据的尺寸进行转换,将四维数据转换成二维数据,这是因为全连接层只能接收二维数据。全连接之后,需要将权重数据转换成原来四维数据,对原始特征图进行加权处理。

2.2 GhostBottleneck模块

在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模块。

第三部分:构建squeeze层

# 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)

欢迎和我交流~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章