【youcans动手学模型】ShuffleNet 模型

欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans



本文用 PyTorch 实现 ShuffleNet 网络模型,使用 CIFAR10 数据集训练模型,进行图像分类。


1. ShuffleNet 网络模型

ShuffleNet 是旷视科技提出的一种计算高效的轻量化模型,论文发表于 CVPR2017。

Xiangyu Zhang, Xinyu Zhou, Jian Sun, ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices, 2017

通讯作者 孙剑,曾任微软亚洲研究院首席研究员、西安交通大学人工智能学院院长、旷视研究院(Megvii Research)院长,2022年因病去世。

【论文下载地址】
ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices

【GitHub地址】:https://github.com/megvii-model/ShuffleNet-Series


1.1 模型简介

ShuffleNet 的设计目标是如何利用有限的计算资源来达到最好的模型精度,实现速度与精度之间的平衡。

ShuffleNet 的主要创新是:(1)使用分组卷积(pointwise group convolution)来取代 1*1 的逐点卷积,(2)提出信道混洗(channel shuffle)来保证信息在不同信道之间的流动,在保持精度的同时显著降低了计算量。

在这里插入图片描述


1.2 论文介绍

【论文摘要】

本文提出了一种计算效率极高的 CNN 架构 ShuffleNet,它是专为计算能力有限的移动设备(10-150 MFLOP)设计的。

ShuffleNet 体系结构采用了两种新的操作,分组逐点卷积(pointwise group convolution)和信道混洗(channel shuffle),在保持精度的同时大大降低了计算成本。

在 ImageNet 分类任务和 MS COCO 目标检测任务的实验中,证明了 ShuffleNet 结构的优越性能。在 40 MFLOP的计算能力下,ImageNet 分类任务的 top-1 误差比 MobileNet 低 7.8%。在 ARM 移动设备上,ShuffleNet 的运行速度比 AlexNet 提高约13倍。


【论文背景】

构建更深更大的卷积神经网络是解决主要视觉识别任务的主要趋势。准确率最高的深度神经网络通常有数百层和数千个通道,因此需要 billion级 FLOPs 的计算量,这就限制了此类模型只能用于高性能的服务器集群。而在实际应用中,常常需要使用几十到几百 MFLOP 的计算能力来达到尽量高的精度,例如无人机、机器人和智能手机等移动平台。现有的很多工作侧重于对基准网络进行修剪、压缩或低位表示,本文的目标则是针对移动计算设备探索一种高效的基础架构。

目前的 Xception 使用了可分离卷积,ResNeXt 提出了一种基于分组卷积和残差连接的模块化卷积块 ,实现性能和计算代价的平衡。但是 1*1 卷积的计算量仍然很大,效率较低,例如在 ResNeXt 中,只有 3*3 层配置分组卷积(group convolutions),每个残差单元中 1*1 卷积的计算量占 93.4%。

我们提出使用逐点群组卷积来降低 1*1 卷积的计算复杂度。 然而,分组卷积只在组内进行卷积,组和组之间没有信息交互 。为了解决这个问题,我们提出了一种新的通道混洗操作,以促进信息在特征通道中流动。对于给定的计算性能,ShuffleNet 支持更多的通道数量,有利于获取更多的编码信息。


【主要创新】

ShuffleNet 的主要创新是:

  • (1)使用分组卷积(pointwise group convolution)来取代 1*1 的逐点卷积。

  • (2)提出信道混洗(channel shuffle)来保证信息在不同信道之间的流动。

分组卷积(Group Convolution)

ResNeXt 验证了分组卷积的有效性,而 MobileNet 利用深度可分离卷积(极限的分组卷积)获得了很好的效果。

对于输入特征图 H ∗ W ∗ C i n H*W*C_{in} HWCin,分组卷积将其输入特征图分为 g 组,每组的通道数为 C i n / g C_{in}/g Cin/g,从而减少参数量和计算量。

在这里插入图片描述

通道混洗(Channel Shuffle)

分组卷积也有缺点,卷积层的每个通道只与上层分组中的几个通道相关,即每个通道的输出只来自一小部分的输入通道,不同组之间的特征图是相互隔离的,因而会降低信息的表达能力。

要解决这个问题,就要让信息在不同分组之间流动起来。这也是可分离卷积在逐通道的深度卷积之后,要使用逐点卷积的原因,目的是实现不同组之间的信息融合。使用 1*1 的逐点卷积实现不同组之间的信息融合,花费的计算量很大,效率很低。

我们提出了一种通道混洗方法来实现不同组之间的信息交流,允许分组卷积从不同的组中得到输入信息,输入和输出的各信道将会紧密相关。通道混洗方法并不进行卷积处理,只是对特征图的分组进行“重组”,从而极大地降低了计算量。

通道混洗的程序实现非常简单,只需要简单的维度操作和转置就可以实现均匀的混洗。

假定将输入层分为 g g g 组,总通道数为 C i n = g ∗ n C_{in}=g*n Cin=gn ,首先将通道维度拆分为 ( g , n ) (g, n) (g,n) 两个维度,然后将这两个维度转置变成 ( n , g ) (n,g) (n,g),最后重新 reshape 成一个维度。

在这里插入图片描述

一维 g ∗ n g*n gn → Reshape 为二维 ( g , n ) (g,n) (g,n) → Transpose 为 ( n , g ) (n,g) (n,g) → Flatten 为一维 ( n ∗ g ) (n*g) (ng)

利用通道混洗就可以充分发挥分组卷积的优点,而克服其缺点。 通道卷积也是可导的,这意味着它可以被嵌入到端到端训练(end to end training)的网络结构中。


【ShuffleNet 单元】

我们设计了小型的 ShuffleNet 单元,如下图所示。

