第5章轻量级DCNN模型 视频讲解 本章学习目标  MobileNet系列  ShuffleNet系列  轻量级DCNN模型对比 第4章介绍了常用的DCNN模型,这些模型在推理过程中都有较大的计算量,很难应用在移动端或嵌入式平台等资源受限的硬件设备上。越来越多的DCNN模型设计开始关注于资源受限场景中的效率问题,提出了多种轻量级的DCNN模型。其主要的设计目标是在保证一定识别精度的前提下,尽可能地减少网络规模(参数量、计算量)。本章将详细介绍轻量级DCNN模型的设计思路,并介绍两种常用的轻量级DCNN模型。本章内容的框图如图51所示。 图51本章内容框图 目前比较成熟的轻量级网络包括Google公司的MobileNet系列,旷视公司的ShuffleNet系列等,这些轻量级的网络模型更适合CPU或是移动端硬件。如图52所示,2016年直至现在,业内提出了SqueezeNet、ShuffleNet、NSANet、MnasNet以及MobileNet(V1、V2和V3)等轻量级网络模型。这些模型使在移动终端、嵌入式设备中运行DCNN模型成为可能。本章将介绍两种常用的轻量级DCNN模型。 图52轻量级网络模型的发展示意图 5.1MobileNet系列 5.1.1MobileNet V1 表51 梳理了MobileNet V1的主要创新点,本节将分别介绍这些创新点。 表51MobileNet V1主要创新点梳理 特性作用 深度可分离卷积替代常规卷积操作,减少参数量及计算量 宽度因子调整神经网络中间产生特征大小的超参数 分辨率因子通过调整输入数据的尺寸,调整网络的计算量 批规范化加速训练收敛速度,提升准确度 1) 深度可分离卷积(Depthwise Separable Convolution) 卷积神经网络中,最费时间的就是其中的卷积运算,MobileNet系列提出通过Depth wise卷积与Pointwise卷积替代常规的卷积,从而降低参数量及运算成本。 常规的卷积如图53所示,对于一张5×5像素3通道彩色输入图片(尺寸5×5×3)。经过3×3卷积核的卷积层(假设输出通道数为4,则卷积核的尺度为3×3×3×4),最终输出4个特征图。深度可分离卷积是将一个完整的卷积运算分解为两步进行,即Depthwise Convolution(DW)与Pointwise Convolution(PW)。不同于常规卷积操作,Depthwise Convolution的一个卷积核负责图像的一个通道,一个通道只被一个卷积核卷积。而常规卷积中每个卷积核是同时操作输入图片的每个通道。同样是对于一张5×5像素3通道彩色输入图片(尺寸为5×5×3)。如图54所示,Depthwise Convolution完成后的特征图数量与输入层的通道数相同,无法扩展特征图。而且这种运算对输入层的每个通道独立进行卷积运算,没有有效地利用不同通道在相同空间位置上的特征信息。因此需要Pointwise Convolution来将这些特征图进行组合生成新的特征图,如图55所示。Pointwise Convolution的运算与常规卷积运算非常相似,它的卷积核的尺寸为 1×1×M,M为预生成的通道数。所以这里的卷积运算会将上一步的输出在深度方向上进行加权组合,生成新的特征图。有几个Pointwise Convolution卷积核就有几个输出特征图,深度可分离卷积(Depthwise Separable)操作的示意图如图56所示。 图53常规的卷积操作示意图 图54Depthwise Convolution操作示意图 图55Pointwise Convolution操作示意图 图56深度可分离卷积操作的示意图 2017年Google提出了MobileNet模型,上述的深度可分离卷积为最大的创新点。以下比较不同卷积方式的参数量(params)和计算量(MultiAdd),假设输入输出大小是一样的。 输入尺寸: hin×win×cin。 输出尺寸: hout×wout×cout。 卷积核大小: k。 标准卷积 params=k2cincout MultiAdd=k2cincout×houtwout Depthwise卷积 params=k2cin MultiAdd=k2cin×houtwout 深度可分离卷积 params=k2cin+cincout MultiAdd=k2cin×houtwout+cincout×houtwout 相对于标准卷积,深度可分离卷积理论上的加速比可达: 1cout+1k 其中,cin为输入通道数,cout为输出通道数,hout和wout为输出特征图的尺寸。 2) 宽度因子 MobileNet本身的网络结构已经比较小而且执行延迟较低,但为了适配一些特定的场景,MobileNet提供了称为宽度因子(Width Multiplier)的超参数。宽度因子可以调整神经网络中间产生特征大小。由于调整的是特征图通道数量,从而可以调整运算量。宽度因子简单来说就是新网络中每一个模块要使用的卷积核数量相较于标准的MobileNet比例。对于深度卷积结合1×1方式的卷积核,计算量为: MultiAdd=k2αcin×houtwout+αcin×αcout×houtwout 其中,α为宽度因子,α常用的配置为1、0.75、0.5、0.25; 当α等于1时就是标准的MobileNet。通过参数α可以非常有效地将计算量和参数数量约减到接近α的平方。表52为MobileNet V1使用不同宽度因子进行网络参数的调整时,在ImageNet上的准确率、计算量、参数数量之间的关系(每一个项中最前面的数字表示α的取值)。可以看到当输入分辨率固定为224×224时,随着宽度因子的减少,模型的计算量和参数越来越小。宽度因子为0.25时,MobileNet的准确率比标准版MobileNet低20%左右,但计算量和参数量几乎只有标准版MobileNet计算量和参数量的10%。对于计算资源和存储资源都十分紧张的移动端平台,通过宽度因子调节网络的参数量的方法非常实用,可以按需调整宽度因子,达到准确率与性能的平衡。宽度因子一般写在MobileNet的前面,例如α MobileNet,α为宽度因子。 表52MobileNet V1使用不同宽度因子的准确率、计算量、参数数量 宽 度 因 子准确率(Top1)计算量(Million)参数量(Million) 1.0MobileNet22470.6%5694.3 0.75MobileNet22468.4%3252.6 0.5MobileNet22463.7%1491.3 0.25MobileNet22450.6%410.5 3) 分辨率因子 MobileNet提供了另一个超参数——分辨率因子(Resolution Multiplier),供用户自定义网络结构。分辨率因子β的取值范围在[0,1]之间,是作用于每一个模块输入尺寸的约减因子,简单来说就是将输入数据以及由此在每一个模块产生的特征图都变小了,结合宽度因子α,深度卷积结合1×1方式的卷积核计算量为: MultiAdd=k2αcin×βhout×βwout+αcin×αcout×βhout×βwout 下表为MobileNet V1使用不同的β系数作用于标准MobileNet时,在ImageNet上对精度和计算量的影响(α固定为1.0)。可以表达为MobileNet-β*S,S为原尺寸224。 表53MobileNet V1使用不同分辨率因子的准确率、计算量、参数数量 分辨率因子准确率(Top1)计算量(Million)参数量(Million) 1.0MobileNet22470.6%5694.3 1.0MobileNet19269.1%4184.2 1.0MobileNet16067.2%2904.2 1.0MobileNet12864.6%1864.2 图像分辨率的变化不会引起参数量的变化,只会改变模型的计算量。如表53所示,224分辨率模型测试ImageNet数据集准确率为70.6%,192分辨率的模型准确率为69.1%,但是计算量减少了151M,在移动平台计算资源紧张的情况下,同样可以通过分辨率因子β调节网络输入特征图的分辨率,做模型精度与计算量的取舍。MobileNet V1的网络结构如表54所示。 表54MobileNet V1的网络结构 种类/步长滤波器尺寸输 入 尺 寸 Conv/s23×3×3×32224×224×3 Conv dw/s13×3×32112×112×32 Conv/s11×1×32×64112×112×32 Conv dw/s23×3×64112×112×64 Conv/s11×1×64×12856×56×64 Conv dw/s13×3×12856×56×128 Conv/s11×1×128×12856×56×128 Conv dw/s23×3×12856×56×128 Conv/s11×1×128×25628×28×128 Conv dw/s13×3×25628×28×256 Conv/s11×1×256×25628×28×256 Conv dw/s23×3×25628×28×256 Conv/s11×1×256×51214×14×256 5×Conv dw/s1 Conv/s13×3×512 1×1×512×51214×14×512 14×14×512 Conv dw/s23×3×512 14×14×512 Conv/s11×1×512×10247×7×512 Conv dw/s23×3×1024 7×7×1024 Conv/s11×1×1024×10247×7×1024 Avg Pool/s1Pool 7×77×7×1024 FC/s11024×10001×1×1024 Softmax/s1Classfier1×1×1000 5.1.2MobileNet V2 2018年,Google推出了MobileNet V2版本,引入了Inverted Residuals和Linear Bottlenecks两个模块。MobileNet V2主要借鉴了ResNet的残差网络的思想,在残差单元上进行了修改,成为Inverted Residuals模块。Linear Bottleneck将最后一层的激活函数从ReLU替换成线性激活函数,而其他层的激活函数依然是ReLU。 1) Inverted Residuals ResNet中提出的残差结构解决训练中随着网络深度的增加而出现的梯度消失问题,使反向传播过程中网络的浅层部分也能有效地得到梯度更新,从而提高特征的表达能力。在ResNet的残差结构的启发下,MobileNet的残差结构如图57所示。Inverted Residual,顾名思义,颠倒的残差,其与ResNet中经典的残差块的结构相反。 图57MobileNet残差结构示意图 图58MobileNet V1和MobileNet V2在深度可分离卷积模块结构上的差异 2) Linear Bottleneck Bottleneck结构首次被提出是在ResNet网络中。该结构第一层使用PW Convolution,第二层使用3×3大小卷积核进行DW Convolution,第三层使用逐点卷积。MobileNet中的Bottleneck结构最后一层PW Convolution使用的激活函数是Linear,所以称其为线性瓶颈结构(Linear Bottleneck)。图58展示了MobileNet V1和MobileNet V2在深度可分离卷积模块结构上的差异。MobileNet V2的网络结构如表55所示,创新点如表56所示。 表55MobileNet V2的网络结构 输入种类膨胀因子(t)输出特征图数量(c)重复次数(n)步长(s) 224×224×3Conv2d—3212 112×112×32Bottleneck11611 112×112×16Bottleneck62422 56×56×24Bottleneck63232 28×28×32Bottleneck66442 14×14×64Bottleneck69631 14×14×96Bottleneck616032 7×7×160Bottleneck632011 7×7×320Conv2d 1×1—128011 7×7×1280Avg Pool 7×7——1— 1×1×1280Conv2d 1×1—k— 表56MobileNet V2主要创新点 特性作用 线性瓶颈结构 (Linear Bottleneck)减少参数量及计算量 Inverted Residuals解决梯度消失问题,提高特征的表达能力 平均池化与逐点卷积代替全连接层,减少参数量 5.1.3MobileNet V3 2019年Google公司提出了MobileNet V3。首先,MobileNet V3中利用了5×5大小的深度卷积代替部分3×3的深度卷积,在神经结构搜索(NAS)技术在搜索MobileNet V3网络结构的过程中,发现使用5×5大小的卷积核比使用3×3大小的卷积核效果更好,且准确率更高; 其次,引入Squeezeandexcitation(SE)模块和HSwish(HS)激活函数以提高模型精度; 最后,结尾两层Pointwise Convolution不使用批规范化(Batch Norm)(MobileNet V3结构图中使用NBN标识部分)。MobileNet V3分为Large和Small两个版本,Large版本适用于计算和存储性能较高的平台,而Small版本适用于硬件性能较低的平台。 在嵌入式设备中计算Sigmoid函数是会耗费相当多的计算资源的,因此提出了HSwish(Hard Version of Swish)作为激活函数。而且随着网络的加深,非线性激活函数的成本也会随之减少。所以,只有在较深的层使用HSwish才能获得更大的优势,激活函数HSwish的表达式如下: H_Swish[x]=xReLU6(x+3)6 可以看出,其非线性结构在保持精度的情况下具有优势,ReLU6ReLU 6是普通的ReLU函数,但限制其最大输出为6。在量化时会避免数值精度的损失,而且具有运行快的特点。 MobileNet V3还引入SE模块,SE模块是一种轻量级的通道注意力模块。如图59所示,一个SE模块主要包含压缩(Squeeze)和激励(Excitation)两部分,W、H表示特征图宽与高,C表示通道数,则输入特征图大小为W×H×C。 图59Squeezeandexcitation模块示意图 表57和表58介绍了MobileNet V3 Large与MobileNet V3 Small的网络结构。同时,我们梳理了MobileNet V3主要创新点,如表59所示。 表57MobileNet V3 Large结构示意图 输入种类输出通道数量(c)SE激活函数步长(s) 224×224×3Conv2d16——HS2 112×112×16Bottleneck 3×316——ReLU1 112×112×16Bottleneck 3×324——ReLU2 56×56×24Bottleneck 3×324——ReLU1 56×56×24Bottleneck 5×540√ReLU2 28×28×40Bottleneck 5×540√ReLU1 28×28×40Bottleneck 5×540√ReLU1 28×28×40Bottleneck 3×380——HS2 14×14×80Bottleneck 3×380——HS1 14×14×80Bottleneck 3×380——HS1 14×14×80Bottleneck 3×380——HS1 14×14×80Bottleneck 3×3112√HS1 14×14×112Bottleneck 3×3112√HS1 14×14×112Bottleneck 5×5160√HS1 14×14×112Bottleneck 5×5160√HS2 7×7×160Bottleneck 5×5160√HS1 7×7×160Conv2d 1×1960——HS1 7×7×960Pool 7×7——HS— 1×1×960Conv2d 1×1 NBN1280—HS1 1×1×1280Conv2d 1×1 NBN—k— 表58MobileNet V3 Small的网络结构 输入种类输出通道数量(c)SE激活函数步长(s) 224×224×3Conv2d 3×316——HS2 112×112×24Bottleneck 3×316√ReLU2 56×56×24Bottleneck 3×324——ReLU2 28×28×24Bottleneck 3×324——ReLU1 28×28×40Bottleneck 5×540√HS1 14×14×40Bottleneck 5×540√HS1 14×14×40Bottleneck 5×548√HS1 14×14×40Bottleneck 5×548√HS1 14×14×48Bottleneck 5×596√HS1 14×14×96Bottleneck 5×596√HS2 7×7×96Bottleneck 5×596√HS1 7×7×96Bottleneck 5×5576√HS1 7×7×96Conv2d 1×1—√HS1 7×7×576Pool 7×71280—HS7 1×1×576Conv2d 1×1——HS1 1×1×1280Conv2d 1×1k—HS— 表59MobileNet V3主要创新点 特性作用 5×5卷积核代替3×3卷积 SE模块使得有效权重大,提高准确率 HSwish激活函数代替部分ReLU激活函数 表510给出了MobileNet系列与常用的DCNN模型在准确度、计算量、参数量的对比结果,可以参考实验结果,根据实际需求选择更适合的网络模型。 表510MobileNet系列与常用DCNN模型在准确度、计算量、参数量的对比结果 模型准确率(Top1)计算量(Million)参数量(Million) VGG1671.5%15300138 GoogleNet69.8%15506.8 MobileNet V170.6%5694.2 MobileNet V272.0%3263.4 MobileNet V3Large75.2%2195.4 MobileNet V3Small67.4%662.9 5.2ShuffleNet系列 5.2.1ShuffleNet V1 ShuffleNet是旷视科技公司提出的一种轻量化CNN模型,与MobileNet一样,主要是希望将其应用于移动端或嵌入式设备端。ShuffleNet的核心是采用了两种操作,Pointwise Group Convolution和Channel Shuffle,保证在保持精度的同时大幅地降低了模型的计算量。 Channel Shuffle的示意图如图510所示。如图510(a)所示,例如,输入的特征图的数量为N,该卷积层的滤波器数量为M,那么M个卷积核中的每一个滤波器都要和N个特征图做卷积,而后相加作为一个卷积的结果。如果引入分组操作的话,设小组数量为g,那么N个输入特征图就被分成g个小组,而M个卷积核也被分成g个小组,然后在做卷积操作的时候,第一个小组中的M/g个滤波器中的和第一个小组的N/g个输入特征图做卷积得到结果,第二个小组到最后一个小组同理。很显然,这种操作可以大幅地减少计算量,因为每个卷积核不再是与全部特征图做卷积,而是和一个组的特征图做卷积。但是如果多个分组操作叠加在一起的话,会产生边界效应,也就是某个输出层仅来自输入层的一小部分,导致训练的特征非常局限。ShuffleNet提出了通过Channel Shuffle来解决这个问题,如图510(b)所示在GConv2之前,先对其输入的特征图做分配,将每个小组分成多个更小的组(Subgroup),然后将不同小组的Subgroup作为GConv2的一个小组的输入,使得GConv2的每一个小组都能卷积输入到的所有小组的特征图,这和510(c)的思想是一样的。 图510Channel Shuffle与Group Convolution示意图 基于上面的设计理念,首先构造如图511所示的ShuffleNet的基本单元。ShuffleNet的基本单元是在残差单元的基础上改进而成的。511(a)为MobileNet残差单元的结构。如图511(b)所示,ShuffleNet进行如下改进。首先将1×1 的Depth Convolution替换成1×1的Group Convolution; 之后在第一个1×1卷积之后增加了一个Channel Shuffle操作。对于残差单元,如果步长(Stride)为1时,由于输入与输出尺度一致便可以直接相加,而当步长为2时,通道数增加,同时特征图的尺度减小,导致输入与输出不匹配。ShuffleNet提出的策略如图511(c)所示,对原输入采用步长为2的3×3平均池化,得到和输出一样大小的特征图,然后将得到的特征图与输出进行连接,而不是相加,其目的主要是降低计算量与参数大小。 图511ShuffleNet的基本单元 基于以上改进的ShuffleNet基本单元,设计的ShuffleNet V1模型结构如表511所示。 表511ShuffleNet V1模型结构 层输出尺寸K尺寸步长重复次数 输出通道(g groups) g=1g=2g=3g=4g=8 Image224×22433333 Conv1112×1123×32 MaxPool56×563×3212424242425 Stage2 28×28 21144200240272384 28×28 13144200240272384 Stage3 14×1421288400480544768 14×1417288400480544768 Stage4 7×72157680096010881536 7×71357680096010881536 GlobalPool1×17×7 FC10001000100010001000 Complexity143M140M137M133M137M 5.2.2ShuffleNet V2 旷视科技又提出了ShuffleNet V2,主要的想法是在设计网络结构时,需要同时考虑计算量FLOPs、内存的访问代价(Memory Access Cost,MAC)、并行化对应的时间,以及不同的部署环境ARM或者GPU。在考虑上述因素的前提下,提出网络设计方面的原则如下。 (G1) 卷积输入的通道数与输出的通道数应该尽量相同,这时MAC最小; (G2) 过多的Group Convolution会增加内存访问时间; (G3) 网络分支数量多会降低并行度; (G4) Element wise(逐个元素运算)的运算增加内存访问时间。在计算FLOPs时往往只考虑卷积中的乘法操作,Element wise(ReLU激活、偏置、单位加等)往往被忽略,这些操作看似数量很少,它对模型速度的影响却很大。 从上面的原则可以看出,ShuffleNet V1 与MobileNet 系列都存在可以改进的空间。ShuffleNet V2在ShuffleNet V1的基础上做的改进方案如下。 (1) 在输入层提出了Channel Split模型,代替原来Group Convolution的作用; (2) 在输出结果时采用连接(Concat)操作,替代以前的加(Add)操作; (3) 移除部分的Channel Shuffle操作。 图512对比了ShuffleNet V1和 V2的基本单元,图512(a)和图512(b)都是ShuffleNet V1里面的基本单元,图512(c)和图512(d)则为ShuffleNet V2的基本单元。 图512ShuffleNet V2的基本单元 在图512(c)中可以看到使用了Channel Split操作,这个操作将输入特征的通道c分成cc′和c′,其目的是尽量控制分支数,以满足G3原则。 相比于ShuffleNet V1,ShuffleNet V2基本单元中的两个1×1卷积不再是分组卷积,满足G2原则,另外两个分支都采用连接操作进行合并,这样使得每个基本单元的输入通道数和输出通道数一样,这个操作可以满足G1原则。 ShuffleNet V2的基本单元中已经没有了ShuffleNet V1中的Add和ReLU操作了,同时Depthwise卷积只在一个分支里面。Channel shuffle以及Channel split这三个Element wise的相关操作已经合并成为一个单独的Element wise操作,这个操作可以满足G4原则。 基于以上改进的ShuffleNet基本单元,设计的ShuffleNet V2模型结构如表512所示。 表512ShuffleNet V2模型结构 层输出尺寸K尺寸步长重复次数 输出通道 0.5×1×1.5×2× Image224×2243333 Conv1112×1123×32 MaxPool56×563×32 124242424 Stage2 28×2821 28×281348116176244 Stage3 14×1421 14×141796232352488 Stage4 7×721 7×713192464704976 Conv57×71×1111024102410242048 GlobalPool1×17×7 FC1000100010001000 FLOPs41M146M299M591M Weights1.4M2.3M3.5M7.4M 5.3轻量级DCNN模型对比 在ShuffleNet V2论文中对多种轻量级DCNN模型进行了对比实验,在COCO目标检测数据集中的检查实验结果如表513所示,介绍FLOPs为500M的情况。可以看出ShuffleNet V2在GPU平台上是最快的,精度也是最高的。 表513轻量级DCNN模型对比 模型mmAP/(%)GPU平台速度(Images/s) ShuffleNet V132.960 MobileNet V230.672 ShuffleNet V233.387 5.4项目实战 构建MobileNet V3和ShuffleNet V2网络模型,并进行测试。 代码清单 5.4.1MobileNet V3模型构建 1. #导入库 2. import torch 3. import torch.nn as nn 4. import torch.nn.functional as F 5. __all__ = ['MobileNet V3', 'mobilenetv3'] 6. 7. def conv_bn(inp, oup, stride, conv_layer=nn.Conv2d, norm_layer=nn.BatchNorm2d, nlin_layer=nn.ReLU): 8. return nn.Sequential( 9. conv_layer(inp, oup, 3, stride, 1, bias=False), 10. norm_layer(oup), 11. nlin_layer(inplace=True) 12. ) 13. 14. 15. def conv_1x1_bn(inp, oup, conv_layer=nn.Conv2d, norm_layer=nn.BatchNorm2d, nlin_layer=nn.ReLU): 16. return nn.Sequential( 17. conv_layer(inp, oup, 1, 1, 0, bias=False), 18. norm_layer(oup), 19. nlin_layer(inplace=True) 20. ) 21. 22. #定义H-Swish(HS)激活函数 23. class Hswish(nn.Module): 24. def __init__(self, inplace=True): 25. super(Hswish, self).__init__() 26. self.inplace = inplace 27. 28. def forward(self, x): 29. return x * F.relu6(x + 3., inplace=self.inplace) / 6. 30. 31. class Hsigmoid(nn.Module): 32. def __init__(self, inplace=True): 33. super(Hsigmoid, self).__init__() 34. self.inplace = inplace 35. 36. def forward(self, x): 37. return F.relu6(x + 3., inplace=self.inplace) / 6. 38. 39. #定义SE模块 40. class SEModule(nn.Module): 41. def __init__(self, channel, reduction=4): 42. super(SEModule, self).__init__() 43. self.avg_pool = nn.AdaptiveAvgPool2d(1) 44. self.fc = nn.Sequential( 45. nn.Linear(channel, channel // reduction, bias=False), 46. nn.ReLU(inplace=True), 47. nn.Linear(channel // reduction, channel, bias=False), 48. Hsigmoid() 49. # nn.Sigmoid() 50. ) 51. 52. def forward(self, x): 53. b, c, _, _ = x.size() 54. y = self.avg_pool(x).view(b, c) 55. y = self.fc(y).view(b, c, 1, 1) 56. return x * y.expand_as(x) 57. 58. 59. class Identity(nn.Module): 60. def __init__(self, channel): 61. super(Identity, self).__init__() 62. 63. def forward(self, x): 64. return x 65. 66. 67. def make_divisible(x, divisible_by=8): 68. import numpy as np 69. return int(np.ceil(x * 1. / divisible_by) * divisible_by) 70. 71. #定义Bottleneck 72. class MobileBottleneck(nn.Module): 73. def __init__(self, inp, oup, kernel, stride, exp, se=False, nl='RE'): 74. super(MobileBottleneck, self).__init__() 75. assert stride in [1, 2] 76. assert kernel in [3, 5] 77. padding = (kernel - 1) // 2 78. self.use_res_connect = stride == 1 and inp == oup 79. conv_layer = nn.Conv2d 80. norm_layer = nn.BatchNorm2d 81. if nl == 'RE': 82. nlin_layer = nn.ReLU # or ReLU6 83. elif nl == 'HS': 84. nlin_layer = Hswish 85. else: 86. raise NotImplementedError 87. if se: 88. SELayer = SEModule 89. else: 90. SELayer = Identity 91. 92. self.conv = nn.Sequential( 93. # pw 94. conv_layer(inp, exp, 1, 1, 0, bias=False), 95. norm_layer(exp), 96. nlin_layer(inplace=True), 97. # dw 98. conv_layer(exp, exp, kernel, stride, padding, groups=exp, bias=False), 99. norm_layer(exp), 100. SELayer(exp), 101. nlin_layer(inplace=True), 102. # pw-linear 103. conv_layer(exp, oup, 1, 1, 0, bias=False), 104. norm_layer(oup), 105. ) 106. 107. def forward(self, x): 108. if self.use_res_connect: 109. return x + self.conv(x) 110. else: 111. return self.conv(x) 112. 113. #定义MobileNet V2网络类 114. class MobileNet V3(nn.Module): 115. def __init__(self, n_class=1000, input_size=224, dropout=0.8, mode='small', width_mult=1.0): 116. super(MobileNet V3, self).__init__() 117. input_channel = 16 118. last_channel = 1280 119. if mode == 'large': 120. #对应表5-7的网络结构 121. mobile_setting = [ 122. # k, exp, c, se, nl, s, 123. [3, 16, 16, False, 'RE', 1], 124. [3, 64, 24, False, 'RE', 2], 125. [3, 72, 24, False, 'RE', 1], 126. [5, 72, 40, True, 'RE', 2], 127. [5, 120, 40, True, 'RE', 1], 128. [5, 120, 40, True, 'RE', 1], 129. [3, 240, 80, False, 'HS', 2], 130. [3, 200, 80, False, 'HS', 1], 131. [3, 184, 80, False, 'HS', 1], 132. [3, 184, 80, False, 'HS', 1], 133. [3, 480, 112, True, 'HS', 1], 134. [3, 672, 112, True, 'HS', 1], 135. [5, 672, 160, True, 'HS', 2], 136. [5, 960, 160, True, 'HS', 1], 137. [5, 960, 160, True, 'HS', 1], 138. ] 139. elif mode == 'small': 140. #对应表5-8的网络结构 141. mobile_setting = [ 142. # k, exp, c, se, nl, s, 143. [3, 16, 16, True, 'RE', 2], 144. [3, 72, 24, False, 'RE', 2], 145. [3, 88, 24, False, 'RE', 1], 146. [5, 96, 40, True, 'HS', 2], 147. [5, 240, 40, True, 'HS', 1], 148. [5, 240, 40, True, 'HS', 1], 149. [5, 120, 48, True, 'HS', 1], 150. [5, 144, 48, True, 'HS', 1], 151. [5, 288, 96, True, 'HS', 2], 152. [5, 576, 96, True, 'HS', 1], 153. [5, 576, 96, True, 'HS', 1], 154. ] 155. else: 156. raise NotImplementedError 157. 158. #构建第一层 159. assert input_size % 32 == 0 160. last_channel = make_divisible(last_channel * width_mult) if width_mult > 1.0 else last_channel 161. self.features = [conv_bn(3, input_channel, 2, nlin_layer=Hswish)] 162. self.classifier = [] 163. 164. #构建 mobile blocks 165. for k, exp, c, se, nl, s in mobile_setting: 166. output_channel = make_divisible(c * width_mult) 167. exp_channel = make_divisible(exp * width_mult) 168. self.features.append(MobileBottleneck(input_channel, output_channel, k, s, exp_channel, se, nl)) 169. input_channel = output_channel 170. 171. #构建最后几层 172. if mode == 'large': 173. last_conv = make_divisible(960 * width_mult) 174. self.features.append(conv_1x1_bn(input_channel, last_conv, nlin_layer=Hswish)) 175. self.features.append(nn.AdaptiveAvgPool2d(1)) 176. self.features.append(nn.Conv2d(last_conv, last_channel, 1, 1, 0)) 177. self.features.append(Hswish(inplace=True)) 178. elif mode == 'small': 179. last_conv = make_divisible(576 * width_mult) 180. self.features.append(conv_1x1_bn(input_channel, last_conv, nlin_layer=Hswish)) 181. #self.features.append(SEModule(last_conv)) # refer to paper Table2, but I think this is a mistake 182. self.features.append(nn.AdaptiveAvgPool2d(1)) 183. self.features.append(nn.Conv2d(last_conv, last_channel, 1, 1, 0)) 184. self.features.append(Hswish(inplace=True)) 185. else: 186. raise NotImplementedError 187. 188. #构建 nn.Sequential 189. self.features = nn.Sequential(*self.features) 190. 191. #构建分类器层 192. self.classifier = nn.Sequential( 193. nn.Dropout(p=dropout),# refer to paper section 6 194. nn.Linear(last_channel, n_class), 195. ) 196. 197. self._initialize_weights() 198. 199. def forward(self, x): 200. x = self.features(x) 201. x = x.mean(3).mean(2) 202. x = self.classifier(x) 203. return x 204. #权重初始化 205. def _initialize_weights(self): 206. for m in self.modules(): 207. if isinstance(m, nn.Conv2d): 208. nn.init.kaiming_normal_(m.weight, mode='fan_out') 209. if m.bias is not None: 210. nn.init.zeros_(m.bias) 211. elif isinstance(m, nn.BatchNorm2d): 212. nn.init.ones_(m.weight) 213. nn.init.zeros_(m.bias) 214. elif isinstance(m, nn.Linear): 215. nn.init.normal_(m.weight, 0, 0.01) 216. if m.bias is not None: 217. nn.init.zeros_(m.bias) 218. 219. 220. def mobilenetv3(pretrained=False, **kwargs): 221. model = MobileNet V3(**kwargs) 222. if pretrained: 223. state_dict = torch.load('mobilenetv3_small_67.4.pth.tar') 224. model.load_state_dict(state_dict, strict=True) 225. 226. return model 227. 228. 229. if __name__ == '__main__': 230. net = mobilenetv3(n_class=2) 231. print('mobilenetv3:\n', net) 232. print('Total params: %.2fM' % (sum(p.numel() for p in net.parameters())/1000000.0)) 233. input_size=(1, 3, 224, 224) 234. x = torch.randn(input_size) 235. out = net(x) 236. #构建SE模块 237. class SEModule(nn.Module): 238. def __init__(self, channel, reduction=4): 239. super(SEModule, self).__init__() 240. self.avg_pool = nn.AdaptiveAvgPool2d(1) 241. self.fc = nn.Sequential( 242. nn.Linear(channel, channel // reduction, bias=False), 243. nn.ReLU(inplace=True), 244. nn.Linear(channel // reduction, channel, bias=False), 245. Hsigmoid() 246. # nn.Sigmoid() 247. ) 248. 249. def forward(self, x): 250. b, c, _, _ = x.size() 251. y = self.avg_pool(x).view(b, c) 252. y = self.fc(y).view(b, c, 1, 1) 253. return x * y.expand_as(x) 254. 255. #Swish和H-Swish激活函数 256. import torch 257. from torch import nn 258. import torch.nn.functional as F 259. import numpy as np 260. import matplotlib.pyplot as plt 261. from torch.autograd import Variable 262. 263. class Hswish(nn.Module): 264. def __init__(self, inplace=True): 265. super(Hswish, self).__init__() 266. self.inplace = inplace 267. 268. def forward(self, x): 269. return x * F.relu6(x + 3., inplace=self.inplace) / 6. 270. 271. class Hsigmoid(nn.Module): 272. def __init__(self, inplace=True): 273. super(Hsigmoid, self).__init__() 274. self.inplace = inplace 275. 276. def forward(self, x): 277. return F.relu6(x + 3., inplace=self.inplace) / 6 278. 279. def _group_conv(x, filters, kernel, stride, groups): 280. """ 281. Group convolution 282. # Arguments 283. x: Tensor, input tensor of with `channels_last` or 'channels_first' data format 284. filters: Integer, number of output channels 285. kernel: An integer or tuple/list of 2 integers, specifying the 286. width and height of the 2D convolution window. 287. strides: An integer or tuple/list of 2 integers, 288. specifying the strides of the convolution along the width and height. 289. Can be a single integer to specify the same value for 290. all spatial dimensions. 291. groups: Integer, number of groups per channel 292. 293. # Returns 294. Output tensor 295. """ 296. 297. channel_axis = 1 if K.image_data_format() == 'channels_first' else -1 298. in_channels = K.int_shape(x)[channel_axis] 299. 300. #每组输入通道数量 301. nb_ig = in_channels // groups 302. #每组输出的通道数量 303. nb_og = filters // groups 304. 305. gc_list = [] 306. #确定过滤器的数量是否可被组的数量整除 307. assert filters % groups == 0 308. 309. for i in range(groups): 310. if channel_axis == -1: 311. x_group = Lambda(lambda z: z[:, :, :, i * nb_ig: (i + 1) * nb_ig])(x) 312. else: 313. x_group = Lambda(lambda z: z[:, i * nb_ig: (i + 1) * nb_ig, :, :])(x) 314. gc_list.append(Conv2D(filters=nb_og, kernel_size=kernel, strides=stride, 315. padding='same', use_bias=False)(x_group)) 316. 317. return Concatenate(axis=channel_axis)(gc_list) 5.4.2ShuffleNet V2模型构建 1. def split(x, groups): 2. out = x.chunk(groups, dim=1) 3. 4. return out 5. 6. 7. def shuffle( x, groups): 8. N, C, H, W = x.size() 9. out = x.view(N, groups, C // groups, H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W) 10. return out 11. #构建Shuffle类 12. class ShuffleUnit(nn.Module): 13. def __init__(self, in_channels, out_channels, stride): 14. super().__init__() 15. mid_channels = out_channels // 2 16. if stride > 1: 17. self.branch1 = nn.Sequential( 18. nn.Conv2d(in_channels, in_channels, 3, stride=stride, padding=1, groups=in_channels, bias=False), 19. nn.BatchNorm2d(in_channels), 20. nn.Conv2d(in_channels, mid_channels, 1, bias=False), 21. nn.BatchNorm2d(mid_channels), 22. nn.ReLU(inplace=True) 23. ) 24. self.branch2 = nn.Sequential( 25. nn.Conv2d(in_channels, mid_channels, 1, bias=False), 26. nn.BatchNorm2d(mid_channels), 27. nn.ReLU(inplace=True), 28. nn.Conv2d(mid_channels, mid_channels, 3, stride=stride, padding=1, groups=mid_channels, bias=False), 29. nn.BatchNorm2d(mid_channels), 30. nn.Conv2d(mid_channels, mid_channels, 1, bias=False), 31. nn.BatchNorm2d(mid_channels), 32. nn.ReLU(inplace=True) 33. ) 34. else: 35. self.branch1 = nn.Sequential() 36. self.branch2 = nn.Sequential( 37. nn.Conv2d(mid_channels, mid_channels, 1, bias=False), 38. nn.BatchNorm2d(mid_channels), 39. nn.ReLU(inplace=True), 40. nn.Conv2d(mid_channels, mid_channels, 3, stride=stride, padding=1, groups=mid_channels, bias=False), 41. nn.BatchNorm2d(mid_channels), 42. nn.Conv2d(mid_channels, mid_channels, 1, bias=False), 43. nn.BatchNorm2d(mid_channels), 44. nn.ReLU(inplace=True) 45. ) 46. self.stride = stride 47. def forward(self, x): 48. if self.stride == 1: 49. x1, x2 = split(x, 2) 50. out = torch.cat((self.branch1(x1), self.branch2(x2)), dim=1) 51. else: 52. out = torch.cat((self.branch1(x), self.branch2(x)), dim=1) 53. out = shuffle(out, 2) 54. return out 55. #构建ShuffleNet V2网络结构 56. class ShuffleNet V2(nn.Module): 57. def __init__(self, channel_num, class_num=settings.CLASSES_NUM): 58. super().__init__() 59. self.conv1 = nn.Sequential( 60. nn.Conv2d(3, 24, 3, stride=2, padding=1, bias=False), 61. nn.BatchNorm2d(24), 62. nn.ReLU(inplace=True) 63. ) 64. self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) 65. self.stage2 = self.make_layers(24, channel_num[0], 4, 2) 66. self.stage3 = self.make_layers(channel_num[0], channel_num[1], 8, 2) 67. self.stage4 = self.make_layers(channel_num[1], channel_num[2], 4, 2) 68. self.conv5 = nn.Sequential( 69. nn.Conv2d(channel_num[2], 1024, 1, bias=False), 70. nn.BatchNorm2d(1024), 71. nn.ReLU(inplace=True) 72. ) 73. self.avgpool = nn.AdaptiveAvgPool2d(1) 74. self.fc = nn.Linear(1024, class_num) 75. def make_layers(self, in_channels, out_channels, layers_num, stride): 76. layers = [] 77. layers.append(ShuffleUnit(in_channels, out_channels, stride)) 78. in_channels = out_channels 79. for i in range(layers_num - 1): 80. ShuffleUnit(in_channels, out_channels, 1) 81. return nn.Sequential(*layers) 82. def forward(self, x): 83. out = self.conv1(x) 84. out = self.maxpool(out) 85. out = self.stage2(out) 86. out = self.stage3(out) 87. out = self.stage4(out) 88. out = self.conv5(out) 89. out = self.avgpool(out) 90. out = out.flatten(1) 91. out = self.fc(out) 92. return out 5.5本章小结 本章介绍了两类近年来最受关注的轻量级DCNN模型,MobileNet系列和ShuffleNet系列,分别由Google公司与旷视科技提出。本章详细阐述了两种系列算法的设计思路、改进的原理与对比实验结果,并给出网络模型结构图、对比实验结果及代码。 5.6习题 1. 常用的轻量化DCNN模型有哪些? 2. 轻量化模型构建的基本思路有哪些? 3. 简述MobileNet系列算法主要的设计思路。 4. 简述ShuffleNet系列算法主要的设计思路。