经典CNN模型(七):MobileNetV1(PyTorch详细注释版)

一. MobileNetV1 神经网络介绍

在传统卷积神经网络因内存和运算量庞大而难以适配移动及嵌入式设备的背景下,2017 年,Google 团队应运推出了 MobileNetV1,这是一种专为资源受限环境设计的轻量级深度学习模型。相较于传统网络如 VGG16,MobileNetV1 在仅牺牲 0.9%的准确率的前提下,实现了模型参数精简至 VGG16 的 1/32,大幅削减了计算复杂度,从而在确保较高识别精度的同时,特别适合在移动设备和嵌入式系统上高效运行。

MobileNetV1 的核心为:深度可分离卷积(Depthwise Separable Convolutions)

二. 概念拓展

1. 深度卷积 / DW卷积(Depthwise Convolution)

深度可分离卷积(Depthwise Separable Convolution)是卷积神经网络(CNNs)中一种高效的卷积方法,它通过将标准卷积操作分解为两步来降低计算复杂度。深度可分离卷积的核心组成部分之一是深度卷积(Depthwise Convolution),简称 DW卷积。下面我将详细说明 DW 卷积的工作原理。

标准卷积与深度卷积的区别

在标准卷积中,每一个输出通道都是通过所有输入通道的卷积加权求和得到的,这意味着每一个输出通道都是所有输入通道信息的融合。这种卷积方式在计算上非常昂贵,因为它涉及到大量的乘法和加法操作。

相比之下,深度卷积(DW卷积)只在每个输入通道上单独应用卷积核,且输出通道数等于输入通道数。也就是说,每个输入通道都有自己的独立卷积核,这个卷积核只对该通道进行操作,并产生一个对应的输出通道。因此,DW 卷积不会在不同输入通道间共享信息,而是分别处理每个通道的信息。

在这里插入图片描述

深度卷积的数学描述

假设我们有一个输入张量 (I) 其形状为 (N, H, W, C),其中 (N) 表示样本数量,(H) 和 (W) 分别表示高度和宽度,而 (C) 是通道数。一个标准的卷积层会用一个 (K, K, C, D) 形状的卷积核来生成一个新的 (D) 通道的输出张量。这里的 (K) 是卷积核的大小,而 (D) 是输出通道数。

然而,在深度卷积中,对于每一个输入通道,我们都有一个独立的卷积核,这个卷积核的形状是 (K, K, 1),因为它是专门作用于单个通道的。因此,对于 (C) 个输入通道,我们将会有 (C) 个这样的卷积核,它们将分别作用于各自的输入通道上,生成同样具有 (C) 个通道的输出张量。

深度卷积是深度可分离卷积的一个组成部分,它通过将标准卷积分解为更简单的操作,大大降低了计算成本,同时保持了模型的准确性。这种技术在 MobileNet 系列模型中得到了广泛应用,使得深度学习能够在资源受限的环境中得以部署。

2. 深度可分离卷积(Depthwise Separable Conv)

深度可分离卷积(Depthwise Separable Convolution)是卷积神经网络(CNN)中用于提高计算效率的一种技术。它由两个连续的卷积层组成:深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)。下面详细介绍这两个步骤以及深度可分离卷积的整体工作原理。
在这里插入图片描述

深度卷积(Depthwise Convolution)

深度卷积是在每个输入通道上单独执行卷积操作。不同于传统的卷积层,其中每个输出通道都是所有输入通道的加权和,深度卷积为每个输入通道使用一个独立的卷积核。这意味着如果输入有(C)个通道,那么就有(C)个卷积核,每个核只作用于一个输入通道。这样,每个输入通道的信息被单独处理,而不是混合在一起。

在这里插入图片描述

逐点卷积(Pointwise Convolution)

逐点卷积也被称为 1x1 卷积,它是在深度卷积之后执行的。这个卷积层不涉及任何滑动窗口,而是对每个位置的像素点执行线性变换。逐点卷积的作用是改变输入特征图的深度(即通道数),可以用来增加或减少通道数,从而实现特征的混合和变换。

