利用Pytorch实现MobileNet网络

目录

1. MobileNet v1网络

2. MobileNet v2网络

2.1 Inverted Residual

2.2 Linear Bottlenecks

2.3 MobileNet v2模型的网络结构

3. MobileNet v3网络

3.1 注意力机制

3.2 重新设计耗时层结构

3.3 重新设计激活函数

3.4 MobileNet v3模型的网络结构

4. 利用Pytorch实现MobileNet

4.1 MobileNet v1、v2

4.2 MobileNet v3

5. 训练结果


1. MobileNet v1网络

在传统的卷积神经网络中,模型参数都比较大,而且对算力的要求很高,很难在移动设备和嵌入式设备上运行,MobileNet实现了将深度学习网络在移动设备和嵌入式设备上运行。

MobileNet网络是Google团队在2017年提出的,是专注于移动端或者嵌入式设备中的轻量级CNN网络。相比传统CNN网络,在准确率小幅度降低的前提下大大减少了模型的参数和运算量。该网络有以下两个亮点:

  • Depthwise Convolution(DW卷积,大大减少了参数数量和运算量)
  • 增加了控制卷积核个数的超参数\alpha和控制输入图像大小的超参数\beta

在传统的卷积操作中,卷积核的channel数等于输入特征矩阵的channel数,输出特征矩阵的channel数等于所使用卷积核的个数:

而对于DW卷积,卷积核的channel数为1,输入特征矩阵的channel数等于卷积核个数等于输出特征矩阵channel数:

Depthwise Separable Conv由DW卷积和PW卷积组成,PW卷积和传统卷积操作相似,只是卷积核大小均为1×1,理论上传统卷积的计算量是Depthwise Separable Conv操作的8到9倍。


2. MobileNet v2网络

在MobileNet的使用当中,DW卷积的卷积核参数大部分为0,这部分的卷积核没有起到作用,这个问题在MobileNet v2中有所改善,该网络有以下两个亮点:

  • Inverted Residual(倒残差结构)
  • Linear Bottlenecks

2.1 Inverted Residual

传统的残差结构是先用1×1的卷积核进行降维,然后通过3×3的卷积核进行卷积处理,最后采用1×1的卷积核进行升维,形成了一个两头大中间小的瓶颈结构;而倒残差结构首先利用1×1的卷积核进行升维,然后通过3×3的卷积核进行DW卷积,最后采用1×1的卷积核进行降维处理,和普通的残差结构正好相反。

在普通的残差结构中采用的是ReLU激活函数,而在倒残差结构中采用的则是ReLU6激活函数:

  • ReLU激活函数:输入值小于0时默认置0,输入值大于0则不进行操作
  • ReLU6激活函数:输入值小于0默认置0,输入值在0~6之间不进行操作,而输入值大于6时则置6,公式为

y=ReLU6\left ( x \right )=min\left ( max\left ( x,0 \right ) ,6\right )


2.2 Linear Bottlenecks

针对倒残差结构的最后一个卷积层,使用了线性的激活函数而非ReLU激活函数,原文作者进行了实验,内容是ReLU激活函数对低维特征信息会造成大量损失,对高维特征信息造成的损失很小,而倒残差结构是两边细中间粗的结构,输出是一个低维的特征向量,因此使用ReLU激活函数损失会比较大,所以使用线性的激活函数进行替代。

MobileNet v2的block块如下图所示:

要注意只有stride=1且输入特征矩阵与输出特征矩阵的shape相同时才有shortcut连接(即左面那种情况)。


2.3 MobileNet v2模型的网络结构

t为扩展因子,即将输入升为多少维;

c为输出特征矩阵深度;

n为bottleneck的重复次数;

s为第一层bottleneck的步长,其余层均为1。


3. MobileNet v3网络

MobileNet v3有以下三个改进点:

  • 更新了Block(bneck,在倒残差结构上进行了简单的改动)
  • 使用了NAS(Neural Architecture Search)搜索参数的技术
  • 重新设计了一些耗时层的结构

