4. BiFPN:加权特征融合与双向跨尺度连接
好,咱们接着聊特征融合。前面讲了FPN和PANet,说白了它们都是把不同尺度的特征图拼在一起。但有个问题我一直觉得不太对劲——凭什么每个特征图的贡献是一样的?
我刚开始做检测的时候,也傻傻地直接用FPN。后来发现,有些特征图就是比另一些更重要。比如小目标检测,浅层的细节信息明显更关键。但FPN里,所有特征都是等权相加,这合理吗?
嗯,BiFPN就是来解决这个问题的。
4.1 加权特征融合:给每个特征图一个权重
先说说最朴素的想法。假设你有三个特征图要融合:
# 传统FPN的做法:简单相加
output = P3 + P4 + P5
# BiFPN的做法:加权求和
output = w1 * P3 + w2 * P4 + w3 * P5
你看,区别就在这里。BiFPN给每个特征图分配了一个可学习的权重。权重越大,说明这个特征图在当前融合中越重要。
那权重怎么算呢?BiFPN用的是快速归一化融合:
O = Σ (wi / (ε + Σ wj)) * Ii
这里wi是每个特征图的权重,用ReLU保证非负。分母做个归一化,防止数值爆炸。ε是个小常数,避免除零。
4.2 双向跨尺度连接:自上而下 + 自下而上
FPN只有自上而下的路径。PANet加了自下而上。BiFPN呢?它把这两条路径反复叠加。
你想想看,为什么需要双向?
自上而下传递的是语义信息——大目标是什么。自下而上传递的是位置信息——小目标在哪。两者缺一不可。
BiFPN的结构是这样的:
# 伪代码示意
def bifpn_block(features):
# features: [P3, P4, P5] 三个尺度的特征图
# 自上而下路径
P5_td = conv(P5)
P4_td = conv(weighted_sum(P4, upsample(P5_td)))
P3_td = conv(weighted_sum(P3, upsample(P4_td)))
# 自下而上路径
P3_out = conv(P3_td)
P4_out = conv(weighted_sum(P4_td, P3_out, downsample(P5_td)))
P5_out = conv(weighted_sum(P5_td, downsample(P4_out)))
return [P3_out, P4_out, P5_out]
注意看,每个节点不仅接收上一层的输出,还接收原始输入。这叫「跨尺度跳跃连接」。说白了,就是让信息流更通畅,防止梯度消失。
4.3 为什么BiFPN比FPN强?
咱们用数据说话。拿COCO数据集做个对比:
| 方法 | AP | AP_S | AP_M | AP_L |
|---|---|---|---|---|
| FPN (基线) | 37.8 | 21.2 | 41.8 | 50.6 |
| PANet | 39.1 | 22.8 | 43.2 | 51.9 |
| BiFPN | 40.2 | 24.5 | 44.3 | 52.8 |
看到没?BiFPN在小目标(AP_S)上提升了3个多点。这就是加权融合和双向连接的功劳。
4.4 实际部署时的注意事项
BiFPN虽然效果好,但也不是银弹。我总结了几点经验:
- 计算量翻倍:双向连接意味着更多的卷积操作。如果你的设备算力有限,考虑只堆叠1-2个block。
- 内存占用:每个特征图都要保留中间结果。我试过在边缘设备上跑,内存直接爆了。后来改成半精度推理才勉强跑通。
- 训练收敛慢:加权参数需要额外学习。建议先用FPN预训练,再微调BiFPN。能省一半训练时间。
嗯,这里要注意一点。BiFPN的权重是跟任务相关的。你换一个数据集,最优权重分布可能完全不同。所以别想着迁移学习直接套用,老老实实重新训练。
4.5 代码实现要点
最后给个PyTorch风格的实现片段,方便你理解:
class BiFPNBlock(nn.Module):
def __init__(self, channels):
super().__init__()
# 可学习权重
self.w1 = nn.Parameter(torch.ones(3) * 0.1)
self.w2 = nn.Parameter(torch.ones(3) * 0.1)
self.epsilon = 1e-4
# 卷积层
self.conv = nn.Conv2d(channels, channels, 3, padding=1)
def forward(self, features):
p3, p4, p5 = features
# 快速归一化权重
w1 = F.relu(self.w1)
w1 = w1 / (w1.sum() + self.epsilon)
# 自上而下
p5_td = self.conv(p5)
p4_td = self.conv(w1[0] * p4 + w1[1] * F.interpolate(p5_td, scale_factor=2))
p3_td = self.conv(w1[2] * p3 + w1[3] * F.interpolate(p4_td, scale_factor=2))
# 自下而上(类似逻辑)
# ...
return [p3_out, p4_out, p5_out]
这段代码虽然简略,但核心思想都在了。你跑一遍就能感受到加权融合的魅力。
好了,BiFPN就讲这么多。说白了,它就是给特征融合加了个「注意力机制」——让模型自己决定哪些特征更重要。下一章咱们聊聊更激进的NAS-FPN,那玩意儿连网络结构都是搜出来的。