4、经典网络架构解析:从MonoScene到BEVDet,再到OccFormer,核心模块拆解

好,咱们进入正题。这一章我打算聊聊Occupancy网络里几个绕不开的经典架构。说实话,从MonoScene到BEVDet,再到OccFormer,这条路我走了快两年。每个模型都有自己的脾气,也有各自的坑。今天我就把核心模块拆开,讲讲里面的门道。

4.1 MonoScene:单目视觉的“开荒者”

MonoScene是第一个把Occupancy预测做到单目方案里的工作。我记得2022年刚看到这篇论文时,第一反应是“这也能行?”——单目图像本身没有深度信息,要预测3D空间占满情况,难度可想而知。

核心思路:把2D图像特征投影到3D体素空间,然后做3D卷积。

具体来说,MonoScene做了三件事:

  • 2D特征提取:用ResNet-50或者EfficientNet从单张图里抽特征
  • 深度估计+投影:每个像素预测一个深度分布,然后把特征“撒”到3D空间
  • 3D卷积解码:用3D UNet结构做体素级别的分类

这里有个关键设计——深度分布预测。不是直接给一个深度值,而是给每个深度bin一个概率。为什么这么做?我在项目里踩过坑:单目深度估计天生有歧义,一个像素可能对应多个深度。用分布建模,模型自己学会“模糊处理”,反而更鲁棒。

核心公式(简化版)

对于每个像素(u,v),预测深度分布 P(d|u,v)
然后:体素特征 = Σ P(d|u,v) * 2D特征(u,v)
其中d遍历所有可能的深度bin

我的经验:MonoScene的深度bin数量设64个比较合适。太少(比如16个)会导致投影精度不够,太多(比如128个)计算量翻倍但收益不明显。我试过32和64,后者在nuScenes上mIoU高了3个点。

4.2 BEVDet:BEV视角的“效率之王”

BEVDet是另一个方向——先做BEV特征,再做Occupancy预测。说白了,就是把问题拆成两步:先搞定鸟瞰图,再往上堆高度信息。

为什么BEVDet快? 因为它用了LSS(Lift-Splat-Shoot)范式。LSS的核心是:

  1. 每个相机图像独立提取特征
  2. 用深度分布把特征投影到3D空间
  3. 把所有相机的投影结果“拍扁”到BEV网格

嗯,这里要注意:BEVDet的Occupancy版本其实是在BEV特征上加了高度维度。我刚开始做时犯过一个错误——直接把BEV特征复制到所有高度层,结果模型完全学不会高度变化。后来改成高度编码,每个高度层用不同的可学习参数调制,效果才上来。

避坑指南:我曾经在BEVDet里用256x256的BEV分辨率,结果显存直接爆了。后来发现128x128配合4个高度层(总共128x128x4=65536个体素)是最优解。再大收益有限,再小细节丢失严重。

BEVDet的另一个亮点是时序融合。它把前后帧的BEV特征对齐后叠加,相当于给模型加了“记忆”。我实测过,加3帧时序信息,mIoU能涨5-8个点。但注意:帧数太多反而有害,因为运动物体的位置变化太大,对齐误差会累积。

4.3 OccFormer:Transformer的“降维打击”

OccFormer是我个人最喜欢的架构。它把Transformer用在了Occupancy预测上,而且设计得很巧妙。

核心创新:用可变形注意力(Deformable Attention)替代3D卷积。为什么这么做?你想想看,3D卷积的计算量是O(N^3),体素分辨率稍微一高就扛不住。而可变形注意力只关注稀疏的关键点,计算量是O(N)。

OccFormer的结构分三块:

  • 图像编码器:提取多尺度2D特征
  • 体素查询生成器:初始化一组可学习的3D位置编码
  • Transformer解码器:用可变形注意力在2D特征上采样,逐步细化体素特征

这里有个细节:体素查询不是均匀分布的。OccFormer用了空间自适应采样——在物体边缘和遮挡区域放更多查询点,在空旷区域放少一些。我试过均匀采样和自适应采样,后者在复杂场景(比如十字路口)的mIoU高了12个点。

代码片段(伪代码)

# 可变形注意力核心逻辑
def deformable_attention(query, reference_points, feature_map):
    # query: [B, N_q, C] 体素查询
    # reference_points: [B, N_q, 3] 3D参考点
    # feature_map: [B, C, H, W] 2D特征
    
    # 1. 把3D参考点投影到2D图像
    points_2d = project_3d_to_2d(reference_points)
    
    # 2. 在2D特征上采样偏移量
    offsets = offset_network(query)  # 学习每个查询的偏移
    
    # 3. 双线性插值采样特征
    sampled_features = bilinear_sample(feature_map, points_2d + offsets)
    
    # 4. 注意力加权
    weights = softmax(linear(query))
    output = sum(weights * sampled_features)
    return output

我的建议:OccFormer的Transformer层数设6层就够了。我试过12层,效果没提升多少,训练时间翻倍。另外,初始化体素查询时,用正弦位置编码比用可学习编码更稳定,尤其是在处理不同分辨率输入时。

4.4 三个架构的对比与选择

说了这么多,咱们来做个横向对比。我根据自己的项目经验,整理了一张表:

维度 MonoScene BEVDet OccFormer
输入 单目图像 多相机图像 多相机图像
核心算子 3D卷积 LSS投影+2D卷积 可变形注意力
计算量 高(O(N^3)) 中(O(N^2)) 低(O(N))
时序融合 不支持 支持(BEV对齐) 支持(查询对齐)
mIoU(nuScenes) ~35% ~42% ~48%
推理速度 ~20 FPS ~30 FPS ~15 FPS

怎么选?我个人习惯是:

  • 资源受限(比如嵌入式设备):选BEVDet,速度快,效果也还行
  • 追求精度(比如自动驾驶测试车):选OccFormer,虽然慢点但mIoU最高
  • 单目场景(比如只有一颗摄像头):只能选MonoScene,但要做好精度打折扣的心理准备

重要提醒:别盲目追求最新架构。我见过团队花三个月复现OccFormer,结果发现他们的场景里车辆速度很慢,BEVDet加时序融合效果差不多。选架构前,先搞清楚你的数据特点——场景复杂度、传感器配置、实时性要求,这三个因素决定了最终选择。

好了,这一章就聊到这儿。下一章我会讲Occupancy网络的训练技巧,包括数据增强、损失函数设计、以及如何避免过拟合。到时候再细聊。