在这里插入图片描述

深度可分离卷积的组合

深度可分离卷积由深度卷积和逐点卷积组合而成。深度卷积首先处理空间信息,而逐点卷积则负责处理通道之间的关系。这种分离使得深度可分离卷积比传统卷积在计算效率上有显著提高,同时保持了良好的模型性能。

计算复杂度比较

在这里插入图片描述

这张图片展示了深度可分离卷积(Depthwise Separated Convolution)与普通卷积(Regular Convolution)在计算复杂度上的对比。从左到右,我们可以看到:

  1. 图片左侧显示的是一个典型的 3 通道输入,经过一组滤波器(Filters)和映射(Maps)的卷积过程。这里,M 代表输入特征图的深度,N 代表输出特征图的深度, D K D_K DK 代表卷积核的大小, D F D_F DF 代表特征图的大小。
  2. 中间的公式展示了深度可分离卷积(DW+PW)的计算复杂度,它由深度卷积(Depthwise Convolution, DW)和逐点卷积(Pointwise Convolution, PW)两部分组成。深度卷积的计算复杂度为: D K ⋅ D K ⋅ M ⋅ D F ⋅ D F D_K \cdot D_K \cdot M \cdot D_F \cdot D_F DKDKMDFDF逐点卷积的计算复杂度为: M ⋅ N ⋅ D F ⋅ D F M \cdot N \cdot D_F \cdot D_F MNDFDF两者相加得到深度可分离卷积的总计算复杂度。
  3. 右侧的公式展示了普通卷积的计算复杂度,为 D K ⋅ D K ⋅ M ⋅ N ⋅ D F ⋅ D F D_K \cdot D_K \cdot M \cdot N \cdot D_F \cdot D_F DKDKMNDFDF
  4. 对比两者,可以看到普通卷积的计算复杂度是深度可分离卷积的 N + D K 2 N + D_K^2 N+DK2 倍。理论上,普通卷积的计算量是 DW+PW 的 8 到 9 倍,这是因为 N + D K 2 = N + 9 N + D_K^2 = N + 9 N+DK2=N+9

总结起来,深度可分离卷积通过将卷积分解为深度卷积和逐点卷积,大大降低了计算复杂度,使得模型在保持相似性能的同时,能够更好地适应资源受限的设备。

三. MobileNetV1 神经网络结构

MobileNetV1 是一种轻量级的深度卷积神经网络,主要用于移动设备和嵌入式系统的图像分类任务。它采用了深度可分离卷积(Depthwise Separable Convolution)来减少计算复杂度和模型大小,以便在资源受限的设备上运行。下面是 MobileNetV1 的基本结构概述:

  1. 输入层:接受 224x224x3 的 RGB 彩色图像作为输入。
  2. 基础块(Base Block):首先是一个标准的 3x3 卷积层,步长为 2,输出通道数为 32。
  3. 深度可分离卷积块(Depthwise Separable Convolution Blocks):接下来是一系列深度可分离卷积块,每个块由三个子层组成:
    • 深度卷积(Depthwise Convolution):使用 3x3 卷积核,步长为 1 或 2,输出通道数与输入相同。
    • 逐点卷积(Pointwise Convolution):使用 1x1 卷积核,步长为 1,将输入通道数转换为目标通道数。
    • 可选的 ReLU6 激活函数和批量归一化(Batch Normalization)。
  4. 全局平均池化(Global Average Pooling):将最后一个深度可分离卷积块的输出进行全局平均池化,得到一个向量。
  5. 全连接层(Fully Connected Layer):将池化后的向量送入一个全连接层,输出类别概率。
  6. Softmax 分类器:最后是一个 softmax 分类器,用于输出类别概率分布。
    在这里插入图片描述

