4. 特征提取基础:基于CNN的图像特征提取(ResNet、MobileNet)
好,咱们进入实战环节的第一个硬核模块——特征提取。
说白了,ADAS感知的第一步,就是让车子“看懂”图像里有什么。而看懂图像,靠的就是特征提取。我这些年做感知算法,最深的体会就是:特征提取的质量,直接决定了后续检测、跟踪的上限。你想想看,如果连特征都提歪了,后面再怎么调参也是白搭。
4.1 为什么ADAS需要CNN做特征提取?
传统方法用HOG、SIFT手工设计特征,在简单场景还行。但到了自动驾驶这种复杂环境——光照突变、雨雪遮挡、行人姿态千奇百怪——手工特征就彻底崩了。
CNN的优势在于:它能自动学习从低级边缘到高级语义的分层特征。我举个例子:
- 浅层卷积:检测边缘、纹理、颜色斑点
- 中层卷积:组合出车轮、车窗、车灯等部件
- 深层卷积:抽象出“这是一辆车”的完整概念
我在项目中遇到过最典型的案例:用传统方法检测夜间行人,漏检率高达30%。换成ResNet做特征提取后,漏检率直接降到5%以下。为什么?因为CNN能学到“红外热辐射”这种手工特征根本想不到的模式。
核心观点:特征提取是ADAS感知的“地基”。地基不牢,地动山摇。
4.2 ResNet:解决“越深越差”的怪圈
2015年之前,大家有个共识:网络越深,效果越好。但实际训练时发现,56层的网络比20层的训练误差还大。这不是过拟合,而是梯度消失/爆炸导致的退化问题。
ResNet的解决方案简单粗暴——引入残差连接(Skip Connection)。
4.2.1 残差块的核心思想
传统网络学习的是映射 H(x),而残差块学习的是残差 F(x) = H(x) - x。说白了,网络只需要学习“输入和输出之间的差异”。
为什么有效?我个人的理解是:残差结构让梯度可以“抄近道”直接回传到浅层。即使中间层梯度消失,跳跃连接也能保证浅层参数得到更新。
# 一个典型的ResNet残差块(PyTorch风格)
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
# 跳跃连接:如果维度不匹配,用1x1卷积调整
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x) # 关键:残差相加
out = self.relu(out)
return out
避坑指南:我曾经在部署时踩过一个坑——残差相加后忘记加ReLU激活。结果特征图数值范围不对,检测头直接崩了。记住:残差块输出一定要过激活函数。
4.2.2 ADAS场景下的ResNet选型
| 模型 | 参数量 | 推理速度(1080Ti) | ADAS适用场景 |
|---|---|---|---|
| ResNet-18 | 11M | ~2ms | 低算力平台、前视单目 |
| ResNet-34 | 21M | ~3ms | 中端域控制器 |
| ResNet-50 | 25M | ~4ms | 高端平台、多传感器融合 |
| ResNet-101 | 44M | ~7ms | 离线训练、高精度需求 |
我个人习惯:量产项目首选ResNet-34或ResNet-50。ResNet-18精度不够,ResNet-101又太慢。你想想看,ADAS要求30fps以上,单帧处理时间不能超过33ms。特征提取这一步就得控制在5ms以内,否则留给检测头的时间就不够了。
4.3 MobileNet:轻量化的艺术
ResNet虽然好,但参数量大,不适合嵌入式设备。这时候MobileNet就派上用场了。
MobileNet的核心创新是深度可分离卷积(Depthwise Separable Convolution)。说白了,就是把标准卷积拆成两步:
- 深度卷积(Depthwise Conv):每个通道单独做卷积,不跨通道
- 逐点卷积(Pointwise Conv):用1x1卷积融合通道信息
这样做的好处是什么?计算量直接降到原来的1/8到1/9。我算过一笔账:标准3x3卷积的计算量是 3×3×C_in×C_out×H×W,而深度可分离卷积是 3×3×C_in×H×W + C_in×C_out×H×W。当C_in和C_out都是256时,后者只有前者的1/9左右。
# MobileNet的核心模块:深度可分离卷积
class DepthwiseSeparableConv(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
# 第一步:深度卷积(每个通道独立)
self.depthwise = nn.Conv2d(
in_channels, in_channels,
kernel_size=3, stride=stride, padding=1,
groups=in_channels # 关键:groups=输入通道数
)
self.bn1 = nn.BatchNorm2d(in_channels)
# 第二步:逐点卷积(1x1融合通道)
self.pointwise = nn.Conv2d(
in_channels, out_channels,
kernel_size=1, stride=1
)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
x = self.relu(self.bn1(self.depthwise(x)))
x = self.relu(self.bn2(self.pointwise(x)))
return x
注意:深度可分离卷积虽然快,但精度会有轻微下降。我在实际项目中对比过:MobileNetV2比ResNet-34精度低约1.5%,但推理速度快了3倍。对于ADAS来说,如果算力紧张,这1.5%的精度损失是可以接受的。
4.3.1 MobileNetV2的改进:倒残差结构
MobileNetV2在V1基础上做了两个关键改进:
- 线性瓶颈(Linear Bottleneck):在逐点卷积后去掉ReLU,保留线性输出。为什么?因为低维空间用ReLU会丢失信息。
- 倒残差(Inverted Residual):先升维再降维。传统残差是“压缩-卷积-扩张”,MobileNetV2反过来“扩张-卷积-压缩”。
嗯,这里要注意:倒残差结构在低维输入时先升维到高维,做完深度卷积后再降维回去。这样做的目的是让深度卷积在高维空间提取特征,信息更丰富。
4.4 实战建议:如何选择特征提取网络?
我根据多年项目经验,总结了一个选型决策树:
- 算力充足(如Orin、EyeQ6):直接上ResNet-50或ResNet-101,精度优先
- 算力中等(如TDA4、EyeQ5):ResNet-34或MobileNetV2,平衡精度和速度
- 算力受限(如MCU、低端DSP):MobileNetV3-Small或ShuffleNet,牺牲精度保实时性
我的经验:不要盲目追求大模型。我曾经在一个项目中用ResNet-101做特征提取,精度确实高,但推理延迟超标,导致整个系统帧率掉到15fps。后来换成ResNet-34,精度只降了0.8%,但帧率稳定在30fps。ADAS是实时系统,帧率就是生命线。
4.5 小结
特征提取是ADAS感知的基石。ResNet用残差连接解决了深度网络的退化问题,MobileNet用深度可分离卷积实现了轻量化。两者各有千秋,选型时要根据实际算力和实时性要求来权衡。
下一章,我会讲如何把这些特征提取网络和检测头结合起来,构建完整的ADAS感知模型。到时候咱们再细聊。