YOLOv5改进系列(6)——替换主干网络之 ShuffleNetV2
🚀 一、ShuffleNet介绍
ShuffleNet系列轻量级卷积神经网络由旷世提出,也是非常有趣的轻量级卷积神经网络,它提出了通道混合的概念,改善了分组卷积存在的问题,加强各组卷积之间的特征交互和信息交流,在改善模型的特征提取方式的同时,增强特征提取的全面性。
1.1 ShuffleNet V1
简介
ShuffleNet V1是计算效率极高的CNN架构,该架构是专为计算能力非常有限(例如10-150 MFLOP)的移动设备设计的。新架构利用了两个新的操作,逐点组卷积和通道混洗,可以在保持准确性的同时大大降低计算成本。 ImageNet分类和MS COCO对象检测的实验证明了ShuffleNet V1优于其他结构的性能,例如在40个MFLOP的计算预算下,比最近的MobileNet [12]在ImageNet分类任务上的top-1错误要低(绝对7.8%)。在基于ARM的移动设备上,ShuffleNet V1的实际速度是AlexNet的13倍,同时保持了相当的准确性。
创新点
- 分组逐点卷积(pointwise group convolution)
- 通道重排(channel shuffle)
网络模型结构

图(a)为一个Resdual block
- ①1×1卷积(降维)+3×3深度卷积+1×1卷积(升维)
- ②之间有BN和ReLU
- ③最后通过add相加
图(b)为输入输出特征图大小不变的ShuffleNet Unit
- ①将第一个用于降低通道数的1×1卷积改为1×1分组卷积 + Channel Shuffle
- ②去掉原3×3深度卷积后的ReLU
- ③ 将第二个用于扩增通道数的1×1卷积改为1×1分组卷积
图(c)为输出特征图大小为输入特征图大小一半的ShuffleNet Unit
- ①将第一个用于降低通道数的1×1卷积改为1×1分组卷积 +Channel Shuffle
- ②令原3×3深度卷积的步长stride=2, 并且去掉深度卷积后的ReLU
- ③将第二个用于扩增通道数的1×1卷积改为1×1分组卷积
- ④shortcut上添加一个3×3平均池化层(stride=2)用于匹配特征图大小
- ⑤对于块的输出,将原来的add方式改为concat方式
1.2 ShuffleNet V2
简介
模型执行效率的准则不能完全取决于FLOPs,经常发现FLOPs差不多的两个模型的运算速度却不一样,因为FLOPs仅仅反映了模型的乘加次数,这种评价往往是片面的。影响模型运行速度的另一个指标也很重要,那就是MAC(memory access cost)内存访问成本。作者充分考虑了不同结构的MAC,从而设计了更加高效的网络模型ShuffleNet V2。
创新点
提出了四条实用准则:
- (1)使用“平衡卷积"(相等的通道数)
- (2)注意使用组卷积的成本
- (3)降低碎片化程度
- (4)减少逐元素操作
网络模型结构

(c) ShuffleNet V2 的基本单元
- ①增加了Channel Split操作,实际上就是把输入通道分为两个部分。
- ②根据G1: 左边分支做恒等映射,右边的分支包含3个连续的卷积,并且输入和输出通道相同,每个分支中的卷积层的输入输出通道数都一致。
- ③根据G2: 两个1x1卷积不再是组卷积。
- ④根据G3: 减少基本单元数。因此有一个分支不做任何操作,直接做恒等映射。
- ⑤根据G4: 两个分支的输出不再是Add元素,而是concat在一起,紧接着是对两个分支concat结果进行channle shuffle,以保证两个分支信息交流。
(d) 用于空间下采样 (2×) 的 ShuffleNet V2 单元
对于下采样模块,不再有channel split,每个分支都有stride=2的下采样,最后concat在一起后,特征图空间大小减半,但是通道数翻倍。
🚀 二、YOLOv5结合ShuffleNet V2
2.1 添加顺序
之前在讲添加注意力机制时我们就介绍过改进网络的顺序,替换主干网络也是大同小异的。
(1)models/common.py --> 加入新增的网络结构
(2) models/yolo.py --> 设定网络结构的传参细节,将ShuffleNet V2类名加入其中。(当新的自定义模块中存在输入输出维度时,要使用qw调整输出维度)
(3) models/yolov5*.yaml --> 修改现有模型结构配置文件
- 当引入新的层时,要修改后续的结构中的from参数
- 当仅替换主千网络时,要注意特征图的变换,/8,/16,/32
(4) train.py --> 修改‘--cfg’默认参数,训练时指定模型结构配置文件
2.2 具体添加步骤
第①步:在common.py中添加ShuffleNet V2模块
将以下代码复制粘贴到common.py文件的末尾
- # 通道重排,跨group信息交流
- def channel_shuffle(x, groups):
- batchsize, num_channels, height, width = x.data.size()
- channels_per_group = num_channels // groups
- # reshape
- x = x.view(batchsize, groups,
- channels_per_group, height, width)
- x = torch.transpose(x, 1, 2).contiguous()
- # flatten
- x = x.view(batchsize, -1, height, width)
- return x
- class CBRM(nn.Module): #conv BN ReLU Maxpool2d
- def __init__(self, c1, c2): # ch_in, ch_out
- super(CBRM, self).__init__()
- self.conv = nn.Sequential(
- nn.Conv2d(c1, c2, kernel_size=3, stride=2, padding=1, bias=False),
- nn.BatchNorm2d(c2),
- nn.ReLU(inplace=True),
- )
- self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
- def forward(self, x):
- return self.maxpool(self.conv(x))
- class Shuffle_Block(nn.Module):
- def __init__(self, ch_in, ch_out, stride):
- super(Shuffle_Block, self).__init__()
- if not (1 <= stride <= 2):
- raise ValueError('illegal stride value')
- self.stride = stride
- branch_features = ch_out // 2
- assert (self.stride != 1) or (ch_in == branch_features << 1)
- if self.stride > 1:
- self.branch1 = nn.Sequential(
- self.depthwise_conv(ch_in, ch_in, kernel_size=3, stride=self.stride, padding=1),
- nn.BatchNorm2d(ch_in),
- nn.Conv2d(ch_in, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
- nn.BatchNorm2d(branch_features),
- nn.ReLU(inplace=True),
- )
- self.branch2 = nn.Sequential(
- nn.Conv2d(ch_in if (self.stride > 1) else branch_features,
- branch_features, kernel_size=1, stride=1, padding=0, bias=False),
- nn.BatchNorm2d(branch_features),
- nn.ReLU(inplace=True),
- self.depthwise_conv(branch_features, branch_features, kernel_size=3, stride=self.stride, padding=1),
- nn.BatchNorm2d(branch_features),
- nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
- nn.BatchNorm2d(branch_features),
- nn.ReLU(inplace=True),
- )
- @staticmethod
- def depthwise_conv(i, o, kernel_size, stride=1, padding=0, bias=False):
- return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i)
- def forward(self, x):
- if self.stride == 1:
- x1, x2 = x.chunk(2, dim=1) # 按照维度1进行split
- out = torch.cat((x1, self.branch2(x2)), dim=1)
- else:
- out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
- out = channel_shuffle(out, 2)
- return out
如下图所示:

第②步:在yolo.py文件里的parse_model函数加入类名
首先找到yolo.py里面parse_model函数的这一行

加入 CBRM,Shuffle_Block两个模块

第③步:创建自定义的yaml文件
首先在models文件夹下复制yolov5s.yaml 文件,粘贴并重命名为 yolov5s_ShuffleNetV2.yaml

然后根据ShuffleNetV2的网络结构来修改配置文件。

yaml文件修改后代码如下:
- # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
- # Parameters
- nc: 20 # number of classes
- depth_multiple: 1.0 # model depth multiple
- width_multiple: 1.0 # layer channel multiple
- anchors:
- - [10,13, 16,30, 33,23] # P3/8
- - [30,61, 62,45, 59,119] # P4/16
- - [116,90, 156,198, 373,326] # P5/32
- # YOLOv5 v6.0 backbone
- backbone:
- # [from, number, module, args]
- # Shuffle_Block: [out, stride]
- [[ -1, 1, CBRM, [ 32 ] ], # 0-P2/4
- [ -1, 1, Shuffle_Block, [ 128, 2 ] ], # 1-P3/8
- [ -1, 3, Shuffle_Block, [ 128, 1 ] ], # 2
- [ -1, 1, Shuffle_Block, [ 256, 2 ] ], # 3-P4/16
- [ -1, 7, Shuffle_Block, [ 256, 1 ] ], # 4
- [ -1, 1, Shuffle_Block, [ 512, 2 ] ], # 5-P5/32
- [ -1, 3, Shuffle_Block, [ 512, 1 ] ], # 6
- ]
- # YOLOv5 v6.0 head
- head:
- [[-1, 1, Conv, [256, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 4], 1, Concat, [1]], # cat backbone P4
- [-1, 1, C3, [256, False]], # 10
- [-1, 1, Conv, [128, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 2], 1, Concat, [1]], # cat backbone P3
- [-1, 1, C3, [128, False]], # 14 (P3/8-small)
- [-1, 1, Conv, [128, 3, 2]],
- [[-1, 11], 1, Concat, [1]], # cat head P4
- [-1, 1, C3, [256, False]], # 17 (P4/16-medium)
- [-1, 1, Conv, [256, 3, 2]],
- [[-1, 7], 1, Concat, [1]], # cat head P5
- [-1, 1, C3, [512, False]], # 20 (P5/32-large)
- [[14, 17, 20], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
- ]
第④步:验证是否加入成功
在yolo.py 文件里面配置改为我们刚才自定义的yolov5s_ShuffleNetV2.yaml


然后运行yolo.py

我们来和上次的MobileNet V3对比一下

可以看到替换主干网络为ShuffleNetV2之后层数变少了;参数量由原来的500多万减少为300多万,大幅度减少了;GFLOPs由12.2变为8.2。
第⑤步:修改train.py中 ‘--cfg’默认参数
我们先找到 train.py 文件的parse_opt函数,然后将第二行‘--cfg’的 default改为'models/yolov5s_ShuffleNetV2.yaml ',然后就可以开始训练啦~

PS: 我用的数据集有1442张,训练100轮,用时7个小时(更换前10个小时左右)。证明ShuffleNetV2的确能够大幅度提升速度,但是精度比原来掉了3个点,还是有点心疼的。