Lidar融合多任务感知模型

这篇教程主要是告诉大家如何利用HAT在自动驾驶数据集 Nuscenes 上训练一个Lidar融合多任务感知模型,包括浮点、量化和定点模型。 下文以配置 configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py 为例介绍如何配置并训练Lidar融合多任务感知模型。

bevfusion_pointpillar_henet_multisensor_multitask_nuscenes 是一个多模态多任务自动驾驶感知模型,接受两种 cameralidar 两种模态的输入,输出动态要素3d检测框和3d占用网格预测。

训练流程

如果你只是想简单的把 bevfusion_pointpillar_henet_multisensor_multitask_nuscenes 的模型训练起来,那么可以首先阅读一下这一章的内容。和其他任务一样,对于所有的训练,评测任务,HAT统一采用 tools + config 的形式来完成。在准备好原始数据集之后,可以通过下面的流程,方便地完成整个训练的流程。

数据集准备

这里以nuscense数据集为例,可以从 https://www.nuscenes.org/nuscenes 下载数据集, 对于Occupancy预测任务,还需要下载OCC的GT,可以从 https://github.com/CVPR2023-3D-Occupancy-Prediction/CVPR2023-3D-Occupancy-Prediction 下载数据集。 这里建议将下载好的数据集,解压到nuscense数据集文件夹中的 occ3d/gts 下。 然后运行以下命令,将lidar, images, occ gt等数据一起打包成lmdb格式:

python3 tools/datasets/nuscenes_packer.py --src-data-dir WORKSAPCE/datasets/nuscenes/ --pack-type lmdb --target-data-dir . --version v1.0-trainval --split-name val --need-occ
python3 tools/datasets/nuscenes_packer.py --src-data-dir WORKSAPCE/datasets/nuscenes/ --pack-type lmdb --target-data-dir . --version v1.0-trainval --split-name train --need-occ

上面这两条命令分别对应着转换训练数据集和验证数据集,打包完成之后,data目录下的文件结构应该如下所示:

tmp_data
    |-- nuscenes
        |-- v1.0-trainval 
            |-- train_lmdb
            |-- val_lmdb

train_lmdb和val_lmdb就是打包之后的训练数据集和验证数据集,也是网络最终读取的数据集。

模型训练

数据集准备好之后,就可以开始训练浮点型的lidar融合多任务感知模型 bevfusion_pointpillar_henet_multisensor_multitask_nuscenes 了。

首先可以使用以下命令评估模型的计算量:

python3 tools/calops.py --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

如果你想要复现整个模型浮点和定点训练流程,你需要:

python3 tools/train.py --stage "float" --config configs/lidar_bevfusion/bevformer_henet_camera_multitask_nuscenes_pretrain.py
python3 tools/train.py --stage "float" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py
python3 tools/train.py --stage "calibration" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py
python3 tools/train.py --stage "qat" --config cconfigs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

以上命令分别完成camera输入的浮点模型预训练,浮点模型训练和定点模型的训练,其中定点模型的训练需要以训练好的浮点模型为基础,具体内容请阅读 量化感知训练(QAT) 章节的内容。

导出定点模型

完成量化训练后,便可以开始导出定点模型。可以通过下面命令来导出:

python3 tools/export_hbir.py --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

模型验证

在完成训练之后,可以得到训练完成的浮点、量化或定点模型。和训练方法类似,我们可以用相同方法来对训好的模型做指标验证,得到为 Floatcalibrationqat 的指标,分别为浮点和量化的指标。

python3 tools/predict.py --stage "float" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py
python3 tools/predict.py --stage "calibration" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py
python3 tools/predict.py --stage "qat" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

和训练模型时类似,--stage 后面的参数为 "float"calibration"qat" 时,分别可以完成对训练好的浮点模型、校准模型和量化模型的验证。

定点模型精度验证也可使用下面命令,但需要注意是必须要先导出hbir:

python3 tools/predict.py --stage "int_infer" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

模型推理和结果可视化

HAT 提供了 infer_hbir.py 脚本提供了对定点模型的推理结果进行可视化展示:

python3 tools/infer_hbir.py --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py --model-inputs ${model_inputs} --save-path ${save_path} --use-dataset

python3 tools/infer_hbir.py --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py --model-inputs ${model_inputs} --save-path ${save_path}

定点模型检查和编译

在HAT中集成的量化训练工具链主要是为了地平线的计算平台准备的,因此,对于量化模型的检查和编译是必须的。 我们在HAT中提供了模型检查的接口,可以在定义好量化模型之后,先检查能否在 BPU 上正常运行:

python3 tools/model_checker.py --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

在模型训练完成后,可以通过 compile_perf_hbir 脚本将量化模型编译成可以上板运行的 hbm 文件,同时该工具也能预估在 BPU 上的运行性能:

python3 tools/compile_perf_hbir.py --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

以上就是从数据准备到生成量化可部署模型的全过程。

训练细节

在这个说明中,我们对模型训练需要注意的一些事项进行说明,主要为 config 的一些相关设置。

模型构建

lidar_network = dict(
    type="CenterPointDetector",
    feature_map_shape=get_feature_map_size(point_cloud_range, voxel_size),
    pre_process=dict(
        type="CenterPointPreProcess",
        pc_range=point_cloud_range,
        voxel_size=voxel_size,
        max_voxels_num=max_voxels,
        max_points_in_voxel=max_num_points,
        norm_range=[-51.2, -51.2, -5.0, 0.0, 51.2, 51.2, 3.0, 255.0],
        norm_dims=[0, 1, 2, 3],
    ),
    reader=dict(
        type="PillarFeatureNet",
        num_input_features=5,
        num_filters=(64,),
        with_distance=False,
        pool_size=(max_num_points, 1),
        voxel_size=voxel_size,
        pc_range=point_cloud_range,
        bn_kwargs=norm_cfg,
        quantize=True,
        use_4dim=True,
        use_conv=True,
        hw_reverse=True,
    ),
    backbone=dict(
        type="PointPillarScatter",
        num_input_features=64,
        use_horizon_pillar_scatter=True,
        quantize=True,
    ),
    neck=dict(
        type="SECONDNeck",
        in_feature_channel=64,
        down_layer_nums=[3, 5, 5],
        down_layer_strides=[2, 2, 2],
        down_layer_channels=[64, 128, 256],
        up_layer_strides=[0.5, 1, 2],
        up_layer_channels=[128, 128, 128],
        bn_kwargs=norm_cfg,
        quantize=True,
        use_relu6=False,
    ),
)
camera_network = dict(
    type="BevFormer",
    out_indices=(-1,),
    backbone=dict(
        type="HENet",
        in_channels=3,
        block_nums=[4, 3, 8, 6],
        embed_dims=[64, 128, 192, 384],
        attention_block_num=[0, 0, 0, 0],
        mlp_ratios=[2, 2, 2, 3],
        mlp_ratio_attn=2,
        act_layer=["nn.GELU", "nn.GELU", "nn.GELU", "nn.GELU"],
        use_layer_scale=[True, True, True, True],
        layer_scale_init_value=1e-5,
        num_classes=1000,
        include_top=False,
        extra_act=[False, False, False, False],
        final_expand_channel=0,
        feature_mix_channel=1024,
        block_cls=["GroupDWCB", "GroupDWCB", "AltDWCB", "DWCB"],
        down_cls=["S2DDown", "S2DDown", "S2DDown", "None"],
        patch_embed="origin",
    ),
    neck=dict(
        type="FPN",
        in_strides=[32],
        in_channels=[384],
        out_strides=[32],
        out_channels=[_dim_],
        bn_kwargs=dict(eps=1e-5, momentum=0.1),
    ),
    view_transformer=dict(
        type="SingleBevFormerViewTransformer",
        bev_h=bev_h_,
        bev_w=bev_w_,
        pc_range=point_cloud_range,
        num_points_in_pillar=2,
        embed_dims=_dim_,
        queue_length=1,
        in_indices=(-1,),
        single_bev=True,
        use_lidar2img=use_lidar2img,
        max_camoverlap_num=max_camoverlap_num,
        virtual_bev_h=64,
        virtual_bev_w=80,
        positional_encoding=dict(
            type="LearnedPositionalEncoding",
            num_feats=_pos_dim_,
            row_num_embed=bev_h_,
            col_num_embed=bev_w_,
        ),
        encoder=dict(
            type="SingleBEVFormerEncoder",
            num_layers=1,
            return_intermediate=False,
            bev_h=bev_h_,
            bev_w=bev_w_,
            embed_dims=_dim_,
            encoder_layer=dict(
                type="SingleBEVFormerEncoderLayer",
                embed_dims=_dim_,
                selfattention=dict(
                    type="HorizonMultiScaleDeformableAttention",
                    embed_dims=_dim_,
                    num_points=2,
                    num_levels=1,
                    grid_align_num=4,
                    batch_first=True,
                    # reduce_align_num=8,
                    feats_size=[[bev_w_, bev_h_]],
                ),
                crossattention=dict(
                    type="HorizonSpatialCrossAttention",
                    max_camoverlap_num=max_camoverlap_num,
                    bev_h=bev_h_,
                    bev_w=bev_w_,
                    deformable_attention=dict(
                        type="HorizonMultiScaleDeformableAttention3D",
                        embed_dims=_dim_,
                        num_points=2,
                        num_levels=_num_levels_,
                        grid_align_num=64,
                        feats_size=[[30, 16]],
                    ),
                    embed_dims=_dim_,
                ),
                dropout=0.1,
            ),
        ),
    ),
)
bev_head = dict(
    type="BEVFormerDetDecoder",
    bev_h=bev_h_,
    bev_w=bev_w_,
    num_query=900,
    embed_dims=_dim_,
    pc_range=point_cloud_range,
    decoder=dict(
        type="DetectionTransformerDecoder",
        num_layers=6,
        return_intermediate=True,
        decoder_layer=dict(
            type="DetrTransformerDecoderLayer",
            crossattention=dict(
                type="HorizonMultiScaleDeformableAttention",
                embed_dims=_dim_,
                num_levels=1,
                grid_align_num=4,
                feats_size=[[bev_w_, bev_h_]],
            ),
            dropout=0.1,
        ),
    ),
    criterion=dict(
        type="BevFormerCriterion",
        assigner=dict(
            type="BevFormerHungarianAssigner3D",
            cls_cost=dict(type="FocalLossCost", weight=2.0),
            reg_cost=dict(type="BBox3DL1Cost", weight=0.25),
        ),
        loss_cls=dict(
            type="FocalLoss",
            loss_name="cls",
            num_classes=num_classes + 1,
            alpha=0.25,
            gamma=2.0,
            loss_weight=2.0,
            reduction="mean",
        ),
        loss_bbox=dict(
            type="L1Loss",
            loss_weight=0.25,
        ),
        pc_range=point_cloud_range,
        bbox_key="lidar_bboxes_labels",
    ),
    post_process=dict(
        type="BevFormerProcess",
        post_center_range=[-61.2, -61.2, -10.0, 61.2, 61.2, 10.0],
        pc_range=point_cloud_range,
        max_num=300,
        num_classes=10,
    ),
)
occ_head = dict(
    type="BevformerOccDetDecoder",
    use_mask=True,
    use_lidar2img=use_lidar2img,
    lidar_input=lidar_input,
    camera_input=camera_input,
    num_classes=num_classes_occ,
    bevsize_scale_ratio=bev_size_occ[0] / bev_size[0],
    occ_head=dict(
        type="BEVOCCHead2D",
        in_dim=256,
        out_dim=128,
        Dz=16,
        num_classes=num_classes_occ,
        use_predicter=True,
        use_upsample=True,
    ),
    loss_occ=dict(
        type="CrossEntropyLoss",
        use_sigmoid=False,
        ignore_index=255,
        loss_weight=12.0,
    ),
)
bev_decoders = []
if use_bev_head:
    bev_decoders.append(bev_head)
