MobileNetV1-论文解读-小型巨人

MobileNetV1 论文解读:小型巨人

原文:towardsdatascience.com/the-tiny-giant-mobilenetv1/

简介

深度学习工程师过去一直专注于提高准确率。他们不断推动极限更高,直到最终意识到他们模型的计算复杂度变得越来越昂贵。这无疑是一个研究人员需要解决的问题,因为我们希望深度学习模型不仅能在高端计算机上工作,也能在小设备上工作。为了克服这个问题,霍华德等人于 2017 年提出了一种极其轻量级的神经网络模型,称为 MobileNet,他们在一篇题为《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》的论文中介绍了这个模型 [1]。实际上,论文中提出的模型是 MobileNet 的第一个版本,通常被称为 MobileNetV1。目前我们已经有四个 MobileNet 版本:从 MobileNetV1 到 MobileNetV4。然而,在这篇文章中,我们只将关注 MobileNetV1,涵盖其架构背后的理念以及如何使用 PyTorch 从头实现它——我将把后续的 MobileNet 版本留到我的下一篇文章中。


深度可分离卷积

为了实现轻量级模型,MobileNet 利用了深度可分离卷积的概念,这在整个网络中几乎被广泛应用。下面的图 1 显示了此层(右)与标准卷积层(左)的结构差异。你可以从图中看到,深度可分离卷积基本上由两种类型的卷积层组成:深度卷积点卷积。此外,当我们构建基于 CNN 的模型时,我们通常遵循卷积-归一化-ReLU的结构。这基本上是为什么在插图中有批归一化和 ReLU 紧随每个卷积层之后的原因。我们将在后续章节中更深入地讨论深度卷积和点卷积。

图 1. 标准卷积层(左)和深度可分离卷积层(右)的结构 [1]。

深度卷积

标准卷积层基本上是一个将group参数设置为 1 的卷积。重要的是要记住,在这种情况下使用 3×3 核实际上意味着将形状为C×3×3 的核应用到输入张量上,其中C是输入通道数。使用这种核形状允许我们一次性从每个 3×3 补丁内的所有通道中聚合信息。这就是为什么标准卷积操作计算量很大,但作为回报,输出张量包含大量信息。如果你仔细观察下面的图 2,标准卷积层对应于权衡线的最左侧。

图片

图 2. 少数与更多卷积组之间的权衡[2]。

如果你已经熟悉组卷积,深度卷积应该很容易理解。组卷积是一种方法,我们将输入张量的通道根据使用的组数进行划分,并在每个组内独立应用卷积。例如,假设我们有一个 64 通道的输入张量,并希望使用 2 组 128 个核对其进行处理。在这种情况下,前 64 个核负责处理输入张量的前 32 个通道,而剩下的 64 个核处理输入张量的最后 32 个通道。这种机制导致每个组有 64 个输出通道。最终的输出张量是通过沿着通道维度连接所有组的张量结果获得的,在这个例子中总共是 128 个通道。

随着我们继续增加组数,我们最终会达到一个称为深度卷积的极端情况,这是组卷积的一个特殊情况,其中组数设置为等于输入通道数。在这种配置下,我们基本上是独立地处理每个通道,导致输入中的每个通道只产生一个输出通道。通过连接所有结果的单通道张量,最终的输出通道数与输入通道数完全相同。这种机制要求我们使用 1×3×3 大小的核而不是C×3×3,从而防止我们在通道轴上进行信息聚合。这允许我们拥有极轻量级的计算,但作为回报,由于缺少通道信息聚合,输出张量包含的信息量较少。

由于 MobileNet 的目标是尽可能快地进行计算,尽管捕获的信息量最少,我们也需要将自己定位在上图权衡线的最右侧。这确实是一个需要解决的问题,这也是我们为什么在后续步骤中采用点卷积的原因。

点卷积

点卷积基本上就是一个标准的卷积,只不过它使用的是大小为 1×1 的核——或者更准确地说,它实际上是C×1×1。这种核形状允许我们在通道轴上聚合信息,而不受空间信息的影响,从而有效地补偿了深度卷积的限制。此外,记住深度卷积单独只能输出与输入相同数量的通道的张量,这限制了我们在设计模型架构时的灵活性。通过在下一步应用点卷积,我们可以将其设置为返回我们想要的任何数量的通道,使我们能够根据需要调整层以适应后续层。

我们可以将深度卷积和点卷积视为两个互补的过程,前者专注于捕捉空间关系,而后者则捕捉通道关系。乍一看,这两个过程可能显得有点低效,因为我们基本上可以使用标准的卷积层同时完成这两个过程。然而,如果我们仔细观察计算复杂度,深度可分离卷积与传统的卷积层相比要轻量得多。在下一节中,我将更详细地讨论如何计算这两种模型中的参数数量,这无疑也会影响计算复杂度。

参数计数计算