在这里插入图片描述

图 (a) 中是一个残差模块,由两个 1*1 的逐点卷积和一个 3*3 的深度卷积 DW 组成,并使用残差连接。3*3 的深度卷积DW是瓶颈层(bottleneck)。

图 (b) 中是我们建立的 ShuffleNet 单元。对于图 (a) 中的残差模块,我们把两个 1*1 卷积替换成逐点分组卷积(GConv),并对第一个分组卷积输出信号进行信道混洗(Channel Shuffle)。

图 © 中是另一种 ShuffleNet 单元。我们使用了 stride=2 的 3*3 的深度卷积,这会使残差连接的特征图大小不同而无法相加,于是在支线上添加了 stride=2 的 3*3 均值池化,并用通道拼接(Concat)代替加法操作。这样处理的目的主要是降低计算量和参数量。

由于使用分组卷积和通道混洗,ShuffleNet 单元的计算复杂度更低。换句话说,对于给定的计算量限制,ShuffleNet 可以使用更多的特征通道数。


【模型结构】

ShuffleNet V1 网络模型的具体结构如下,其中 g 是分组数。

模型主要分为 3 个阶段,由 16个 ShuffleNet 单元堆叠而成。每个阶段特征图的空间尺寸减半,而通道数量倍增。

在这里插入图片描述

在 Stage2 阶段,由于输入通道数较少,因此没有使用分组卷积。


【模型性能】

ShuffleNet 模型性能如下表所示。

(1)采用通道混洗之后,模型的性能更好 ,证明了通道混洗的有效性。

(2)分组数量 g 越大,模型的性能越好。

(3)与轻量模型比较:与 MobileNet、SqueezeNet 模型相比,ShuffleNet 模型的计算复杂度更低,精度更好。

(4)与经典模型比较:与 VGG-16、GoogleNet、AlexNet 模型相比,在准确率相当的情况下 ShuffleNet 模型的计算复杂度更低。


2. 在 PyTorch 中定义 ShuffleNet V1 模型类

2.1 分组卷积与通道混洗

分组卷积与通道混洗是 ShuffleNet 的核心。

分组卷积(Group Convolution)可以直接在 PyTorch 的 nn.Conv2d 实现 ,通过参数 groups 设置。例程如下。

# 基本的卷积 BN-ReLU 层
class baseConv(nn.Module):
    def __init__(self, ch_in, ch_out, kernel_size, stride, groups, hasRelu=False):
        super(baseConv, self).__init__()
        pad = kernel_size//2
        self.baseconv = nn.Sequential(
            nn.Conv2d(in_channels=ch_in, out_channels=ch_out, kernel_size=kernel_size,
                      stride=stride, padding=pad, groups=groups, bias=False),
            nn.BatchNorm2d(ch_out),
            activate()
        )

    def forward(self, x):
        out = self.baseconv(x)
        return out

通道混洗(Channel Shuffle)的例程如下。

# 通道混洗 (Channel Shuffle)
def ChannelShuffle(x, groups):
    # Channel shuffle: [N,C,H,W] -> [N,g,C/g,H,W] -> [N,C/g,g,H,w] -> [N,C,H,W]
    N, C, H, W = x.size()
    g = groups
    return x.view(N, g, C//g, H, W).permute(0, 2, 1, 3, 4).reshape(N, C, H, W)

2.2 ShuffleNet 单元

我们首先实现 ShuffleNet 中 stride=1 的基本单元 :

class ShuffleNetUnit1(nn.Module):  # ShuffleNet unit for stride=1
    def __init__(self, in_channels, out_channels, groups=3):
        super(ShuffleNetUnit1, self).__init__()
        assert in_channels == out_channels
        assert out_channels % 4 == 0
        mid_channels = out_channels//4
        self.groups = groups
        self.group_conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1,
                                     groups=groups, stride=1)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.depthwise_conv3 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3,
                                         padding=1, stride=1, groups=mid_channels)
        self.bn4 = nn.BatchNorm2d(mid_channels)
        self.group_conv5 = nn.Conv2d(mid_channels, out_channels, kernel_size=1,
                                     stride=1, groups=groups)
        self.bn6 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        out = self.group_conv1(x)
        out = F.relu(self.bn2(out))
        out = ChannelShuffle(out, groups=self.groups)
        out = self.depthwise_conv3(out)
        out = self.bn4(out)
        out = self.group_conv5(out)
        out = self.bn6(out)
        out = F.relu(x + out)
        return out

然后实现 ShuffleNet 中 stride=2 的基本单元 :

class ShuffleNetUnit2(nn.Module):  # ShuffleNet unit for stride=2
    def __init__(self, in_channels, out_channels, groups=3):
        super(ShuffleNetUnit2, self).__init__()
        out_channels -= in_channels
        assert out_channels % 4 == 0
        mid_channels = out_channels//4
        self.groups = groups
        self.group_conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1,
                                     groups=groups, stride=1)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.depthwise_conv3 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3,
                                         padding=1, stride=2, groups=mid_channels)
        self.bn4 = nn.BatchNorm2d(mid_channels)
        self.group_conv5 = nn.Conv2d(mid_channels, out_channels,  kernel_size=1,
                                     groups=groups, stride=1)
        self.bn6 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        out = self.group_conv1(x)
        out = F.relu(self.bn2(out))
        out = ChannelShuffle(out, groups=self.groups)
        out = self.depthwise_conv3(out)
        out = self.bn4(out)
        out = self.group_conv5(out)
        out = self.bn6(out)
        x = F.avg_pool2d(x, 3, stride=2, padding=1)
        out = F.relu(torch.cat([x, out], dim=1))
        return out

2.3 自定义 ShuffleNet V1 模型类

最后定义 g = 3 g=3 g=3 的 ShuffleNet V1 模型类如下:

