1. 目标检测中的特征金字塔:从FPN到PAN的进化之路
在目标检测任务中,处理多尺度目标一直是个棘手的问题。想象一下,你要在一张图片中同时识别出近处的行人、远处的车辆和更远处的交通标志,这些目标的尺寸差异可能达到数十倍。传统方法就像用同一把尺子测量大象和蚂蚁,效果自然不理想。
特征金字塔网络(FPN)的提出改变了这一局面。它通过构建自顶向下(Top-down)的特征金字塔,将高层语义信息传递给低层特征。这就像给检测系统装上了"望远镜"和"显微镜",让模型既能看清大目标,也能捕捉小细节。但FPN有个明显缺陷:信息流动是单向的,低层的精确定位信息无法有效传递给高层。
我在实际项目中就遇到过这种情况:当处理密集小目标时,FPN的表现总是不尽如人意。直到尝试了路径聚合网络(PAN),检测精度才有了显著提升。这让我意识到,双向信息流动对目标检测有多重要。
2. FPN的工作原理与局限性
2.1 FPN的基本结构
FPN的核心思想很简单:通过横向连接(lateral connection)和上采样(upsampling),构建从深层到浅层的特征金字塔。具体实现通常包含三个关键步骤:
- 自底向上的特征提取:Backbone网络(如ResNet)会自然生成不同尺度的特征图,我们记作C1到C5,其中C5的感受野最大
- 自顶向下的特征传播:从C5开始,通过上采样将高层特征与低层特征融合
- 横向连接:使用1×1卷积对齐通道数后,将Backbone的特征与上采样特征相加
# 简化版FPN实现示例 class FPN(nn.Module): def __init__(self, backbone_channels=[256, 512, 1024, 2048], fpn_channels=256): super().__init__() # 横向连接的1×1卷积 self.lateral_convs = nn.ModuleList([ nn.Conv2d(channels, fpn_channels, 1) for channels in backbone_channels ]) # 上采样使用的3×3卷积 self.fpn_convs = nn.ModuleList([ nn.Conv2d(fpn_channels, fpn_channels, 3, padding=1) for _ in backbone_channels ]) def forward(self, features): laterals = [conv(feat) for conv, feat in zip(self.lateral_convs, features)] # 自顶向下路径 for i in range(len(laterals)-1, 0, -1): laterals[i-1] += F.interpolate(laterals[i], scale_factor=2) return [conv(feat) for conv, feat in zip(self.fpn_convs, laterals)]2.2 FPN的三大痛点
虽然FPN在很多场景表现不错,但在实际部署中我发现几个明显问题:
- 信息衰减问题:低层的精确位置信息在向上传递过程中会逐渐丢失。就像传话游戏,经过多层传递后原始信息已经失真
- 小目标检测不稳定:对小目标的检测结果波动较大,特别是当目标密集出现时
- 特征融合不充分:简单的相加操作可能无法有效融合不同层次的特征
有次我在处理卫星图像时,FPN对小建筑物的召回率比大建筑物低了近15个百分点。这促使我开始寻找更好的特征融合方案。
3. PAN的创新设计:双向信息高速公路
3.1 双向路径聚合的核心理念
PAN的核心创新在于增加了自底向上(Bottom-up)的路径增强。如果说FPN是单行道,那么PAN就是双向八车道的高速公路。这种设计带来了三个关键优势:
- 位置信息保留:低层的精确位置信息可以向上传递,改善定位精度
- 多级特征复用:每个层次的特征都能被多次利用,提高特征利用率
- 自适应特征选择:网络可以自主决定哪些信息需要向上或向下传递
在实际项目中,改用PAN后,小目标的检测AP提升了约8%,而计算量仅增加了不到5%。这个性价比让我印象深刻。
3.2 PAN的代码级实现细节
不同框架对PAN的实现各有特色。以mmdetection和nanodet为例,我们看看实际工程中的处理方式:
# mmdetection风格的PAN实现 class PAN(nn.Module): def __init__(self, in_channels=[256, 512, 1024, 2048], out_channels=256): super().__init__() # 自顶向下路径(同FPN) self.top_down_layers = nn.ModuleList([ nn.Conv2d(ch, out_channels, 1) for ch in in_channels ]) # 自底向上路径新增的卷积层 self.bottom_up_convs = nn.ModuleList() for i in range(len(in_channels)-1): self.bottom_up_convs.append(nn.Sequential( nn.Conv2d(out_channels, out_channels, 3, stride=2, padding=1), nn.BatchNorm2d(out_channels), nn.ReLU() )) def forward(self, features): # 自顶向下路径 top_down = [] for i in range(len(features)-1, -1, -1): if i == len(features)-1: top_down.append(self.top_down_layers[i](features[i])) else: top_down.append(self.top_down_layers[i](features[i]) + F.interpolate(top_down[-1], scale_factor=2)) # 自底向上路径 bottom_up = [top_down[-1]] for i in range(len(top_down)-1): bottom_up.append(self.bottom_up_convs[i](bottom_up[-1]) + top_down[-i-2]) return bottom_up[::-1] # 按从浅到深的顺序返回nanodet的实现更加轻量,直接使用插值进行特征融合:
# nanodet风格的轻量级PAN class LightPAN(nn.Module): def forward(self, features): # 自顶向下 for i in range(len(features)-1, 0, -1): features[i-1] += F.interpolate(features[i], scale_factor=2) # 自底向上 for i in range(len(features)-1): features[i+1] += F.interpolate(features[i], scale_factor=0.5) return features4. PAN在YOLO系列中的应用实践
4.1 YOLOv4中的PAN改进
YOLOv4对原始PAN做了几处重要改进:
- 跨阶段连接:借鉴CSPNet思想,减少计算冗余
- SPP模块集成:在PAN路径中加入空间金字塔池化,增强感受野
- Mish激活函数:替代ReLU获得更好的梯度流动
这些改进使得YOLOv4在保持速度优势的同时,精度大幅提升。我在工业质检项目中测试发现,相比YOLOv3,v4对小缺陷的检测率提升了12%。
4.2 轻量化PAN设计技巧
对于资源受限的场景,可以考虑以下优化策略:
- 通道裁剪:减少PAN中特征图的通道数,如从256减至128
- 深度可分离卷积:替换标准3×3卷积,降低计算量
- 部分连接:只对关键层级进行双向连接
# 轻量级PAN实现示例 class LitePAN(nn.Module): def __init__(self, channels=128): super().__init__() # 使用深度可分离卷积 self.dw_conv = nn.Sequential( nn.Conv2d(channels, channels, 3, padding=1, groups=channels), nn.BatchNorm2d(channels), nn.ReLU(), nn.Conv2d(channels, channels, 1), nn.BatchNorm2d(channels), nn.ReLU() ) def forward(self, features): # 只对P3-P5进行双向连接 p3, p4, p5 = features[-3:] # 自顶向下 p4 += F.interpolate(p5, scale_factor=2) p3 += F.interpolate(p4, scale_factor=2) # 自底向上 p4 += self.dw_conv(F.avg_pool2d(p3, 2)) p5 += self.dw_conv(F.avg_pool2d(p4, 2)) return [p3, p4, p5]5. 特征融合的进阶思考
5.1 加法 vs 拼接:哪种融合方式更好?
在实现PAN时,特征融合有两种主要方式:
| 融合方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 特征相加 | 计算量小,参数少 | 可能丢失部分信息 | 计算资源受限 |
| 特征拼接 | 保留完整信息 | 增加通道数,提升计算量 | 精度优先场景 |
我的经验是:对于轻量级模型,加法更合适;而对于追求精度的场景,拼接效果更好。可以像这样灵活选择:
def merge_features(feat1, feat2, method='add'): if method == 'add': return feat1 + feat2 elif method == 'concat': return torch.cat([feat1, feat2], dim=1) else: raise ValueError(f"Unknown merge method: {method}")5.2 特征金字塔的层级选择
不是所有层级都适合加入PAN。通过实验发现:
- 对于输入尺寸为640×640的图像,P3-P5(stride 8/16/32)通常足够
- 更高分辨率的P2(stride 4)会显著增加计算量,但对小目标提升有限
- 更深的P6-P7(stride 64/128)对大目标检测有帮助
在无人机图像检测项目中,我最终选择了P3-P5的配置,在精度和速度间取得了最佳平衡。