对于Block的更新主要有亮点:加入了SE模块,更新了激活函数。结构图如下:


3.1 注意力机制

针对得到的输出矩阵的每个channel进行池化处理,得到一维向量的元素个数等于channel数;经过第一个全连接层,节点个数是channel个数的1/4,激活函数是ReLU;经过第二个全连接层,节点个数等于channel个数,激活函数是hard-sigmoid;最后输出的向量是对矩阵的每个channel分析出了权重关系,重要的channel会分配一个比较大的权重,原理图如下:


3.2 重新设计耗时层结构

减少第一个卷积层的卷积核个数,从32减到16,并且不会影响准确率;精简Last Stage,精简之前的模型如下:

精简之后的模型:


3.3 重新设计激活函数

当时比较常用的激活函数是swish激活函数:

swishx=x\cdot \sigma \left ( x \right )

其中

\sigma \left ( x \right )=\frac{1}{1+e^{-x}}

这个激活函数的计算和求导很复杂;对于移动端的设备,为了加速都会进行量化操作,而swish激活函数对于量化操作很复杂。针对这两个问题,提出了h-swish激活函数:

h-swish\left [ x \right ]=x\cdot \frac{ReLU6\left ( x+3 \right )}{6}

式子中的后半部分是h-sigmoid激活函数。

从图中可以看出,两种激活函数很相似,可以用h-swish替代swish使用,还可以简化量化操作。


3.4 MobileNet v3模型的网络结构


4. 利用Pytorch实现MobileNet

4.1 MobileNet v1、v2

在MobileNet v2网络当中,所有的卷积层基本都是卷积+BN+ReLU6激活函数组成,首先定义这个组合:

class ConvBNReLU(nn.Sequential):
    # 输入特征矩阵深度,输出特征矩阵深度,卷积核大小,步长,groups=1是普通卷积、=in_channel是DW卷积
    def __init__(self, in_channel, out_channel, kernel_size=3, stride=1, groups=1):
        # 填充参数
        padding = (kernel_size - 1) // 2
        super(ConvBNReLU, self).__init__(
            # 首先是卷积操作:输入特征矩阵深度,输出特征矩阵深度,卷积核大小,步长,填充参数,groups为默认值,不需要偏置
            nn.Conv2d(in_channel, out_channel, kernel_size, stride, padding, groups=groups, bias=False),
            # BN层,输入为卷积操作的输出
            nn.BatchNorm2d(out_channel),
            # 激活函数
            nn.ReLU6(inplace=True)
        )

倒残差结构:

# 倒残差结构
class InvertedResidual(nn.Module):
    # 输入特征矩阵深度,输出特征矩阵深度,步长,深度扩大多少倍
    def __init__(self, in_channel, out_channel, stride, expand_ratio):
        super(InvertedResidual, self).__init__()
        # 第一层卷积层,扩展深度
        hidden_channel = in_channel * expand_ratio
        # 定义一个布尔变量,步长为1且输入和输出特征矩阵相等时,采用捷径分支
        self.use_shortcut = stride == 1 and in_channel == out_channel

        # 层列表
        layers = []
        # t = 1时没有对输入特征矩阵进行扩充,此时不需要第一层卷积层
        if expand_ratio != 1:
            # 1x1 pointwise conv
            layers.append(ConvBNReLU(in_channel, hidden_channel, kernel_size=1))
        # extend函数能够一次性批量插入很多元素
        layers.extend([
            # 3x3 depthwise conv,DW卷积的groups为输入特征矩阵深度
            ConvBNReLU(hidden_channel, hidden_channel, stride=stride, groups=hidden_channel),
            # 1x1 pointwise conv(linear)
            nn.Conv2d(hidden_channel, out_channel, kernel_size=1, bias=False),
            # BN层
            nn.BatchNorm2d(out_channel),
        ])

        # *的解析见博客http://t.csdn.cn/CnTEA
        self.conv = nn.Sequential(*layers)

    # 前向传播,判断是否使用捷径分支
    def forward(self, x):
        if self.use_shortcut:
            return x + self.conv(x)
        else:
            return self.conv(x)

