084、DyHead 动态检测头:Scale加Space加Task 三维注意力的 Attention 偏移量计算 084、DyHead 动态检测头Scale加Space加Task 三维注意力的 Attention 偏移量计算去年在调一个无人机小目标检测项目时我遇到了一个诡异的精度瓶颈——换了各种Backbone、NeckmAP始终卡在0.52上不去。当时用的还是标准的YOLOv5检测头直觉告诉我问题出在检测头对不同尺度目标的适应能力上。直到我把DyHead塞进去mAP直接跳到0.61我才意识到原来检测头才是那个被低估的瓶颈。从静态到动态检测头为什么需要“注意力偏移”传统检测头本质上就是个卷积全连接的组合拳对每个空间位置、每个尺度、每个任务分支都一视同仁。但现实场景中大目标和小目标需要的感受野不同分类和回归任务关注的特征也不同。这就好比让一个厨师同时做川菜和粤菜却要求他用同一套刀工和火候——显然不合理。DyHead的核心思想就是让检测头学会“看人下菜碟”。它引入了一个三维注意力机制在Scale尺度、Space空间、Task任务三个维度上动态调整特征响应。这里的“注意力偏移量”就是关键——它不是简单地给特征图乘个权重而是学习一个偏移量让注意力聚焦到真正重要的位置。三维注意力的数学本质别被公式吓到先看DyHead的注意力计算公式我尽量用人话讲清楚W(F) σ( f( 1/N ∑(δ( ∑(W_k · F) ) · W_q · F) ) )别急着关页面。这个公式拆开看就三件事尺度感知聚合对不同尺度的特征图做加权求和相当于让网络自己决定“当前这个目标该看哪个尺度的特征”空间位置编码通过3x3深度可分离卷积生成空间注意力图告诉网络“哪里重要”任务特定偏移用两个并行的1x1卷积分别生成分类和回归的偏移量让两个任务各取所需这里有个容易踩坑的地方公式里的σ是Sigmoid不是Softmax。我第一次实现时用了Softmax结果梯度全炸了。因为Sigmoid输出的是0-1之间的独立概率而Softmax会强制所有位置的概率和为1这在空间注意力里会导致“此消彼长”的竞争关系反而抑制了多目标检测。代码实现从理论到PyTorch的落地直接看核心代码我习惯把注意力模块拆成三个子模块classDyHeadBlock(nn.Module):def__init__(self,in_channels,level3):super().__init__()# 尺度注意力每个尺度一个可学习的权重self.scale_attnnn.Parameter(torch.ones(level,1,1,1))# 空间注意力深度可分离卷积别用普通卷积参数量会爆炸self.spatial_convnn.Conv2d(in_channels,in_channels,3,padding1,groupsin_channels)# 任务注意力两个分支分别生成偏移量self.task_conv_clsnn.Conv2d(in_channels,in_channels,1)self.task_conv_regnn.Conv2d(in_channels,in_channels,1)defforward(self,x):# x是list包含不同尺度的特征图# 尺度注意力先做插值统一尺寸再加权求和# 这里踩过坑直接resize会丢失信息建议用F.interpolate的bilinear模式feats[]fori,featinenumerate(x):feats.append(F.interpolate(feat,sizex[0].shape[2:],modebilinear))feat_stacktorch.stack(feats,dim0)# [level, B, C, H, W]scale_weightF.softmax(self.scale_attn,dim0)# 别用sigmoid这里需要归一化scale_out(feat_stack*scale_weight).sum(dim0)# 空间注意力生成偏移量注意这里要加残差spatial_offsetself.spatial_conv(scale_out)# 别这样写直接乘spatial_offset会导致梯度消失# 正确做法用Sigmoid生成0-1的权重spatial_weighttorch.sigmoid(spatial_offset)spatial_outscale_out*spatial_weightscale_out# 残差连接# 任务注意力生成分类和回归的偏移量cls_offsetself.task_conv_cls(spatial_out)reg_offsetself.task_conv_reg(spatial_out)# 这里有个trick偏移量要经过tanh限制范围否则训练初期会乱飘cls_offsettorch.tanh(cls_offset)*0.1# 限制在[-0.1, 0.1]reg_offsettorch.tanh(reg_offset)*0.1# 最终输出原始特征加上偏移量cls_outspatial_outcls_offset reg_outspatial_outreg_offsetreturncls_out,reg_out这段代码里有个细节值得注意尺度注意力用Softmax空间注意力用Sigmoid。为什么因为尺度维度上不同尺度的权重是互斥的一个目标只能属于一个尺度范围而空间维度上不同位置可以同时被关注。偏移量计算的精髓为什么是“偏移”而不是“权重”传统注意力机制是乘性权重比如SE-Net的通道注意力。但DyHead用的是加性偏移——让特征在原始基础上“偏移”到更合适的位置。这个设计背后的直觉是检测头已经学到了不错的特征只需要微调不需要重头学。偏移量的计算过程可以理解为先通过尺度注意力找到“该看哪个尺度的特征”再通过空间注意力找到“该看哪个位置”最后通过任务注意力找到“分类和回归各自该关注什么”这三个步骤是串行的但每个步骤都保留了残差连接。我试过去掉残差结果训练loss直接下不去。残差在这里不是锦上添花而是雪中送炭——它保证了梯度能顺畅地流回Backbone。实际部署时的血泪教训在NVIDIA Jetson上部署DyHead时我遇到了一个性能问题深度可分离卷积在推理时比普通卷积慢。排查后发现是PyTorch的group convolution在推理优化上做得不够好。解决方案有两个用torch.jit.script把整个DyHead模块编译成TorchScript推理速度提升30%或者把深度可分离卷积替换成普通3x3卷积精度下降不到0.5%但速度翻倍另外训练时一定要用梯度裁剪。DyHead的偏移量虽然加了tanh限制但训练初期梯度仍然可能爆炸。我习惯把max_norm设为0.1效果不错。个人经验什么时候该用DyHead不是所有场景都适合DyHead。如果你的数据集目标尺度单一比如人脸检测或者任务分支简单比如只有分类那DyHead带来的收益可能不如直接堆层数。但如果你遇到以下情况强烈建议试试多尺度目标检测小目标大目标混在一起分类和回归任务冲突严重比如分类准但定位差检测头成为性能瓶颈换Backbone没效果最后说个玄学DyHead对学习率比较敏感。我习惯把检测头的学习率设为Backbone的0.1倍然后用余弦退火调度。如果发现训练震荡先检查学习率再检查梯度裁剪最后才怀疑代码写错了。