尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

YOLOv13改进策略【卷积层篇】| 2023 AKConv 任意核卷积,采样点数量和形状都由网络说了算

YOLOv13改进策略【卷积层篇】| 2023 AKConv 任意核卷积,采样点数量和形状都由网络说了算 本文基于YOLOv13 官方仓库iMoonLab/yolov13ultralytics 8.3.63 fork实测整理Windows/CPU 全程可跑。标准卷积的参数量永远和 k² 锁死3×3 就是 9 个采样点想改只能换奇数尺寸。AKConvConvolutional Kernel with Arbitrary Sampled Shapes and Arbitrary SizeZhang et al., 2023, arXiv:2311.11587把这件事彻底放开——采样点的初始形状任意、数量 N 任意5 个、10 个都行每个点再配一个可学习偏移用双线性重采样取特征、列卷积聚合。本文把它替换进 v13n 的第 1 层下采样卷积实测仅1,706 参数。前言卷积层改进的替换式玩法层数不变、无顺移。AKConv 出自 2023 年论文《AKConv: Convolutional Kernel with Arbitrary Sampled Shapes and Arbitrary Size》官方源码 github.com/CV-ZhangXin/AKConv官方类名 LDConv是采样几何路线DCNv2 → 本篇里最激进的一个DCNv2 还要在 k² 网格上变形AKConv 连网格本身都不要了。替换点选在 v13n backbone 的层 1输入 16 通道 320×320 → 输出 32 通道 160×160与普通 stride-2 卷积完全对齐与 CoordConv/RFAConv/ODConv 同位四者构成第一个下采样卷积怎么改的完整对照组。专栏目录YOLOv13改进目录一览专栏地址YOLOv13改进专栏——持续更新各方向即插即用改进一、替换点分析backbone: - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2, 1, 2]] # 1-P2/4 ← 本文替换这一层 ...层数不变无顺移。层 1 的输入 16 通道、输出 32 通道、stride 2——AKConv 官方实现原生支持 stride偏移预测卷积与初始采样网格按步长对齐本文已对 40×40、s2 验证输出恰为 20×20可直接替换下采样卷积。二、AKConv 原理动机标准卷积核的采样点数与 k² 绑定且初始形状固定为正方形网格。目标轮廓各不相同9 点方格未必是最优采样拓扑同时参数量随 k² 增长太快AKConv 的三步初始采样形状 p_nN 个采样点的初始位置按近似方形 余数补点排布如 N5 是 2×2 网格再补 1 个点形状由超参 N 决定、参数量为零注册为 buffer可学习偏移一个 3×3 卷积p_conv零初始化为每个输出位置预测 N 个偏移实际采样坐标 规则步进网格 p_0 p_n offset双线性重采样 列卷积在 N 个任意坐标上双线性采样特征把 N 个采样值沿行方向堆叠交给一个(N,1)的列卷积聚合BNSiLU——卷积核读的是一条由任意位置串起来的采样列stride 的实现p_conv按 stride 下采样、初始网格 p_0 按 stride 步进列卷积以(N,1)步长把堆叠特征压回输出分辨率——s2 时输出恰为输入一半可直接替换下采样层偏移分支的梯度缩放官方对p_conv挂了_set_lr钩子把偏移分支的梯度缩放 0.1 倍防止训练初期偏移抖动官方原样保留。三、实现代码由官方 CV-ZhangXin/AKConv 等价适配① 官方einops.rearrange的空间重排以 permute/reshape 等价替换② 采样坐标显式绑定 offset 的 device/dtype官方写法固定 CPU 张量GPU 上会报错③meshgrid显式传indexingij。已做同权重输出逐位比对三种配置下 max diff 0import math import torch import torch.nn as nn class AKConv(nn.Module): AKConv 任意核卷积 (Zhang et al., 2023)官方 CV-ZhangXin/AKConv 等价内嵌。 卷积核的采样点数量与形状任意每个输出位置先在 Nnum_param 个初始网格 可学偏移 的坐标上双线性重采样特征再把 N 个采样值沿行方向堆叠交给 (N,1) 列卷积聚合—— 参数量只随 N 线性增长而不是 k²。 def __init__(self, inc, outc, num_param5, stride1, biasNone): super().__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), biasbias), nn.BatchNorm2d(outc), nn.SiLU()) # 官方注明加 BNSiLU 与 YOLO 的 Conv 对齐 self.p_conv nn.Conv2d(inc, 2 * num_param, kernel_size3, padding1, stridestride) nn.init.constant_(self.p_conv.weight, 0) # 偏移零初始化起点即固定初始采样形状 self.p_conv.register_full_backward_hook(self._set_lr) self.register_buffer(p_n, self._get_p_n(Nself.num_param)) 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) N offset.size(1) // 2 # (b, 2N, h, w) 输入坐标系下的 N 个采样点坐标 p self._get_p(offset, x.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(dim1) * x_q_lt \ g_rb.unsqueeze(dim1) * x_q_rb \ g_lb.unsqueeze(dim1) * x_q_lb \ g_rt.unsqueeze(dim1) * x_q_rt x_offset self._reshape_x_offset(x_offset, self.num_param) out self.conv(x_offset) return out # generating the initial sampled shapes for the AKConv with different sizes. def _get_p_n(self, N): base_int round(math.sqrt(self.num_param)) row_number self.num_param // base_int mod_number self.num_param % base_int p_n_x, p_n_y torch.meshgrid( torch.arange(0, row_number), torch.arange(0, base_int), indexingij) 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), indexingij) 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) return p_n # no zero-padding def _get_p_0(self, h, w, N, dtype, device): p_0_x, p_0_y torch.meshgrid( torch.arange(0, h * self.stride, self.stride, devicedevice), torch.arange(0, w * self.stride, self.stride, devicedevice), indexingij) 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_0 def _get_p(self, offset, dtype): N, h, w offset.size(1) // 2, offset.size(2), offset.size(3) # (1, 2N, h, w) 初始采样网格规则步进网格 p_0 初始采样形状 p_n 可学偏移 p_0 self._get_p_0(h, w, N, dtype, offset.device) p p_0 self.p_n.type_as(p_0) offset return p def _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(dim1).expand(-1, c, -1, -1, -1).contiguous().view(b, c, -1) x_offset x.gather(dim-1, indexindex).contiguous().view(b, c, h, w, N) return x_offset # Stacking resampled features in the row direction. staticmethod def _reshape_x_offset(x_offset, num_param): b, c, h, w, n x_offset.size() # 等价于官方 rearrange(b c h w n - b c (h n) w)把 N 个采样值沿行方向堆叠 return x_offset.permute(0, 1, 2, 4, 3).reshape(b, c, h * n, w).contiguous()注意签名与Conv对齐c1 自动传入c2/num_param/stride 来自 yaml注册用卷积类分支c2 宽度缩放。四、添加步骤v13 版1. 修改ultralytics/nn/modules/conv.pyAKConv类粘贴到 conv.py 末尾。2. 修改ultralytics/nn/modules/__init__.pyfrom .conv import (...)里追加AKConv,__all__里给最后一项DSConv补逗号后追加AKConv。3. 修改ultralytics/nn/tasks.py顶部导入块加AKConvparse_model()里新增卷积类分支c2 宽度缩放elif m is AKConv: # 卷积类c1 自动传入c2 按宽度系数缩放 c1 ch[f] c2 args[0] if c2 ! nc: c2 make_divisible(min(c2, max_channels) * width, 8) args [c1, c2, *args[1:]]插入位置elif m is FullPAD_Tunnel:之后、else:之前。五、yaml 模型文件只改第 1 层其余与官方 yolov13.yaml 逐行一致AKConv参数目标通道、采样点数 N、strideN5 为论文示例的近似方形 补点形状# Ultralytics YOLOv13n AKConv 任意核卷积 nc: 80 scales: n: [0.50, 0.25, 1024] s: [0.50, 0.50, 1024] l: [1.00, 1.00, 512] x: [1.00, 1.50, 512] backbone: # [from, repeats, module, args] - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, AKConv, [128, 5, 2]] # 1-P2/4 ★AKConv替换stride-2卷积层数不变 - [-1, 2, DSC3k2, [256, False, 0.25]] # 2 - [-1, 1, Conv, [256, 3, 2, 1, 4]] # 3-P3/8 - [-1, 2, DSC3k2, [512, False, 0.25]] # 4 - [-1, 1, DSConv, [512, 3, 2]] # 5-P4/16 - [-1, 4, A2C2f, [512, True, 4]] # 6 - [-1, 1, DSConv, [1024, 3, 2]] # 7-P5/32 - [-1, 4, A2C2f, [1024, True, 1]] # 8 head: - [[4, 6, 8], 2, HyperACE, [512, 8, True, True, 0.5, 1, both]] # 9 - [-1, 1, nn.Upsample, [None, 2, nearest]] # 10 - [ 9, 1, DownsampleConv, []] # 11 - [[6, 9], 1, FullPAD_Tunnel, []] # 12 - [[4, 10], 1, FullPAD_Tunnel, []] # 13 - [[8, 11], 1, FullPAD_Tunnel, []] # 14 - [-1, 1, nn.Upsample, [None, 2, nearest]] # 15 - [[-1, 12], 1, Concat, [1]] # 16 cat backbone P4 - [-1, 2, DSC3k2, [512, True]] # 17 - [[-1, 9], 1, FullPAD_Tunnel, []] # 18 - [17, 1, nn.Upsample, [None, 2, nearest]] # 19 - [[-1, 13], 1, Concat, [1]] # 20 cat backbone P3 - [-1, 2, DSC3k2, [256, True]] # 21 - [10, 1, Conv, [256, 1, 1]] # 22 - [[21, 22], 1, FullPAD_Tunnel, []] # 23 - [-1, 1, Conv, [256, 3, 2]] # 24 - [[-1, 18], 1, Concat, [1]] # 25 cat head P4 - [-1, 2, DSC3k2, [512, True]] # 26 - [[-1, 9], 1, FullPAD_Tunnel, []] # 27 - [26, 1, Conv, [512, 3, 2]] # 28 - [[-1, 14], 1, Concat, [1]] # 29 cat head P5 - [-1, 2, DSC3k2, [1024,True]] # 30 - [[-1, 11], 1, FullPAD_Tunnel, []] # 31 - [[23, 27, 31], 1, Detect, [nc]] # 32 Detect(P3, P4, P5)六、成功运行结果yolo detect train modelyolov13n-AKConv.yaml datamydata.yaml epochs100 imgsz640 batch4Python 方式from ultralytics import YOLO model YOLO(yolov13n-AKConv.yaml).load(yolov13n.pt) model.train(datamydata.yaml, epochs100, imgsz640, batch4)本文实测YOLOv13 官方仓库Windows CPUv13n 结构替换层 1基线 yolov13n: 2,494,151 parameters, 6.5 GFLOPs层1为 2,368 参数 AKConv: 2,495,857 parameters, 6.6 GFLOPs层1为 4,074 参数 3 epochs completed. Results saved to runs\detect\v13_akconv_smoke✅ 断言全部通过① 适配版与官方LDConveinops 版同权重输出逐位一致40×40 s2、40×40 s1、39×39 s1 N10 三种配置 max diff 0② 第 1 层为AKConv总参数量恰为基线 1,706③ 层数不变整网 640 前向正常、反向梯度可达本层3 轮冒烟训练正常收敛。参数量分解c116, c232, N5, s2部件参数量说明列卷积 Conv(16→32, (5,1))2,560聚合 5 个采样点BN(32)64p_conv Conv(16→10, 3×3, s2)1,450偏移预测零初始化 0.1 梯度缩放初始采样形状 p_n0固定 buffer不参与训练合计4,0741,706原层为 g2 分组卷积 2,368模型ParamsGFLOPs层1参数变化说明YOLOv13n 基线2,494,1516.52,368官方 stride-2 分组卷积AKConv2,495,8576.64,0741,706N5 任意形状采样七、总结AKConv 的三点记忆锚采样点数量 N 与形状任意初始形状零参数p_n 只是个 buffer、重采样 堆叠 列卷积三段式N 个采样值沿行堆叠(N,1) 卷积一步聚合、偏移分支梯度缩放 0.1官方防抖设计。它与 DCNv2 同走采样几何路线互为对照DCNv2 在 k² 网格上学偏移参数多、拓扑固定AKConv 连拓扑都可调参数随 N 线性也和 ODConv 篇同位替换层 1——ODConv 换核权重怎么动态AKConv 换采样点放哪里。觉得有帮助的话点赞收藏关注三连支持一下评论区欢迎交流你的实验结果~专栏目录YOLOv13改进目录一览 专栏地址YOLOv13改进专栏——持续更新各方向即插即用改进
返回列表