假设我们有一个大小为 3×H×W的图像,其中HW分别代表图像的高度和宽度。为了这个示例,让我们假设我们即将使用 16 个大小为 5×5 的核来处理这个图像,其中步长设置为 1,填充设置为 2(在这种情况下相当于padding = same)。根据这种配置,输出张量的大小将是 16×H×W。如果我们使用标准的卷积层,参数的数量将是 5×5×3×16 = 1200(不包括偏置),这个数字是基于图 3 中的方程得出的。在这种情况下,使用偏置项并不是严格必要的,但如果使用,总参数数将是(5×5×3+1) × 16 = 1216。

图 3. 计算卷积层参数数量的方程 [2]。

现在我们来计算深度可分离卷积的参数数量,以产生相同的张量维度。按照相同的公式,深度卷积部分(不包括偏置)将是 5×5×1×3 = 75。或者,如果我们也考虑偏置,那么我们将有(5×5×1+1) × 3 = 78 个可训练参数。在这种情况下,深度卷积的输入通道数被认为是 1,因为每个核只负责处理单个通道。对于点卷积部分,参数数量将是 1×1×3×16 = 48(不包括偏置)或者(1×1×3+1) × 16 = 64(包括偏置)。现在,为了获得整个深度可分离卷积过程中的总参数数量,我们可以简单地计算 75+48 = 123(不包括偏置)或者 78+64 = 142(包括偏置)——与标准卷积相比,这几乎减少了 90%的参数数量!从理论上讲,这种极端的参数数量下降会导致模型容量大大降低。但这只是理论。稍后我会向你展示 MobileNet 是如何在准确度方面与其他模型保持同步的。


MobileNetV1 的详细架构

下图 4 详细展示了 MobileNetV1 架构的全貌。深度卷积层是标记为dw的行,而点卷积层则是具有 1×1 滤波器形状的层。请注意,每个dw层后面都跟着一个 1×1 卷积,这表明整个架构主要由深度可分离卷积组成。此外,如果你仔细观察架构,你会看到空间下采样是通过步长为 2 的深度卷积来完成的(注意表中带有s2的行)。在这里,你可以看到每次我们减半空间维度时,通道数翻倍以补偿空间信息的损失。

图 4. MobileNetV1 架构的全貌[1]。

宽度和分辨率乘数

MobileNet 的作者们通过引入所谓的宽度分辨率乘数,即正式表示为αρ,提出了一种新的参数调整机制。α参数可以从技术上自由调整,但作者建议使用 1.0、0.75、0.5 或 0.25。这个参数通过减少所有卷积层产生的通道数来工作。例如,如果我们把α设置为 0.5,网络中的第一个卷积层将把 3 通道输入转换为 16,而不是 32。另一方面,ρ用于调整输入张量的空间维度。需要注意的是,尽管理想情况下我们应该为这个参数分配一个浮点数,但在实践中,直接确定输入图像的实际分辨率更为可取。在这种情况下,作者建议使用 224、192、160 和 128,其中 224×224 的输入大小对应于ρ = 1。上面图 4 中显示的架构遵循默认配置,其中αρ都设置为 1。


实验结果

作者们进行了大量实验来证明 MobileNet 的鲁棒性。首先讨论的结果是下面图 5 中显示的,在这个实验中,他们试图找出深度可分离卷积层的使用如何影响性能。表格的第二行显示了我在图 4 中展示的架构的结果,而第一行是当层被替换为传统卷积时的结果。从这里我们可以看到,使用传统 CNN 的 MobileNet 的准确率确实高于使用深度可分离卷积的准确率。然而,如果我们考虑到乘法和加法(mult-adds)的数量以及参数计数,我们可以清楚地看到,具有传统卷积层的模型需要更多的计算成本和内存使用,仅仅是为了在准确率上略有提高。因此,通过深度可分离卷积,尽管 MobileNet 的模型复杂度显著降低,作者们证明了模型容量仍然很高。

图 5. 具有深度可分离卷积层(第二行)的 MobileNet 与其全卷积对应版本(第一行)的性能比较 [1]。

我之前解释的αρ参数主要用于提供灵活性,考虑到并非所有任务都需要最高级别的 MobileNet 能力。作者最初在 1000 类 ImageNet 数据集上进行了实验,但在实践中,我们可能只需要模型对具有较少类别的数据集进行分类。在这种情况下,选择这两个参数的较低值可能更可取,因为它可以加快推理过程,同时模型仍然有足够的容量来适应分类任务。更具体地说,关于α,使用较小的这个参数值会导致 MobileNet 的精度降低。但这是在 1000 类数据集上的结果。如果我们的数据集更简单且类别更少,使用较小的α可能仍然可行。下面图 6 中每个模型旁边写着的 1.0、0.75、0.5 和 0.25 的值对应于使用的α

图 6. 宽度乘数如何影响模型精度、操作次数和参数数量[1]。