class ShuffleNetV1(nn.Module):  # ShuffleNet for groups=3
    def __init__(self, groups=3, in_channels=3, num_classes=100):
        super(ShuffleNetV1, self).__init__()

        self.conv1 = nn.Conv2d(in_channels, 24, 3, stride=2, padding=1)
        stage2_seq = [ShuffleNetUnit2(24, 240, groups=3)] + \
                     [ShuffleNetUnit1(240, 240, groups=groups) for i in range(3)]
        self.stage2 = nn.Sequential(*stage2_seq)
        stage3_seq = [ShuffleNetUnit2(240, 480, groups=3)] + \
                     [ShuffleNetUnit1(480, 480, groups=groups) for i in range(7)]
        self.stage3 = nn.Sequential(*stage3_seq)
        stage4_seq = [ShuffleNetUnit2(480, 960, groups=groups)] + \
                     [ShuffleNetUnit1(960, 960, groups=groups) for i in range(3)]
        self.stage4 = nn.Sequential(*stage4_seq)
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(960, num_classes)

    def forward(self, x):
        x = self.conv1(x)
        x = F.max_pool2d(x, 3, stride=2, padding=1)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        out = F.softmax(x, dim=1)
        return out

2.4 旷视版 ShuffleNet V1 模型类

旷视开源了 ShuffleNet Series 模型,涵盖了 ShuffleNet V1、V2、V2+ 等 6个模型,详见:https://github.com/megvii-model/ShuffleNet-Series。

# 定义 ShuffleV1Block
class ShuffleV1Block(nn.Module):
    def __init__(self, inp, oup, *, group, first_group, mid_channels, ksize, stride):
        super(ShuffleV1Block, self).__init__()
        self.stride = stride
        assert stride in [1, 2]

        self.mid_channels = mid_channels
        self.ksize = ksize
        pad = ksize // 2
        self.pad = pad
        self.inp = inp
        self.group = group

        if stride == 2:
            outputs = oup - inp
        else:
            outputs = oup

        branch_main_1 = [
            # pw
            nn.Conv2d(inp, mid_channels, 1, 1, 0, groups=1 if first_group else group, bias=False),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True),
            # dw
            nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
            nn.BatchNorm2d(mid_channels),
        ]
        branch_main_2 = [
            # pw-linear
            nn.Conv2d(mid_channels, outputs, 1, 1, 0, groups=group, bias=False),
            nn.BatchNorm2d(outputs),
        ]
        self.branch_main_1 = nn.Sequential(*branch_main_1)
        self.branch_main_2 = nn.Sequential(*branch_main_2)

        if stride == 2:
            self.branch_proj = nn.AvgPool2d(kernel_size=3, stride=2, padding=1)

    def forward(self, old_x):
        x = old_x
        x_proj = old_x
        x = self.branch_main_1(x)
        if self.group > 1:
            x = self.channel_shuffle(x)
        x = self.branch_main_2(x)
        if self.stride == 1:
            return F.relu(x + x_proj)
        elif self.stride == 2:
            return torch.cat((self.branch_proj(x_proj), F.relu(x)), 1)

    def channel_shuffle(self, x):
        batchsize, num_channels, height, width = x.data.size()
        assert num_channels % self.group == 0
        group_channels = num_channels // self.group

        x = x.reshape(batchsize, group_channels, self.group, height, width)
        x = x.permute(0, 2, 1, 3, 4)
        x = x.reshape(batchsize, num_channels, height, width)

        return x

    
# 定义 ShuffleNetV1_Megvii
class ShuffleNetV1_Megvii(nn.Module):
    def __init__(self, input_size=224, n_class=1000, model_size='2.0x', group=None):
        super(ShuffleNetV1_Megvii, self).__init__()
        print('model size is ', model_size)

        assert group is not None

        self.stage_repeats = [4, 8, 4]
        self.model_size = model_size
        if group == 3:
            if model_size == '0.5x':
                self.stage_out_channels = [-1, 12, 120, 240, 480]
            elif model_size == '1.0x':
                self.stage_out_channels = [-1, 24, 240, 480, 960]
            elif model_size == '1.5x':
                self.stage_out_channels = [-1, 24, 360, 720, 1440]
            elif model_size == '2.0x':
                self.stage_out_channels = [-1, 48, 480, 960, 1920]
            else:
                raise NotImplementedError
        elif group == 8:
            if model_size == '0.5x':
                self.stage_out_channels = [-1, 16, 192, 384, 768]
            elif model_size == '1.0x':
                self.stage_out_channels = [-1, 24, 384, 768, 1536]
            elif model_size == '1.5x':
                self.stage_out_channels = [-1, 24, 576, 1152, 2304]
            elif model_size == '2.0x':
                self.stage_out_channels = [-1, 48, 768, 1536, 3072]
            else:
                raise NotImplementedError

        # building first layer
        input_channel = self.stage_out_channels[1]
        self.first_conv = nn.Sequential(
            nn.Conv2d(3, input_channel, 3, 2, 1, bias=False),
            nn.BatchNorm2d(input_channel),
            nn.ReLU(inplace=True),
        )
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.features = []
        for idxstage in range(len(self.stage_repeats)):
            numrepeat = self.stage_repeats[idxstage]
            output_channel = self.stage_out_channels[idxstage+2]

            for i in range(numrepeat):
                stride = 2 if i == 0 else 1
                first_group = idxstage == 0 and i == 0
                self.features.append(ShuffleV1Block(input_channel, output_channel,
                                            group=group, first_group=first_group,
                                            mid_channels=output_channel // 4, ksize=3, stride=stride))
                input_channel = output_channel

        self.features = nn.Sequential(*self.features)

        self.globalpool = nn.AvgPool2d(7)

        self.classifier = nn.Sequential(nn.Linear(self.stage_out_channels[-1], n_class, bias=False))
        self._initialize_weights()

    def forward(self, x):
        x = self.first_conv(x)
        x = self.maxpool(x)
        x = self.features(x)

        x = self.globalpool(x)
        x = x.contiguous().view(-1, self.stage_out_channels[-1])
        x = self.classifier(x)
        return x

    def _initialize_weights(self):
        for name, m in self.named_modules():
            if isinstance(m, nn.Conv2d):
                if 'first' in name:
                    nn.init.normal_(m.weight, 0, 0.01)
                else:
                    nn.init.normal_(m.weight, 0, 1.0 / m.weight.shape[1])
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0.0001)
                nn.init.constant_(m.running_mean, 0)
            elif isinstance(m, nn.BatchNorm1d):
                nn.init.constant_(m.weight, 1)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0.0001)
                nn.init.constant_(m.running_mean, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)