if use_occ_head:
    bev_decoders.append(occ_head)
# model settings
model = dict(
    type="BevFusion",
    lidar_network=lidar_network,
    camera_network=camera_network,
    bev_decoders=bev_decoders,
    bev_h=bev_h_,
    bev_w=bev_w_,
    use_lidar_head=False,
    camera_input=camera_input,
    lidar_input=lidar_input,
)

其中,model 下面的 type 表示定义的模型名称,剩余的变量表示模型的其他组成部分。这样定义模型的好处在于我们可以很方便的替换我们想要的结构。 bevfusion_pointpillar_henet_multisensor_multitask_nuscenes 模型主要由 lidar networkcamera_networkbev_decoders 三个部分组成, 其中 bev_decoders 包含 BEVFormerDetDecoderBevformerOccDetDecoder 两个模块,分别输出3d检测框和3d占用网格预测。

数据增强

model 的定义一样,数据增强的流程是通过在config配置文件中定义 data_loaderval_data_loader 这两个dict来实现的,分别对应着训练集和验证集的处理流程。 例如训练集的定义为:

train_dataset = dict(
    type="NuscenesBevSequenceDataset",
    data_path=os.path.join(data_rootdir, "train_lmdb"),
    map_size=map_size,
    map_path=meta_rootdir,
    with_lidar_bboxes=use_lidar2img,
    with_bev_bboxes=False,
    with_ego_bboxes=True,
    bev_range=point_cloud_range,
    need_lidar=True,
    num_sweeps=9,
    load_dim=5,
    use_dim=[0, 1, 2, 3, 4],
    num_seq=1,
    with_ego_occ=False,
    with_lidar_occ=use_lidar2img,
    transforms=[
        dict(type="MultiViewsImgResize", size=input_size),
        dict(type="MultiViewsImgFlip"),
        dict(type="MultiViewsImgRotate", rot=(-5.4, 5.4)),
        dict(type="BevBBoxRotation", rotation_3d_range=(-0.3925, 0.3925)),
        dict(type="MultiViewsPhotoMetricDistortion"),
        dict(
            type="MultiViewsImgTransformWrapper",
            transforms=[
                dict(type="PILToTensor"),
                dict(type="BgrToYuv444", rgb_input=True),
                dict(type="Normalize", mean=128, std=128),
            ],
        ),
    ],
)

