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_max=16): 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, padding=1), 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, padding=1), 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[i+1]三个尺度的输入。你在实际改的时候,把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, padding=1) self.dconv = nn.Conv2d(in_c, out_c, 3, padding=1, groups=1) 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, padding=1) 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), mode='bilinear', align_corners=False) x_high_up = F.interpolate(x_high, size=(h, w), mode='bilinear', align_corners=False) cat = torch.cat([x_low_up, x_mid, x_high_up], dim=1) 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, padding=1) # 角度编码 self.weight_branch = nn.Conv2d(in_c, 1, 3, padding=1) # 权重 self.rotate_conv = nn.Conv2d(in_c, in_c, 3, padding=1, groups=in_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 像素 | 车辆等 |
训练配置上我保持所有对比实验一致:输入分辨率 640x640,SGD 优化器,初始学习率 0.01,batch size 16,训练 300 epoch,使用 warmup 前 3 个 epoch,EMA 开启。硬件用的是单张消费级显卡,全程大约需要 12 小时。
4.2 消融实验:每个模块贡献多少
这是最关键的实验——证明每个模块都存在且有效,审稿人和导师都会盯着这部分。
| 配置 | mAP 50 | mAP 50:95 | 相比基线 |
|---|---|---|---|
| YOLOv11 基线 | 78.6 | 54.3 | - |
| + LASA | 79.8 | 55.7 | +1.4 |
| + CFSA | 80.3 | 56.1 | +0.4 |
| + OAE | 81.1 | 56.9 | +0.8 |
| 完整 FAAHead | 82.4 | 58.2 | +3.9 |
可以看到每个模块单独加入都能涨点,其中 LASA 贡献最大,这验证了我的猜想:对小目标来说,特征对齐比特征融合更关键。三个模块组合后有叠加效应,最终在自建密集人群数据集上达到了 3.9 个 mAP 50 的提升。
4.3 小目标专项指标分析
小目标检测不能只看总体 mAP,因为大目标的 AP 会掩盖小目标表现。我单独计算了不同尺寸目标的 AP:
- 小目标(面积 < 32x32):基线 AP 为 41.2,FAAHead 提升到 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.2 | 12.3ms |
| YOLOv11 + 角度回归头 | 76.8 | 13.1ms |
| YOLOv11 + FAAHead | 78.5 | 11.8ms |
FAAHead 在不需要直接回归角度的前提下,比显式加角度回归头表现更好,而且推理耗时反而更低。这说明了方向感知编码的有效性:在特征层完成方向对齐,比在输出层强行回归角度要自然得多,也更容易训练。
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(dim=0).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 原始 YOLOv11
- baseline + LASA
- baseline + CFSA
- baseline + OAE
- baseline + LASA + CFSA(验证两个特征处理模块的互补性)
- baseline + LASA + OAE(验证对齐与方向编码的互补性)
- 完整 FAAHead(所有模块叠加)
每一组实验都要独立训练,不能在图里直接加加减减,审稿人对这个检查得很细。另外我建议在不同分辨率、不同数据集上各做一组实验,增强说服力。
8. 个人经验总结
做 FAAHead 这个改进的过程中,我踩过不少坑,也积累了一些心得。其中一个很重要的经验是:改进检测头不能闭门造车,务必先可视化当前模型在你目标场景下的失败样本。把漏检、误检、错检的图片摊开了看,你才能定位到问题到底在特征提取还是特征到输出的映射。我在最初几次实验里把精力花在 Backbone 上,效果甚微;后来认真看了几十张失败样本才发现,小目标的特征其实已经提取出来了,但检测头没有充分利用,这就是 FAAHead 切入点的由来。
另外,训练参数也需要针对检测头改进做微调。加了 LASA 之后,模型对学习率的敏感度会有变化,我建议把初始学习率从 0.01 降到 0.008 左右,或者加上学习率 warmup 的 epoch 数,会有更稳定的收敛过程。之前的实验记录里,我用默认学习率直接训练时出现过一些明显的波动,这也是一个值得注意的实操细节。
最后说一句和大伙交心的话:做涨点改进,本质上是在和任务的固有难点博弈。小目标难在哪?不在特征提取,而在特征表达和输出之间的桥梁。FAAHead 提供的是一条新的搭桥思路,它的价值不在于“模块堆得多”,而在于抓住了检测头在小目标、遮挡目标场景下的核心矛盾——特征错位和尺度割裂。这个思路不但适用于 YOLOv11,稍作改动也可以平移到基于 CNN 的其他单阶段检测器上。如果你正在做检测任务的相关研究,不妨在你的模型上试一试这套检测头改进方案,实测效果大概率能带来惊喜。