例程 Begin_Shuffle_CIFAR_2.py 使用 ShuffleNet V1 模型(旷世开源版本)进行模型训练和推理,经过 20 轮左右的训练,使用验证集图片进行验证,模型准确率达到 85%。继续训练到50轮次,可以进一步降低训练损失函数值,验证集的准确率保持在 90% 左右。


3. 基于 ShuffleNetV1 模型的 CIFAR10 图像分类

3.1 PyTorch 建立神经网络模型的基本步骤

使用 PyTorch 建立、训练和使用神经网络模型的基本步骤如下。

  1. 准备数据集(Prepare dataset):加载数据集,对数据进行预处理。
  2. 建立模型(Design the model):实例化模型类,定义损失函数和优化器,确定模型结构和训练方法。
  3. 模型训练(Model trainning):使用训练数据集对模型进行训练,确定模型参数。
  4. 模型推理(Model inferring):使用训练好的模型进行推理,对输入数据预测输出结果。
  5. 模型保存与加载(Model saving/loading):保存训练好的模型,以便以后使用或部署。

以下按此步骤讲解 ShuffleNetV1 模型的例程。


3.2 加载 CIFAR10 数据集

通用数据集的样本结构均衡、信息高效,而且组织规范、易于处理。使用通用的数据集训练神经网络,不仅可以提高工作效率,而且便于评估模型性能。

PyTorch 提供了一些常用的图像数据集,预加载在 torchvision.datasets 类中。torchvision 模块实现神经网络所需的核心类和方法, torchvision.datasets 包含流行的数据集、模型架构和常用的图像转换方法。

CIFAR 数据集是一个经典的图像分类小型数据集,有 CIFAR10 和 CIFAR100 两个版本。CIFAR10 有 10 个类别,CIFAR100 有 100 个类别。CIFAR10 每张图像大小为 32*32,包括飞机、小汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车 10 个类别。CIFAR10 共有 60000 张图像,其中训练集 50000张,测试集 10000张。每个类别有 6000张图片,数据集平衡。

加载和使用 CIFAR 数据集的方法为:

torchvision.datasets.CIFAR10()
torchvision.datasets.CIFAR100()

CIFAR 数据集可以从官网下载:http://www.cs.toronto.edu/~kriz/cifar.html 后使用,也可以使用 datasets 类自动加载(如果本地路径没有该文件则自动下载)。

下载数据集时,使用预定义的 transform 方法进行数据预处理,包括调整图像尺寸、标准化处理,将数据格式转换为张量。标准化处理所使用 CIFAR10 数据集的均值和方差为 (0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)。transform_train在训练过程中,增加随机性,提高泛化能力。

大型训练数据集不能一次性加载全部样本来训练,可以使用 Dataloader 类自动加载数据。Dataloader 是一个迭代器,基本功能是传入一个 Dataset 对象,根据参数 batch_size 生成一个 batch 的数据。

需要说明的是,虽然 ShuffleNetV1 模型可以使用 32*32 的图片,但例程中将图像大小调整为 (w,h)=(224,224),可以获得更好的性能。

使用 DataLoader 类加载 CIFAR-10 数据集的例程如下。

    # (1) 将 [0,1] 的 PILImage 转换为[-1,1]的Tensor
    transform_train = transforms.Compose([
        transforms.RandomHorizontalFlip(),  # 随机水平翻转
        transforms.RandomRotation(10),  # 随机旋转
        transforms.RandomAffine(0, shear=10, scale=(0.9, 1.1)),
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
        transforms.Resize(224),  # 图像大小调整为 (w,h)=(224,224)
        transforms.ToTensor(),  # 将图像转换为张量 Tensor
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])
    # 测试集不需要进行数据增强
    transform = transforms.Compose([
        transforms.Resize(224),  # 图像大小调整为 (w,h)=(224,224)
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])

    # (2) 加载 CIFAR10 数据集
    batchsize = 128
    # 加载 CIFAR10 数据集, 如果 root 路径加载失败, 则自动在线下载
    # 加载 CIFAR10 训练数据集, 50000张训练图片
    train_set = torchvision.datasets.CIFAR10(root='../dataset', train=True,
                                            download=True, transform=transform_train)
    # train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize,
                                              shuffle=True, num_workers=8)
    # 加载 CIFAR10 验证数据集, 10000张验证图片
    test_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
                                           download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=256,
                                              shuffle=True, num_workers=8)
    # 创建生成器,用 next 获取一个批次的数据
    valid_data_iter = iter(test_loader)  # _SingleProcessDataLoaderIter 对象
    valid_images, valid_labels = next(valid_data_iter)  # images: [batch,3,32,32], labels: [batch]
    valid_size = valid_labels.size(0)  # 验证数据集大小,batch
    print(valid_images.shape, valid_labels.shape)

    # 定义类别名称,CIFAR10 数据集的 10个类别
    classes = ('plane', 'car', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck')

3.3 建立 ShuffleNetV1 网络模型

建立一个 ShuffleNetV1 网络模型进行训练,包括三个步骤:

  • 实例化 ShuffleNetV1 模型对象;
  • 设置训练的损失函数;
  • 设置训练的优化器。

torch.nn.functional 模块提供了各种内置损失函数,本例使用交叉熵损失函数 CrossEntropyLoss。

torch.optim 模块提供了各种优化方法,本例使用 Adam 优化器。注意要将 model 的参数 model.parameters() 传给优化器对象,以便优化器扫描需要优化的参数。

    # (3) 构造 ShuffleNet 网络模型/旷世开源版
    model = ShuffleNetV1_Megvii(input_size=224, n_class=10, model_size='0.5x', group=3)
    model.to(device)  # 将网络分配到指定的device中
    print(model)

    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()  # 定义损失函数 CrossEntropy
    optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4)  # 定义优化器 SGD

