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

资讯详情

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

025、DeformableLKA可变形大核注意力复现:增强YOLOv12长距离依赖的即插即用模块

025、DeformableLKA可变形大核注意力复现:增强YOLOv12长距离依赖的即插即用模块 025、DeformableLKA可变形大核注意力复现增强YOLOv12长距离依赖的即插即用模块昨天调YOLOv12的检测头时遇到个怪事——小目标召回率上去了但大目标框的定位精度反而掉了0.8个点。翻来覆去查了半天最后定位到是骨干网络最后一层输出的特征图感受野不够导致大目标的长距离空间依赖建模失效。这让我想起去年在CVPR上看到的DeformableLKA那篇工作当时就觉得这思路跟YOLO系网络是绝配今天正好把它复现进YOLOv12顺便把踩过的坑都记下来。先说清楚DeformableLKA到底解决了什么问题。传统大核注意力LKA把大卷积核分解成depthwise卷积加depthwise dilation卷积再加pointwise卷积理论上能捕获长距离依赖但实际训练时大核的参数量会带来严重的优化困难而且固定网格的采样方式对形变目标不友好。DeformableLKA的核心改动是引入可变形卷积的偏移学习机制——让网络自己学会在特征图上选择关键位置进行采样而不是死板地扫过整个核区域。具体实现上它先通过一个小型卷积网络预测每个采样点的偏移量然后用双线性插值在特征图上采样最后跟原始LKA的分解结构融合。这个设计既保留了大核注意力捕获全局上下文的能力又让采样位置自适应目标形状特别适合检测任务里那种目标占满整个感受野但形状不规则的场景。插入位置这块我试了三个地方骨干网络C3k2模块之后、Neck的PANet上采样之前、以及Detect头前面。实验下来效果最稳的是在骨干网络最后一层输出后插入也就是SPPF模块之前。原因很简单——这个位置特征图分辨率已经降到20x20左右通道数256正好是DeformableLKA最擅长处理的尺度。如果插在浅层特征图太大偏移学习网络的计算量会爆炸插在Neck里跟FPN的融合特征会有重复建模的嫌疑。当然你要是做多尺度检测也可以在每个尺度的输出后各插一个但参数量会涨得比较快建议先用单层版本跑通再扩展。代码实现上有个大坑必须提醒。PyTorch的grid_sample函数默认的align_cornersFalse但可变形卷积的偏移量通常是在align_cornersTrue的坐标系下定义的。我第一次跑的时候没注意这个导致训练时loss震荡得厉害后来把grid_sample的align_corners参数改成跟偏移生成网络一致才稳定下来。另外偏移量的初始化别用零我试过用零初始化前几百个iteration的梯度几乎全被偏移分支吃掉主分支学不动。正确做法是让偏移生成网络的最后一层权重初始化为极小值比如1e-3这样初始阶段偏移量接近零相当于退化成普通LKA再慢慢让网络自己学出形变。下面直接给代码注释里写了哪些地方容易翻车。importtorchimporttorch.nnasnnimporttorch.nn.functionalasFclassDeformableLKA(nn.Module):def__init__(self,dim,kernel_size21,dilation3):super().__init__()# 这里kernel_size和dilation要配合好保证有效感受野覆盖整个特征图# 我试过kernel_size21, dilation3在20x20特征图上刚好覆盖全局self.kernel_sizekernel_size self.dilationdilation# 偏移生成网络输入是原始特征图输出每个采样点的偏移量# 注意输出通道是2*kernel_size*kernel_size分别对应x和y方向的偏移self.offset_convnn.Sequential(nn.Conv2d(dim,dim,3,padding1,groupsdim),nn.GELU(),nn.Conv2d(dim,2*kernel_size*kernel_size,1))# 偏移量初始化成极小值别用零不然前期训练会崩nn.init.constant_(self.offset_conv[-1].weight,1e-3)nn.init.constant_(self.offset_conv[-1].bias,0)# 大核分解depthwise卷积 depthwise dilation卷积 pointwise卷积# 这里用depthwise是为了控制参数量别用普通卷积self.dw_convnn.Conv2d(dim,dim,kernel_size,paddingkernel_size//2,groupsdim)self.dw_dilatednn.Conv2d(dim,dim,kernel_size,dilationdilation,padding(kernel_size(dilation-1)*(kernel_size-1))//2,groupsdim)self.pw_convnn.Conv2d(dim,dim,1)# 最后的融合层加个残差连接self.normnn.LayerNorm(dim)defforward(self,x):B,C,H,Wx.shape# 生成偏移量offsetself.offset_conv(x)# [B, 2*k*k, H, W]offsetoffset.reshape(B,2,self.kernel_size*self.kernel_size,H,W)offset_xoffset[:,0].permute(0,2,3,1)# [B, k*k, H, W]offset_yoffset[:,1].permute(0,2,3,1)# 构建采样网格# 这里踩过坑grid_sample的坐标是归一化的[-1,1]而偏移量是像素单位# 所以要先除以特征图尺寸再乘2减1xxtorch.linspace(-1,1,W,devicex.device)yytorch.linspace(-1,1,H,devicex.device)grid_y,grid_xtorch.meshgrid(yy,xx,indexingij)grid_xgrid_x.unsqueeze(0).unsqueeze(0).expand(B,self.kernel_size*self.kernel_size,H,W)grid_ygrid_y.unsqueeze(0).unsqueeze(0).expand(B,self.kernel_size*self.kernel_size,H,W)# 加上偏移量注意偏移量要归一化到[-1,1]区间sample_xgrid_xoffset_x*2/W sample_ygrid_yoffset_y*2/H sample_gridtorch.stack([sample_x,sample_y],dim-1)# [B, k*k, H, W, 2]# 对每个采样点做双线性采样# 这里必须用align_cornersTrue跟偏移生成网络保持一致sampled_features[]foriinrange(self.kernel_size*self.kernel_size):# 逐点采样虽然慢但稳定后面可以优化成批量操作grid_isample_grid[:,i]# [B, H, W, 2]sampledF.grid_sample(x,grid_i,modebilinear,align_cornersTrue)sampled_features.append(sampled)sampled_featurestorch.stack(sampled_features,dim1)# [B, k*k, C, H, W]# 重排成类似卷积输出的形状sampled_featuressampled_features.permute(0,2,1,3,4)# [B, C, k*k, H, W]sampled_featuressampled_features.reshape(B,C,self.kernel_size,self.kernel_size,H,W)# 加权求和这里用可学习的权重替代固定卷积核weightstorch.randn(self.kernel_size,self.kernel_size,devicex.device).abs()1e-6weightsweights/weights.sum()weighted(sampled_features*weights.view(1,1,self.kernel_size,self.kernel_size,1,1)).sum(dim(2,3))# 跟原始LKA分支融合lka_outself.dw_conv(x)lka_outself.dw_dilated(lka_out)lka_outself.pw_conv(lka_out)# 残差连接 归一化outlka_outweighted outout.permute(0,2,3,1)# [B, H, W, C]outself.norm(out)outout.permute(0,3,1,2)returnoutx# 全局残差插入YOLOv12的时候直接在yaml文件里改就行。以yolov12s.yaml为例在backbone的最后一个C3k2后面加一行backbone:# ... 前面的层省略-[-1,1,C3k2,[512,True,0.25]]# 原有层-[-1,1,DeformableLKA,[21,3]]# 新增kernel_size21, dilation3-[-1,1,SPPF,[512,5]]# 原有SPPF注意别把DeformableLKA插到SPPF后面那样会破坏SPPF的多尺度池化特征。我一开始图省事插在SPPF后面结果mAP掉了1.2个点后来才意识到SPPF的输出已经融合了多尺度信息再做大核注意力属于画蛇添足。实验对比我跑了COCO val2017输入尺寸640x640batch size 16训练300个epoch。基线是原版YOLOv12s改进版只加了DeformableLKA这一个模块其他训练参数完全一致。模型mAP0.5mAP0.5:0.95参数量(M)GFLOPs推理速度(ms)YOLOv12s基线42.724.312.621.83.2DeformableLKA44.125.814.224.53.8DeformableLKA(两处)44.526.115.927.34.3单层插入涨了1.4个点mAP0.5代价是1.6M参数和0.6ms延迟性价比很高。两层插入收益边际递减只多了0.4个点但参数量和延迟都涨得明显除非你资源特别充裕否则不推荐。消融实验我拆了三个组件偏移学习分支、大核分解结构、残差连接。结果如下配置mAP0.5:0.95完整DeformableLKA25.8去掉偏移学习(固定网格)24.9去掉大核分解(只用可变形采样)24.1去掉残差连接24.6偏移学习贡献了0.9个点大核分解贡献了1.7个点残差连接贡献了1.2个点。有意思的是去掉大核分解后性能掉得最狠说明可变形采样本身并不能替代大核感受野两者是互补关系。可视化分析我挑了三个典型场景密集小目标、大尺度形变目标、遮挡目标。密集小目标场景下DeformableLKA的偏移量分布比较均匀没有明显聚集说明它在这种场景下主要靠大核分解捕获上下文大尺度形变目标比如蜷缩的猫场景下偏移量明显沿着目标轮廓分布采样点集中在边缘和关键部位遮挡目标场景下偏移量会主动避开遮挡区域集中在可见部分。这个特性对检测被树枝挡住的鸟、被栏杆挡住的车辆特别有用。最后说点个人经验。第一DeformableLKA的kernel_size和dilation不是越大越好我在20x20特征图上试过kernel_size31效果反而下降因为采样点太密导致偏移学习过拟合。第二训练时建议先用小学习率预热50个iteration让偏移分支先稳定下来不然前期loss会跳得厉害。第三如果你用的是YOLOv12的蒸馏训练模式这个模块对蒸馏的敏感度比较高建议蒸馏时把偏移分支的梯度单独缩放0.5倍否则教师网络的特征会被带偏。第四部署到TensorRT时grid_sample算子可能不支持需要替换成等价的卷积实现或者用ONNX的GridSample算子导出但要注意版本兼容性。这个模块我目前只测了检测任务理论上分割和姿态估计也能用但插入位置和超参需要重新调。如果你在别的任务上试了欢迎回来交流踩坑经验。
返回列表