前言

在当今人工智能和计算机视觉领域,模型的性能与效率始终是备受关注的焦点。尤其是在移动设备和嵌入式系统的应用场景中,对模型的轻量性和高性能有着更为迫切的需求。MobileNetV1 作为早期的轻量级卷积神经网络,虽在一定程度上实现了模型的轻量化,但在准确率和模型大小方面仍存在不足。为了进一步提升移动设备上的使用体验,谷歌团队于 2018 年提出了 MobileNetV2 架构。本文将深入探讨 MobileNetV2 的网络背景、创新点、结构组成,并给出其代码实现,旨在为读者全面解析这一优秀的轻量级卷积神经网络。


MobileNetV2

一、网络背景

MobileNetV1 在轻量性和高性能方面有所不足,为提升移动设备使用体验,谷歌团队于 2018 年提出 MobileNetV2 架构。相较于 MobileNetV1,它具有更高的准确率和更小的网络模型。
论文下载:Inverted Residuals and Linear Bottlenecks.pdf


二、网络的创新

在论文(paper)的Table 2中给出了网络结构的架构表,具体内容见对应图示。
在这里插入图片描述

2.1 Inverted Residuals(倒残差结构)

  1. ResNet残差结构

    • ResNet残差结构的过程:先使用1x1的卷积对输入特征矩阵进行降维,减少输入特征矩阵的通道(channel)数量;接着通过3x3的卷积核进行特征提取;最后使用1x1的卷积核进行升维。其结构特点是两边深(通道数多),中间浅(通道数少)。
      在这里插入图片描述
  2. MobileNetV2倒残差结构

    • 结构特点:与ResNet残差结构不同,MobileNetV2采用的倒残差结构是先升维,再降维,结构特点为中间深(通道数多),两边浅(通道数少)。网络结构表格中的bottleneck就是倒残差结构。
      在这里插入图片描述

    • 具体过程

      • 首先通过一个1x1卷积层进行升维处理,卷积后会连接批量归一化(BN)层和Relu6激活函数。
      • 紧接着是一个3x3大小的深度可分离卷积(DW卷积),卷积后同样连接BN层和Relu6激活函数。
      • 最后一个卷积层是1x1卷积,起到降维作用,卷积后仅连接BN结构,未使用Relu6激活函数。倒残差结构的具体过程见对应图示。
        在这里插入图片描述
    • 优势:在MobileNetV1中,DW卷积的个数受限于上一层的输出通道数,无法自由改变。而在加入逐点卷积(PW卷积,即升维卷积)之后,DW卷积的个数取决于PW卷积的输出通道数,该通道数可任意指定,从而解除了3x3卷积核个数的限制。


2.2 Relu6

在倒残差结构图中,卷积后使用的激活函数从 MobileNetV1 的 Relu 变为 Relu6。Relu6 的表达式为

y = R e L U 6 ( x ) = m i n ( m a x ( x , 0 ) , 6 ) y = ReLU6(x) = min(max(x, 0), 6) y=ReLU6(x)=min(max(x,0),6)

在这里插入图片描述

从其图像可知,Relu6 是 Relu 的变种。当输入小于 0 时,输出为 0;输入大于 6 时,输出为 6;输入在 0 到 6 之间时, y = x y = x y=x

使用 Relu6 替代 Relu 的主要原因在于,在移动端设备采用 float16 低精度时,Relu6 能有更好的数值分辨率。Relu 的输出范围是 0 到正无穷,若不对其激活范围加以限制,激活值可能会非常大且分布范围广,低精度的 float16 难以精确描述如此大范围的数值,从而导致精度损失。而 Relu6 在进行量化时,能有更好的量化表现并减少精度下降。


2.3 Linear Bottlenecks

在倒残差结构的最后一个 1x1 卷积层,采用线性激活函数,也就是不使用像 ReLU 或 ReLU6 这类非线性激活函数,直接输出结果。


2.4 Shortcut

  • 来源:Shortcut并非当前网络所提出,而是由残差结构引入。

  • 结构图示:存在两种倒残差结构,左侧为带有shortcut连接的倒残差结构,右侧为无shortcut连接的倒残差结构。
    在这里插入图片描述

  • 作用:Shortcut通过将输入与输出直接相加,让网络在深度较大时仍能正常训练。

  • 连接条件:仅当stride = 1,且输入特征矩阵与输出特征矩阵的shape相同时,才会有shortcut连接。


2.5 拓展因子

在 paper 的 Table 1 中给出了倒残差结构的特征矩阵。
在这里插入图片描述

其中,输入特征矩阵的高和宽分别用 h 和 w 表示,通道数用 k 表示。拓展因子用 t 表示,它代表升维时的比例。例如,当输入特征矩阵的维度为 32 时,升维后的维度就是 32×t,在表格中用 tk 表示。 k ′ k^{\prime} k 则表示降维时卷积核的个数。


