学习资源站

07-替换主干网络之 ShuffleNetV2_yolov5结构改进shufflenet

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文件的末尾

  1. # 通道重排,跨group信息交流
  2. def channel_shuffle(x, groups):
  3. batchsize, num_channels, height, width = x.data.size()
  4. channels_per_group = num_channels // groups
  5. # reshape
  6. x = x.view(batchsize, groups,
  7. channels_per_group, height, width)
  8. x = torch.transpose(x, 1, 2).contiguous()
  9. # flatten
  10. x = x.view(batchsize, -1, height, width)
  11. return x
  12. class CBRM(nn.Module): #conv BN ReLU Maxpool2d
  13. def __init__(self, c1, c2): # ch_in, ch_out
  14. super(CBRM, self).__init__()
  15. self.conv = nn.Sequential(
  16. nn.Conv2d(c1, c2, kernel_size=3, stride=2, padding=1, bias=False),
  17. nn.BatchNorm2d(c2),
  18. nn.ReLU(inplace=True),
  19. )
  20. self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  21. def forward(self, x):
  22. return self.maxpool(self.conv(x))
  23. class Shuffle_Block(nn.Module):
  24. def __init__(self, ch_in, ch_out, stride):
  25. super(Shuffle_Block, self).__init__()
  26. if not (1 <= stride <= 2):
  27. raise ValueError('illegal stride value')
  28. self.stride = stride
  29. branch_features = ch_out // 2
  30. assert (self.stride != 1) or (ch_in == branch_features << 1)
  31. if self.stride > 1:
  32. self.branch1 = nn.Sequential(
  33. self.depthwise_conv(ch_in, ch_in, kernel_size=3, stride=self.stride, padding=1),
  34. nn.BatchNorm2d(ch_in),
  35. nn.Conv2d(ch_in, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
  36. nn.BatchNorm2d(branch_features),
  37. nn.ReLU(inplace=True),
  38. )
  39. self.branch2 = nn.Sequential(
  40. nn.Conv2d(ch_in if (self.stride > 1) else branch_features,
  41. branch_features, kernel_size=1, stride=1, padding=0, bias=False),
  42. nn.BatchNorm2d(branch_features),
  43. nn.ReLU(inplace=True),
  44. self.depthwise_conv(branch_features, branch_features, kernel_size=3, stride=self.stride, padding=1),
  45. nn.BatchNorm2d(branch_features),
  46. nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
  47. nn.BatchNorm2d(branch_features),
  48. nn.ReLU(inplace=True),
  49. )
  50. @staticmethod
  51. def depthwise_conv(i, o, kernel_size, stride=1, padding=0, bias=False):
  52. return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i)
  53. def forward(self, x):
  54. if self.stride == 1:
  55. x1, x2 = x.chunk(2, dim=1) # 按照维度1进行split
  56. out = torch.cat((x1, self.branch2(x2)), dim=1)
  57. else:
  58. out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
  59. out = channel_shuffle(out, 2)
  60. return out

如下图所示:


第②步:在yolo.py文件里的parse_model函数加入类名

首先找到yolo.py里面parse_model函数的这一行

加入 CBRMShuffle_Block两个模块


第③步:创建自定义的yaml文件  

首先在models文件夹下复制yolov5s.yaml 文件,粘贴并重命名为 yolov5s_ShuffleNetV2.yaml 

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

 yaml文件修改后代码如下: 

  1. # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
  2. # Parameters
  3. nc: 20 # number of classes
  4. depth_multiple: 1.0 # model depth multiple
  5. width_multiple: 1.0 # layer channel multiple
  6. anchors:
  7. - [10,13, 16,30, 33,23] # P3/8
  8. - [30,61, 62,45, 59,119] # P4/16
  9. - [116,90, 156,198, 373,326] # P5/32
  10. # YOLOv5 v6.0 backbone
  11. backbone:
  12. # [from, number, module, args]
  13. # Shuffle_Block: [out, stride]
  14. [[ -1, 1, CBRM, [ 32 ] ], # 0-P2/4
  15. [ -1, 1, Shuffle_Block, [ 128, 2 ] ], # 1-P3/8
  16. [ -1, 3, Shuffle_Block, [ 128, 1 ] ], # 2
  17. [ -1, 1, Shuffle_Block, [ 256, 2 ] ], # 3-P4/16
  18. [ -1, 7, Shuffle_Block, [ 256, 1 ] ], # 4
  19. [ -1, 1, Shuffle_Block, [ 512, 2 ] ], # 5-P5/32
  20. [ -1, 3, Shuffle_Block, [ 512, 1 ] ], # 6
  21. ]
  22. # YOLOv5 v6.0 head
  23. head:
  24. [[-1, 1, Conv, [256, 1, 1]],
  25. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  26. [[-1, 4], 1, Concat, [1]], # cat backbone P4
  27. [-1, 1, C3, [256, False]], # 10
  28. [-1, 1, Conv, [128, 1, 1]],
  29. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  30. [[-1, 2], 1, Concat, [1]], # cat backbone P3
  31. [-1, 1, C3, [128, False]], # 14 (P3/8-small)
  32. [-1, 1, Conv, [128, 3, 2]],
  33. [[-1, 11], 1, Concat, [1]], # cat head P4
  34. [-1, 1, C3, [256, False]], # 17 (P4/16-medium)
  35. [-1, 1, Conv, [256, 3, 2]],
  36. [[-1, 7], 1, Concat, [1]], # cat head P5
  37. [-1, 1, C3, [512, False]], # 20 (P5/32-large)
  38. [[14, 17, 20], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
  39. ]

第④步:验证是否加入成功

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个点,还是有点心疼的。