💡💡💡本专栏所有程序均经过测试,可成功执行💡💡💡
专栏目录: 《YOLOv5入门 + 改进涨点》专栏介绍 & 专栏目录 |目前已有40+篇内容,内含各种Head检测头、损失函数Loss、Backbone、Neck、NMS等创新点改进
在卷积神经网络中,传统的训练方法是在每一层学习一个单一的静态卷积核。然而,最新的研究通过学习多个卷积核的线性组合并利用输入依赖的注意力进行加权,这种方法称为动态卷积,它可以提高轻量级CNN的准确性,同时保持高效的推理。尽管如此,目前的研究仅考虑了卷积核数量这一维度的动态性,而忽略了卷积核的空间大小、输入和输出通道数等其他三个维度。为了解决这个问题,研究者提出了全维度动态卷积(ODConv),这是一种更为通用和优雅的动态卷积设计。ODConv采用一种新颖的多维注意力机制和并行策略,能够在卷积层的卷积核空间的所有四个维度上学习互补的注意力。ODConv可以作为一个常规卷积的替代品,被集成到许多CNN架构中。文章在介绍主要的原理后,将手把手教学如何进行模块的代码添加和修改,并将修改后的完整代码放在文章的最后,方便大家一键运行,小白也可轻松上手实践。以帮助您更好地学习深度学习目标检测YOLO系列的挑战。
目录
1. 原理
论文地址:OMNI-DIMENSIONAL DYNAMIC CONVOLUTION——点击即可跳转
官方代码:官方代码仓库——点击即可跳转
全维动态卷积 (ODConv) 是一种广义动态卷积设计,它通过结合多维注意力机制扩展了动态卷积的概念。以下是 ODConv 背后主要原理的详细解释:
ODConv 的关键原理:
动态卷积基础知识:
传统卷积神经网络 (CNN) 使用静态卷积核,这意味着相同的核应用于所有输入样本。相比之下,动态卷积使用多个卷积核的组合,每个核都由依赖于输入特征的注意力机制加权。这使得卷积操作依赖于输入。
现有动态卷积的局限性:
现有的动态卷积方法,如 CondConv 和 DyConv,仅在核空间的一个维度(核的数量)上应用动态属性。这忽略了卷积核的另外三个维度:空间大小、输入通道数和输出通道数。
-
多维注意力机制: ODConv 通过引入多维注意力机制解决了这一限制。它不仅针对卷积核数量学习和应用注意力权重,还针对每个卷积核的空间大小、输入通道和输出通道学习和应用注意力权重。这确保了基于输入特征对卷积核进行更全面、更细粒度的动态调整。
-
并行注意力策略: ODConv 并行计算四种类型的注意力(针对空间大小、输入通道、输出通道和卷积核数量)。然后将这些注意力结合起来调节卷积核,增强网络中卷积操作的特征提取能力。
-
效率和性能: 通过利用更详细和多样化的注意力机制,ODConv 可以用更少的参数实现更好的性能。它显示了各种 CNN 架构(轻量级和大型)的显著准确性改进,而无需大幅增加模型大小。
-
泛化和应用: ODConv 可以集成到许多现有的 CNN 架构中,作为常规卷积的直接替代品。它不仅可以改进分类任务,还可以很好地转移到其他任务,例如对象检测。
示意图比较:
-
现有方法:
CondConv 和 DyConv 为每个内核计算单个注意力标量,从而对内核的所有过滤器进行统一调整。
-
ODConv:
为内核空间的不同维度计算多个注意力,从而可以对卷积内核进行更细致入微和有效的调制。
实施概述:
注意力机制:
-
空间注意力:捕捉空间相关特征。
-
通道注意力:调整每个输入通道的权重。
-
过滤器注意:调节每个过滤器的输出特征。
-
内核注意:根据输入特征在多个内核中进行选择。
结果:
-
ODConv 在 ImageNet 和 MS-COCO 等数据集上针对各种 CNN 主干显示出显着的准确性改进。
-
与其他动态卷积方法和注意模块相比,它表现出卓越的性能,同时参数效率更高。
总之,ODConv 通过结合全面的多维注意机制增强了动态卷积方法,从而在各种 CNN 架构中实现了更好的性能和效率。
2. 将ODConv添加到YOLOv8代码
2.1 ODConv代码实现
关键步骤一: 将下面代码粘贴到/yolov5-6.1/models/common.py文件中
class ODConv2d_3rd(nn.Conv2d):
def __init__(self, in_channels, out_channels, kernel_size,
stride=1, padding=0, dilation=1, groups=1, bias=True,
K=4, r=1 / 16, save_parameters=False,
padding_mode='zeros', device=None, dtype=None) -> None:
factory_kwargs = {'device': device, 'dtype': dtype}
self.K = K
self.r = r
self.save_parameters = save_parameters
super().__init__(in_channels, out_channels, kernel_size, stride,
padding, dilation, groups, bias, padding_mode)
del self.weight
self.weight = nn.Parameter(torch.empty((
K,
out_channels,
in_channels // groups,
*self.kernel_size,
), **factory_kwargs))
if bias:
del self.bias
self.bias = nn.Parameter(torch.empty(K, out_channels, **factory_kwargs))
hidden_dim = max(int(in_channels * r), 16) #设置下限为16
self.gap = nn.AdaptiveAvgPool2d(1)
self.reduction = nn.Linear(in_channels, hidden_dim)
self.fc = nn.Conv2d(in_channels, hidden_dim, 1, bias = False)
self.bn = nn.BatchNorm2d(hidden_dim)
self.act = nn.ReLU(inplace=True)
# self.act = nn.SiLU(inplace=True)
self.fc_f = nn.Linear(hidden_dim, out_channels)
if not save_parameters or self.kernel_size[0] * self.kernel_size[1] > 1:
self.fc_s = nn.Linear(hidden_dim, self.kernel_size[0] * self.kernel_size[1])
if not save_parameters or in_channels // groups > 1:
self.fc_c = nn.Linear(hidden_dim, in_channels // groups)
if not save_parameters or K > 1:
self.fc_w = nn.Linear(hidden_dim, K)
self.reset_parameters()
def reset_parameters(self) -> None:
fan_out = self.kernel_size[0] * self.kernel_size[1] * self.out_channels // self.groups
for i in range(self.K):
self.weight.data[i].normal_(0, math.sqrt(2.0 / fan_out))
if self.bias is not None:
self.bias.data.zero_()
def extra_repr(self):
return super().extra_repr() + f', K={self.K}, r={self.r:.4}'
def get_weight_bias(self, context):
B, C, H, W = context.shape
if C != self.in_channels:
raise ValueError(
f"Expected context{[B, C, H, W]} to have {self.in_channels} channels, but got {C} channels instead")
# x = self.gap(context).squeeze(-1).squeeze(-1) # B, c_in
# x = self.reduction(x) # B, hidden_dim
x = self.gap(context)
x = self.fc(x)
if x.size(0)>1:
x = self.bn(x)
x = x.squeeze(-1).squeeze(-1)
x = self.act(x)
attn_f = self.fc_f(x).sigmoid() # B, c_out
attn = attn_f.view(B, 1, -1, 1, 1, 1) # B, 1, c_out, 1, 1, 1
if hasattr(self, 'fc_s'):
attn_s = self.fc_s(x).sigmoid() # B, k * k
attn = attn * attn_s.view(B, 1, 1, 1, *self.kernel_size) # B, 1, c_out, 1, k, k
if hasattr(self, 'fc_c'):
attn_c = self.fc_c(x).sigmoid() # B, c_in // groups
attn = attn * attn_c.view(B, 1, 1, -1, 1, 1) # B, 1, c_out, c_in // groups, k, k
if hasattr(self, 'fc_w'):
attn_w = self.fc_w(x).softmax(-1) # B, n
attn = attn * attn_w.view(B, -1, 1, 1, 1, 1) # B, n, c_out, c_in // groups, k, k
weight = (attn * self.weight).sum(1) # B, c_out, c_in // groups, k, k
weight = weight.view(-1, self.in_channels // self.groups, *self.kernel_size) # B * c_out, c_in // groups, k, k
bias = None
if self.bias is not None:
if hasattr(self, 'fc_w'):
bias = attn_w @ self.bias
else:
bias = self.bias.tile(B, 1)
bias = bias.view(-1) # B * c_out
return weight, bias
def forward(self, input, context=None):
B, C, H, W = input.shape
if C != self.in_channels:
raise ValueError(
f"Expected input{[B, C, H, W]} to have {self.in_channels} channels, but got {C} channels instead")
weight, bias = self.get_weight_bias(context or input)
output = nn.functional.conv2d(
input.view(1, B * C, H, W), weight, bias,
self.stride, self.padding, self.dilation, B * self.groups) # 1, B * c_out, h_out, w_out
output = output.view(B, self.out_channels, *output.shape[2:])
return output
def debug(self, input, context=None):
B, C, H, W = input.shape
if C != self.in_channels:
raise ValueError(
f"Expected input{[B, C, H, W]} to have {self.in_channels} channels, but got {C} channels instead")
output_size = [
((H, W)[i] + 2 * self.padding[i] - self.dilation[i] * (self.kernel_size[i] - 1) - 1) // self.stride[i] + 1
for i in range(2)
]
weight, bias = self.get_weight_bias(context or input)
weight = weight.view(B, self.groups, self.out_channels // self.groups, -1) # B, groups, c_out // groups, c_in // groups * k * k
unfold = nn.functional.unfold(
input, self.kernel_size, self.dilation, self.padding, self.stride) # B, c_in * k * k, H_out * W_out
unfold = unfold.view(B, self.groups, -1, output_size[0] * output_size[1]) # B, groups, c_in // groups * k * k, H_out * W_out
output = weight @ unfold # B, groups, c_out // groups, H_out * W_out
output = output.view(B, self.out_channels, *output_size) # B, c_out, H_out * W_out
if bias is not None:
output = output + bias.view(B, self.out_channels, 1, 1)
return output
class ODConv_3rd(nn.Module):
# Standard convolution
def __init__(self, c1, c2, k=1, s=1, kerNums=1, g=1, p=None, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super().__init__()
self.conv = ODConv2d_3rd(c1, c2, k, s, autopad(k, p), groups=g, K=kerNums)
self.bn = nn.BatchNorm2d(c2)
self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
return self.act(self.conv(x))
ODConv(Omni-Dimensional Dynamic Convolution)在处理图像时的主要步骤可以总结为以下几个阶段,每个阶段都利用其独特的多维度注意力机制来增强图像处理效果:
1. 特征提取 (Feature Extraction)
-
输入:原始图像或图像的特征图。
-
操作:通过卷积层提取图像的初始特征。
-
ODConv 特性:使用多个静态卷积核获取初步的图像特征。
2. 多维度注意力机制 (Multi-Dimensional Attention Mechanism)
-
输入:初步提取的特征图。
-
操作:
-
计算多维度注意力权重:
-
空间注意力 (Spatial Attention):为卷积核的空间维度分配权重,捕捉空间相关特征。
-
通道注意力 (Channel Attention):为卷积核的输入通道分配权重,捕捉通道间的依赖关系。
-
滤波器注意力 (Filter Attention):为卷积核的输出通道分配权重,增强输出特征的表达能力。
-
核选择注意力 (Kernel Attention):为多个卷积核分配权重,选择最适合当前输入特征的卷积核组合。
-
-
组合注意力权重:将上述注意力权重结合起来,动态调整卷积核参数。
-
3. 动态卷积运算
-
输入:经过注意力机制调整的卷积核和输入特征图。
-
操作:
-
应用动态卷积:使用调整后的卷积核进行卷积运算,生成新的特征图。
-
输出特征图:动态卷积后产生的特征图包含了更丰富和更准确的特征信息。
-
4. 特征融合 (Feature Fusion)
-
输入:动态卷积后产生的特征图。
-
操作:将不同层次的特征图进行融合,进一步增强图像特征。
-
ODConv 特性:通过多层次的动态卷积特征融合,提升图像表示的能力。
5. 高层任务 (High-Level Tasks)
-
输入:经过特征融合的图像特征图。
-
操作:将特征图输入到高层任务模块,如分类、检测、分割等任务中。
-
ODConv 特性:由于特征图经过了多维度注意力机制的增强和动态卷积的处理,ODConv在高层任务中表现出更高的精度和效果。
6. 后处理 (Post-Processing)
-
输入:高层任务的输出结果。
-
操作:对输出结果进行必要的后处理,如非极大值抑制、边缘修正等,以得到最终的图像处理结果。
-
ODConv 特性:由于ODConv在特征提取和特征融合阶段已经提升了图像特征的质量,后处理阶段能够更高效地处理图像结果。
总结
ODConv 处理图像的主要步骤包括特征提取、多维度注意力机制、动态卷积运算、特征融合、高层任务和后处理。通过在卷积运算中引入多维度的动态注意力机制,ODConv 能够更全面和准确地提取和表示图像特征,从而在各种图像处理任务中表现出色。
2.2 新增yaml文件
关键步骤二:在下/yolov5-6.1/models下新建文件 yolov5_ODConv.yaml并将下面代码复制进去
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Parameters
nc: 80 # 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]
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
[-1, 1, ODConv_3rd, [128, 3, 2, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 6, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
# YOLOv5 v6.0 head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)
[-1, 1, Conv, [256, 3, 2]],
[[-1, 14], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)
[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
温馨提示:本文只是对yolov5l基础上添加模块,如果要对yolov5n/l/m/x进行添加则只需要指定对应的depth_multiple 和 width_multiple。
# YOLOv5n
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.25 # layer channel multiple
# YOLOv5s
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
# YOLOv5l
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
# YOLOv5m
depth_multiple: 0.67 # model depth multiple
width_multiple: 0.75 # layer channel multiple
# YOLOv5x
depth_multiple: 1.33 # model depth multiple
width_multiple: 1.25 # layer channel multiple
2.3 注册模块
关键步骤三:在yolo.py中注册添加“ODConv_3rd",
2.4 执行程序
在train.py中,将cfg的参数路径设置为yolov5_ODConv.yaml的路径
建议大家写绝对路径,确保一定能找到
🚀运行程序,如果出现下面的内容则说明添加成功🚀
3. 完整代码分享
https://pan.baidu.com/s/191KmWmkZUVXIsWeW8nd7zQ?pwd=amr9
提取码: amr9
4. GFLOPs
关于GFLOPs的计算方式可以查看:百面算法工程师 | 卷积基础知识——Convolution
未改进的GFLOPs
改进后的GFLOPs
5. 进阶
可以和损失函数的修改相结合,效果可能会更好
YOLOv5改进 | 损失函数 | EIoU、SIoU、WIoU、DIoU、FocusIoU等多种损失函数
6. 总结
ODConv(Omni-Dimensional Dynamic Convolution)的主要原理是通过引入多维度注意力机制来动态调整卷积核参数,从而在卷积神经网络中实现更灵活和精确的特征提取。它通过计算并应用空间注意力、通道注意力、滤波器注意力和核选择注意力,全面地调整卷积核在各个维度上的权重。这种多维度的动态调整机制使得ODConv能够根据输入图像的特征动态选择最合适的卷积核组合,从而在各类高层视觉任务(如图像分类、目标检测和图像分割)中实现显著的性能提升,同时保持较高的参数效率。