Table 1 显示了 MobileNetV1 的具体参数配置,包括每层的类型(Type)、步长(Stride)、过滤器形状(Filter Shape)和输入尺寸(Input Size)。请注意,表格中的 “Conv” 表示标准卷积,“Conv dw” 表示深度卷积,“FC” 表示全连接层。此外,“dw” 后面的数字表示卷积核的大小,“s1” 和 “s2” 分别表示步长为 1 和 2。

MobileNetV1 使用了一系列的 depthwise separable convolution blocks,其中一些步长为 2,意味着特征图的尺寸减半。例如,第一个 depthwise separable block 的步长为 2,所以输入尺寸从 224x224x3 减小到了 112x112x32。接着,第二个 depthwise separable block 的步长也为 2,输入尺寸再次减半,变为 56x56x64。如此递推,直到最后一层的步长为 2 的 depthwise separable block,输入尺寸减小到 7x7x1024。最后,全局平均池化将特征图降维为一个向量,然后通过全连接层和 softmax 分类器得出最终的类别概率。

四. MobileNetV1 模型亮点

MobileNetV1 的亮点主要集中在以下几个方面:

  1. 深度可分离卷积(Depthwise Separable Convolutions):
    MobileNetV1 引入了深度可分离卷积的概念,将标准卷积操作分解为深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)。深度卷积在每个输入通道上独立进行卷积,而逐点卷积则对深度卷积的结果进行线性组合。这种方法显著减少了模型的参数数量和计算复杂度,使得模型更加轻量级。

  2. 模型大小与计算效率:
    通过深度可分离卷积,MobileNetV1 实现了极高的计算效率和较小的模型大小。相比于传统的卷积神经网络,如 VGG16,MobileNetV1 的参数数量仅有前者的 1/32,同时在计算复杂度上也大幅降低,这使得模型能够在移动设备和嵌入式系统上高效运行。

  3. 可扩展性:
    MobileNetV1 提供了宽度乘数(Width Multiplier)的概念,允许用户根据设备的计算能力调整模型的宽度,即模型中卷积层的输出通道数。这意味着用户可以根据硬件资源的不同,灵活地调整模型的复杂度和精度。

  4. 速度与精度的平衡:
    MobileNetV1 在减少模型大小和计算量的同时,依然保持了相当高的分类准确率。相比于VGG16,MobileNetV1 在ImageNet数据集上的 Top-1 准确率仅降低了 0.9%,但在模型大小和计算效率上却有着质的飞跃。

MobileNetV1 的亮点在于它通过深度可分离卷积实现了高效、轻量级的模型设计,同时保持了良好的分类性能,使得模型能够广泛应用于多种计算机视觉任务和设备上。

五. MobileNetV1代码实现

开发环境配置说明:本项目使用 Python 3.6.13 和 PyTorch 1.10.2 构建,适用于CPU环境。

  • model.py:定义网络模型
  • train.py:加载数据集并训练,计算 loss 和 accuracy,保存训练好的网络参数
  • predict.py:用自己的数据集进行分类测试
  1. model.py
# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision.datasets import CIFAR10
from torchvision.transforms import transforms
from torch.autograd import Variable


class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(DepthwiseSeparableConv, self).__init__()

        self.depthwise_conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1, groups=in_channels)
        self.pointwise_conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.depthwise_conv(x)
        x = self.pointwise_conv(x)
        x = self.relu(x)
        return x