3.4 ShuffleNetV1 模型训练

PyTorch 模型训练的基本步骤是:

  1. 前馈计算模型的输出值;
  2. 计算损失函数值;
  3. 计算权重 weight 和偏差 bias 的梯度;
  4. 根据梯度值调整模型参数;
  5. 将梯度重置为 0(用于下一循环)。

在模型训练过程中,可以使用验证集数据评价训练过程中的模型精度,以便控制训练过程。模型验证就是用验证数据进行模型推理,前向计算得到模型输出,但不反向计算模型误差,因此需要设置 torch.no_grad()。

使用 PyTorch 进行模型训练的例程如下。

    # (4) 训练 ShuffleNet 模型
    epoch_list = []  # 记录训练轮次
    loss_list = []  # 记录训练集的损失值
    accu_list = []  # 记录验证集的准确率
    num_epochs = 50  # 训练轮次
    for epoch in range(num_epochs):  # 训练轮次 epoch
        running_loss = 0.0  # 每个轮次的累加损失值清零
        for step, data in enumerate(train_loader, start=0):  # 迭代器加载数据
            optimizer.zero_grad()  # 损失梯度清零
            inputs, labels = data  # inputs: [batch,3,224,224] labels: [batch]
            outputs = model(inputs.to(device))  # 正向传播
            loss = criterion(outputs, labels.to(device))  # 计算损失函数
            loss.backward()  # 反向传播
            optimizer.step()  # 参数更新

            # 累加训练损失值
            running_loss += loss.item()
            # if step%100==99:  # 每 100 个 step 打印一次训练信息
            #     print("\t epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))

        # 计算每个轮次的验证集准确率
        with torch.no_grad():  # 验证过程, 不计算损失函数梯度
            outputs_valid = model(valid_images.to(device))  # 模型对验证集进行推理, [batch, 10]
        pred_labels = torch.max(outputs_valid, dim=1)[1]  # 预测类别, [batch]
        accuracy = torch.eq(pred_labels, valid_labels.to(device)).sum().item() / valid_size * 100  # 计算准确率
        print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))

        # 记录训练过程的统计数据
        epoch_list.append(epoch)  # 记录迭代次数
        loss_list.append(running_loss)  # 记录训练集的损失函数
        accu_list.append(accuracy)  # 记录验证集的准确率

程序运行结果如下:

Epoch 0: train loss=656.7643, accuracy=59.77%
Epoch 1: train loss=483.2695, accuracy=68.36%
Epoch 2: train loss=403.0473, accuracy=70.31%

Epoch 48: train loss=129.2714, accuracy=89.45%
Epoch 49: train loss=126.6563, accuracy=90.62%

使用 ShuffleNet V1 模型(旷世开源版本)进行模型训练和推理,经过 20 轮的训练,使用验证集图片进行验证,模型准确率达到 85%。继续训练到 50轮次,训练损失函数值进一步降低,验证集的准确率保持在 90% 左右。

在这里插入图片描述


3.5 ShuffleNetV1 模型的保存与加载

模型训练好以后,将模型保存起来,以便下次使用。PyTorch 中模型保存主要有两种方式,一是保存模型权值,二是保存整个模型。本例使用 model.state_dict() 方法以字典形式返回模型权值,torch.save() 方法将权值字典序列化到磁盘,将模型保存为 .pth 文件。

    # (5) 保存 ShuffleNet 网络模型
    save_path = "../models/ShuffleNet_Cifar2"
    model_cpu = model.cpu()  # 将模型移动到 CPU
    model_path = save_path + ".pth"  # 模型文件路径
    torch.save(model.state_dict(), model_path)  # 保存模型权值
    # 优化结果写入数据文件
    result_path = save_path + ".csv"  # 优化结果文件路径
    WriteDataFile(epoch_list, loss_list, accu_list, result_path)

使用训练好的模型,首先要实例化模型类,然后调用 load_state_dict() 方法加载模型的权值参数。

    # 以下模型加载和模型推理,可以是另一个独立的程序
    # (6) 加载 ShuffleNet 网络模型进行推理
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 检测并指定设备
    # 加载 Squeeze 预训练模型
    model = ShuffleNetV1_Megvii(input_size=224, n_class=10, model_size='0.5x', group=3)
    model.to(device)  # 将网络分配到指定的device中
    model_path = "../models/ShuffleNet_Cifar2.pth"
    model.load_state_dict(torch.load(model_path))
    model.eval()  # 模型推理模式

需要特别注意的是:

(1)PyTorch 中的 .pth 文件只保存了模型的权值参数,而没有模型的结构信息,因此必须先实例化模型对象,再加载模型参数。