三、网络的结构

在paper的Table 2中给出了网络结构的架构表。表中各参数含义如下:
在这里插入图片描述

  • Input:每一层结构的输入矩阵尺寸和通道数。
  • Operator:具体的操作类型。
  • t(拓展因子):倒置残差结构中第一个 1x1 卷积将原来通道放大的倍数。在 MobileNetV2 中,第一层倒残差结构的拓展因子 t = 1,其它倒残差结构的拓展因子 t = 6。需要注意的是,当 t = 1 时,第一个瓶颈(bottleneck)结构不会进行升维操作,即没有第一个 Conv2D 层。
  • c(输出通道数):该层操作输出的通道数量。
  • n(重复次数):bottleneck 模块重复的次数。
  • s(步距):步长参数。对于重复的 bottleneck 模块,仅第一次 bottleneck 的 DW 卷积使用该步距,其余重复的 bottleneck 模块步距为 1。

网络的最后一层是卷积层,其起到全连接层的作用。用 k 表示输出的类别数量,例如在 ImageNet 数据集上,k 的值为 1000。

此外,在每个 DW(深度可分离卷积)卷积之后都会进行 batchNorm 操作。

综上所述,该网络结构基于一系列的瓶颈(bottleneck)模块,通过不同的参数设置实现了高效的特征提取和分类功能。在设计网络时,依据不同的输入、输出要求以及计算资源限制,合理调整各层的参数,以达到最优的性能。


四、代码

# 导入PyTorch的神经网络模块
from torch import nn
import  torch

# 定义一个组合模块,包含卷积层、批归一化层和激活函数层
# 该模块用于将常见的卷积操作与批归一化和激活函数组合在一起,方便复用
class ConvBNRelu(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=0, groups=1,
                 activation=True):
        # 调用父类的构造函数
        super(ConvBNRelu, self).__init__()
        # 如果padding设置为'same',则自动计算填充值,以保持输入输出尺寸相同
        if padding == 'same':
            padding = (kernel_size - 1) // 2
        # 构建卷积层
        # in_channels: 输入通道数
        # out_channels: 输出通道数
        # kernel_size: 卷积核大小
        # stride: 步长
        # padding: 填充大小
        # groups: 分组卷积的组数,groups=1为标准卷积,groups=in_channels为深度可分离卷积
        # bias=False: 不使用偏置项,因为后续会使用批归一化
        self.layers = [nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
                                 kernel_size=kernel_size, stride=stride, padding=padding, groups=groups, bias=False)]
        # 添加批归一化层,用于加速模型收敛和提高模型稳定性
        self.layers.append(nn.BatchNorm2d(out_channels))
        # 如果activation为True,则添加激活函数层
        # 使用ReLU6激活函数,它将输出限制在[0, 6]范围内,适合移动端部署
        if activation:
            self.layers.append(nn.ReLU6(inplace=True))
        # 将layers列表中的模块按顺序组合成一个Sequential模块
        self.layers2 = nn.Sequential(*self.layers)

    def forward(self, x):
        # 前向传播,将输入x依次通过layers2中的模块
        return self.layers2(x)

# 定义倒残差块,这是MobileNetV2的核心组件
# 其主要思想是先通过1x1卷积升维,再使用深度可分离卷积处理,最后通过1x1卷积降维
class InvertedResidualBlock(nn.Module):
    def __init__(self, in_channels, t, c, s):
        """
        参数:
            in_channels: 输入通道数
            t: 扩展因子,决定升维倍数
            c: 输出通道数
            s: 步长,用于控制特征图的尺寸
        """
        # 调用父类的构造函数
        super(InvertedResidualBlock, self).__init__()
        # 扩展卷积层,使用1x1卷积增加通道数
        self.exp = ConvBNRelu(in_channels=in_channels, out_channels=in_channels * t, kernel_size=1, stride=1)
        # 深度卷积层,使用3x3深度可分离卷积处理空间信息
        # 深度可分离卷积将通道分离处理,减少参数数量
        self.depthwise = ConvBNRelu(in_channels=in_channels * t, out_channels=in_channels * t, kernel_size=3, stride=s, padding=1, groups=in_channels * t)
        # 逐点卷积层,使用1x1卷积降低通道数
        # activation=False: 不使用激活函数,保留特征信息
        self.pointwise = ConvBNRelu(in_channels=in_channels * t, out_channels=c, kernel_size=1, activation=False)
        # 判断是否进行残差连接
        # 当输入输出通道数相同且步长为1时,使用残差连接,有助于缓解梯度消失问题
        self.is_res = in_channels == c and s == 1

    def forward(self, x):
        # 保存输入作为残差
        identity = x
        # 依次通过扩展卷积、深度卷积和逐点卷积
        x = self.exp(x)
        x = self.depthwise(x)
        x = self.pointwise(x)
        # 如果满足残差连接条件,则进行残差连接
        if self.is_res:
            x = identity + x
        return x