class MobileNetV1(nn.Module):
    def __init__(self, num_classes=1000):
        super(MobileNetV1, self).__init__()

        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1)
        self.relu = nn.ReLU(inplace=True)

        self.dw_separable_conv1 = DepthwiseSeparableConv(32, 64)
        self.dw_separable_conv2 = DepthwiseSeparableConv(64, 128)
        self.dw_separable_conv3 = DepthwiseSeparableConv(128, 128)
        self.dw_separable_conv4 = DepthwiseSeparableConv(128, 256)
        self.dw_separable_conv5 = DepthwiseSeparableConv(256, 256)
        self.dw_separable_conv6 = DepthwiseSeparableConv(256, 512)
        self.dw_separable_conv7 = DepthwiseSeparableConv(512, 512)
        self.dw_separable_conv8 = DepthwiseSeparableConv(512, 512)
        self.dw_separable_conv9 = DepthwiseSeparableConv(512, 512)
        self.dw_separable_conv10 = DepthwiseSeparableConv(512, 512)
        self.dw_separable_conv11 = DepthwiseSeparableConv(512, 512)
        self.dw_separable_conv12 = DepthwiseSeparableConv(512, 1024)
        self.dw_separable_conv13 = DepthwiseSeparableConv(1024, 1024)

        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(1024, num_classes)

    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)

        x = self.dw_separable_conv1(x)
        x = self.dw_separable_conv2(x)
        x = self.dw_separable_conv3(x)
        x = self.dw_separable_conv4(x)
        x = self.dw_separable_conv5(x)
        x = self.dw_separable_conv6(x)
        x = self.dw_separable_conv7(x)
        x = self.dw_separable_conv8(x)
        x = self.dw_separable_conv9(x)
        x = self.dw_separable_conv10(x)
        x = self.dw_separable_conv11(x)
        x = self.dw_separable_conv12(x)
        x = self.dw_separable_conv13(x)

        x = self.avg_pool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x
  1. train.py
import torch
import torch.nn as nn
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.optim as optim
from model import MobileNetV1
import os
import json
import torchvision.models.mobilenet


device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# print(device)