(2)模型对象必须与模型参数严格对应,才能正常使用。注意即使都是 ShuffleNetV1 模型,模型类的具体定义也可能有细微的区别。如果从一个来源获取模型类的定义,从另一个来源获取模型参数文件,就很容易造成模型结构与参数不能匹配。

(3)无论从 PyTorch 模型仓库加载的模型和参数,或从其它来源获取的预训练模型,或自己训练得到的模型,模型加载的方法都是相同的,也都要注意模型结构与参数的匹配问题。


3.6 模型检验

使用加载的 ShuffleNetV1 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。

使用测试集数据进行模型推理,根据模型预测结果与图片标签进行比较,可以检验模型的准确率。模型验证集与模型检验集不能交叉使用,但为了简化例程在本程序中未做区分。

    # (7) 模型检测
    correct = 0
    total = 0
    for data in test_loader:  # 迭代器加载测试数据集
        imgs, labels = data  # torch.Size([batch,3,224,224) torch.Size([batch])
        # print(imgs.shape, labels.shape)
        outputs = model(imgs.to(device))  # 正向传播, 模型推理, [batch, 10]
        labels_pred = torch.max(outputs, dim=1)[1]  # 模型预测的类别 [batch]
        # _, labels_pred = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += torch.eq(labels_pred, labels.to(device)).sum().item()
    accuracy = 100. * correct / total
    print("Test samples: {}".format(total))
    print("Test accuracy={:.2f}%".format(accuracy))

使用测试集进行模型推理,测试模型准确率为 87.04%。

Test samples: 10000
Test accuracy=87.04%


3.7 模型推理

使用加载的 ShuffleNetV1 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。

从测试集中提取几张图片,或者读取图像文件,进行模型推理,获得图片的分类类别。在提取图片或读取文件时,要注意对图片格式和图片大小进行适当的转换。

    # (8) 提取测试集图片进行模型推理
    batch = 8  # 批次大小
    data_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
                                           download=False, transform=None)
    plt.figure(figsize=(9, 6))
    for i in range(batch):
        imgPIL = data_set[i][0]  # 提取 PIL 图片
        label = data_set[i][1]  # 提取 图片标签
        # 预处理/模型推理/后处理
        imgTrans = transform(imgPIL)  # 预处理变换, torch.Size([3,224,224])
        imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1,3,224,224])
        outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
        indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
        index = indexes[0].item()  # 预测类别,整数
        # 绘制第 i 张图片
        imgNP = np.array(imgPIL)  # PIL -> Numpy
        out_text = "label:{}/model:{}".format(classes[label], classes[index])
        plt.subplot(2, 4, i+1)
        plt.imshow(imgNP)
        plt.title(out_text)
        plt.axis('off')
    plt.tight_layout()
    plt.show()

结果如下。

在这里插入图片描述

    # (9) 读取图像文件进行模型推理
    from PIL import Image
    filePath = "../images/img_car_01.jpg"  # 数据文件的地址和文件名
    imgPIL = Image.open(filePath)  # PIL 读取图像文件, <class 'PIL.Image.Image'>

    # 预处理/模型推理/后处理
    imgTrans = transform(imgPIL)  # 预处理变换, torch.Size([3,224,224])
    imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1,3,224,224])
    outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
    indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
    percentages = nn.functional.softmax(outputs, dim=1)[0] * 100
    index = indexes[0].item()  # 预测类别,整数
    percent = percentages[index].item()  # 预测类别的概率,浮点数

    # 绘制第 i 张图片
    imgNP = np.array(imgPIL)  # PIL -> Numpy
    out_text = "Prediction:{}, {}, {:.2f}%".format(index, classes[index], percent)
    print(out_text)
    plt.imshow(imgNP)
    plt.title(out_text)
    plt.axis('off')
    plt.tight_layout()
    plt.show()

结果如下。

在这里插入图片描述


4. 基于 ShuffleNetV1 模型对 CIFAR10 进行图像分类的完整例程

本文的完整例程如下。

# Begin_Shuffle_CIFAR_1.py
# Shuffle model for beginner with PyTorch
# 经典模型: ShuffleNet V1 模型 CIFAR10 图像分类
# Copyright: [email protected]
# Crated: Huang Shan, 2023/06/04

# _*_coding:utf-8_*_
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from matplotlib import pyplot as plt
import numpy as np

# https://iq.opengenus.org/shufflenet-implementation-using-pytorch/

# 通道重排
def ChannelShuffle(x, groups):
    # Channel shuffle: [N,C,H,W] -> [N,g,C/g,H,W] -> [N,C/g,g,H,w] -> [N,C,H,W]
    N, C, H, W = x.size()
    g = groups
    return x.view(N, g, C//g, H, W).permute(0, 2, 1, 3, 4).reshape(N, C, H, W)

class ShuffleNetUnit1(nn.Module):  # ShuffleNet unit for stride=1
    def __init__(self, in_channels, out_channels, groups=3):
        super(ShuffleNetUnit1, self).__init__()
        assert in_channels == out_channels
        assert out_channels % 4 == 0
        mid_channels = out_channels//4
        self.groups = groups
        self.group_conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1,
                                     groups=groups, stride=1)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.depthwise_conv3 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3,
                                         padding=1, stride=1, groups=mid_channels)
        self.bn4 = nn.BatchNorm2d(mid_channels)
        self.group_conv5 = nn.Conv2d(mid_channels, out_channels, kernel_size=1,
                                     stride=1, groups=groups)
        self.bn6 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        out = self.group_conv1(x)
        out = F.relu(self.bn2(out))
        out = ChannelShuffle(out, groups=self.groups)
        out = self.depthwise_conv3(out)
        out = self.bn4(out)
        out = self.group_conv5(out)
        out = self.bn6(out)
        out = F.relu(x + out)
        return out

