目录
1 AKConv原理
1.1 Define the initial sampling position
1.2 Alterable convolutional operation
1.3 Extended AKConv
2 YOLOv8中加入AKConv模块
2.1 AKConv.py文件配置
2.2 task.py配置
2.3 创建添加优化点模块的yolov8-AKConv.yaml
2.4 训练
1 AKConv原理
AKConv: Convolutional Kernel with Arbitrary Sampled Shapes andArbitrary Number of Parameters
摘要:基于卷积运算的神经网络在深度学习领域取得了令人瞩目的成果,但标准卷积运算存在两个固有的缺陷。一方面,卷积运算仅限于局部窗口,无法捕获其他位置的信息, 并且它的采样形状是固定的。 另一方面,卷积核的大小固定为k×k,是一个固定的正方形,参数的数量往往随大小呈平方增长。 很明显,不同数据集和不同位置的目标的形状和大小是不同的。 具有固定样本形状和正方形的卷积核不能很好地适应不断变化的目标。 针对上述问题,本工作探索了可改变核卷积(AKConv),它赋予卷积核任意数量的参数和任意采样形状,为网络开销和性能之间的权衡提供更丰富的选择。 在 AKConv 中,我们通过新的坐标生成算法定义任意大小的卷积核的初始位置。 为了适应目标的变化,我们引入了偏移量来调整每个位置的样本形状。 此外,我们通过使用具有相同大小和不同初始采样形状的 AKConv 来探索神经网络的效果。 AKConv 通过不规则卷积运算完成高效特征提取的过程,为卷积采样形状带来更多探索选择。 在代表性数据集 COCO2017、VOC 7+12 和 VisDrone-DET2021 上进行的物体检测实验充分展示了 AKConv 的优势。 AKConv可以作为即插即用的卷积运算来替代卷积运算来提高网络性能。
1.1 Define the initial sampling position
定义初始采样位置卷积神经网络基于卷积运算,通过规则采样网格将特征定位在相应位置。在规则采样网格中给出了3×3卷积运算。设R表示采样网格,则R表示如下:
然而,采样网格是规则的,而AKConv的目标是不规则形状的卷积核。因此,为了允许不规则卷积核具有采样网格,我们创建了一种任意大小卷积的算法,该算法生成卷积核Pn的初始采样坐标。首先,我们将采样网格生成为规则采样网格,然后为剩余的采样点创建不规则网格,最后,我们将它们缝合以生成整体采样网格。伪代码如算法1所示。
如图2所示,它表明初始采样坐标是为任意大小的卷积生成的。正则卷积的采样网格以(0,0)点为中心。虽然不规则卷积在许多大小上都没有中心,但为了适应所使用的卷积的大小,我们在算法中设置左上角(0,0)点作为采样原点。
在定义了不规则卷积的初始坐标Pn之后,位置P0处的相应卷积运算可以定义如下:
这里,w表示卷积参数。然而,不规则的卷积运算是不可能实现的,因为不规则的采样坐标不能与相应大小的卷积运算相匹配,例如,大小为5、7和13的卷积。聪明的是,我们提出的AKConv实现了这一点。
1.2 Alterable convolutional operation
很明显,标准卷积采样位置是固定的,这导致卷积只能提取当前窗口的局部信息,而不能捕获其他位置的信息。可变形Conv通过卷积运算来学习偏移,以调整初始规则模式的采样网格。该方法在一定程度上弥补了卷积运算的不足。然而,标准卷积和可变形卷积是规则采样网格,不允许具有任意数量参数的卷积核。此外,随着卷积核的大小增加,它们的卷积参数的数量往往会增加一个平方,这对硬件环境来说是不友好的。因此,我们提出了一种新的可变卷积运算(AKConv)。如图3所示,它展示了尺寸为5的AKConv的整体结构。
与可变形Conv类似,在AKConv中,首先通过卷积运算获得相应核的偏移量,卷积运算的维数为(B,2N,H,W),其中N是卷积核的大小。以图3为例,N=5。然后,通过对偏移和原始坐标(P0+Pn)求和来获得修改后的坐标。最后通过插值和重采样获得相应位置的特征。很难提取与不规则卷积核的采样位置相对应的特征。为了解决这个问题,经过深入思考,我们发现有很多方法可以解决。在可变形Conv和RFAConv中,他们在空间维度上堆叠了3×3卷积特征。然后,使用步长为3的卷积运算来提取特征。但是,此方法针对方形采样形状。因此,可以将特征堆叠在行或列上,以使用列卷积或行卷积来提取与不规则采样形状相对应的特征。提取特征以使用适当大小和步长的卷积核。此外,我们可以将特征转换为四个维度(C,N,H,W),然后使用具有步长和卷积大小(N,1,1)的Conv3d来提取特征。当然,我们也可以将通道维度上的特征叠加到(CN,H,W),然后使用1×1卷积将维度降到(C,H,W)。因此,上述所有方法都可以提取与不规则采样形状相对应的特征。只需要重塑特征并使用相应的卷积运算。因此,在图3中,最后的“重塑”和“Conv”表示上述任何方法。此外,为了清楚地显示AKConv的过程,在图3中重新采样后,我们将卷积对应的特征的维度放在第三个维度中。然而,当代码被实现时,它位于最后一个维度。
根据RFAConv和Deformable Conv,我们在列方向上堆叠重新采样的特征,然后使用大小为(N,1)和步长为(N,1)的行卷积。因此,AKConv可以完美地完成不规则卷积特征提取过程。AKConv通过不规则卷积完成特征提取过程,可以根据偏移量灵活调整样本形状,为卷积采样形状带来更多探索选择。与标准卷积和可变形卷积不同,它们受到规则卷积核思想的限制。
1.3 Extended AKConv
我们认为AKConv的设计是一种新颖的设计,它实现了从不规则和任意采样的形状卷积核中提取特征的壮举。即使不使用可变形卷积中的偏移思想,AKConv仍然可以制作各种卷积核形状。因为,AKConv可以使用初始坐标进行重新采样,以显示各种更改。如图4所示,我们为大小为5的卷积设计了各种初始采样形状。在图4中,我们只展示了一些尺寸为5的示例。然而,AKConv的大小可以是任意的,因此,随着大小的增加,AKConv的初始卷积采样形状变得更加丰富甚至无限。考虑到目标形状在数据集之间变化,设计与采样形状相对应的卷积运算至关重要。AKConv通过根据相位特定域设计具有相应形状的卷积运算来完全实现这一点。它也可以类似于可变形Conv,通过添加可学习的偏移来动态适应对象的变化。对于特定的任务,卷积核的初始采样位置的设计是一个重要的问题,因为它是一个先验知识。与Qi等人一样,他们为细长管状结构分割任务提出了具有相应形状的采样坐标,但他们的形状选择仅适用于细长管状结构。
AKConv真正实现了对任意数量的任意形状进行卷积核运算的过程,它可以使卷积核呈现出各种形状。可变形卷积是为了弥补规则卷积的缺点而设计的。而DSConv是为特定的物体形状而设计的。他们没有探索任意大小的卷积和任意样本形状的卷积。AKConv的设计通过允许卷积运算通过Offset有效地提取不规则样本形状的特征来解决这些问题。AKConv允许卷积具有任意数量的卷积参数,并允许卷积呈现各种各样的形状。
2 YOLOv8中加入AKConv模块
2.1 AKConv.py文件配置
下载YOLOv8官方代码,在ultralytics/nn文件夹新建extra_modules文件夹并新建AKConv.py文件
AKconv.py文件代码参考官方提供代码,如下
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint as checkpoint
import math
import numpy as np
from einops import rearrangeclass AKConv(nn.Module):def __init__(self, inc, outc, num_param=5, stride=1, bias=None):super(AKConv, self).__init__()self.num_param = num_paramself.stride = strideself.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)@staticmethoddef _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 + 1q_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 pp = 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)# bilinearx_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_rtx_offset = self._reshape_x_offset(x_offset, self.num_param)out = self.conv(x_offset)return out# generating the inital sampled shapes for the AKConv with different sizes.def _get_p_n(self, N, dtype):base_int = round(math.sqrt(self.num_param))row_number = self.num_param // base_intmod_number = self.num_param % base_intp_n_x, p_n_y = torch.meshgrid(torch.arange(0, row_number),torch.arange(0, base_int))p_n_x = torch.flatten(p_n_x)p_n_y = torch.flatten(p_n_y)if mod_number > 0:mod_p_n_x, mod_p_n_y = torch.meshgrid(torch.arange(row_number, row_number + 1),torch.arange(0, mod_number))mod_p_n_x = torch.flatten(mod_p_n_x)mod_p_n_y = torch.flatten(mod_p_n_y)p_n_x, p_n_y = torch.cat((p_n_x, mod_p_n_x)), torch.cat((p_n_y, mod_p_n_y))p_n = torch.cat([p_n_x, p_n_y], 0)p_n = p_n.view(1, 2 * N, 1, 1).type(dtype)return p_n# no zero-paddingdef _get_p_0(self, h, w, N, dtype):p_0_x, p_0_y = torch.meshgrid(torch.arange(0, h * self.stride, self.stride),torch.arange(0, w * self.stride, self.stride))p_0_x = torch.flatten(p_0_x).view(1, 1, h, w).repeat(1, N, 1, 1)p_0_y = torch.flatten(p_0_y).view(1, 1, h, w).repeat(1, N, 1, 1)p_0 = torch.cat([p_0_x, p_0_y], 1).type(dtype)return p_0def _get_p(self, offset, dtype):N, h, w = offset.size(1) // 2, offset.size(2), offset.size(3)# (1, 2N, 1, 1)p_n = self._get_p_n(N, dtype)# (1, 2N, h, w)p_0 = self._get_p_0(h, w, N, dtype)p = p_0 + p_n + offsetreturn pdef _get_x_q(self, x, q, N):b, h, w, _ = q.size()padded_w = x.size(3)c = x.size(1)# (b, c, h*w)x = x.contiguous().view(b, c, -1)# (b, h, w, N)index = q[..., :N] * padded_w + q[..., N:] # offset_x*w + offset_y# (b, c, h*w*N)index = index.contiguous().unsqueeze(dim=1).expand(-1, c, -1, -1, -1).contiguous().view(b, c, -1)x_offset = x.gather(dim=-1, index=index).contiguous().view(b, c, h, w, N)return x_offset# Stacking resampled features in the row direction.@staticmethoddef _reshape_x_offset(x_offset, num_param):b, c, h, w, n = x_offset.size()# using Conv3d# x_offset = x_offset.permute(0,1,4,2,3), then Conv3d(c,c_out, kernel_size =(num_param,1,1),stride=(num_param,1,1),bias= False)# using 1 × 1 Conv# x_offset = x_offset.permute(0,1,4,2,3), then, x_offset.view(b,c×num_param,h,w) finally, Conv2d(c×num_param,c_out, kernel_size =1,stride=1,bias= False)# using the column conv as follow, then, Conv2d(inc, outc, kernel_size=(num_param, 1), stride=(num_param, 1), bias=bias)x_offset = rearrange(x_offset, 'b c h w n -> b c (h n) w')return x_offset
2.2 task.py配置
然后找到ultralytics/nn/tasks.py文件下里的parse_model函数,将类名加入进去,如下所示
2.3 创建添加优化点模块的yolov8-AKConv.yaml
# Ultralytics YOLO 🚀, GPL-3.0 license# Parameters
nc: 80 # number of classes
depth_multiple: 0.33 # scales module repeats
width_multiple: 0.25 # scales convolution channels# YOLOv8.0n backbone
backbone:# [from, repeats, module, args]- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2- [-1, 1, AKConv, [128, 6, 2]] # 1-P2/4- [-1, 3, C2f, [128, True]]- [-1, 1, AKConv, [256, 6, 2]] # 3-P3/8- [-1, 6, C2f, [256, True]]- [-1, 1, AKConv, [512, 6, 2]] # 5-P4/16- [-1, 6, C2f, [512, True]]- [-1, 1, AKConv, [1024, 6, 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, AKConv, [256, 6, 2]]- [[-1, 12], 1, Concat, [1]] # cat head P4- [-1, 3, C2f, [512]] # 18 (P4/16-medium)- [-1, 1, AKConv, [512, 6, 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)
2.4 训练
完成上述配置之后,可以对数据集进行训练