data_transform = {
    "train" : transforms.Compose([transforms.RandomResizedCrop(224),   # 随机裁剪
                                  transforms.RandomHorizontalFlip(),   # 随机翻转
                                  transforms.ToTensor(),
                                  transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
    "val" : transforms.Compose([transforms.Resize(256),      # 长宽比不变,最小边长缩放到256
                                transforms.CenterCrop(224),  # 中心裁剪到 224x224
                                transforms.ToTensor(),
                                transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}

#   获取数据集所在的根目录
#   通过os.getcwd()获取当前的目录,并将当前目录与".."链接获取上一层目录
data_root = os.path.abspath(os.path.join(os.getcwd(), ".."))

#   获取花类数据集路径
image_path = data_root + "/data_set/flower_data/"

#   加载数据集
train_dataset = datasets.ImageFolder(root=image_path + "/train",
                                     transform=data_transform["train"])

#   获取训练集图像数量
train_num = len(train_dataset)

#   获取分类的名称
#   {'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
flower_list = train_dataset.class_to_idx

#   采用遍历方法,将分类名称的key与value反过来
cla_dict = dict((val, key) for key, val in flower_list.items())

#   将字典cla_dict编码为json格式
json_str = json.dumps(cla_dict, indent=4)
with open("class_indices.json", "w") as json_file:
    json_file.write(json_str)

batch_size = 16
train_loader = DataLoader(train_dataset,
                          batch_size=batch_size,
                          shuffle=True,
                          num_workers=0)

validate_dataset = datasets.ImageFolder(root=image_path + "/val",
                                        transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = DataLoader(validate_dataset,
                             batch_size=batch_size,
                             shuffle=True,
                             num_workers=0)

#   定义模型
net = MobileNetV1(num_classes=5)   # 实例化模型
net.to(device)
# model_weight_path = "./mobilenet_v2.pth"
# #   载入模型权重
# pre_weights = torch.load(model_weight_path)
# #   删除分类权重
# pre_dict = {k: v for k, v in pre_weights.items() if "classifier" not in k}
# missing_keys, unexpected_keys = net.load_state_dict(pre_dict, strict=False)
# #   冻结除最后全连接层以外的所有权重
# for param in net.features.parameters():
#     param.requires_grad = False

loss_function = nn.CrossEntropyLoss()   # 定义损失函数
#pata = list(net.parameters())   # 查看模型参数
optimizer = optim.Adam(net.parameters(), lr=0.0001)  # 定义优化器

#   设置存储权重路径
save_path = './mobilenetV1.pth'
best_acc = 0.0
for epoch in range(1):
    # train
    net.train()  # 用来管理Dropout方法:训练时使用Dropout方法,验证时不使用Dropout方法
    running_loss = 0.0  # 用来累加训练中的损失
    for step, data in enumerate(train_loader, start=0):
        #   获取数据的图像和标签
        images, labels = data

        #   将历史损失梯度清零
        optimizer.zero_grad()

        #   参数更新
        outputs = net(images.to(device))                   # 获得网络输出
        loss = loss_function(outputs, labels.to(device))   # 计算loss
        loss.backward()                                    # 误差反向传播
        optimizer.step()                                   # 更新节点参数

        #   打印统计信息
        running_loss += loss.item()
        #   打印训练进度
        rate = (step + 1) / len(train_loader)
        a = "*" * int(rate * 50)
        b = "." * int((1 - rate) * 50)
        print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
    print()

    # validate
    net.eval()  # 关闭Dropout方法
    acc = 0.0
    #   验证过程中不计算损失梯度
    with torch.no_grad():
        for data_test in validate_loader:
            test_images, test_labels = data_test
            outputs = net(test_images.to(device))
            predict_y = torch.max(outputs, dim=1)[1]
            #   acc用来累计验证集中预测正确的数量
            #   对比预测值与真实标签,sum()求出预测正确的累加值,item()获取累加值
            acc += (predict_y == test_labels.to(device)).sum().item()
        accurate_test = acc / val_num
        #   如果当前准确率大于历史最优准确率
        if accurate_test > best_acc:
            #   更新历史最优准确率
            best_acc = accurate_test
            #   保存当前权重
            torch.save(net.state_dict(), save_path)
        #   打印相应信息
        print("[epoch %d] train_loss: %.3f  test_accuracy: %.3f"%
              (epoch + 1, running_loss / step, acc / val_num))

print("Finished Training")
  1. predict.py
import os
import json

import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt

from model import MobileNetV1


def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    data_transform = transforms.Compose(
        [transforms.Resize(256),
         transforms.CenterCrop(224),
         transforms.ToTensor(),
         transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])

    # load image
    img_path = "./郁金香.png"
    assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
    img = Image.open(img_path)
    plt.imshow(img)
    # [N, C, H, W]
    img = data_transform(img)
    # expand batch dimension
    img = torch.unsqueeze(img, dim=0)

    # read class_indict
    json_path = './class_indices.json'
    assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)

    with open(json_path, "r") as f:
        class_indict = json.load(f)

    # create model
    model = MobileNetV1(num_classes=5).to(device)

    # load model weights
    weights_path = "./mobilenetV1.pth"
    assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
    model.load_state_dict(torch.load(weights_path, map_location=device))

    # prediction
    model.eval()
    with torch.no_grad():
        # predict class
        output = torch.squeeze(model(img.to(device))).cpu()
        predict = torch.softmax(output, dim=0)
        predict_cla = torch.argmax(predict).numpy()

    print_res = "class: {}   prob: {:.3}".format(class_indict[str(predict_cla)],
                                                 predict[predict_cla].numpy())
    plt.title(print_res)
    for i in range(len(predict)):
        print("class: {:10}   prob: {:.3}".format(class_indict[str(i)],
                                                  predict[i].numpy()))
    plt.show()


if __name__ == '__main__':
    main()

六. 参考内容

  1. 李沐. (2019). 动手学深度学习. 北京: 人民邮电出版社. [ISBN: 978-7-115-51364-9]
  2. 霹雳吧啦Wz. (202X). 深度学习实战系列 [在线视频]. 哔哩哔哩. URL
  3. PyTorch. (n.d.). PyTorch官方文档和案例 [在线资源]. URL
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值