定义MobileNet v2的网络结构:

# 网络结构
class MobileNetV2(nn.Module):
    # 类别个数和v1网络中的alpha参数
    def __init__(self, num_classes=1000, alpha=1.0, round_nearest=8):
        super(MobileNetV2, self).__init__()
        # 块,注意不是实例化而是赋值
        block = InvertedResidual
        # 输入特征矩阵深度
        input_channel = _make_divisible(32 * alpha, round_nearest)
        # 输出通道数
        last_channel = _make_divisible(1280 * alpha, round_nearest)
        # 对应网络结构的四个参数值
        inverted_residual_setting = [
            # t, c, n, s
            [1, 16, 1, 1],
            [6, 24, 2, 2],
            [6, 32, 3, 2],
            [6, 64, 4, 2],
            [6, 96, 3, 1],
            [6, 160, 3, 2],
            [6, 320, 1, 1],
        ]

        features = []
        # 第一个卷积层
        features.append(ConvBNReLU(3, input_channel, stride=2))
        # building inverted residual residual blockes
        for t, c, n, s in inverted_residual_setting:
            # 调整每个block输出特征矩阵深度
            output_channel = _make_divisible(c * alpha, round_nearest)
            # 每个block块的具体结构
            for i in range(n):
                # s为第一层的步距,其他层均为1
                stride = s if i == 0 else 1
                features.append(block(input_channel, output_channel, stride, expand_ratio=t))
                # 将output_channel传入input_channel,作为下一层的输入
                input_channel = output_channel
        # 倒数第三个卷积层
        features.append(ConvBNReLU(input_channel, last_channel, 1))
        # 特征提取层结束
        # 将特征提取网络结构传入
        self.features = nn.Sequential(*features)

        # 分类器
        # 平均池化下采样层,输出特征矩阵高和宽为1×1
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        # Dropout层和全连接层
        self.classifier = nn.Sequential(
            nn.Dropout(0.2),
            nn.Linear(last_channel, num_classes)
        )

        # 初始化权重
        for m in self.modules():
            # 遍历每一个子模块
            if isinstance(m, nn.Conv2d):
                # 如果是卷积层就对权重进行初始化
                nn.init.kaiming_normal_(m.weight, mode='fan_out')
                # 如果存在偏置则将偏置设置为0
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, nn.BatchNorm2d):
                # 如果是BN层将方差设置为1,均值设置为0
                nn.init.ones_(m.weight)
                nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                # 如果是全连接层,将权重设置为均值为0,方差为0.01的正态分布,偏置设置为0
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)

    # 前向传播
    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        # 将输出展平
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

4.2 MobileNet v3

对于MobileNet v3网络,多了SE模块:

# 注意力模块
class SqueezeExcitation(nn.Module):
    # squeeze_factor:第一个全连接层节点个数是输入的1/4
    def __init__(self, input_c: int, squeeze_factor: int = 4):
        super(SqueezeExcitation, self).__init__()
        squeeze_c = _make_divisible(input_c // squeeze_factor, 8)
        # 全连接层
        self.fc1 = nn.Conv2d(input_c, squeeze_c, 1)
        self.fc2 = nn.Conv2d(squeeze_c, input_c, 1)

    def forward(self, x: Tensor):
        # 自适应的平均池化操作,将每一个channel上的数据平均池化到1×1的大小
        scale = F.adaptive_avg_pool2d(x, output_size=(1, 1))
        scale = self.fc1(scale)
        scale = F.relu(scale, inplace=True)
        scale = self.fc2(scale)
        # hardsigmoid激活函数
        scale = F.hardsigmoid(scale, inplace=True)
        # 直接相乘
        return scale * x

整个Block块结构:

# 整个Block块结构
class InvertedResidual(nn.Module):
    def __init__(self,
                 cnf: InvertedResidualConfig,
                 norm_layer: Callable[..., nn.Module]):
        super(InvertedResidual, self).__init__()

        # 步长只能为1或2
        if cnf.stride not in [1, 2]:
            raise ValueError("illegal stride value.")

        # 判断是否使用捷径分支
        self.use_res_connect = (cnf.stride == 1 and cnf.input_c == cnf.out_c)

        layers: List[nn.Module] = []
        # 判断使用什么激活函数
        activation_layer = nn.Hardswish if cnf.use_hs else nn.ReLU

        # expand
        # 只有第一个bneck没有1×1的升维卷积层
        if cnf.expanded_c != cnf.input_c:
            layers.append(ConvBNActivation(cnf.input_c,
                                           cnf.expanded_c,
                                           kernel_size=1,
                                           norm_layer=norm_layer,
                                           activation_layer=activation_layer))

        # depthwise
        layers.append(ConvBNActivation(cnf.expanded_c,
                                       cnf.expanded_c,
                                       kernel_size=cnf.kernel,
                                       stride=cnf.stride,
                                       groups=cnf.expanded_c,
                                       norm_layer=norm_layer,
                                       activation_layer=activation_layer))

        if cnf.use_se:
            layers.append(SqueezeExcitation(cnf.expanded_c))

        # project
        layers.append(ConvBNActivation(cnf.expanded_c,
                                       cnf.out_c,
                                       kernel_size=1,
                                       norm_layer=norm_layer,
                                       # 线性激活,没有做任何处理
                                       activation_layer=nn.Identity))

        self.block = nn.Sequential(*layers)
        self.out_channels = cnf.out_c
        self.is_strided = cnf.stride > 1

    def forward(self, x: Tensor):
        result = self.block(x)
        # 是否使用捷径分支
        if self.use_res_connect:
            result += x

        return result

MobileNet v3网络结构:

class MobileNetV3(nn.Module):
    # 一系列结构参数列表,倒数第二个全连接层输出节点个数,分类类别个数,InvertedResidual模块
    def __init__(self,
                 inverted_residual_setting: List[InvertedResidualConfig],
                 last_channel: int,
                 num_classes: int = 1000,
                 block: Optional[Callable[..., nn.Module]] = None,
                 norm_layer: Optional[Callable[..., nn.Module]] = None):
        super(MobileNetV3, self).__init__()

        # 数据检查
        if not inverted_residual_setting:
            raise ValueError("The inverted_residual_setting should not be empty.")
        elif not (isinstance(inverted_residual_setting, List) and
                  all([isinstance(s, InvertedResidualConfig) for s in inverted_residual_setting])):
            raise TypeError("The inverted_residual_setting should be List[InvertedResidualConfig]")

        if block is None:
            block = InvertedResidual

        if norm_layer is None:
            # partial用法http://t.csdn.cn/k80JE
            norm_layer = partial(nn.BatchNorm2d, eps=0.001, momentum=0.01)

        layers: List[nn.Module] = []

        # building first layer
        # 获得第一个卷积层输出的channel
        firstconv_output_c = inverted_residual_setting[0].input_c
        layers.append(ConvBNActivation(3,
                                       firstconv_output_c,
                                       kernel_size=3,
                                       stride=2,
                                       norm_layer=norm_layer,
                                       activation_layer=nn.Hardswish))
        # building inverted residual blocks
        for cnf in inverted_residual_setting:
            layers.append(block(cnf, norm_layer))

        # building last several layers
        # 获得最后一个bneck结构的输出channel
        lastconv_input_c = inverted_residual_setting[-1].out_c
        # 160 * 6
        lastconv_output_c = 6 * lastconv_input_c
        # 最后一个卷积层
        layers.append(ConvBNActivation(lastconv_input_c,
                                       lastconv_output_c,
                                       kernel_size=1,
                                       norm_layer=norm_layer,
                                       activation_layer=nn.Hardswish))
        self.features = nn.Sequential(*layers)
        # 池化层和两个全连接层
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Sequential(nn.Linear(lastconv_output_c, last_channel),
                                        nn.Hardswish(inplace=True),
                                        nn.Dropout(p=0.2, inplace=True),
                                        nn.Linear(last_channel, num_classes))

        # initial weights
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode="fan_out")
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.ones_(m.weight)
                nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)

    def _forward_impl(self, x: Tensor):
        x = self.features(x)
        x = self.avgpool(x)
        # 展平处理
        x = torch.flatten(x, 1)
        x = self.classifier(x)

        return x

    def forward(self, x: Tensor):
        return self._forward_impl(x)

