第4章:图像特征提取基础——ResNet/ResNeXt与FPN结构详解
各位同学,欢迎来到第四章。从这一章开始,我们正式进入BEV感知的“视觉前端”——图像特征提取。
你想想看,BEV感知的第一步是什么?是把环视相机拍到的6张图,变成统一的空间特征。那怎么变?总得先把每张图里的车道线、车辆、行人这些“语义”给抽出来吧。这就是Backbone和FPN干的事。
我个人习惯把这一章称为“地基”。地基没打好,后面的Transformer、时序融合全是空中楼阁。好,咱们开始。
4.1 为什么需要多尺度特征?
先问一个问题:一张图片里,大卡车和小行人,它们的特征尺度一样吗?
显然不一样。大卡车可能占据图像的一半,小行人可能只有几十个像素。如果只用最后一层特征图(比如下采样32倍),小目标早就“消失”了。
我在项目中遇到过最典型的例子:用纯ResNet-50的最后一层特征做检测,远处的锥桶几乎全漏检。后来加了FPN,召回率直接提升了15个点。嗯,这就是多尺度的价值。
所以,现代BEV方案(比如BEVDet、BEVFormer)的标配是:
- Backbone:输出多尺度特征图(C2, C3, C4, C5)
- FPN/Neck:把这些特征融合成P2, P3, P4, P5,供后续使用
4.2 ResNet:经典Backbone的搭建逻辑
ResNet的核心创新,说白了就是“残差连接”。为什么要有残差?因为网络一深,梯度就消失了。我刚开始做实验时,堆到50层以上,loss死活不降。加上残差连接后,问题迎刃而解。
一个标准的ResNet Block长这样:
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 捷径分支:如果维度不匹配,用1x1卷积对齐
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels * self.expansion:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels * self.expansion, 1, stride, bias=False),
nn.BatchNorm2d(out_channels * self.expansion)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x) # 残差连接
out = F.relu(out)
return out
这里有个细节:expansion参数。BasicBlock的expansion=1,Bottleneck的expansion=4。为什么Bottleneck要扩大4倍?因为Bottleneck先用1x1降维,3x3提取特征,再用1x1升维。这样计算量小,但表达能力不弱。
4.3 ResNeXt:分组卷积的威力
ResNeXt和ResNet的区别在哪?一句话:分组卷积。
ResNet的每个Block内部,卷积核是“全连接”的——所有输入通道都参与计算。ResNeXt把通道分成若干组(group),每组独立做卷积,最后再拼接。这样做的好处是:在相同参数量下,分组数越多,表达能力越强。
我记得有一次对比实验:在nuScenes数据集上,ResNeXt-101比ResNet-101的mAP高了1.2个点,推理速度只慢了5%。如果你对精度有极致要求,ResNeXt是个好选择。
代码实现上,ResNeXt的Block和ResNet几乎一样,只是多了一个参数:
class ResNeXtBlock(nn.Module):
expansion = 2 # ResNeXt的expansion通常为2
def __init__(self, in_channels, out_channels, stride=1, cardinality=32):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
# 分组卷积:cardinality就是组数
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride, 1,
groups=cardinality, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, 1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)
# ... 省略shortcut部分
4.4 FPN结构详解:从C2到P2的魔法
FPN(Feature Pyramid Network)是2017年提出的,但至今仍是BEV感知的标配。它的核心思想就八个字:自底向上,自顶向下。
具体来说:
- 自底向上:Backbone前向传播,得到C2, C3, C4, C5(分辨率依次减半)
- 自顶向下:从C5开始,上采样后与C4融合,得到P4;再上采样与C3融合,得到P3;以此类推
- 横向连接:用1x1卷积对齐通道数,避免直接相加时维度不匹配
为什么FPN有效?因为高层特征有强语义(知道“这是车”),低层特征有高分辨率(知道“车在哪”)。FPN把两者结合,既知道是什么,又知道在哪。
标准的FPN实现如下:
class FPN(nn.Module):
def __init__(self, in_channels_list, out_channels):
super().__init__()
# 横向连接:1x1卷积对齐通道
self.lateral_convs = nn.ModuleList([
nn.Conv2d(in_c, out_channels, 1) for in_c in in_channels_list
])
# 上采样后的平滑卷积
self.fpn_convs = nn.ModuleList([
nn.Conv2d(out_channels, out_channels, 3, 1, 1) for _ in in_channels_list
])
def forward(self, inputs):
# inputs: [C2, C3, C4, C5]
laterals = [conv(x) for conv, x in zip(self.lateral_convs, inputs)]
# 自顶向下融合
for i in range(len(laterals)-1, 0, -1):
laterals[i-1] += F.interpolate(laterals[i], scale_factor=2, mode='nearest')
# 平滑输出
outputs = [conv(x) for conv, x in zip(self.fpn_convs, laterals)]
return outputs # [P2, P3, P4, P5]
4.5 在BEV感知中的实际应用
在BEVDet或BEVFormer中,Backbone+FPN的输出会进入“视图变换”模块(比如LSS中的深度估计,或Transformer中的交叉注意力)。但不管后续怎么变,前端输出的特征质量直接决定了BEV感知的上限。
我建议你在搭建时注意以下几点:
- 输入尺寸:一般用256x704或512x1408(宽高比约2.75,适配环视相机)
- 输出尺度:通常取P2, P3, P4(分辨率分别为输入的1/4, 1/8, 1/16)
- 通道数:统一到256,方便后续处理
举个例子,如果你用ResNet-50+FPN,输入是256x704,那么:
| 特征层 | 分辨率 | 通道数 | 感受野 |
|---|---|---|---|
| P2 | 64x176 | 256 | 小(适合行人、锥桶) |
| P3 | 32x88 | 256 | 中(适合车辆) |
| P4 | 16x44 | 256 | 大(适合卡车、背景) |
4.6 本章小结
好,我们来捋一下这章的核心:
- ResNet:残差连接解决梯度消失,Bottleneck结构降低计算量
- ResNeXt:分组卷积提升表达能力,但要注意硬件兼容性
- FPN:自底向上+自顶向下,融合多尺度特征,输出P2-P5
- 实践建议:ResNet-50+FPN是BEV感知的黄金组合,输出通道统一256
下一章,我们会讲“视图变换”——怎么把图像特征投影到BEV空间。那是BEV感知最核心的一步,也是最有意思的一步。到时候见。
公众号:蓝海资料掘金营,微信deep3321