class ShuffleNetUnit2(nn.Module):  # ShuffleNet unit for stride=2
    def __init__(self, in_channels, out_channels, groups=3):
        super(ShuffleNetUnit2, self).__init__()
        out_channels -= in_channels
        assert out_channels % 4 == 0
        mid_channels = out_channels//4
        self.groups = groups
        self.group_conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1,
                                     groups=groups, stride=1)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.depthwise_conv3 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3,
                                         padding=1, stride=2, groups=mid_channels)
        self.bn4 = nn.BatchNorm2d(mid_channels)
        self.group_conv5 = nn.Conv2d(mid_channels, out_channels,  kernel_size=1,
                                     groups=groups, stride=1)
        self.bn6 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        out = self.group_conv1(x)
        out = F.relu(self.bn2(out))
        out = ChannelShuffle(out, groups=self.groups)
        out = self.depthwise_conv3(out)
        out = self.bn4(out)
        out = self.group_conv5(out)
        out = self.bn6(out)
        x = F.avg_pool2d(x, 3, stride=2, padding=1)
        out = F.relu(torch.cat([x, out], dim=1))
        return out

class ShuffleNetV1(nn.Module):  # ShuffleNet for groups=3
    def __init__(self, groups=3, in_channels=3, num_classes=100):
        super(ShuffleNetV1, self).__init__()

        self.conv1 = nn.Conv2d(in_channels, 24, 3, stride=2, padding=1)
        stage2_seq = [ShuffleNetUnit2(24, 240, groups=3)] + \
                     [ShuffleNetUnit1(240, 240, groups=groups) for i in range(3)]
        self.stage2 = nn.Sequential(*stage2_seq)
        stage3_seq = [ShuffleNetUnit2(240, 480, groups=3)] + \
                     [ShuffleNetUnit1(480, 480, groups=groups) for i in range(7)]
        self.stage3 = nn.Sequential(*stage3_seq)
        stage4_seq = [ShuffleNetUnit2(480, 960, groups=groups)] + \
                     [ShuffleNetUnit1(960, 960, groups=groups) for i in range(3)]
        self.stage4 = nn.Sequential(*stage4_seq)
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(960, num_classes)

    def forward(self, x):
        x = self.conv1(x)
        x = F.max_pool2d(x, 3, stride=2, padding=1)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        out = F.softmax(x, dim=1)
        return out


# 优化结果写入数据文件
import pandas as pd
def WriteDataFile(epoch_list, loss_list, accu_list, filepath):
    # print("def WriteDataFile()")
    optRecord = {
    
    
        "epoch": epoch_list,
        "train_loss": loss_list,
        "accuracy": accu_list}
    dfRecord = pd.DataFrame(optRecord)
    dfRecord.to_csv(filepath, index=False, encoding="utf_8_sig")
    print("写入数据文件: %s 完成。" % filepath)
    return