同样,这也适用于ρ参数,它负责改变输入图像的分辨率。下面图 7 显示了当我们使用不同的输入分辨率时实验结果的样子。结果与前面图中的结果有些相似,即随着输入图像的减小,准确度得分降低。重要的是要记住,以这种方式降低输入分辨率也会减少操作次数,但不会影响参数数量。这本质上是因为被计数的参数是权重和偏差,在 CNN 的情况下,它们对应于内核内的值。因此,只要我们不变更卷积层的配置,参数数量将保持不变。另一方面,操作次数会随着输入分辨率的降低而减少,因为处理较小图像中的像素数量少于处理较大图像中的像素数量。

图 7. 输入分辨率如何影响模型精度、操作次数和参数数量[1]。

除了比较αρ的不同值之外,作者还比较了 MobileNet 与其他流行模型。我们可以从图 8 中看到,最大的 MobileNet 变体(使用最大的αρ)在保持最低的计算复杂度的同时,与 GoogLeNet(InceptionV1)和 VGG16 实现了相当的精度。这基本上是我将这篇文章命名为“The Tiny Giant”的原因——轻量级但强大。

图 8. MobileNet 在保持较低的计算复杂度和参数数量的同时,实现了与流行模型相当精度[1]。

此外,作者还比较了较小的 MobileNet 变体与其他小型模型。对我来说,图 9 中有趣的是,尽管 SqueezeNet 的参数计数低于 MobileNet,但 MobileNet 的操作数比 SqueezeNet 小 22 倍以上,同时仍然保持更高的准确率。

图 9. 与流行模型[1]相比,较小的 MobileNet 变体的性能。


MobileNetV1 实现

既然我们已经理解了 MobileNetV1 背后的理念,我们现在可以进入代码部分。我将要实现的架构基于图 4 中的表格。像往常一样,我们首先需要做的是导入所需的模块。

# Codeblock 1
import torch
import torch.nn as nn
from torchinfo import summary

接下来,我们初始化几个可配置的参数,以便我们可以根据需要调整模型大小。在下面的代码块 2 中,我将α表示为ALPHA,其值可以更改为 0.75、0.5 或 0.25,如果我们希望模型更小。我们没有为ρ指定任何变量,因为我们可以直接将IMAGE_SIZE更改为 192、160 或 128,正如我们之前讨论的那样。

# Codeblock 2
BATCH_SIZE  = 1
IMAGE_SIZE  = 224
IN_CHANNELS = 3
NUM_CLASSES = 1000
ALPHA       = 1

第一次卷积

如果我们回到图 4,我们可以看到 MobileNet 基本上只由重复的模式组成,即深度可分离卷积后跟点卷积。然而,请注意,图中的第一行并不遵循这个模式,因为它实际上只是一个标准的卷积层。由于这个原因,我们需要为这个创建一个单独的类,我在下面的代码块 3 中将其称为FirstConv

# Codeblock 3
class FirstConv(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(in_channels=3, 
                              out_channels=int(32*ALPHA),    #(1)
                              kernel_size=3,    #(2)
                              stride=2,         #(3)
                              padding=1,        #(4)
                              bias=False)       #(5)
        self.bn = nn.BatchNorm2d(num_features=int(32*ALPHA))
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.bn(self.conv(x)))
        return x

记住,MobileNet 遵循conv-BN-ReLU结构。因此,我们需要在这个类的__init__()方法中初始化这三个层。卷积层本身被设置为接受 3 个输入通道并输出 32 个通道。由于我们希望这个输出通道数是可调整的,我们需要在标记为#(1)的行中将它乘以ALPHA。记住,在乘法之后我们需要将数据类型改为整数,因为通道数使用浮点数是没有意义的。接下来,在#(2)#(3)行中,我们将内核大小设置为 3,步长设置为 2。使用这种配置,结果的张量空间维度将是输入的一半。此外,使用 3×3 大小的内核隐式地要求我们将填充设置为 1 以实现padding = same#(4))。在这种情况下,我们不会利用偏置项,这也是为什么我们将bias参数设置为False#(5))的原因。实际上,当我们使用conv-BN-ReLU结构时,这是一种标准做法,因为最终批归一化层会将卷积核的值分布重新集中在 0 周围,从而抵消卷积核内应用的偏置。

为了找出FirstConv类是否正常工作,我们将使用下面的 Codeblock 4 对其进行测试。在这里,我们初始化了层,并传递了一个模拟单个 224×224 大小的 RGB 图像的张量。您可以在生成的输出中看到,我们的卷积层成功地将空间维度下采样到 112×112,同时将通道数扩展到 32。

# Codeblock 4
first_conv = FirstConv()
x = torch.randn((1, 3, 224, 224))

out = first_conv(x)
out.shape
# Codeblock 4 Output
torch.Size([1, 32, 112, 112])

