YOLOv8改进-论文笔记】 AKConv(可改变核卷积):任意数量的参数和任意采样形状的即插即用的卷积
YOLO目标检测创新改进与实战案例专栏
专栏目录: YOLO有效改进系列及项目实战目录 包含卷积,主干 注意力,检测头等创新机制 以及 各种目标检测分割项目实战案例
专栏链接: YOLO目标检测创新改进与实战案例
介绍
AKConv介绍
AKConv(可改变核卷积),主要用来解决传统卷积中固有的缺陷。
-
卷积窗口的固定大小:
传统卷积中,每个神经元只关注输入数据中一个固定大小的局部区域,而不能有效地捕捉到其他窗口的信息。这在处理全局上下文信息时可能会限制网络的性能。
-
卷积核尺寸的固定性:
传统卷积网络中的卷积核大小通常是固定的(如 3x3, 5x5)。这种固定尺寸的核可能不适合捕捉所有尺度的特征。例如,较小的核可能适合捕捉细粒度的特征,而较大的核可能更适合捕捉更宽泛的特征。固定的卷积核尺寸和结构限制了网络在处理多尺度特征时的灵活性和有效性。
摘要
基于卷积操作的神经网络在深度学习领域取得了显著成果,但标准卷积操作中存在两个固有缺陷。一方面,卷积操作仅限于局部窗口,无法从其他位置捕获信息,且其采样形状是固定的。另一方面,卷积核的大小固定为 k×k,这是一个固定的正方形形状,随着大小的增长,参数的数量呈平方增长。显然,在不同数据集和不同位置,目标的形状和大小是多样的。具有固定采样形状和正方形的卷积核不适应目标的变化。针对上述问题,本工作探索了可变核卷积(AKConv),它使卷积核具有任意数量的参数和任意采样形状,为网络开销与性能之间的权衡提供了更丰富的选择。在AKConv中,我们通过一种新的坐标生成算法定义了任意大小卷积核的初始位置。为适应目标的变化,我们引入偏移量来调整每个位置样本的形状。此外,我们通过使用相同大小和不同初始采样形状的AKConv来探索神经网络的效果。AKConv通过不规则卷积操作完成高效的特征提取过程,并为卷积采样形状带来更多探索选项。在代表性数据集COCO2017、VOC 7+12和VisDrone-DET2021上的目标检测实验充分展示了AKConv的优势。AKConv可以作为即插即用的卷积操作替换卷积操作以提高网络性能。相关任务的代码可在 https://github.com/CV-ZhangXin/AKConv 找到。
AKConv的创新特点:
-
卷积核的灵活性:AKConv支持卷积核参数的任意化设定,能够根据具体的应用需求自由调整其大小与形状,以更精准地适配不同尺度的目标特征。
-
初始采样位置生成算法:AKConv引入了一种创新的算法,用于为各种尺寸的卷积核确定初始采样位置,增强了网络处理不同大小目标的适应性。
-
动态采样位置偏移:为了应对目标形状的多样性,AKConv采用动态偏移技术调整采样位置,以实现更精确的特征捕捉。
-
模型参数与计算效率的优化:AKConv的参数数量可线性调节,有助于在受限的硬件资源下实现计算效率的优化,特别适合于资源受限的轻量级模型部署。
yoloV8引入AKConv
新建ultralytics/nn/Conv/AKConv.py
import torch.nn as nn
import torch
from einops import rearrange
import math
class AKConv(nn.Module):
def __init__(self, inc, outc, num_param, stride=1, bias=None):
super(AKConv, self).__init__()
self.num_param = num_param
self.stride = stride
self.conv = nn.Sequential(nn.Conv2d(inc, outc, kernel_size=(num_param, 1), stride=(num_param, 1), bias=bias),
nn.BatchNorm2d(outc),
nn.SiLU()) # the conv adds the BN and SiLU to compare original Conv in YOLOv5.
self.p_conv = nn.Conv2d(inc, 2 * num_param, kernel_size=3, padding=1, stride=stride)
nn.init.constant_(self.p_conv.weight, 0)
self.p_conv.register_full_backward_hook(self._set_lr)
@staticmethod
def _set_lr(module, grad_input, grad_output):
grad_input = (grad_input[i] * 0.1 for i in range(len(grad_input)))
grad_output = (grad_output[i] * 0.1 for i in range(len(grad_output)))
def forward(self, x):
# N is num_param.
offset = self.p_conv(x)
dtype = offset.data.type()
N = offset.size(1) // 2
# (b, 2N, h, w)
p = self._get_p(offset, dtype)
# (b, h, w, 2N)
p = p.contiguous().permute(0, 2, 3, 1)
q_lt = p.detach().floor()
q_rb = q_lt + 1
q_lt = torch.cat([torch.clamp(q_lt[..., :N], 0, x.size(2) - 1), torch.clamp(q_lt[..., N:], 0, x.size(3) - 1)],
dim=-1).long()
q_rb = torch.cat([torch.clamp(q_rb[..., :N], 0, x.size(2) - 1), torch.clamp(q_rb[..., N:], 0, x.size(3) - 1)],
dim=-1).long()
q_lb = torch.cat([q_lt[..., :N], q_rb[..., N:]], dim=-1)
q_rt = torch.cat([q_rb[..., :N], q_lt[..., N:]], dim=-1)
# clip p
p = torch.cat([torch.clamp(p[..., :N], 0, x.size(2) - 1), torch.clamp(p[..., N:], 0, x.size(3) - 1)], dim=-1)
# bilinear kernel (b, h, w, N)
g_lt = (1 + (q_lt[..., :N].type_as(p) - p[..., :N])) * (1 + (q_lt[..., N:].type_as(p) - p[..., N:]))
g_rb = (1 - (q_rb[..., :N].type_as(p) - p[..., :N])) * (1 - (q_rb[..., N:].type_as(p) - p[..., N:]))
g_lb = (1 + (q_lb[..., :N].type_as(p) - p[..., :N])) * (1 - (q_lb[..., N:].type_as(p) - p[..., N:]))
g_rt = (1 - (q_rt[..., :N].type_as(p) - p[..., :N])) * (1 + (q_rt[..., N:].type_as(p) - p[..., N:]))
# resampling the features based on the modified coordinates.
x_q_lt = self._get_x_q(x, q_lt, N)
x_q_rb = self._get_x_q(x, q_rb, N)
x_q_lb = self._get_x_q(x, q_lb, N)
x_q_rt = self._get_x_q(x, q_rt, N)
# bilinear
x_offset = g_lt.unsqueeze(dim=1) * x_q_lt + \
g_rb.unsqueeze(dim=1) * x_q_rb + \
g_lb.unsqueeze(dim=1) * x_q_lb + \
g_rt.unsqueeze(dim=1) * x_q_rt
x_offset = self._reshape_x_offset(x_offset, self.num_param)
out = self.conv(x_offset)
return out
修改tasks.py
详见:https://blog.csdn.net/shangyanaf/article/details/135661842