if __name__ == '__main__':
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(device)

    # (1) 将 [0,1] 的 PILImage 转换为[-1,1]的Tensor
    transform_train = transforms.Compose([
        transforms.RandomHorizontalFlip(),  # 随机水平翻转
        transforms.RandomRotation(10),  # 随机旋转
        transforms.RandomAffine(0, shear=10, scale=(0.9, 1.1)),
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
        transforms.Resize(224),  # 图像大小调整为 (w,h)=(224,224)
        transforms.ToTensor(),  # 将图像转换为张量 Tensor
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])
    # 测试集不需要进行数据增强
    transform = transforms.Compose([
        transforms.Resize(224),  # 图像大小调整为 (w,h)=(224,224)
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])

    # (2) 加载 CIFAR10 数据集
    batchsize = 128
    # 加载 CIFAR10 数据集, 如果 root 路径加载失败, 则自动在线下载
    # 加载 CIFAR10 训练数据集, 50000张训练图片
    train_set = torchvision.datasets.CIFAR10(root='../dataset', train=True,
                                            download=True, transform=transform_train)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize,
                                           shuffle=True, num_workers=4)
    # 加载 CIFAR10 验证数据集, 10000张验证图片
    test_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
                                           download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=128,
                                           shuffle=True, num_workers=4)

    # 创建生成器,用 next 获取一个批次的数据
    valid_data_iter = iter(test_loader)  # _SingleProcessDataLoaderIter 对象
    valid_images, valid_labels = next(valid_data_iter)  # images: [batch,3,224,224], labels: [batch]
    valid_size = valid_labels.size(0)  # 验证数据集大小,batch
    print(valid_images.shape, valid_labels.shape)

    # 定义类别名称,CIFAR10 数据集的 10个类别
    classes = ('plane', 'car', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck')

    # (3) 构造 ShuffleNet 网络模型
    model = ShuffleNetV1(groups=3, in_channels=3, num_classes=10)  # 实例化 ShuffleNet 网络模型
    model.to(device)  # 将网络分配到指定的device中
    print(model)

    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()  # 定义损失函数 CrossEntropy
    optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4)  # 定义优化器 SGD

    # (4) 训练 ShuffleNet 模型
    epoch_list = []  # 记录训练轮次
    loss_list = []  # 记录训练集的损失值
    accu_list = []  # 记录验证集的准确率
    num_epochs = 50  # 训练轮次
    for epoch in range(num_epochs):  # 训练轮次 epoch
        running_loss = 0.0  # 每个轮次的累加损失值清零
        for step, data in enumerate(train_loader, start=0):  # 迭代器加载数据
            optimizer.zero_grad()  # 损失梯度清零
            inputs, labels = data  # inputs: [batch,3,224,224] labels: [batch]
            outputs = model(inputs.to(device))  # 正向传播
            loss = criterion(outputs, labels.to(device))  # 计算损失函数
            loss.backward()  # 反向传播
            optimizer.step()  # 参数更新

            # 累加训练损失值
            running_loss += loss.item()
            # if step%100==99:  # 每 100 个 step 打印一次训练信息
            #     print("\t epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))

        # 计算每个轮次的验证集准确率
        with torch.no_grad():  # 验证过程, 不计算损失函数梯度
            outputs_valid = model(valid_images.to(device))  # 模型对验证集进行推理, [batch, 10]
        pred_labels = torch.max(outputs_valid, dim=1)[1]  # 预测类别, [batch]
        accuracy = torch.eq(pred_labels, valid_labels.to(device)).sum().item() / valid_size * 100  # 计算准确率
        print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))

        # 记录训练过程的统计数据
        epoch_list.append(epoch)  # 记录迭代次数
        loss_list.append(running_loss)  # 记录训练集的损失函数
        accu_list.append(accuracy)  # 记录验证集的准确率

    # (5) 保存 ShuffleNet 网络模型
    save_path = "../models/ShuffleNet_Cifar1"
    model_cpu = model.cpu()  # 将模型移动到 CPU
    model_path = save_path + ".pth"  # 模型文件路径
    torch.save(model.state_dict(), model_path)  # 保存模型权值
    # 优化结果写入数据文件
    result_path = save_path + ".csv"  # 优化结果文件路径
    WriteDataFile(epoch_list, loss_list, accu_list, result_path)

    # 训练结果可视化
    plt.figure(figsize=(11, 5))
    plt.suptitle("ShuffleNet Model in CIFAR10")
    plt.subplot(121), plt.title("Train loss")
    plt.plot(epoch_list, loss_list)
    plt.xlabel('epoch'), plt.ylabel('loss')
    plt.subplot(122), plt.title("Valid accuracy")
    plt.plot(epoch_list, accu_list)
    plt.xlabel('epoch'), plt.ylabel('accuracy')
    plt.show()

    # # 以下模型加载和模型推理,可以是另一个独立的程序
    # # (6) 加载 ShuffleNet 网络模型进行推理
    # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 检测并指定设备
    # # 加载 Squeeze 预训练模型
    # model = ShuffleNetV1(num_classes=10)  # 实例化 ShuffleNet 网络模型
    # model.to(device)  # 将网络分配到指定的device中
    # model_path = "../models/ShuffleNet_Cifar1.pth"
    # model.load_state_dict(torch.load(model_path))
    # model.eval()  # 模型推理模式
    # 
    # # (7) 模型检测
    # correct = 0
    # total = 0
    # for data in test_loader:  # 迭代器加载测试数据集
    #     imgs, labels = data  # torch.Size([batch,3,224,224) torch.Size([batch])
    #     # print(imgs.shape, labels.shape)
    #     outputs = model(imgs.to(device))  # 正向传播, 模型推理, [batch, 10]
    #     labels_pred = torch.max(outputs, dim=1)[1]  # 模型预测的类别 [batch]
    #     # _, labels_pred = torch.max(outputs.data, 1)
    #     total += labels.size(0)
    #     correct += torch.eq(labels_pred, labels.to(device)).sum().item()
    # accuracy = 100. * correct / total
    # print("Test samples: {}".format(total))
    # print("Test accuracy={:.2f}%".format(accuracy))
    #
    # # (8) 提取测试集图片进行模型推理
    # batch = 8  # 批次大小
    # data_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
    #                                        download=False, transform=None)
    # plt.figure(figsize=(9, 6))
    # for i in range(batch):
    #     imgPIL = data_set[i][0]  # 提取 PIL 图片
    #     label = data_set[i][1]  # 提取 图片标签
    #     # 预处理/模型推理/后处理
    #     imgTrans = transform(imgPIL)  # 预处理变换, torch.Size([3,224,224])
    #     imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1,3,224,224])
    #     outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
    #     indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
    #     index = indexes[0].item()  # 预测类别,整数
    #     # 绘制第 i 张图片
    #     imgNP = np.array(imgPIL)  # PIL -> Numpy
    #     out_text = "label:{}/model:{}".format(classes[label], classes[index])
    #     plt.subplot(2, 4, i+1)
    #     plt.imshow(imgNP)
    #     plt.title(out_text)
    #     plt.axis('off')
    # plt.tight_layout()
    # plt.show()
    # 
    # # (9) 读取图像文件进行模型推理
    # from PIL import Image
    # filePath = "../images/img_car_01.jpg"  # 数据文件的地址和文件名
    # imgPIL = Image.open(filePath)  # PIL 读取图像文件, <class 'PIL.Image.Image'>
    # 
    # # 预处理/模型推理/后处理
    # imgTrans = transform(imgPIL)  # 预处理变换, torch.Size([3,224,224])
    # imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1,3,224,224])
    # outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
    # indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
    # percentages = nn.functional.softmax(outputs, dim=1)[0] * 100
    # index = indexes[0].item()  # 预测类别,整数
    # percent = percentages[index].item()  # 预测类别的概率,浮点数
    # 
    # 
    # # 绘制第 i 张图片
    # imgNP = np.array(imgPIL)  # PIL -> Numpy
    # out_text = "Prediction:{}, {}, {:.2f}%".format(index, classes[index], percent)
    # print(out_text)
    # plt.imshow(imgNP)
    # plt.title(out_text)
    # plt.axis('off')
    # plt.tight_layout()
    # plt.show()


参考文献:

  1. Xiangyu Zhang, Xinyu Zhou, Jian Sun, ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices, 2017

【本节完】


版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】ShuffleNet 模型
Copyright 2023 youcans, XUPT
Crated:2023-06-30


猜你喜欢

转载自blog.csdn.net/youcans/article/details/131482458
今日推荐