深度可分离卷积

首次卷积完成后,我们现在可以开始处理重复的深度卷积-逐点卷积层。由于这种模式是深度可分离卷积的核心思想,在下面的代码中,我将两种类型的卷积层封装在一个名为DepthwiseSeparableConv的类中。

# Codeblock 5
class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_channels, out_channels, downsample=False):  #(1)
        super().__init__()

        in_channels  = int(in_channels*ALPHA)    #(2)
        out_channels = int(out_channels*ALPHA)   #(3)       

        if downsample:    #(4)
            stride = 2
        else:
            stride = 1

        self.dwconv = nn.Conv2d(in_channels=in_channels,
                                out_channels=in_channels,     #(5)
                                kernel_size=3,                #(6)
                                stride=stride,                #(7)
                                padding=1,
                                groups=in_channels,           #(8)
                                bias=False)
        self.bn0 = nn.BatchNorm2d(num_features=in_channels)   #(9)

        self.pwconv = nn.Conv2d(in_channels=in_channels,   
                                out_channels=out_channels,    #(10)
                                kernel_size=1,                #(11)
                                stride=1,                     #(12)
                                padding=0,                    #(13)
                                groups=1,                     #(14)
                                bias=False)
        self.bn1 = nn.BatchNorm2d(num_features=out_channels)  #(15)

        self.relu = nn.ReLU()    #(16)

    def forward(self, x):
        print(f'original\t: {x.size()}')

        x = self.relu(self.bn0(self.dwconv(x)))
        print(f'after dw conv\t: {x.size()}')

        x = self.relu(self.bn1(self.pwconv(x)))
        print(f'after pw conv\t: {x.size()}')

        return x

与在初始化阶段不接收任何输入参数的FirstConv不同,在这里我们将DepthwiseSeparableConv类设置为接收几个输入,如上面的 Codeblock 5 中的#(1)行所示。我这样做是因为我们希望这个类在整个网络的所有深度可分离卷积层中都是可重用的,其中每个层的行为都略有不同。

在图 4 中,我们可以看到,在第一层将 3 通道图像扩展到 32 之后,这个通道数在后续过程中增加到 64、128,一直增加到 1024。这基本上是我将这个类设置为接受输入和输出通道数(in_channelsout_channels)的原因,这样我们就可以用灵活的通道配置初始化层。同时,我们也需要记住,我们需要根据ALPHA调整这些通道数。这可以通过代码中的#(2)#(3)行简单地完成。此外,在这里我还创建了一个名为downsample的标志作为输入参数,默认设置为False。这个标志负责确定层是否会减少空间维度。再次回到图 4,您会注意到有些情况下我们会将空间维度减半,也有一些其他情况下维度会被保留。每次我们想要执行下采样时,我们需要将步长设置为 2,但如果我们不这样做,我们将这个参数设置为 1(#(4))。