训练策略

为了训练一个精度高的模型,好的训练策略是必不可少的。对于每一个训练任务而言,相应的训练策略同样都定义在其中的config文件中,从 float_trainer 这个变量就可以看出来。

对于这个多网融合模型,我们建议先训练 lidar 输入的模型,再训练 camera 输入的模型,最后训练 lidar + camera 的融合模型,以获得较好的效果。 其中 lidar 输入的模型可以参考 centerpoint 模型训练, camera 输入的模型可以参考 “configs/lidar_bevfusion/bevformer_henet_camera_multitask_nuscenes_pretrain.py” config训练浮点预训练模型,命令为:

python3 tools/train.py --stage "float" --config configs/lidar_bevfusion/bevformer_henet_camera_multitask_nuscenes_pretrain.py

雷达融合多任务模型的 float_trainer 的配置如下,可以看到我们分别加载了camera输入和lidar输入的预训练模型。

float_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=model,
    data_loader=data_loader,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        converters=[
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    camera_ckpt_dir, "float-checkpoint-last.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
            ),
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    lidar_ckpt_dir, "float-checkpoint-last.pth.tar"
                ),
                state_dict_update_func=partial(
                    update_state_dict_by_add_prefix, prefix="lidar_net."
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
            ),
        ],
    ),
    optimizer=dict(
        type=torch.optim.AdamW,
        betas=(0.95, 0.99),
        params={
            "camera_net.backbone": dict(lr_mult=0.1),
        },
        lr=4e-4,
        weight_decay=0.01,
    ),
    batch_processor=batch_processor,
    num_epochs=24,
    # num_steps=10,
    stop_by="epoch",
    device=None,
    callbacks=[
        stat_callback,
        loss_show_update,
        dict(
            type="CosineAnnealingLrUpdater",
            warmup_len=500,
            warmup_by="step",
            warmup_lr_ratio=1.0 / 3,
            step_log_interval=500,
            stop_lr=2e-4 * 1e-3,
        ),
        grad_callback,
        val_callback,
        ckpt_callback,
    ],
    sync_bn=True,
    train_metrics=dict(
        type="LossShow",
    ),
    val_metrics=val_metrics,
)