5. 训练结果

MobileNet v2采用预训练权重,只训练全连接层的结果:

using cuda:0 device.
Using 4 dataloader workers every process
using 3306 images for training, 364 images for validation.
train epoch[1/5] loss:1.007: 100%|██████████| 207/207 [00:09<00:00, 22.49it/s]
valid epoch[1/5]: 100%|██████████| 23/23 [00:03<00:00,  6.31it/s]
[epoch 1] train_loss: 1.245  val_accuracy: 0.794
train epoch[2/5] loss:0.813: 100%|██████████| 207/207 [00:07<00:00, 27.63it/s]
valid epoch[2/5]: 100%|██████████| 23/23 [00:03<00:00,  6.26it/s]
[epoch 2] train_loss: 0.864  val_accuracy: 0.824
train epoch[3/5] loss:0.580: 100%|██████████| 207/207 [00:07<00:00, 28.38it/s]
valid epoch[3/5]: 100%|██████████| 23/23 [00:03<00:00,  6.28it/s]
[epoch 3] train_loss: 0.716  val_accuracy: 0.865
train epoch[4/5] loss:0.818: 100%|██████████| 207/207 [00:07<00:00, 28.45it/s]
valid epoch[4/5]: 100%|██████████| 23/23 [00:03<00:00,  6.46it/s]
[epoch 4] train_loss: 0.626  val_accuracy: 0.857
train epoch[5/5] loss:0.488: 100%|██████████| 207/207 [00:07<00:00, 28.32it/s]
valid epoch[5/5]: 100%|██████████| 23/23 [00:03<00:00,  6.34it/s]
[epoch 5] train_loss: 0.587  val_accuracy: 0.857
Finished Training

MobileNet v3 large采用预训练权重,只训练全连接层的结果:

using cuda:0 device.
Using 4 dataloader workers every process
using 3306 images for training, 364 images for validation.
train epoch[1/5] loss:0.831: 100%|██████████| 207/207 [00:09<00:00, 22.47it/s]
valid epoch[1/5]: 100%|██████████| 23/23 [00:03<00:00,  6.40it/s]
[epoch 1] train_loss: 0.889  val_accuracy: 0.868
train epoch[2/5] loss:0.824: 100%|██████████| 207/207 [00:07<00:00, 27.67it/s]
valid epoch[2/5]: 100%|██████████| 23/23 [00:03<00:00,  6.50it/s]
[epoch 2] train_loss: 0.508  val_accuracy: 0.887
train epoch[3/5] loss:0.370: 100%|██████████| 207/207 [00:07<00:00, 27.11it/s]
valid epoch[3/5]: 100%|██████████| 23/23 [00:03<00:00,  6.56it/s]
[epoch 3] train_loss: 0.451  val_accuracy: 0.901
train epoch[4/5] loss:0.841: 100%|██████████| 207/207 [00:07<00:00, 27.61it/s]
valid epoch[4/5]: 100%|██████████| 23/23 [00:03<00:00,  6.34it/s]
[epoch 4] train_loss: 0.411  val_accuracy: 0.904
train epoch[5/5] loss:0.195: 100%|██████████| 207/207 [00:07<00:00, 27.60it/s]
valid epoch[5/5]: 100%|██████████| 23/23 [00:03<00:00,  6.32it/s]
[epoch 5] train_loss: 0.378  val_accuracy: 0.904
Finished Training
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值