可变形卷积
- 🚀🚀🚀前言
- 一、1️⃣ 什么是可变形卷积
- 二、2️⃣如何在yolov5中添加DCNv2模块
- 2.1 🎓 修改common.py模块
- 2.2 ✨修改yolo.py文件
- 2.3 ⭐️修改yolov5s.yaml文件
- 2.4 🎯训练可能报错结果
- 三、3️⃣DCNv2实验结果
- 3.1 🎓 正常训练
- 3.2 ✨修改实验1
- 3.3 ⭐️修改实验2
- 3.4 🎯修改实验3
👀🎉📜系列文章目录
【YOLOv5改进系列(1)】高效涨点----使用EIoU、Alpha-IoU、SIoU、Focal-EIOU替换CIou
【YOLOv5改进系列(2)】高效涨点----Wise-IoU详细解读及使用Wise-IoU(WIOU)替换CIOU
【YOLOv5改进系列(3)】高效涨点----Optimal Transport Assignment:OTA最优传输方法
🚀🚀🚀前言
目前可变形卷积一共有4个版本,分别是DCNv1~DCNv4,在本文中使用的是带有可变形卷积DCNv2的方法替换yolov5-v6.0版本中的C3模块。本次使用的数据集是热轧钢带的六种典型表面缺陷数据集,通过在backbone的不同位置添加可变形卷积进行训练,经过实验测试结果,最后的map@0.5提高了近5个百分点。
一、1️⃣ 什么是可变形卷积
🚀概念
可变形卷积指卷积核在每个元素上增加了一个参数方向参数,这样卷积核就能在训练过程中扩展到很大的范围。它允许卷积核在空间上进行动态调整,以适应目标对象的形变或者复杂结构。
传统的卷积操作是固定的,卷积核的权重在整个图像上是不变的,而可变形卷积则引入了额外的参数,用于控制卷积核的采样位置。这些额外的参数通常是由一个学习模块来学习得到的,通常称为偏移量(offset)。在进行可变形卷积时,卷积核的采样位置会根据输入特征图的内容进行动态调整,从而实现对目标对象形状的适应性。
🔥为什么要使用可变形卷积?
卷积核的目的是为了提取输入物的特征。传统的卷积核通常是固定尺寸、固定大小的。这种卷积核存在的最大问题就是,对于未知的变化适应性差,泛化能力差。网络内部缺乏能够解决这些问题的模块,这会产生显著的问题,同一 CNN 层的激活单元的感受野尺寸都相同,这对于编码位置信息的浅层神经网络并不可取,因为不同的位置可能对应有不同尺度或者不同形变的物体,这些层需要能够自动调整尺度或者感受野的方法。再比如,目标检测虽然效果很好但是都依赖于基于特征提取的边界框,这并不是最优的方法,尤其是对于非网格状的物体而言。
✨因此,希望卷积核可以根据实际情况调整本身的形状,更好的提取输入的特征。
- 更好的感受野覆盖:由于卷积核的采样位置可以自适应地调整,因此可变形卷积可以更好地适应目标对象的形状和结构,从而提高感受野的覆盖能力。
- 对目标形变的鲁棒性:可变形卷积可以处理目标对象形变的情况,例如目标的畸变、变形或者姿态变化等,从而提高了模型的鲁棒性和泛化能力。
- 更强的表达能力:通过引入额外的偏移参数,可变形卷积可以增加模型的表达能力,使其能够更好地捕捉目标对象的细微特征和结构信息。
二、2️⃣如何在yolov5中添加DCNv2模块
2.1 🎓 修改common.py模块
找到models
文件下的common.py
文件,将鼠标定位到该文件的最后一行代码。
📌将下面修改的的可变形卷积DCNv2
模块代码粘贴到最后一行,这段代码的作用就是构建带有DCNv2的C3
模块
# ====================添加DCN V2=====================
class DCNv2(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1,
padding=1, dilation=1, groups=1, deformable_groups=1):
super(DCNv2, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = (kernel_size, kernel_size)
self.stride = (stride, stride)
self.padding = (padding, padding)
self.dilation = (dilation, dilation)
self.groups = groups
self.deformable_groups = deformable_groups
self.weight = nn.Parameter(
torch.empty(out_channels, in_channels, *self.kernel_size)
)
self.bias = nn.Parameter(torch.empty(out_channels))
out_channels_offset_mask = (self.deformable_groups * 3 *
self.kernel_size[0] * self.kernel_size[1])
self.conv_offset_mask = nn.Conv2d(
self.in_channels,
out_channels_offset_mask,
kernel_size=self.kernel_size,
stride=self.stride,
padding=self.padding,
bias=True,
)
self.bn = nn.BatchNorm2d(out_channels)
self.act = Conv.default_act
self.reset_parameters()
def forward(self, x):
offset_mask = self.conv_offset_mask(x)
o1, o2, mask = torch.chunk(offset_mask, 3, dim=1)
offset = torch.cat((o1, o2), dim=1)
mask = torch.sigmoid(mask)
x = torch.ops.torchvision.deform_conv2d(
x,
self.weight,
offset,
mask,
self.bias,
self.stride[0], self.stride[1],
self.padding[0], self.padding[1],
self.dilation[0], self.dilation[1],
self.groups,
self.deformable_groups,
True
)
x = self.bn(x)
x = self.act(x)
return x
def reset_parameters(self):
n = self.in_channels
for k in self.kernel_size:
n *= k
std = 1. / math.sqrt(n)
self.weight.data.uniform_(-std, std)
self.bias.data.zero_()
self.conv_offset_mask.weight.data.zero_()
self.conv_offset_mask.bias.data.zero_()
class Bottleneck_DCN(nn.Module):
# Standard bottleneck
def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = DCNv2(c_, c2, 3, 1, groups=g)
self.add = shortcut and c1 == c2
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class C3_DCN(C3):
# 带有DCNv2的C3模块
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e)
self.m = nn.Sequential(*(Bottleneck_DCN(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
2.2 ✨修改yolo.py文件
📌首先找到models
文件夹下的yolo.py
,在该文件中找到parse_model
网络解析函数,大概是在第300行左右。
📌在parse_model函数中需要在两处添加C3_DCN
(也就是带有DCN的C3模块),添加位置如下:
2.3 ⭐️修改yolov5s.yaml文件
🚀找到models
文件夹下的yolov5s.yaml
配置文件,前面我们已经定义好C3_DCN
模块,接下来就是需要将可变形卷积替换掉原始的C3
模块。
☀️温馨提示:看过很多论文添加可变形卷积,一般是在backbone特征提取部分添加,如果有兴趣也可以在Neck特征融合部分添加可变形卷积。
我是将backbone
的特征金字塔特征融合输出的三个部分C3模块替换成了C3_DCN
可变形卷积,我添加的比较多,训练过程中的参数量也会大大增加,所以会增加训练时间,如果不想训练速度太慢,也可以只替换最后一个C3
模块。
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args] 输出640*640
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 320*320 64表示输出的channel,但是需要乘以width_multiple,6表示卷积核的大小,2表示padding大小,2表示stride大小
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4 160*160*128 2表示stride大小
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8 80*80*256
[-1, 6, C3_DCN, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16 40*40*512
[-1, 9, C3_DCN, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 20*20
[-1, 3, C3_DCN, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
2.4 🎯训练可能报错结果
在将C3模块替换成C3_DCN模块之后可能会报一个错误,也是我遇到的,报错内容如:
Variable._execution_engine.run_backward( # Calls into the C++ engine to run the backward pass RuntimeError: adaptive_max_pool2d_backward_cuda does not have a deterministic implementation, but you set ‘torch.use_deterministic_algorithms(True)’. You can turn off determinism just for this operation, or you can use the ‘warn_only=True’ option
如果出现这个错误可以通过寻找报错来源可以通过修改train.py
文件中的
init_seeds(opt.seed + 1 + RANK, deterministic=True)
将True改成False
init_seeds(opt.seed + 1 + RANK, deterministic=False)
opt.seed + 1 + RANK
的目的是使每个进程具有不同的随机种子。通过在种子值中添加进程排名,可以确保每个进程使用不同的种子,从而生成独立的随机序列。deterministic=True
参数的作用是启用确定性算法
⚡️目前我也不知道为什么修改初始化随机种子就能解决这个问题。有大佬了解的可以评论区讨论。
三、3️⃣DCNv2实验结果
3.1 🎓 正常训练
F1置信度分数为0.71、map@0.5=0.779
3.2 ✨修改实验1
修改内容:只修改backbone网络中的最后一个C3模块,将其替换成C3_DCN。
backbone:
# [from, number, module, args] 输出640*640
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 320*320 64表示输出的channel,但是需要乘以width_multiple,6表示卷积核的大小,2表示padding大小,2表示stride大小
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4 160*160*128 2表示stride大小
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8 80*80*256
[-1, 6, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16 40*40*512
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 20*20
[-1, 3, C3_DCN, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
实验结果:F1置信度分数为0.76、map@0.5=0.801,两个评估指标都有所增长。
3.3 ⭐️修改实验2
修改内容:只修改backbone网络中的第2个和最后一个C3模块,将其替换成C3_DCN。
backbone:
# [from, number, module, args] 输出640*640
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 320*320 64表示输出的channel,但是需要乘以width_multiple,6表示卷积核的大小,2表示padding大小,2表示stride大小
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4 160*160*128 2表示stride大小
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8 80*80*256
[-1, 6, C3_DCN, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16 40*40*512
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 20*20
[-1, 3, C3_DCN, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
实验结果:F1置信度分数为0.76、map@0.5=0.819,两个评估指标都有所增长。
3.4 🎯修改实验3
修改内容:将backbone
的特征金字塔特征融合输出的三个部分C3模块替换成了C3_DCN
可变形卷积
backbone:
# [from, number, module, args] 输出640*640
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 320*320 64表示输出的channel,但是需要乘以width_multiple,6表示卷积核的大小,2表示padding大小,2表示stride大小
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4 160*160*128 2表示stride大小
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8 80*80*256
[-1, 6, C3_DCN, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16 40*40*512
[-1, 9, C3_DCN, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 20*20
[-1, 3, C3_DCN, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
实验结果:F1置信度分数为0.75、map@0.5=0.818,两个评估指标都有所增长。相较于修改实验2,这一次训练时间大大增加,但是评估指标并没有明显增加。