
1. 项目背景与改进动机做目标检测的同行这两年应该都有同一个感受YOLO系列迭代越来越快但检测头的进化速度明显跟不上主干网络和 Neck 的节奏。YOLOv11 的 C3k2 结构和注意力机制的加入让特征提取能力上了一个台阶可到了最终输出层——也就是检测头——依然沿用了解耦头的框架思路。这个瓶颈在小目标、遮挡目标、旋转目标这三类场景下暴露得尤其明显。我先说说为什么要专门在检测头上做文章。很多改进论文喜欢在 Backbone 里堆模块C2f 换 C3k2、加注意力、加 SPFF这些确实能提升特征质量但有个问题特征图质量再高检测头如果不具备自适应的特征对齐和融合能力最终输出的回归框和分类置信度依然会被噪声干扰。尤其是小目标在深层特征图上可能只剩几个像素的响应检测头如果只是简单卷积分类回归信息被稀释是必然的。遮挡目标更麻烦可见区域和遮挡边界在特征空间里是错位的如果检测头不能感知这种错位定位和分类大概率会互相干扰。有人可能会问旋转目标不是应该改检测头的回归分支吗确实常规的 xywh 回归形式对旋转框无能为力但 FAAHead 的思路不是简单加一个角度参数而是在特征层面预先完成方向感知对齐让旋转目标的特征分布从源头就变得更容易被回归分支拟合。这一点后面我会展开讲。这套改进方案我在多个数据集上做了验证包括公开的遥感旋转目标数据集和自建的密集遮挡场景数据集平均涨点幅度在 2.4~4.1 个 mAP 点之间。单看数字可能不算惊爆但在不增加推理延迟、不引入额外后处理的前提下拿到这个收益性价比其实很高。2. 检测头痛点与 FAAHead 的核心设计思路2.1 原版检测头在小目标、遮挡目标上的三个先天缺陷原版 YOLOv11 检测头本质上还是 anchor-free 的解耦结构每个尺度一个检测头分别输出分类分支和回归分支。这套设计在常规中大型目标上没问题但碰上小目标和遮挡目标有三个缺陷会被放大。特征对齐缺失是第一个问题。分类分支和回归分支共享的是同一组特征图但两个任务的敏感区域完全不同分类关注的是目标的类别显著性区域回归关注的是目标的边界信息。小目标的边界信息本来就弱两个分支使用完全相同的特征图回归分支很难从微弱边界中提取有效线索。多尺度信息融合不足是第二个问题。YOLOv11 虽然通过 PANet 结构融合了不同尺度的特征但到了检测头里每个尺度只处理自己对应的那层特征图。小目标在 P3 层80x80有响应在 P5 层20x20可能完全消失而 P5 层的语义信息对分辨小目标的类别又很重要。检测头只能被动接受单一尺度特征浪费了跨层互补的机会。遮挡边界语义混淆是第三个问题。遮挡目标的边界是“假边界”——它只是遮挡物的边缘并不是目标的真实轮廓。原版检测头没有显式去建模这种边界不确定性导致回归分支经常把遮挡物的边界当成目标边界框就会偏向遮挡物一侧。2.2 FAAHead特征对齐与自适应聚合FAAHead 的全称是 Feature Alignment and Aggregation Head核心思路是解决上面三个问题。整套设计包含三个模块轻量级自适应空间对齐模块LASA在分类和回归分支前各加一个可变形卷积的轻量化变体让每个分支自己学习特征偏移量也就是让特征采样位置自适应地靠近分支真正关心的区域。分类分支往类别响应中心靠回归分支往边界方向靠。跨尺度特征选择聚合模块CFSA引入一个特征选择门控机制将相邻尺度的特征图按通道注意力加权融合。P3 和 P4 的信息可以流进 P5 检测头但融合权重是可学习的模型自己决定每个尺度贡献多少。方向感知编码模块OAE借鉴旋转检测中角度回归的思路但做得更轻量——不是在回归头里直接加角度参数而是在特征中编码目标的近似朝向。通过预测一个方向向量场把旋转目标的特征分布规整到接近正框的分布形态。这三个模块的权重非常轻加起来参数量大约只占原检测头的 12% 左右。对推理速度的影响可以忽略不计因为这个量级的计算量对 GPU 来说根本感知不到。2.3 为什么是“改进检测头”而不是“换主干”我在做方案选型时考虑过另一条路线把 Backbone 换得更强比如引入大 kernel 卷积、密集连接等结构。但这带来了几个问题训练成本增加、预训练权重失效、部署时模型体积变大。更重要的是很多场景下瓶颈根本不在特征提取而在特征到输出的映射环节。尤其在小目标和遮挡目标场景Backbone 已经提取到了有效特征只是检测头用不好这些特征。改进检测头是四两拨千斤的做法训练稳定、改动范围可控、消融实验也容易做。3. FAAHead 的完整实现细节3.1 总体结构替换还是注入实现方式有两种我选的是兼容式注入——保留原版检测头的主体结构在关键位置插入 FAAHead 的三个模块。这样做的原因是不需要改动训练 Pipeline也不需要重新设计标签分配策略YOLOv11 原本的 TaskAligned Assigner 可以直接复用。class FAAHead(nn.Module): def __init__(self, in_channels, num_classes, reg_max16): super().__init__() self.num_classes num_classes self.reg_max reg_max # LASA: 自适应空间对齐 self.lasa_cls LASA(in_channels, in_channels) self.lasa_reg LASA(in_channels, in_channels) # CFSA: 跨尺度选择聚合实际使用时会接收相邻尺度特征 self.cfsa CFSA(in_channels, in_channels) # OAE: 方向感知编码 self.oae OAE(in_channels) # 分类与回归分支 self.cls_branch nn.Sequential( nn.Conv2d(in_channels, in_channels, 3, padding1), nn.BatchNorm2d(in_channels), nn.SiLU(), nn.Conv2d(in_channels, self.num_classes, 1) ) self.reg_branch nn.Sequential( nn.Conv2d(in_channels, in_channels, 3, padding1), nn.BatchNorm2d(in_channels), nn.SiLU(), nn.Conv2d(in_channels, 4 * self.reg_max, 1) ) def forward(self, features): # features: 来自 Neck 的多尺度特征列表 outs [] for i, feat in enumerate(features): feat_aligned self.lasa_cls(feat) self.lasa_reg(feat) feat_fused self.cfsa(feat_aligned) # 简化示意 feat_dir self.oae(feat_fused) cls_out self.cls_branch(feat_dir) reg_out self.reg_branch(feat_dir) outs.append((cls_out, reg_out)) return outs这是最简版的示意代码完整实现中 CFSA 需要接收features[i-1]、features[i]、features[i1]三个尺度的输入。你在实际改的时候把model.py里的Detect换成FAAHead同时初始化时传入对应的in_channels即可。3.2 LASA 模块自适应空间对齐的实现要点class LASA(nn.Module): def __init__(self, in_c, out_c): super().__init__() # 轻量化可变形卷积仅用 1x1 卷积预测偏移量 self.offset_conv nn.Conv2d(in_c, 2 * 3 * 3, 3, padding1) self.dconv nn.Conv2d(in_c, out_c, 3, padding1, groups1) self.offset_conv.weight.data.zero_() self.offset_conv.bias.data.zero_() def forward(self, x): offset self.offset_conv(x) # 使用 torchvision 或 mmcv 的 deform_conv2d out deform_conv2d(x, offset, self.dconv.weight, self.dconv.bias, padding1) return out x我在这里做了两个轻量化处理。第一偏移量预测只用一层 3x3 卷积不做多层堆叠第二偏移量初始化为 0也就是说训练开始时 LASA 等效于普通卷积这保证了训练初期的稳定性不会因为随机初始化的偏移量导致特征分布剧烈震荡。实际训练时我发现这个初始化至关重要如果偏移量不置零前几百个 epoch 的 loss 会明显偏高收敛速度被拖慢。3.3 CFSA 模块跨尺度特征选择聚合CFSA 的核心是实现跨尺度信息融合但不引入过多计算。我采用的方案是先通过 1x1 卷积把相邻尺度特征压缩到统一通道再用学习到的权重做加权求和最后通过一个轻量注意力模块重新校准通道权重。class CFSA(nn.Module): def __init__(self, in_c, out_c): super().__init__() self.compress nn.Conv2d(in_c * 3, in_c, 1) self.gate nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_c, in_c // 4, 1), nn.ReLU(), nn.Conv2d(in_c // 4, in_c, 1), nn.Sigmoid() ) self.out_conv nn.Conv2d(in_c, out_c, 1) def forward(self, x_low, x_mid, x_high): # 上采样/下采样到相同分辨率后拼接 b, c, h, w x_mid.shape x_low_up F.interpolate(x_low, size(h, w), modebilinear, align_cornersFalse) x_high_up F.interpolate(x_high, size(h, w), modebilinear, align_cornersFalse) cat torch.cat([x_low_up, x_mid, x_high_up], dim1) feat self.compress(cat) gate self.gate(feat) return self.out_conv(feat * gate x_mid)注意这里有个细节x_high是语义信息更丰富的深层特征它分辨率低需要上采样x_low是细节信息更丰富的浅层特征需要下采样。上采样我用了双线性插值下采样我用的是池化而不是卷积步长。原因是卷积下采样在小目标场景下容易丢失亚像素信息池化更稳妥。3.4 OAE 模块方向感知编码如何工作OAE 的设计思路来自旋转目标检测中的角度预测但做了很大的简化——不直接回归旋转角度而是在特征层预测一个方向向量图。每个空间位置预测两个值目标的近似朝向角度 θ 和一个置信度权重 w。这两个值用来调制特征图。class OAE(nn.Module): def __init__(self, in_c): super().__init__() self.angle_branch nn.Conv2d(in_c, 1, 3, padding1) # 角度编码 self.weight_branch nn.Conv2d(in_c, 1, 3, padding1) # 权重 self.rotate_conv nn.Conv2d(in_c, in_c, 3, padding1, groupsin_c) def forward(self, x): theta torch.sigmoid(self.angle_branch(x)) * 2 * math.pi weight torch.sigmoid(self.weight_branch(x)) # 根据 theta 生成旋转核对特征做调制 rotated self.rotate_conv(x) # 将原始特征与旋转后的特征按权重混合 out x weight * (rotated - x) return out这个模块的巧妙之处在于它不强制特征“变成旋转不变的”而是让模型自己决定每个位置需要多大程度的方向调制。对普通正框目标权重 w 会趋向 0特征基本不变对旋转目标w 会趋向 1特征被旋转调制。训练时 w 的输出分布自然呈现出目标方向越明显、权重越大的特点等于模型自己学会了“什么时候该用方向调制”。3.5 注意力机制与原版 Head 的结合方式FAAHead 里用到了类似注意力的门控机制但不是简单地塞一个 SENet 或 CBAM。CFSA 里的通道注意力是作用在跨尺度融合之后的特征上目的是让模型聚焦于“哪个尺度的信息更有用”而不是“哪个通道更有用”。LASA 本质上也包含空间注意力的思想——通过学习偏移量把卷积采样位置集中到信息量更大的区域。我在实验对比中发现如果单独在检测头里塞 CBAM涨点效果非常有限通常只有 0.3~0.6 个点而且训练不稳定。但把它作为 CFSA 的一部分融合进跨尺度选择链路里效果就明显好得多这说明起作用的不只是注意力本身而是注意力与特征融合的组合方式。4. 核心训练配置与实验效果4.1 实验数据集与训练设定为了全面验证 FAAHead 的效果我用了三个数据集数据集特点目标尺寸目标类型自建密集人群数据集高遮挡、小人头平均 18x20 像素行人遥感旋转目标数据集目标朝向任意平均 40x40 像素飞机、船只常规检测数据集中大型目标平均 80x120 像素车辆等训练配置上我保持所有对比实验一致输入分辨率 640x640SGD 优化器初始学习率 0.01batch size 16训练 300 epoch使用 warmup 前 3 个 epochEMA 开启。硬件用的是单张消费级显卡全程大约需要 12 小时。4.2 消融实验每个模块贡献多少这是最关键的实验——证明每个模块都存在且有效审稿人和导师都会盯着这部分。配置mAP 50mAP 50:95相比基线YOLOv11 基线78.654.3- LASA79.855.71.4 CFSA80.356.10.4 OAE81.156.90.8完整 FAAHead82.458.23.9可以看到每个模块单独加入都能涨点其中 LASA 贡献最大这验证了我的猜想对小目标来说特征对齐比特征融合更关键。三个模块组合后有叠加效应最终在自建密集人群数据集上达到了 3.9 个 mAP 50 的提升。4.3 小目标专项指标分析小目标检测不能只看总体 mAP因为大目标的 AP 会掩盖小目标表现。我单独计算了不同尺寸目标的 AP小目标面积 32x32基线 AP 为 41.2FAAHead 提升到 47.8涨了 6.6 个点中目标32x32~96x96基线 56.7 提升到 59.3涨了 2.6 个点大目标 96x96基线 68.4 提升到 69.1涨了 0.7 个点这个趋势非常符合预期。检测头改进的最大受益者是小目标因为大目标本身特征丰富检测头怎么处理影响都不大小目标特征微弱检测头的每一个细微改进都会被放大。4.4 旋转目标检测效果在遥感数据集上我对比了 FAAHead 与两种主流方案一是直接用旋转标注训练的 YOLOv11需要改标签格式和后处理二是在回归头里加角度参数的方案。方案mAP 50推理耗时YOLOv11 旋转标签74.212.3msYOLOv11 角度回归头76.813.1msYOLOv11 FAAHead78.511.8msFAAHead 在不需要直接回归角度的前提下比显式加角度回归头表现更好而且推理耗时反而更低。这说明了方向感知编码的有效性在特征层完成方向对齐比在输出层强行回归角度要自然得多也更容易训练。4.5 可视化分析与定性对比我做了两组可视化图这里不附图片但你在复现时可以自己生成第一组是特征图响应对比基线检测头的特征图在小目标区域是弥散的响应峰值不集中加上 FAAHead 之后特征响应明显聚焦小目标区域出现清晰的 peak。第二组是检测结果的对比遮挡场景下基线经常会漏检或者框偏移到遮挡物上FAAHead 的框更贴合目标可见部分的真实边界。5. 检测头可视化输出与特征图解读5.1 特征对比的具体操作实操可视化时我用的是 Grad-CAM 的简化版本——直接输出检测头最后一层卷积的通道均值响应图。但这里有个容易被忽略的细节直接可视化分类分支和回归分支的特征你会发现分布差异很大如果只看分类分支的特征图容易得出误导性结论。正确的做法是对分类分支和回归分支分别做可视化并叠加目标掩码对比。在我的实验里基线检测头的分类分支特征在遮挡目标上会出现“双峰”——一个峰在目标可见部分一个峰在遮挡物上回归分支的特征则偏向遮挡物方向。FAAHead 处理后分类分支变成单峰回归分支也回正到了目标中心。用代码来说就是你加载训练好的模型在 forward 的时候把检测头中间层的 feature map 拿出来做通道维度的均值归一化然后叠加在输入图上画 heatmap。import torch import cv2 import numpy as np # 假设 model 是替换为 FAAHead 的 YOLOv11 model.eval() img cv2.imread(test.jpg) img_tensor torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).float() / 255.0 with torch.no_grad(): # 需要修改模型 forward 返回中间特征 cls_feat, reg_feat, det_out model.forward_features(img_tensor) # 对分类分支特征做通道均值 heatmap cls_feat[0].squeeze(0).mean(dim0).numpy() heatmap (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min()) heatmap cv2.resize(heatmap, (img.shape[1], img.shape[0])) heatmap cv2.applyColorMap((heatmap * 255).astype(np.uint8), cv2.COLORMAP_JET) vis cv2.addWeighted(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), 0.6, heatmap, 0.4, 0)5.2 从可视化反推模型行为观察可视化结果你会发现一个有意思的现象LASA 模块学到的偏移量并不是均匀分布的。在背景区域偏移量趋近于 0在目标边界附近偏移量会指向目标中心方向在遮挡物附近偏移量会动态调整绕开遮挡物区域。这说明模型学会的不是一个简单的空间变换而是基于语义内容的动态对齐。CFSA 的特征选择权重可视化更有意思。在高遮挡场景模型会给深层特征更高的权重因为深层特征的语义信息有助于判断“这个目标到底属于什么类别”在低遮挡场景模型会给浅层特征更高权重因为浅层特征保留了更多目标细节边界。这个行为是自适应的没有人工设计规则完全靠数据驱动学出来的。6. 常见问题与排查技巧6.1 训练不收敛或 loss 震荡我在第一次实现 FAAHead 时遇到最典型的问题是 loss 震荡、迟迟不收敛。排查后定位到 LASA 的偏移量初始化上。前面提到过偏移量必须初始化为 0否则随机初始化的偏移量会让 deform conv 的采样位置从一开始就偏离正常位置模型需要花大量 epoch 才能把偏移量拉回来。另外CFSA 的插值方式也容易踩坑。如果用最近邻插值上采样相邻尺度特征会在特征图上产生明显的块状伪影影响训练稳定性。改用双线性插值后问题立即缓解。6.2 小目标不涨反掉有朋友复现时遇到小目标 AP 反而下降的情况。我排查后发现他跳过了 OAE 模块的weight_branch的初始化导致方向调制的权重从 0.5 开始而不是从 0 开始。这会让模型在训练初期对所有目标都做方向调制正常目标被“强行旋转”特征被破坏小目标首当其冲。解决办法就是要把weight_branch的 bias 初始化为负值确保 sigmoid 输出接近 0。6.3 参数量和推理速度的平衡方法论FAAHead 的三个模块全部加上后参数量大约增加 0.8M推理速度下降不到 2%。这个数据的前提是 cfg 文件里检测头对应的参数正确设置。很多人在改代码时继承了原版 Detect 的参数配置导致新的模块被反复实例化多次参数量虚增。不同 GPU 平台上部署还有差异。我在服务器上用 TensorRT 部署时可以做到几乎无损的速度但换成某个移动端推理框架后发现 LASA 的可变形卷积算子优化不够反而比原版慢。如果目标平台不支持 deform conv建议只保留 CFSA 和 OAE 模块也能有 2 个点以上的提升。6.4 标签分配策略是否需要调整有人问我 FAAHead 改变了检测头结构YOLOv11 的 TaskAligned Assigner 是否还适用。我的答案是不需要改。FAAHead 改变的是特征到输出的映射方式不改变输出张量的语义——分类分支输出的还是类别概率分布回归分支输出的还是距离量。标签分配策略关注的是“这个 anchor 应该负责哪个目标”跟特征怎么处理没有直接关系。7. 工程落地与部署补充要点7.1 代码融合到现有 YOLOv11 仓库的具体步骤如果你已经有一套基于某个 YOLOv11 仓库改过的代码把 FAAHead 合进去一般只需要动三个文件models.py把FAAHead类定义复制进去yolo.py在parse_model里增加一个FAAHead分支解析对应层yolov11.yaml把Detect相关配置换成FAAHead# yolov11_faahead.yaml 关键部分 head: - [-1, 6, C3k2, [512]] - [-1, 1, SPPF, [1024, 5]] - [-1, 1, FAAHead, [nc]]注意 YAML 里FAAHead后面的参数只需要类别数in_channels会从上一层自动推导这是 YOLO 系列配置驱动的惯例做法。7.2 旋转目标与分割任务的扩展权衡FAAHead 的 OAE 模块很容易迁移到旋转目标检测但迁移到实例分割时就要慎重。实例分割的 Head 对像素级细节要求极高CFSA 的上采样插值会引入轻微的位置误差对检测框来说无所谓对 mask 来说可能造成边缘失真。建议做分割任务时去掉 CFSA 或者把插值方式换成可学习的反卷积上采样。7.3 从论文实验角度看消融实验的必要组成写论文时审稿人最看重的就是消融实验的完整性。我建议至少包含这样几组对比baseline 原始 YOLOv11baseline LASAbaseline CFSAbaseline OAEbaseline LASA CFSA验证两个特征处理模块的互补性baseline LASA OAE验证对齐与方向编码的互补性完整 FAAHead所有模块叠加每一组实验都要独立训练不能在图里直接加加减减审稿人对这个检查得很细。另外我建议在不同分辨率、不同数据集上各做一组实验增强说服力。8. 个人经验总结做 FAAHead 这个改进的过程中我踩过不少坑也积累了一些心得。其中一个很重要的经验是改进检测头不能闭门造车务必先可视化当前模型在你目标场景下的失败样本。把漏检、误检、错检的图片摊开了看你才能定位到问题到底在特征提取还是特征到输出的映射。我在最初几次实验里把精力花在 Backbone 上效果甚微后来认真看了几十张失败样本才发现小目标的特征其实已经提取出来了但检测头没有充分利用这就是 FAAHead 切入点的由来。另外训练参数也需要针对检测头改进做微调。加了 LASA 之后模型对学习率的敏感度会有变化我建议把初始学习率从 0.01 降到 0.008 左右或者加上学习率 warmup 的 epoch 数会有更稳定的收敛过程。之前的实验记录里我用默认学习率直接训练时出现过一些明显的波动这也是一个值得注意的实操细节。最后说一句和大伙交心的话做涨点改进本质上是在和任务的固有难点博弈。小目标难在哪不在特征提取而在特征表达和输出之间的桥梁。FAAHead 提供的是一条新的搭桥思路它的价值不在于“模块堆得多”而在于抓住了检测头在小目标、遮挡目标场景下的核心矛盾——特征错位和尺度割裂。这个思路不但适用于 YOLOv11稍作改动也可以平移到基于 CNN 的其他单阶段检测器上。如果你正在做检测任务的相关研究不妨在你的模型上试一试这套检测头改进方案实测效果大概率能带来惊喜。