第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升维。这样计算量小,但表达能力不弱。

我的经验:在BEV任务中,我建议直接用ResNet-50或ResNet-101。ResNet-18太浅,多尺度特征不够丰富。ResNet-152又太重,训练速度慢。ResNet-50是个很好的平衡点。

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部分
避坑指南:我曾经在部署时踩过一个坑——某些硬件对分组卷积的支持不好,推理速度反而比普通卷积慢。所以,如果你要部署到边缘设备,先查一下硬件文档,别盲目上ResNeXt。

4.4 FPN结构详解:从C2到P2的魔法

FPN(Feature Pyramid Network)是2017年提出的,但至今仍是BEV感知的标配。它的核心思想就八个字:自底向上,自顶向下

具体来说:

  1. 自底向上:Backbone前向传播,得到C2, C3, C4, C5(分辨率依次减半)
  2. 自顶向下:从C5开始,上采样后与C4融合,得到P4;再上采样与C3融合,得到P3;以此类推
  3. 横向连接:用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]
关键参数:in_channels_list通常为[512, 1024, 2048](对应ResNet-50的C3, C4, C5)。out_channels一般设为256,这是BEV感知中的“黄金通道数”。

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 大(适合卡车、背景)
我的习惯:在训练时,我会把P2, P3, P4都送入视图变换模块。P5虽然语义强,但分辨率太低,对BEV空间的位置编码帮助不大。当然,如果你做的是纯语义分割任务,P5还是有用的。

4.6 本章小结

好,我们来捋一下这章的核心:

  • ResNet:残差连接解决梯度消失,Bottleneck结构降低计算量
  • ResNeXt:分组卷积提升表达能力,但要注意硬件兼容性
  • FPN:自底向上+自顶向下,融合多尺度特征,输出P2-P5
  • 实践建议:ResNet-50+FPN是BEV感知的黄金组合,输出通道统一256

下一章,我们会讲“视图变换”——怎么把图像特征投影到BEV空间。那是BEV感知最核心的一步,也是最有意思的一步。到时候见。

公众号:蓝海资料掘金营,微信deep3321