💡💡💡本专栏所有程序均经过测试,可成功执行💡💡💡
专栏目录 :《YOLOv8改进有效涨点》专栏介绍 & 专栏目录 | 目前已有40+篇内容,内含各种Head检测头、损失函数Loss、Backbone、Neck、NMS等创新点改进——点击即可跳转
在卷积神经网络中,传统的训练方法是在每一层学习一个单一的静态卷积核。然而,最新的研究通过学习多个卷积核的线性组合并利用输入依赖的注意力进行加权,这种方法称为动态卷积,它可以提高轻量级CNN的准确性,同时保持高效的推理。尽管如此,目前的研究仅考虑了卷积核数量这一维度的动态性,而忽略了卷积核的空间大小、输入和输出通道数等其他三个维度。为了解决这个问题,研究者提出了全维度动态卷积(ODConv),这是一种更为通用和优雅的动态卷积设计。ODConv采用一种新颖的多维注意力机制和并行策略,能够在卷积层的卷积核空间的所有四个维度上学习互补的注意力。ODConv可以作为一个常规卷积的替代品,被集成到许多CNN架构中。文章在介绍主要的原理后,将手把手教学如何进行模块的代码添加和修改,并将修改后的完整代码放在文章的最后,方便大家一键运行,小白也可轻松上手实践。以帮助您更好地学习深度学习目标检测YOLO系列的挑战。
专栏地址:YOLOv8改进——更新各种有效涨点方法——点击即可跳转
目录
1. 原理
2. 将C2f_ODConv添加到YOLOv8代码
2.1 C2f_ODConv代码实现
2.2 更改init.py文件
2.3 新增yaml文件
2.4 注册模块
2.5 执行程序
3. 完整代码分享
4. GFLOPs
5. 进阶
6. 总结
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. 将C2f_ODConv添加到YOLOv8代码
2.1 C2f_ODConv代码实现
关键步骤一:将下面代码粘贴到在/ultralytics/ultralytics/nn/modules/conv.py中,并在该文件的__all__中添加“C2f_ODConv”
class Attention(nn.Module):
def __init__(self, in_planes, out_planes, kernel_size=3, groups=1, reduction=0.0625, kernel_num=4, min_channel=16):
super(Attention, self).__init__()
attention_channel = max(int(in_planes * reduction), min_channel)
self.kernel_size = kernel_size
self.kernel_num = kernel_num
self.temperature = 1.0
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Conv2d(in_planes, attention_channel, 1, bias=False)
self.bn = nn.BatchNorm2d(attention_channel)
self.relu = nn.ReLU(inplace=True)
self.channel_fc = nn.Conv2d(attention_channel, in_planes, 1, bias=True)
self.func_channel = self.get_channel_attention
if in_planes == groups and in_planes == out_planes: # depth-wise convolution
self.func_filter = self.skip
else:
self.filter_fc = nn.Conv2d(attention_channel, out_planes, 1, bias=True)
self.func_filter = self.get_filter_attention
if kernel_size == 1: # point-wise convolution
self.func_spatial = self.skip
else:
self.spatial_fc = nn.Conv2d(attention_channel, kernel_size * kernel_size, 1, bias=True)
self.func_spatial = self.get_spatial_attention
if kernel_num == 1:
self.func_kernel = self.skip
else:
self.kernel_fc = nn.Conv2d(attention_channel, kernel_num, 1, bias=True)
self.func_kernel = self.get_kernel_attention
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
if isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def update_temperature(self, temperature):
self.temperature = temperature
@staticmethod
def skip(_):
return 1.0
def get_channel_attention(self, x):
channel_attention = torch.sigmoid(self.channel_fc(x).view(x.size(0), -1, 1, 1) / self.temperature)
return channel_attention
def get_filter_attention(self, x):
filter_attention = torch.sigmoid(self.filter_fc(x).view(x.size(0), -1, 1, 1) / self.temperature)
return filter_attention
def get_spatial_attention(self, x):
spatial_attention = self.spatial_fc(x).view(x.size(0), 1, 1, 1, self.kernel_size, self.kernel_size)
spatial_attention = torch.sigmoid(spatial_attention / self.temperature)
return spatial_attention
def get_kernel_attention(self, x):
kernel_attention = self.kernel_fc(x).view(x.size(0), -1, 1, 1, 1, 1)
kernel_attention = F.softmax(kernel_attention / self.temperature, dim=1)
return kernel_attention
def forward(self, x):
x = self.avgpool(x)
x = self.fc(x)
# x = self.bn(x) # 在外面我提供了一个bn这里会报错
x = self.relu(x)
return self.func_channel(x), self.func_filter(x), self.func_spatial(x), self.func_kernel(x)
class ODConv2d(nn.Module):
def __init__(self, in_planes, out_planes, kernel_size, stride=1, padding=1, dilation=1, groups=1,
reduction=0.0625, kernel_num=4):
super(ODConv2d, self).__init__()
in_planes = in_planes
self.in_planes = in_planes
self.out_planes = out_planes
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
self.dilation = dilation
self.groups = groups
self.kernel_num = kernel_num
self.attention = Attention(in_planes, out_planes, kernel_size, groups=groups,
reduction=reduction, kernel_num=kernel_num)
self.weight = nn.Parameter(torch.randn(kernel_num, out_planes, in_planes // groups, kernel_size, kernel_size),
requires_grad=True)
self._initialize_weights()
if self.kernel_size == 1 and self.kernel_num == 1:
self._forward_impl = self._forward_impl_pw1x
else:
self._forward_impl = self._forward_impl_common
def _initialize_weights(self):
for i in range(self.kernel_num):
nn.init.kaiming_normal_(self.weight[i], mode='fan_out', nonlinearity='relu')
def update_temperature(self, temperature):
self.attention.update_temperature(temperature)
def _forward_impl_common(self, x):
# Multiplying channel attention (or filter attention) to weights and feature maps are equivalent,
# while we observe that when using the latter method the models will run faster with less gpu memory cost.
channel_attention, filter_attention, spatial_attention, kernel_attention = self.attention(x)
batch_size, in_planes, height, width = x.size()
x = x * channel_attention
x = x.reshape(1, -1, height, width)
aggregate_weight = spatial_attention * kernel_attention * self.weight.unsqueeze(dim=0)
aggregate_weight = torch.sum(aggregate_weight, dim=1).view(
[-1, self.in_planes // self.groups, self.kernel_size, self.kernel_size])
output = F.conv2d(x, weight=aggregate_weight, bias=None, stride=self.stride, padding=self.padding,
dilation=self.dilation, groups=self.groups * batch_size)
output = output.view(batch_size, self.out_planes, output.size(-2), output.size(-1))
output = output * filter_attention
return output
def _forward_impl_pw1x(self, x):
channel_attention, filter_attention, spatial_attention, kernel_attention = self.attention(x)
x = x * channel_attention
output = F.conv2d(x, weight=self.weight.squeeze(dim=0), bias=None, stride=self.stride, padding=self.padding,
dilation=self.dilation, groups=self.groups)
output = output * filter_attention
return output
def forward(self, x):
return self._forward_impl(x)
class Bottleneck_ODConv(nn.Module):
"""Standard bottleneck."""
def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
"""Initializes a bottleneck module with given input/output channels, shortcut option, group, kernels, and
expansion.
"""
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, k[0], 1)
self.cv2 = ODConv2d(c_, c2, k[1][0], 1, groups=g)
self.add = shortcut and c1 == c2
def forward(self, x):
"""'forward()' applies the YOLO FPN to input data."""
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class C2f_ODConv(nn.Module):
"""Faster Implementation of CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
"""Initialize CSP bottleneck layer with two convolutions with arguments ch_in, ch_out, number, shortcut, groups,
expansion.
"""
super().__init__()
self.c = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, 2 * self.c, 1, 1)
self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
self.m = nn.ModuleList(
Bottleneck_ODConv(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
def forward(self, x):
"""Forward pass through C2f layer."""
y = list(self.cv1(x).chunk(2, 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
def forward_split(self, x):
"""Forward pass using split() instead of chunk()."""
y = list(self.cv1(x).split((self.c, self.c), 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
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 更改init.py文件
关键步骤二:修改modules文件夹下的__init__.py文件,先导入函数
然后在下面的__all__中声明函数
2.3 新增yaml文件
关键步骤三:在 \ultralytics\ultralytics\cfg\models\v8下新建文件 yolov8_ODConv.yaml并将下面代码复制进去
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv8 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
# Parameters
nc: 80 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs
s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs
m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs
l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs
x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs
# YOLOv8.0n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f_ODConv, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f_ODConv, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f, [512]] # 12
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 15 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f, [512]] # 18 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f, [1024]] # 21 (P5/32-large)
- [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)
温馨提示:因为本文只是对yolov8基础上添加模块,如果要对yolov8n/l/m/x进行添加则只需要指定对应的depth_multiple 和 width_multiple。
# YOLOv8n
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.25 # layer channel multiple
max_channels: 1024 # max_channels
# YOLOv8s
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
max_channels: 1024 # max_channels
# YOLOv8l
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
max_channels: 512 # max_channels
# YOLOv8m
depth_multiple: 0.67 # model depth multiple
width_multiple: 0.75 # layer channel multiple
max_channels: 768 # max_channels
# YOLOv8x
depth_multiple: 1.33 # model depth multiple
width_multiple: 1.25 # layer channel multiple
max_channels: 512 # max_channels
2.4 注册模块
关键步骤四:在parse_model函数中进行注册,添加C2f_ODConv,
2.5 执行程序
在train.py中,将model的参数路径设置为yolov8_c2f_ODConv.yaml的路径
建议大家写绝对路径,确保一定能找到
from ultralytics import YOLO
# Load a model
# model = YOLO('yolov8n.yaml') # build a new model from YAML
# model = YOLO('yolov8n.pt') # load a pretrained model (recommended for training)
model = YOLO(r'/projects/ultralytics/ultralytics/cfg/models/v8/yolov8_c2f_ODConv.yaml') # build from YAML and transfer weights
# Train the model
model.train(device = [3], batch=16)
🚀运行程序,如果出现下面的内容则说明添加成功🚀
3. 完整代码分享
https://pan.baidu.com/s/1bLvDgIs9leo6ZCCRZ-X93w?pwd=h4u4
提取码: h4u4
4. GFLOPs
关于GFLOPs的计算方式可以查看:百面算法工程师 | 卷积基础知识——Convolution
未改进的YOLOv8nGFLOPs
改进后的GFLOPs
5. 进阶
可以与其他的注意力机制或者损失函数等结合,进一步提升检测效果
6. 总结
ODConv(Omni-Dimensional Dynamic Convolution)的主要原理是通过引入多维度注意力机制来动态调整卷积核参数,从而在卷积神经网络中实现更灵活和精确的特征提取。它通过计算并应用空间注意力、通道注意力、滤波器注意力和核选择注意力,全面地调整卷积核在各个维度上的权重。这种多维度的动态调整机制使得ODConv能够根据输入图像的特征动态选择最合适的卷积核组合,从而在各类高层视觉任务(如图像分类、目标检测和图像分割)中实现显著的性能提升,同时保持较高的参数效率。