0 概述-
在自动驾驶感知算法中BEV感知成为热点话题,BEV感知可以弥补2D感知的缺陷构建3D“世界”,更有利于下游任务和特征融合。为响应市场需求,地平线集成了基于bev的纯视觉算法,目前已支持ipm-based 、lss-based、 transformer-based(Geometry-guided Kernel Transformer、detr3d) 的多种bev视觉转换方法。本文为lss-based的BEV多任务感知算法介绍和使用说明。-
该示例为参考算法,仅作为在J5上模型部署的设计参考,非量产算法
1 性能精度指标-
数据集
img_shape
Stage 1
grid_size
num_points
Stage 2
Nuscenes
256x704
Backbone
Neck
128x128
10
Backbone
Neck
efficientnetb0
fastscnn
efficientnetb0
bifpn
注:目前 grid_sample的input_feature H,W ∈ [1, 1024] 且 H* W ≤ 720*1024
性能精度表现:
性能(FPS/单核)
分割精度(浮点/定点)iou
检测精度(浮点/定点)
divider
ped_crossing
Boundary
Others
NDS
mAP
138
46.55/47.45
27.91/ 28.44
47.06/ 46.03
85.59/ 84.49
0.3009/ 0.2990
0.2065/ 0.2066
注:-
stage1为image encoder;stage2为bev encoder;-
Nuscenes 数据集官方介绍:Nuscenes
2 模型介绍
2.1 模型框架
bev_lss 是基于bev_depth结构实现的,使用多视图的6张RGB图像作为输入,输出是目标的3D Box和BEV分割结果。首先使用2D主干对多视角图像获取2D 特征。然后将img_feature作为depth_net输入获得深度特征,将深度特征和img_encoder_feature 分别转换为BEV视角后生成点云特征,最后,接上任务特定的head,输出多任务结果。模型主要包括以下部分:-
**Part1—2D Image Encoder:**图像特征提取层。使用2D主干网络(efficientnet)和FastSCNN输出不同分辨率的特征图。返回最后一层–下采样至1/16原图大小层,用于下一步投影至3D 坐标系中。-
**Part2—View transformer:**将img_encoder_feature生成深度特征,将深度特征和img_encoder_feature 做bev 空间转换后生成视锥点云特征。-
**Part3—Bev transforms:**对bev特征做数据增强,仅发生在训练阶段。-
**Part4—3D BEV Encoder:**BEV特征提取层。-
**Part5—BEV Decoder:**分为Detection Head和Segmentation Head。得到统一的BEV特征后,使用DepthwiseSeparableFCNHead
进行bev分割,分割种类为[“others”, “divider”, “ped_crossing”, “Boundary”]。使用DepthwiseSeparableCenterPointHead
进行3D目标检测任务,检测的类别为[“car”,“truck”,“bus”,“barrier”,“bicycle”,“pedestrian”]。-
1.2 源码说明-
Config文件-
configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py
为该模型的配置文件,定义了模型结构、数据集加载,和整套训练流程,所需参数的说明在算子定义中会给出。配置文件主要内容包括:
#基础参数配置
task_name = "bev_lss_efficientnetb0_multitask_nuscenes"
batch_size_per_gpu = 4
device_ids = [0, 1, 2, 3]
#bev参数配置
bev_size = (51.2, 51.2, 0.8)
grid_size = (128, 128)
map_size = (15, 30, 0.15)
# 模型结构定义
model = dict(
type="ViewFusion",
backbone=dict(
type="efficientnet",
model_type="b0",
...
),
neck=dict(
type="FastSCNNNeck",
...
),
view_transformer=dict(
type="LSSTransformer", #lss transform
...
)
bev_transforms=[...],
bev_encoder=dict(
type="BevEncoder",
...
),
bev_decoders=[
dict(
type="BevSegDecoder",
...
),
dict(
type="BevDetDecoder",
...
)
],
)
deploy_model = dict(...)
...
# 数据加载
data_loader = dict(
type=torch.utils.data.DataLoader,
...
)
val_data_loader = dict(...)
#不同step的训练策略配置
float_trainer=dict(...)
calibration_trainer=dict(...)
int_trainer=dict(...)
#精度验证阶段配置
float_predictor=dict(...)
calibration_predictor=dict(...)
int_predictor=dict(...)
#编译配置
compile_cfg = dict(
march=march,
...
)
注: 如果需要复现精度,config中的训练策略最好不要修改,否则可能会有意外的训练情况出现。
img_encoder
来自6个view的image作为输入通过共享的backbone(efficientnet)和neck(FastSCNN)输出经过encoder后的feature,feature_shape为(6*B,C,1/16H,1/16W)。encoder即对多个view的img_feature 做特征提取,过程见下图:
对应代码:hat/models/backbones/efficientnet.py
hat/models/necks/fast_scnn.py
view_transformer-
bev_lss 使用的转换方法是LSS实现,流程如下图所示:
实现路径在hat/models/task_modules/view_fusion/view_transformer.py
的-
LSSTransformer。可将bev_lss的view_transformer分为3个部分:-
- 生成深度特征
- 对深度特征和img_encoder_feature做bev坐标转换
- 生成视锥点云特征(frustum features)
接下来将对这三个部分做具体介绍的具体代码实现:
生成深度特征-
生成depth为60的depth_feature,对depth_feature计算深度的score值。-
对应代码:
self.depth_net = ConvModule2d(
in_channels=in_channels,
out_channels=depth,
kernel_size=1,
padding=0,
stride=1,
bias=False,
)
depth = self.softmax(self.depth_net(feats))
生成 bev_feature-
为了减少mul计算量, 先把深度特征和 feature 分开做bev视角转换:
class LSSTransformer(ViewTransformer):
...
def _spatial_transfom(self, feats, points):
...
feat = feat.view(B, C, -1, W)
dfeat = dfeat.view(B, 1, -1, H * W)
homo_feats = []
for i in range(self.num_points):
homo_feat = self.grid_sample(
feat, # [1,64,6*16,44]
self.quant_stub(points[i]),
) #[1,64,128,128]
homo_dfeat = self.dgrid_sample(
dfeat, # [1,1,6*60,16*44]
self.dquant_stub(points[i + self.num_points]),
) # [1,1,128,128]
homo_feat = self.floatFs.mul(homo_feat, homo_dfeat)#视锥点云特征
homo_feats.append(homo_feat)
其中,point的生成在_gen_reference_point
,计算逻辑如下:
生成点云特征
class LSSTransformer(ViewTransformer):
...
def _spatial_transfom(self, feats, points):
...
for i in range(self.num_points):
homo_feat = self.grid_sample(
feat,
self.quant_stub(points[i]),
)
#num_points=10
homo_dfeat = self.dgrid_sample(
dfeat,
self.dquant_stub(points[i + self.num_points]),
)
homo_feat = self.floatFs.mul(homo_feat, homo_dfeat)
为了不遗失坐落在相同voxel中的点云特征,将对每个voxel都采样10次,最终将每个点云特征相加得到128x128x64的BEV特征图:
trans_feat = homo_feats[0]
for f in homo_feats[1:]:
trans_feat = self.floatFs.add(trans_feat, f)
bev transform
bev的数据增强仅发生在训练过程中,在 BEV 下做了 rotate的数据增强,作用域是 view transformer 的输出。配置如下:
bev_transforms=[
dict(
type="BevRotate",
bev_size=bev_size,
rot=(-0.3925, 0.3925),
),
],
bev_encoder
bev_encoder过程是对bev_feature 做特征提取的过程,backbone为efficientnet-b0,neck为BiFPN。流程见下图:
对应代码:hat/models/task_modules/view_fusion/encoder.py
class BevEncoder(nn.Module):
def __init__(self, backbone: nn.Module, neck: nn.Module):
super(BevEncoder, self).__init__()
self.backbone = backbone
self.neck = neck
def forward(self, feat, meta):
feat = self.backbone(feat)
if self.neck is not None:
feat = self.neck(feat)
return feat
bev_head
seg_head-
本模型的分割头为DepthwiseSeparableFCNHead
, conv为SeparableConvModule2d
-
对应代码:hat/models/task_modules/fcn/head.py
class DepthwiseSeparableFCNHead(FCNHead):
def __init__(self, in_channels, feat_channels, num_convs=1, **kwargs):
super(DepthwiseSeparableFCNHead, self).__init__(
in_channels=in_channels, feat_channels=feat_channels, **kwargs
)
self.convs = nn.Sequential(
SeparableConvModule2d(
in_channels,
...
dw_norm_layer=nn.BatchNorm2d(
),
),
class FCNHead(nn.Module):
def __init__(self,...):
...
def forward(self, inputs: List[torch.Tensor]):
x = inputs[self.input_index]
x = self.convs(x)
if self.dropout:
x = self.dropout(x)
seg_pred = self.cls_seg(x)
if self.training:
if self.upsample_output_scale:
seg_pred = self.resize(seg_pred)
if self.argmax_output:
seg_pred = seg_pred.argmax(dim=1)
if self.dequant_output:
seg_pred = self.dequant(seg_pred)
return seg_pred
det_head-
检测为多task检测,主要分为:
tasks = [
dict(name="car", num_class=1, class_names=["car"]),
dict(
name="truck",
num_class=2,
class_names=["truck", "construction_vehicle"],
),
dict(name="bus", num_class=2, class_names=["bus", "trailer"]),
dict(name="barrier", num_class=1, class_names=["barrier"]),
dict(name="bicycle", num_class=2, class_names=["motorcycle", "bicycle"]),
dict(
name="pedestrian",
num_class=2,
class_names=["pedestrian", "traffic_cone"],
),
]
在Nuscenes数据集中,目标的类别一共被分为了6个大类,网络给每一个类都分配了一个head,装在headlist中,而每个head内部都为预测的参数。
bev_det的分割头为DepthwiseSeparableCenterPointHead
-
对应代码:hat/models/task_modules/centerpoint/head.py
class DepthwiseSeparableCenterPointHead(CenterPointHead):
def _make_conv(
self,
...
):
pw_norm_layer = nn.BatchNorm2d(in_channels, **self.bn_kwargs)
pw_act_layer = nn.ReLU(inplace=True)
return SeparableConvModule2d(
in_channels=in_channels,
...
)
def _make_task(self, **kwargs):
return DepthwiseSeparableTaskHead(**kwargs)
class CenterPointHead(nn.Module):
def __init__(self,...):
self.shared_conv = nn.Sequential(
*(
self._make_conv(
in_channels=in_channels if i == 0 else share_conv_channels,
...
)
for i in range(share_conv_num)
)
)
#head module
for num_cls in num_classes:
heads = copy.deepcopy(common_heads)
heads.update({"heatmap": (num_cls, num_heatmap_convs)})
task_head = self._make_task(
...,
)
self.task_heads.append(task_head)
def forward(self, feats):
rets = []
feats = feats[0]
feats = self.shared_conv(feats)
for task in self.task_heads:
rets.append(task(feats))
forward时,经过共享的SeparableConv后,将feature再分别传入task_heads做task_pred。-
在hat/models/task_modules/centerpoint/head.py
的TaskHead
对不同的task定义conv_layers:
class DepthwiseSeparableTaskHead(TaskHead):
def _make_conv(
self,
in_channels,
...
):
return SeparableConvModule2d(
in_channels=in_channels,
...
)
class TaskHead(nn.Module):
def __init__(...):
...
for head in self.heads:
classes, num_conv = self.heads[head]
...
#head_conv
for _ in range(num_conv - 1):
conv_layers.append(
self._make_conv(
...
)
)
c_in = head_conv_channels
#cls_layer
conv_layers.append(
ConvModule2d(
in_channels=head_conv_channels,
out_channels=classes,
...
)
)
conv_layers = nn.Sequential(*conv_layers)
def forward(self, x):
ret_dict = {}
for head in self.heads:
ret_dict[head] = self.dequant(self.__getattr__(head)(x))
return ret_dict
bev_decoder
多任务模型的decoder分为分割和检测的解码,在分割任务中使用FCNDecoder
,在检测任务中使用-
CenterPointDecoder
,具体实现流程见下图:
对应代码:-
hat/models/task_modules/centerpoint/decoder.py
-
hat/models/task_modules/fcn/decoder.py
3 浮点模型训练-
3.1 Before Start-
3.1.1 发布物及环境部署-
step1:获取发布物-
下载OE包horizon_j5_open_explorer_v$version$.tar.gz
,获取方式见地平线开发者社区 OpenExplorer算法工具链 版本发布-
step2:解压发布包
tar -xzvf horizon_j5_open_explorer_v$version$.tar.gz
解压后文件结构如下:
|-- bsp
|-- ddk
| |-- package
| `-- samples
| |-- ai_benchmark
| |-- ai_forward_view_sample
| |-- ai_toolchain
| | |-- ...
| | |-- horizon_model_train_sample
| | `-- model_zoo
| |-- model_zoo
| `-- vdsp_rpc_sample
|-- README-CN
|-- README-EN
|-- resolve_all.sh
`-- run_docker.sh
其中horizon_model_train_sample为
参考算法模块,包含以下模块:
|-- horizon_model_train_sample #参考算法示例
| |-- plugin_basic #qat 基础示例
| `-- scripts #模型配置文件、运行脚本
step3:拉取docker环境
docker pull openexplorer/ai_toolchain_ubuntu_20_j5_gpu:v$version$
#启动容器,具体参数可根据实际需求配置
#-v 用于将本地的路径挂载到 docker 路径下
nvidia-docker run -it --shm-size="15g" -v `pwd`:/WORKSPACE openexplorer/ai_toolchain_ubuntu_20_j5_gpu:v$version$
3.1.2 数据集准备-
3.1.2.1 数据集下载-
进入nuscenes官网,根据提示完成账户的注册,下载Full dataset(v1.0)、CAN bus expansion和Map expansion(v1.3)这三个项目下的文件。下载后的压缩文件为:
|-- nuScenes-map-expansion-v1.3.zip
|-- can_bus.zip
|-- v1.0-mini.tar
|-- v1.0-trainval01_blobs.tar
|-- ...
|-- v1.0-trainval10_blobs.tar
`-- v1.0-trainval_meta.tar
Full dataset(v1.0)包含多个子数据集,如果不需要进行v1.0-trainval数据集的浮点训练和精度验证,可以只下载v1.0-mini数据集进行小场景的训练和验证。
将下载完成的v1.0-trainval01_blobs.tar~v1.0-trainval10_blobs.tar、v1.0-trainval_meta.tar和can_bus.zip进行解压,解压后的目录如下所示:
|--nuscenes
|-- can_bus #can_bus.zip解压后的目录
|-- samples #v1.0-trainvalXX_blobs.tar解压后的目录
| |-- CAM_BACK
| |-- ...
| |-- CAM_FRONT_RIGHT
| |-- ...
| `-- RADAR_FRONT_RIGHT
|-- sweeps
| |-- CAM_BACK
| |-- ...
| |-- CAM_FRONT_RIGHT
| |-- ...
| `-- RADAR_FRONT_RIGHT
|-- v1.0-trainval #v1.0-trainval_meta.tar解压后的数据
|-- attribute.json
| ...
`-- visibility.json
3.1.2.2 数据集打包-
进入 horizon_model_train_sample/scripts
目录,使用以下命令将训练数据集和验证数据集打包,格式为lmdb:
#pack train_Set
python3 tools/datasets/nuscenes_packer.py --src-data-dir /WORKSPACE/nuscenes/ --pack-type lmdb --target-data-dir /WORKSPACE/tmp_data/nuscenes/v1.0-trainval --version v1.0-trainval --split-name train
#pack val_Set
python3 tools/datasets/nuscenes_packer.py --src-data-dir /WORKSPACE/nuscenes/ --pack-type lmdb --target-data-dir /WORKSPACE/tmp_data/nuscenes/v1.0-trainval --version v1.0-trainval --split-name val
--src-data-dir为解压后的nuscenes数据集目录;-
--target-data-dir为打包后数据集的存储目录;-
--version 选项为[“v1.0-trainval”, “v1.0-test”, “v1.0-mini”],如果进行全量训练和验证设置为v1.0-trainval,如果仅想了解模型的训练和验证过程,则可以使用v1.0-mini数据集;v1.0-test数据集仅为测试场景,未提供注释。-
全量的nuscenes数据集较大,打包时间较长。每打包完100张会在终端有打印提示,其中train打包约28100张,val打包约6000张。
数据集打包命令执行完毕后会在target-data-dir
下生成train_lmdb
和val_lmdb
,train_lmdb
和val_lmdb
就是打包之后的训练数据集和验证数据集为config中的data_rootdir
。
|-- tmp_data
| |-- nuscenes
| | |-- v1.0-trainval
| | | |-- train_lmdb #打包后的train数据集
| | | | |-- data.mdb
| | | | `-- lock.mdb
| | | `-- val_lmdb #打包后的val数据集
| | | | |-- data.mdb
| | | | `-- lock.mdb
2.1.2.3 meta文件夹构建-
在tmp_data/nuscenes
下创建meta文件夹,将v1.0-trainval_meta.tar
压缩包解压至meta,得到meta/maps
文件夹,再将nuScenes-map-expansion-v1.3.zip
压缩包解压至meta/maps
文件夹下,解压后的目录结构为:
|-- tmp_data
| |-- nuscenes
| | |-- meta
| | | |-- maps #nuScenes-map-expansion-v1.3.zip解压后的目录
| | | | |-- 36092f0b03a857c6a3403e25b4b7aab3.png
| | | | |-- ...
| | | | |-- 93406b464a165eaba6d9de76ca09f5da.png
| | | | |-- prediction
| | | | |-- basemap
| | | | |-- expansion
| | | |-- v1.0-trainval #v1.0-trainval_meta.tar解压后的目录
| | | |-- attribute.json
| | | ...
| | | |-- visibility.json
| | `-- v1.0-trainval
| | | |-- train_lmdb #打包后的train数据集
| | | `-- val_lmdb #打包后的val数据集
3.1.3 config配置-
在进行模型训练和验证之前,需要对configs文件中的部分参数进行配置,一般情况下,我们需要配置以下参数:
- device_ids、batch_size_per_gpu:根据实际硬件配置进行device_ids和每个gpu的batchsize的配置;
- ckpt_dir:浮点、calib、量化训练的权重路径配置,权重下载链接在config文件夹下的README中;
- data_rootdir:2.1.2.2中打包的数据集路径配置;
- meta_rootdir :2.1.2.3中创建的meta文件夹的路径配置;
- float_trainer下的checkpoint_path:浮点训练时backbone的预训练权重所在路径,可以使用README的# Backbone Pretrained ckpt中ckpt download提供的float-checkpoint-best.pth.tar权重文件。
- infer_cfg:指定模型输入,在infer.py脚本使用时需配置;
3.2 浮点模型训练-
config文件中的参数配置完成后,使用以下命令训练浮点模型:
python3 tools/train.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --stage float
float训练后模型ckpt的保存路径为config配置的
ckpt_callback
中save_dir
的值,默认为ckpt_dir
。
3.3 浮点模型精度验证-
浮点模型训练完成以后,通过指定训好的float_checkpoint_path,使用以下命令验证已经训练好的模型精度:
python3 tools/predict.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --stage float
验证完成后,会在终端打印浮点模型在验证集上检测和分割精度,如下所示:
Per-class results:
Object Class AP ATE ASE AOE AVE AAE
car 0.416 0.571 0.184 0.400 1.329 0.209
...
2023-06-06 18:24:09,976 INFO [nuscenes_metric.py:358] Node[0] NDS: 0.3060, mAP:0.2170
...
2023-06-06 18:24:10,513 INFO [mean_iou.py:170] Node[0] ~~~~ MeanIOU Summary metrics ~~~~
Summary:
Scope mIoU mAcc aAcc
global 51.47 75.62 86.29
Per Class Results:
Class IoU Acc
others 85.10 88.42
...
2023-06-06 18:24:10,524 INFO [metric_updater.py:320] Node[0] Epoch[0] Validation bev_lss: NDS[0.3060] MeanIOU[tensor(0.5147, device='cuda:0')]
4 模型量化和编译-
完成浮点训练后,还需要进行量化训练和编译,才能将定点模型部署到板端。地平线对该模型的量化采用horizon_plugin框架,经过Calibration+定点模型转换后,使用compile
的工具将量化模型编译成可以上板运行的hbm
文件。-
4.1 Calibration-
模型改造完成并完成浮点训练后,便可进行 Calibration。此过程通过在模型中插入 Observer 的方式,在 forward 过程中统计各处的数据分布情况,从而计算出合理的量化参数。和浮点训练的方式一样,将checkpoint_path指定为训好的浮点权重路径。 通过运行下面的脚本就可以开启模型的Calibration过程:
python3 tools/train.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --stage calibration
4.2 Calibration 模型精度验证-
calibration完成以后,可以使用以下命令验证经过calib后模型的精度:
python3 tools/predict.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --stage calibration
验证完成后,会在终端输出calib模型在验证集上检测和分割精度,格式见3.3。-
对于LSS模型,仅需做calib 即可满足量化精度,无需做qat训练!
4.3 量化模型精度验证-
指定calibration-checkpoint后,通过运行以下命令进行量化模型的精度验证:
python3 tools/predict.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --stage int_infer
验证完成后,会在终端输出int模型在验证集上检测和分割精度,格式见3.3。
4.4 仿真上板精度验证-
除了上述模型验证之外,我们还提供和上板完全一致的精度验证方法,可以通过下面的方式完成:
python3 tools/align_bpu_validation.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py
4.5 量化模型编译-
在训练完成之后,可以使用compile
的工具用来将量化模型编译成可以上板运行的hbm
文件,同时该工具也能预估在BPU上的运行性能,可以采用以下脚本:
python3 tools/compile_perf.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --out-dir ./ --opt 3
--opt为优化等级,取值范围为0~3,数字越大优化等级越高,编译时间也会越-
长;-
可以指定–out_dir为编译后产出物的存放路径,默认在ckpt_dir的compile文件夹下
运行后,ckpt_dir的compile目录下会产出以下文件:
|-- compile
| |-- .html #模型在bpu上的静态性能数据
| |-- .json
| |-- model.hbm #板端部署的模型
| |-- model.hbir #编译过程的中间文件
`-- model.pt #模型的pt文件
5 其他工具
5.1 结果可视化
如果你希望可以看到训练出来的模型单帧的检测效果,我们的tools文件夹下面同样提供了预测及可视化的脚本,你只需要运行以下脚本即可:
python3 tools/infer.py --config configs/bev/bev_lss_efficientnetb0_multitask_nuscenes.py --save-path ./
需在config文件中配置infer_cfg字段。
可视化结果将会在save-path路径下输出。-
可视化示例:
6 板端部署-
6.1 上板性能实测-
使用hrt_model_exec perf
工具将生成的.hbm文件上板做BPU性能FPS实测,hrt_model_exec perf
参数如下:
hrt_model_exec perf --model_file {model}.hbm \
--thread_num 8 \
--frame_count 2000 \
--core_id 0 \
--profile_path '.'
6.2 AIBenchmark 示例-
OE开发包中提供了LSS的AI Benchmark示例,位于:ddk/samples/ai_benchmark/j5/qat/script/bev/bev_lss
,具体使用可以参考开发者社区J5算法工具链产品手册-AIBenchmark评测示例-
可在板端使用以下命令执行做模型评测:
#性能数据
sh fps.sh
#单帧延迟数据
sh latency.sh