仍然使用上面的 Codeblock 5,接下来我们需要做的是初始化这些层本身。正如我们之前讨论过的,深度卷积负责捕捉像素之间的空间关系,这也是内核大小被设置为 3×3(#(6))的原因。为了使输入通道能够独立处理,我们可以简单地将groupsout_channels参数设置为与输入通道数相同(#(8)#(5))。值得注意的是,如果我们将out_channels设置为超过输入通道数——比如说,是两倍大——那么每个通道将由 2 个内核处理。最后,对于深度卷积层,行#(7)stride参数可以是 1 或 2,这取决于我们之前讨论过的下采样标志。

同时,点卷积使用 1×1 内核(#(11)),因为它不是为了捕捉空间信息。这实际上是我们将填充设置为 0(#(13))的原因,因为这个内核大小本身无法减少空间维度。另一方面,groups参数被设置为 1(#(14)),因为我们希望这个层能够一次性捕捉所有通道的信息。与深度卷积层不同,在这里我们可以使用所需的任意数量的内核,这对应于输出张量中的通道数(#(10))。同时,步长被固定设置为 1(#(12)),因为我们永远不会使用这个层进行下采样。

在这里,我们需要初始化两个独立的批量归一化层,放置在深度卷积和点卷积之后(#(9)#(15))。至于 ReLU 激活函数,我们只需要初始化一次(#(16)),因为它只是一个映射函数,没有任何可训练的参数。正因为如此,我们可以在网络中多次重用相同的 ReLU 实例。

现在,让我们通过传递一个虚拟张量通过它来检查我们的DepthwiseSeparableConv类是否正常工作。这里我为这个类准备了两个测试用例。第一个是我们不执行下采样的情况,第二个是我们执行下采样的情况。在下面的图 10 中,我想进行的两个测试分别使用了绿色和蓝色突出显示的层。

图片

图 10。我们将模拟以测试 DepthwiseSeparableConv 类的绿色和蓝色突出显示的层[1][2]。

要创建绿色部分,我们可以简单地使用DepthwiseSeparableConv类,并将输入和输出通道数设置为 32 和 64,如代码块 6 所示(#(1–2))。传递downsample = False并不是必需的,因为我们已经将其设置为默认配置(#(3))——但我还是这样做,只是为了清晰起见。虚拟张量x的形状也配置为 32×112×112,这与层的输入形状完全匹配(#(4))。

# Codeblock 6
depthwise_sep_conv = DepthwiseSeparableConv(in_channels=32,     #(1)
                                            out_channels=64,    #(2)
                                            downsample=False)   #(3)
x = torch.randn((1, int(32*ALPHA), 112, 112))                   #(4)

x = depthwise_sep_conv(x)

如果您运行上面的代码,屏幕上应该出现以下输出。在这里,您可以看到深度卷积层返回的张量与输入具有完全相同的形状(#(1))。在经过点卷积处理后的张量之后,通道数从 32 增加到 64(#(2))。这个结果证明我们的DepthwiseSeparableConv类在非下采样过程中工作正常。我们将在后续测试中使用这个输出张量作为蓝色层的输入。

# Codeblock 6 Output
original       : torch.Size([1, 32, 112, 112])
after dw conv  : torch.Size([1, 32, 112, 112])    #(1)
after pw conv  : torch.Size([1, 64, 112, 112])    #(2)

第二个测试与第一个测试非常相似,不同之处在于这里我们需要根据蓝色层的输入和输出通道数来配置模型。不仅如此,downsample参数还需要设置为True,因为我们希望层将空间维度减半。下面是代码块 7 的详细信息。

# Codeblock 7
depthwise_sep_conv = DepthwiseSeparableConv(in_channels=64, 
                                            out_channels=128,
                                            downsample=True)

x = depthwise_sep_conv(x)
# Codeblock 7 Output
original       : torch.Size([1, 64, 112, 112])
after dw conv  : torch.Size([1, 64, 56, 56])    #(1)
after pw conv  : torch.Size([1, 128, 56, 56])   #(2)

我们可以从上面的输出中看到,空间下采样工作正常,因为深度卷积层成功地将 112×112 的图像转换为 56×56(#(1))。在点卷积层的帮助下,通道轴最终扩展到 128,使其准备好输入到后续层。

基于我上面展示的两个测试,已经证明我们的DepthwiseSeparableConv类是正确的,因此可以用来构建整个 MobileNetV1 架构。


整个 MobileNetV1 架构

我将所有内容封装在一个类中,我称之为MobileNetV1。由于这个类相当长,我将其拆分为代码块 8a 和 8b。如果您想亲自运行这段代码,只需确保这两个代码块位于同一个笔记本单元中。

现在,让我们从这个类的__init__()方法开始。在这里要做的第一件事是初始化我们之前创建的FirstConv层(#(1))。接下来我们需要初始化的层是 MobileNet 的核心思想,即深度可分离卷积,其中每个这样的层都由深度卷积和点卷积组成。在这个实现中,我决定从depthwise_sep_conv0开始命名这些对,一直到depthwise_sep_conv8。如果你回到图 4,你会注意到下采样过程与非下采样层交替进行。这可以通过将层的downsample标志设置为True来实现,对于层号 1、3、5 和 7。depthwise_sep_conv6有点特殊,因为它实际上不是一个独立的层。相反,它是一系列具有相同规格的深度可分离卷积,重复了 5 次。

# Codeblock 8a
class MobileNetV1(nn.Module):
    def __init__(self):
        super().__init__()

        self.first_conv = FirstConv()    #(1)

        self.depthwise_sep_conv0 = DepthwiseSeparableConv(in_channels=32, 
                                                          out_channels=64)

        self.depthwise_sep_conv1 = DepthwiseSeparableConv(in_channels=64, 
                                                          out_channels=128, 
                                                          downsample=True)

        self.depthwise_sep_conv2 = DepthwiseSeparableConv(in_channels=128, 
                                                          out_channels=128)

        self.depthwise_sep_conv3 = DepthwiseSeparableConv(in_channels=128, 
                                                          out_channels=256, 
                                                          downsample=True)

        self.depthwise_sep_conv4 = DepthwiseSeparableConv(in_channels=256, 
                                                          out_channels=256)

        self.depthwise_sep_conv5 = DepthwiseSeparableConv(in_channels=256, 
                                                          out_channels=512, 
                                                          downsample=True)

        self.depthwise_sep_conv6 = nn.ModuleList(
            [DepthwiseSeparableConv(in_channels=512, out_channels=512) for _ in range(5)]
        )

        self.depthwise_sep_conv7 = DepthwiseSeparableConv(in_channels=512, 
                                                          out_channels=1024, 
                                                          downsample=True)

        self.depthwise_sep_conv8 = DepthwiseSeparableConv(in_channels=1024,  #(2)
                                                          out_channels=1024)

        num_out_channels = self.depthwise_sep_conv8.pwconv.out_channels      #(3)

        self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1,1))      #(4)
        self.fc = nn.Linear(in_features=num_out_channels,           #(5)
                            out_features=NUM_CLASSES)
        self.softmax = nn.Softmax(dim=1)                            #(6)

由于我们已经到达了最后的DepthwiseSeparableConv层(#(2)),接下来我们需要初始化三个额外的层:一个平均池化层(#(4))、一个全连接层(#(5))和一个 softmax 激活函数层(#(6))。你需要记住的一件事是,尽管depthwise_sep_conv8产生的输出通道数似乎固定为 1024,但实际上,如果我们改变ALPHA,这个输出通道数将会不同。为了使我们的实现能够适应这种变化,我们需要使用代码在第#(3)行生成的实际输出通道数,然后将其用作全连接层(#(5))的输入大小。

关于 Codeblock 8b 中的forward()方法,我认为没有必要进行解释,因为我们在这里基本上只是将一个张量从一层传递到后续层。

# Codeblock 8b
    def forward(self, x):
        x = self.first_conv(x)
        print(f"after first_conv\t\t: {x.shape}")

        x = self.depthwise_sep_conv0(x)
        print(f"after depthwise_sep_conv0\t: {x.shape}")

        x = self.depthwise_sep_conv1(x)
        print(f"after depthwise_sep_conv1\t: {x.shape}")

        x = self.depthwise_sep_conv2(x)
        print(f"after depthwise_sep_conv2\t: {x.shape}")

        x = self.depthwise_sep_conv3(x)
        print(f"after depthwise_sep_conv3\t: {x.shape}")

        x = self.depthwise_sep_conv4(x)
        print(f"after depthwise_sep_conv4\t: {x.shape}")

        x = self.depthwise_sep_conv5(x)
        print(f"after depthwise_sep_conv5\t: {x.shape}")

        for i, layer in enumerate(self.depthwise_sep_conv6):
            x = layer(x)
            print(f"after depthwise_sep_conv6 #{i}\t: {x.shape}")

        x = self.depthwise_sep_conv7(x)
        print(f"after depthwise_sep_conv7\t: {x.shape}")

        x = self.depthwise_sep_conv8(x)
        print(f"after depthwise_sep_conv8\t: {x.shape}")

        x = self.avgpool(x)
        print(f"after avgpool\t\t\t: {x.shape}")

        x = torch.flatten(x, start_dim=1)
        print(f"after flatten\t\t\t: {x.shape}")

        x = self.fc(x)
        print(f"after fc\t\t\t: {x.shape}")

        x = self.softmax(x)
        print(f"after softmax\t\t\t: {x.shape}")

        return x

现在我们通过运行以下测试代码来看看我们的 MobileNetV1 是否工作正常。

# Codeblock 9
mobilenetv1 = MobileNetV1()
x = torch.randn((BATCH_SIZE, IN_CHANNELS, IMAGE_SIZE, IMAGE_SIZE))

out = mobilenetv1(x)

下面是输出结果的样子。我们可以看到,我们的虚拟图像张量成功通过了first_conv层,一直到达最终的输出层。在卷积阶段,我们可以看到随着我们进入更深的层,空间维度减小了,同时通道数增加了。之后,我们应用了一个平均池化层,它通过从每个通道取平均值来工作。我们可以这样说,在这个点上,每个 7×7 大小的单个通道现在都表示为一个单一值,这实际上就是空间维度下降到 1×1(#(1))的原因。然后,这个张量被展平(#(2)),以便我们可以使用全连接层(#(3))进一步处理它。

# Codeblock 9 Output
after first_conv             : torch.Size([1, 32, 112, 112])
after depthwise_sep_conv0    : torch.Size([1, 64, 112, 112])
after depthwise_sep_conv1    : torch.Size([1, 128, 56, 56])
after depthwise_sep_conv2    : torch.Size([1, 128, 56, 56])
after depthwise_sep_conv3    : torch.Size([1, 256, 28, 28])
after depthwise_sep_conv4    : torch.Size([1, 256, 28, 28])
after depthwise_sep_conv5    : torch.Size([1, 512, 14, 14])
after depthwise_sep_conv6 #0 : torch.Size([1, 512, 14, 14])
after depthwise_sep_conv6 #1 : torch.Size([1, 512, 14, 14])
after depthwise_sep_conv6 #2 : torch.Size([1, 512, 14, 14])
after depthwise_sep_conv6 #3 : torch.Size([1, 512, 14, 14])
after depthwise_sep_conv6 #4 : torch.Size([1, 512, 14, 14])
after depthwise_sep_conv7    : torch.Size([1, 1024, 7, 7])
after depthwise_sep_conv8    : torch.Size([1, 1024, 7, 7])
after avgpool                : torch.Size([1, 1024, 1, 1])    #(1)
after flatten                : torch.Size([1, 1024])          #(2)
after fc                     : torch.Size([1, 1000])          #(3)
after softmax                : torch.Size([1, 1000])

如果你想了解更详细的架构,我们可以使用我们之前导入的 torchinfo 库中的 summary() 函数。如果你向下滚动下面的输出结果,我们可以看到这个模型大约包含 420 万个可训练参数,这个数字与图 5、6、7 和 8 中写下的数字相匹配。我也尝试用不同的 ALPHA 初始化相同的模型,并发现这些数字与图 6 中的表格相匹配。正因为如此,我认为我们的 MobileNetV1 实现是正确的。

# Codeblock 10
mobilenetv1 = MobileNetV1()
summary(mobilenetv1, input_size=(BATCH_SIZE, IN_CHANNELS, IMAGE_SIZE, IMAGE_SIZE))
# Codeblock 10 Output
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
MobileNetV1                              [1, 1000]                 --
├─FirstConv: 1-1                         [1, 32, 112, 112]         --
│    └─Conv2d: 2-1                       [1, 32, 112, 112]         864
│    └─BatchNorm2d: 2-2                  [1, 32, 112, 112]         64
│    └─ReLU: 2-3                         [1, 32, 112, 112]         --
├─DepthwiseSeparableConv: 1-2            [1, 64, 112, 112]         --
│    └─Conv2d: 2-4                       [1, 32, 112, 112]         288
│    └─BatchNorm2d: 2-5                  [1, 32, 112, 112]         64
│    └─ReLU: 2-6                         [1, 32, 112, 112]         --
│    └─Conv2d: 2-7                       [1, 64, 112, 112]         2,048
│    └─BatchNorm2d: 2-8                  [1, 64, 112, 112]         128
│    └─ReLU: 2-9                         [1, 64, 112, 112]         --
├─DepthwiseSeparableConv: 1-3            [1, 128, 56, 56]          --
│    └─Conv2d: 2-10                      [1, 64, 56, 56]           576
│    └─BatchNorm2d: 2-11                 [1, 64, 56, 56]           128
│    └─ReLU: 2-12                        [1, 64, 56, 56]           --
│    └─Conv2d: 2-13                      [1, 128, 56, 56]          8,192
│    └─BatchNorm2d: 2-14                 [1, 128, 56, 56]          256
│    └─ReLU: 2-15                        [1, 128, 56, 56]          --
├─DepthwiseSeparableConv: 1-4            [1, 128, 56, 56]          --
│    └─Conv2d: 2-16                      [1, 128, 56, 56]          1,152
│    └─BatchNorm2d: 2-17                 [1, 128, 56, 56]          256
│    └─ReLU: 2-18                        [1, 128, 56, 56]          --
│    └─Conv2d: 2-19                      [1, 128, 56, 56]          16,384
│    └─BatchNorm2d: 2-20                 [1, 128, 56, 56]          256
│    └─ReLU: 2-21                        [1, 128, 56, 56]          --
├─DepthwiseSeparableConv: 1-5            [1, 256, 28, 28]          --
│    └─Conv2d: 2-22                      [1, 128, 28, 28]          1,152
│    └─BatchNorm2d: 2-23                 [1, 128, 28, 28]          256
│    └─ReLU: 2-24                        [1, 128, 28, 28]          --
│    └─Conv2d: 2-25                      [1, 256, 28, 28]          32,768
│    └─BatchNorm2d: 2-26                 [1, 256, 28, 28]          512
│    └─ReLU: 2-27                        [1, 256, 28, 28]          --
├─DepthwiseSeparableConv: 1-6            [1, 256, 28, 28]          --
│    └─Conv2d: 2-28                      [1, 256, 28, 28]          2,304
│    └─BatchNorm2d: 2-29                 [1, 256, 28, 28]          512
│    └─ReLU: 2-30                        [1, 256, 28, 28]          --
│    └─Conv2d: 2-31                      [1, 256, 28, 28]          65,536
│    └─BatchNorm2d: 2-32                 [1, 256, 28, 28]          512
│    └─ReLU: 2-33                        [1, 256, 28, 28]          --
├─DepthwiseSeparableConv: 1-7            [1, 512, 14, 14]          --
│    └─Conv2d: 2-34                      [1, 256, 14, 14]          2,304
│    └─BatchNorm2d: 2-35                 [1, 256, 14, 14]          512
│    └─ReLU: 2-36                        [1, 256, 14, 14]          --
│    └─Conv2d: 2-37                      [1, 512, 14, 14]          131,072
│    └─BatchNorm2d: 2-38                 [1, 512, 14, 14]          1,024
│    └─ReLU: 2-39                        [1, 512, 14, 14]          --
├─ModuleList: 1-8                        --                        --
│    └─DepthwiseSeparableConv: 2-40      [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-1                  [1, 512, 14, 14]          4,608
│    │    └─BatchNorm2d: 3-2             [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-3                    [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-4                  [1, 512, 14, 14]          262,144
│    │    └─BatchNorm2d: 3-5             [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-6                    [1, 512, 14, 14]          --
│    └─DepthwiseSeparableConv: 2-41      [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-7                  [1, 512, 14, 14]          4,608
│    │    └─BatchNorm2d: 3-8             [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-9                    [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-10                 [1, 512, 14, 14]          262,144
│    │    └─BatchNorm2d: 3-11            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-12                   [1, 512, 14, 14]          --
│    └─DepthwiseSeparableConv: 2-42      [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-13                 [1, 512, 14, 14]          4,608
│    │    └─BatchNorm2d: 3-14            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-15                   [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-16                 [1, 512, 14, 14]          262,144
│    │    └─BatchNorm2d: 3-17            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-18                   [1, 512, 14, 14]          --
│    └─DepthwiseSeparableConv: 2-43      [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-19                 [1, 512, 14, 14]          4,608
│    │    └─BatchNorm2d: 3-20            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-21                   [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-22                 [1, 512, 14, 14]          262,144
│    │    └─BatchNorm2d: 3-23            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-24                   [1, 512, 14, 14]          --
│    └─DepthwiseSeparableConv: 2-44      [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-25                 [1, 512, 14, 14]          4,608
│    │    └─BatchNorm2d: 3-26            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-27                   [1, 512, 14, 14]          --
│    │    └─Conv2d: 3-28                 [1, 512, 14, 14]          262,144
│    │    └─BatchNorm2d: 3-29            [1, 512, 14, 14]          1,024
│    │    └─ReLU: 3-30                   [1, 512, 14, 14]          --
├─DepthwiseSeparableConv: 1-9            [1, 1024, 7, 7]           --
│    └─Conv2d: 2-45                      [1, 512, 7, 7]            4,608
│    └─BatchNorm2d: 2-46                 [1, 512, 7, 7]            1,024
│    └─ReLU: 2-47                        [1, 512, 7, 7]            --
│    └─Conv2d: 2-48                      [1, 1024, 7, 7]           524,288
│    └─BatchNorm2d: 2-49                 [1, 1024, 7, 7]           2,048
│    └─ReLU: 2-50                        [1, 1024, 7, 7]           --
├─DepthwiseSeparableConv: 1-10           [1, 1024, 7, 7]           --
│    └─Conv2d: 2-51                      [1, 1024, 7, 7]           9,216
│    └─BatchNorm2d: 2-52                 [1, 1024, 7, 7]           2,048
│    └─ReLU: 2-53                        [1, 1024, 7, 7]           --
│    └─Conv2d: 2-54                      [1, 1024, 7, 7]           1,048,576
│    └─BatchNorm2d: 2-55                 [1, 1024, 7, 7]           2,048
│    └─ReLU: 2-56                        [1, 1024, 7, 7]           --
├─AdaptiveAvgPool2d: 1-11                [1, 1024, 1, 1]           --
├─Linear: 1-12                           [1, 1000]                 1,025,000
├─Softmax: 1-13                          [1, 1000]                 --
==========================================================================================
Total params: 4,231,976
Trainable params: 4,231,976
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 568.76
==========================================================================================
Input size (MB): 0.60
Forward/backward pass size (MB): 80.69
Params size (MB): 16.93
Estimated Total Size (MB): 98.22
==========================================================================================

结束

关于 MobileNetV1 的内容基本上就这些了。我非常鼓励你尝试玩转上面的模型。如果你想对其进行图像分类训练,你可以根据数据集中可用的类别数量调整输出层中的神经元数量。你也可以尝试探索不同的 αρ,以找到最适合你情况的准确性和效率的值。此外,由于这个实现实际上是从零开始的,因此也可以更改论文中未明确提到的其他一些内容,例如 depthwise_sep_conv6 层的重复次数,甚至可以使用大于 1 的 αρ。嗯,从我们的 MobileNetV1 实现中基本上有很多东西可以探索!你还可以在我的 GitHub 仓库 [3] 中访问这篇文章中使用的代码。

如果你在我的解释或代码中发现了任何错误,请随时评论。感谢阅读!


参考文献

[1] Andrew G. Howard 等人. MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications. Arxiv. arxiv.org/abs/1704.04861 [访问日期:2025 年 4 月 7 日].

[2] 图片最初由作者创建。

[3] MuhammadArdiPutra. 《微型巨兽》— MobileNetV1. GitHub. github.com/MuhammadArdiPutra/medium_articles/blob/main/The%20Tiny%20Giant%20-%20MobileNetV1.ipynb [访问日期:2025 年 4 月 7 日].

posted @ 2026-03-27 09:57  布客飞龙II  阅读(0)  评论(0)    收藏  举报