float_trainer 从大局上定义了我们的训练方式,包括使用多卡分布式训练(distributed_data_parallel_trainer),模型训练的epoch次数,以及优化器的选择。 同时 callbacks 中体现了模型在训练过程中使用到的小策略以及您想实现的操作,包括学习率的变换方式(CosineAnnealingLrUpdater),在训练过程中验证模型的指标(Validation),以及保存(Checkpoint)模型的操作。 当然,如果你有自己希望模型在训练过程中实现的操作,也可以按照这种dict的方式添加。

通过上面的介绍,你应该对config文件的功能有了一个比较清楚的认识。然后通过前面提到的训练脚本,就可以训练一个高精度的纯浮点的检测模型。当然训练一个好的检测模型不是我们最终的目的,它只是做为一个pretrain为我们后面训练定点模型服务的。

量化模型训练

当我们有了纯浮点模型之后,就可以开始训练相应的定点模型了。和浮点训练的方式一样,我们只需要通过运行下面的脚本就可以得到伪量化模型了:

python3 tools/train.py --stage "calibration" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py
python3 tools/train.py --stage "qat" --config configs/lidar_bevfusion/bevfusion_pointpillar_henet_multisensor_multitask_nuscenes.py

可以看到,我们的配置文件没有改变,只改变了 stage 的类型, 其中calibration流程可以为QAT的量化训练提供一个更好的初始化参数。

我们使用模版配置了 Float2CalibrationFloat2QAT 将模型分别转换为calibration和qat模型,具体的calibration 和 QAT 的config为:

cali_qconfig_setter = (default_calibration_qconfig_setter,)
qat_qconfig_setter = (default_qat_fixed_act_qconfig_setter,)


float2calibration = dict(
    type="Float2Calibration",
    convert_mode="jit-strip",
    example_data_loader=calibration_example_data_loader,
    qconfig_setter=cali_qconfig_setter,
)


float2qat = dict(
    type="Float2QAT",
    convert_mode="jit-strip",
    example_data_loader=calibration_example_data_loader,
    qconfig_setter=qat_qconfig_setter,
)


calibration_trainer = dict(
    type="Calibrator",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        qat_mode="fuse_bn",
        converters=[
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    ckpt_dir, "float-checkpoint-last.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
            ),
            dict(
                type="RepModel2Deploy",
            ),
            float2calibration,
            dict(
                type="FixWeightQScale",
            ),
        ],
    ),
    data_loader=calibration_data_loader,
    batch_processor=calibration_batch_processor,
    num_steps=100,
    device=None,
    callbacks=[
        stat_callback,
        val_callback,
        ckpt_callback,
    ],
    val_metrics=val_metrics,
    log_interval=20,
)

qat_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        qat_mode="fuse_bn",
        converters=[
            dict(
                type="RepModel2Deploy",
            ),
            float2qat,
            dict(
                type="FixWeightQScale",
            ),
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    ckpt_dir, "calibration-checkpoint-last.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
            ),
        ],
    ),
    data_loader=data_loader,
    optimizer=dict(
        type=torch.optim.AdamW,
        lr=2e-5,
        weight_decay=1e-3,
    ),
    batch_processor=batch_processor,
    num_epochs=16,
    device=None,
    callbacks=[
        stat_callback,
        loss_show_update,
        dict(
            type="StepDecayLrUpdater",
            lr_decay_id=[10],
            step_log_interval=500,
        ),
        grad_callback,
        val_callback,
        ckpt_callback,
    ],
    train_metrics=dict(
        type="LossShow",
    ),
    val_metrics=val_metrics,
)

训练策略不同

正如我们之前所说,量化训练其实是在纯浮点训练基础上的finetue。因此量化训练的时候,我们的初始学习率相比float小很多, 训练的epoch次数也大大减少,最重要的是 model 定义的时候,我们的 pretrained 需要设置成已经训练出来的纯浮点模型的地址。

做完这些简单的调整之后,就可以开始训练我们的量化模型了。