# 定义瓶颈层,由多个倒残差块堆叠而成
class BottleneckLayer(nn.Module):
    def __init__(self, in_channels, t, c, n, s):
        """
        参数:
            in_channels: 输入通道数
            t: 扩展因子
            c: 输出通道数
            n: 堆叠的残差块数量
            s: 第一个残差块的步长,其余残差块步长为1
        """
        # 调用父类的构造函数
        super(BottleneckLayer, self).__init__()
        # 构建堆叠的倒残差块
        # 第一个残差块可能改变通道数和分辨率,后续残差块保持通道数和分辨率不变
        self.stack = nn.Sequential(
            InvertedResidualBlock(in_channels, t, c, s),
            # 使用生成器表达式创建剩余的n-1个倒残差块,步长为1
            *(InvertedResidualBlock(c, t, c, 1) for _ in range(n - 1))
        )

    def forward(self, x):
        # 前向传播,将输入x依次通过堆叠的倒残差块
        return self.stack(x)

# 定义MobileNetV2主模型
class MobileNetV2(nn.Module):
    def __init__(self, num_classes=1000):
        # 调用父类的构造函数
        super(MobileNetV2, self).__init__()
        # 初始化卷积层,用于提取基础特征
        # 输入通道数为3(RGB图像),输出通道数为32
        # 卷积核大小为3,步长为2,填充为1
        self.c1 = ConvBNRelu(in_channels=3, out_channels=32, kernel_size=3, stride=2, padding=1)
        # 定义7个瓶颈层模块,逐步提取特征并降低空间分辨率
        # 每个瓶颈层由多个倒残差块堆叠而成
        self.bottleneck1 = BottleneckLayer(32, 1, 16, 1, 1)  # 第一层不进行下采样
        self.bottleneck2 = BottleneckLayer(16, 6, 24, 2, 2)
        self.bottleneck3 = BottleneckLayer(24, 6, 32, 3, 2)
        self.bottleneck4 = BottleneckLayer(32, 6, 64, 4, 2)
        self.bottleneck5 = BottleneckLayer(64, 6, 96, 3, 1)  # 保持分辨率不变
        self.bottleneck6 = BottleneckLayer(96, 6, 160, 3, 2)
        self.bottleneck7 = BottleneckLayer(160, 6, 320, 1, 1)
        # 最终特征提取层,使用1x1卷积将通道数提升到1280
        self.c2 = ConvBNRelu(320, 1280, 1)
        # 全局平均池化层,将特征图压缩为1x1的大小
        # 无论输入特征图的尺寸如何,输出的特征图尺寸都为1x1
        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
        # 输出层,使用1x1卷积将通道数映射为类别数
        self.c3 = nn.Conv2d(in_channels=1280, out_channels=num_classes, kernel_size=1)

    def forward(self, x):
        # 特征提取路径
        # 依次通过初始化卷积层和7个瓶颈层
        x = self.c1(x)
        x = self.bottleneck1(x)
        x = self.bottleneck2(x)
        x = self.bottleneck3(x)
        x = self.bottleneck4(x)
        x = self.bottleneck5(x)
        x = self.bottleneck6(x)
        x = self.bottleneck7(x)
        x = self.c2(x)
        # 分类路径
        # 通过全局平均池化将特征图压缩为1x1
        x = self.avg_pool(x)
        # 通过输出层将通道数映射为类别数
        x = self.c3(x)
        # 展平为二维张量,方便后续处理
        out = x.view(x.size(0), -1)
        return out

if __name__ == '__main__':
    # 模型测试
    # 输入5张224x224的RGB图像
    model = MobileNetV2()
    x = torch.rand(5, 3, 224, 224)
    # 打印模型输出的形状,预期输出形状为[5, 1000]
    print(model(x).shape)

总结

本文围绕 MobileNetV2 展开了全面而深入的介绍。首先阐述了其提出的背景,即 MobileNetV1 在轻量性和高性能方面的不足促使谷歌团队研发出 MobileNetV2,该架构具有更高的准确率和更小的网络模型。接着详细分析了 MobileNetV2 的创新点,包括倒残差结构、Relu6 激活函数、线性瓶颈、Shortcut 连接以及拓展因子等,这些创新使得模型在性能和效率上有了显著提升。然后介绍了网络的结构,解释了架构表中各参数的含义,说明了网络如何基于一系列瓶颈模块实现高效的特征提取和分类功能。最后给出了基于 PyTorch 的代码实现,包括组合模块、倒残差块、瓶颈层和主模型的定义,并进行了模型测试。通过本文的介绍,读者能够全面了解 MobileNetV2 的原理和实现方式,为其在实际应用中的使用提供参考。

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