10.4.13. GaNet车道线检测模型训练

这篇教程主要是告诉大家如何利用HAT在车道线数据集 CuLane 上从头开始训练一个 GaNet 模型,包括浮点、量化和定点模型。

CuLane 是车道线检测中用的比较多的数据集,很多先进的车道线检测研究都会优先基于这个数据集做好验证。 开始训练模型之前,第一步是准备好数据集,这里我们下载官方的数据集以及相应的标签数据 CuLaneDataset , 需要注意的是, annotations_new.tar.gz 这个文件必须要最后解压。 解压缩之后数据目录结构如下所示:

tmp_data
  |-- driver_100_30frame
      |-- xxx.MP4
          |-- xxx.lines.txt
          |-- xxx.jpg
  |-- driver_161_90frame
  |-- driver_182_30frame
  |-- driver_193_90frame
  |-- driver_23_30frame
  |-- driver_37_30frame
  |-- list
      |-- test_split
          |-- test0_normal.txt
          |-- test1_crowd.txt
          |-- test2_hlight.txt
          |-- test3_shadow.txt
          |-- test4_noline.txt
          |-- test5_arrow.txt
          |-- test6_curve.txt
          |-- test7_cross.txt
          |-- test8_night.txt
      |-- test.txt
      |-- train_gt.txt
      |-- train.txt
      |-- val_gt.txt
      |-- val.txt

其中 list/train.txt 里面是训练数据的路径, list/test.txt 里面是测试数据的路径。

10.4.13.1. 训练流程

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

10.4.13.1.1. 数据集准备

为了提升训练速度,我们对原始的数据集做了一个打包,将其转换为 LMDB 格式的数据集。只需要运行下面的脚本, 就可以成功实现转换:

python3 tools/datasets/culane_packer.py --src-data-dir ${data-dir} --split-name train --pack-type lmdb  --num-workers 10 --target-data-dir ${target-data-dir}
python3 tools/datasets/culane_packer.py --src-data-dir ${data-dir} --split-name test --pack-type lmdb  --num-workers 10 --target-data-dir ${target-data-dir}

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

${target-data-dir}
  |-- train_lmdb
  |-- test_lmdb

train_lmdbtest_lmdb 就是打包之后的训练数据集和验证数据集,接下来就可以开始训练模型。

10.4.13.1.2. 模型训练

在网络开始训练之前,你可以使用以下命令先计算一下网络的计算量和参数数量:

python3 tools/calops.py --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

下一步就可以开始训练。训练也可以通过下面的脚本来完成, 在训练之前需要确认配置中数据集路径是否已经切换到已经打包好的数据集路径。

python3 tools/train.py --stage "float" --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py
python3 tools/train.py --stage "calibration" --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py
python3 tools/train.py --stage "int_infer" --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

由于HAT算法包使用了注册机制,使得每一个训练任务都可以按照这种 train.py 加上 config 配置文件的形式启动。 train.py 是统一的训练脚本,与任务无关,我们需要训练什么样的任务、使用什么样的数据集以及训练相关的超参数设置都在指定的 config 配置文件里面。 上面的命令中 --stage 后面的参数可以是 "float""calibration""int_infer", 分别可以完成浮点模型、量化模型的训练以及量化模型到定点模型的转化, 其中量化模型的训练依赖于上一步浮点训练产出的浮点模型,定点模型的转化依赖于量化训练产生的量化模型。

10.4.13.1.3. 模型验证

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

python3 tools/predict.py --stage "float" --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

python3 tools/predict.py --stage "calibration" --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

python3 tools/predict.py --stage "int_infer" --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

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

10.4.13.1.4. 模型推理

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

python3 tools/infer.py --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py --model-inputs img:${img-path} --save-path ${save_path}

10.4.13.1.5. 仿真上板精度验证

除了上述模型验证之外,我们还提供和上板完全一致的精度验证方法,可以通过下面的方式完成:

python3 tools/align_bpu_validation.py --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

10.4.13.1.6. 定点模型检查和编译

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

python3 tools/model_checker.py --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

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

python3 tools/compile_perf.py --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

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

10.4.13.1.7. ONNX模型导出

如果想要导出onnx模型, 运行下面的命令即可:

python3 tools/export_onnx.py --config configs/lane_pred/ganet/ganet_mixvargenet_culane.py

10.4.13.2. 训练细节

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

10.4.13.2.1. 模型构建

GaNet 的网络结构可以参考 论文 ,这里不做详细介绍。 我们通过在 config 配置文件中定义 model 这样的一个 dict 型变量,就可以方便的实现对模型的定义和修改。

from hat.models.backbones.mixvargenet import MixVarGENetConfig
bn_kwargs = {}
radius = 2
hid_dim = 32
attn_ratio = 4

model = dict(
    type="GaNet",
    backbone=dict(
            type='MixVarGENet',
            net_config=[
                [MixVarGENetConfig(in_channels=32, out_channels=32, head_op='mixvarge_f2', stack_ops=[], stride=1, stack_factor=1, fusion_strides=[], extra_downsample_num=0)],
                [MixVarGENetConfig(in_channels=32, out_channels=32, head_op='mixvarge_f4', stack_ops=['mixvarge_f4', 'mixvarge_f4'], stride=2, stack_factor=1, fusion_strides=[], extra_downsample_num=0)],
                [MixVarGENetConfig(in_channels=32, out_channels=64, head_op='mixvarge_f4', stack_ops=['mixvarge_f4', 'mixvarge_f4'], stride=2, stack_factor=1, fusion_strides=[], extra_downsample_num=0)],
                [MixVarGENetConfig(in_channels=64, out_channels=96, head_op='mixvarge_f2_gb16', stack_ops=['mixvarge_f2_gb16', 'mixvarge_f2_gb16', 'mixvarge_f2_gb16', 'mixvarge_f2_gb16', 'mixvarge_f2_gb16', 'mixvarge_f2_gb16'], stride=2, stack_factor=1, fusion_strides=[], extra_downsample_num=0)],
                [MixVarGENetConfig(in_channels=96, out_channels=160, head_op='mixvarge_f2_gb16', stack_ops=['mixvarge_f2_gb16', 'mixvarge_f2_gb16'], stride=2, stack_factor=1, fusion_strides=[], extra_downsample_num=0)]
            ],
            disable_quanti_input =False,
            input_channels = 3,
            input_sequence_length = 1,
            num_classes = 1000,
            bn_kwargs = bn_kwargs,
            include_top = False,
            bias = True,
            output_list =[2, 3, 4],
    ),
    neck = dict(
        type="GaNetNeck",
        fpn_module = dict(
            type="FPN",
            in_strides=[8, 16, 32],
            in_channels=[64, 96, hid_dim],
            out_strides=[8, 16, 32],
            out_channels=[hid_dim, hid_dim, hid_dim],
        ),
        attn_in_channels=[160],
        attn_out_channels=[hid_dim],
        attn_ratios=[attn_ratio],
        pos_shape=(1, 10, 25)
    ),
    head=dict(
        type="GaNetHead",
        in_channel=hid_dim,
    ),
    targets=dict(
        type="GaNetTarget",
        hm_down_scale=8,
        radius=radius,
    ),
    post_process=dict(
        type="GaNetDecoder",
        root_thr=1,
        kpt_thr=0.4,
        cluster_thr=5,
        downscale=8,
    ),
    losses=dict(
        type="GaNetLoss",
        loss_kpts_cls=dict(
            type="LaneFastFocalLoss",
            loss_weight=1.0,
        ),
        loss_pts_offset_reg=dict(
            type="L1Loss",
            loss_weight=0.5,
        ),
        loss_int_offset_reg=dict(
            type="L1Loss",
            loss_weight=1.0,
        ),
    ),
)

模型除了 backbone 之外,还有 neckheadtargetspost_processlosses 模块, 在 GaNet 中, backbone 主要是提取图像的特征, neck 主要是特征增强, head 主要是由特征来得到预测的车道线关键点的分数和偏移。 targets 是训练时从 gt 中得到训练的target, post_process 主要是后处理部分,推理的时候使用。 losses 部分采用 论文中的 LaneFastFocalLossL1Loss 来作为训练的 loss, loss_weight 是对应的 loss 的权重。

10.4.13.2.2. 数据增强

model 的定义一样,数据增强的流程是通过在 config 配置文件中定义 train_data_loaderval_data_loader 这两个 dict 来实现的, 分别对应着训练集和验证集的处理流程。以 train_data_loader 为例, 数据增强使用了 FixedCropRandomFlipResizeRandomSelectOneRGBShiftHueSaturationValueJPEGCompressMeanBlurMedianBlurRandomBrightnessContrastShiftScaleRotateRandomResizedCrop 来增加训练数据的多样性,增强模型的泛化能力。

train_data_loader = dict(
    type=torch.utils.data.DataLoader,
    dataset=dict(
        type="CuLaneDataset",
        data_path=train_data_path,
        to_rgb=True,
        transforms=[
            dict(
                type="FixedCrop",
                size=(0, 270, 1640, 320),
            ),
            dict(
                type="RandomFlip",
                px=0.5,
                py=0.0,
            ),
            dict(
                type="Resize",
                img_scale=(320, 800),
                multiscale_mode='value',
                keep_ratio=False,
            ),
            dict(
                type='RandomSelectOne',
                transforms=[
                    dict(
                        type="RGBShift",
                        r_shift_limit=(-10, 10),
                        g_shift_limit=(-10, 10),
                        b_shift_limit=(-10, 10),
                        p=1.0,
                    ),
                    dict(
                        type="HueSaturationValue",
                        hue_range=(-10, 10),
                        sat_range=(-15, 15),
                        val_range=(-10, 10),
                        p=1.0,
                    ),
                ],
                p=0.7,
            ),
            dict(
                type="JPEGCompress",
                p = 0.2,
                max_quality = 85,
                min_quality = 95,
            ),
            dict(
                type='RandomSelectOne',
                transforms=[
                    dict(
                        type="MeanBlur",
                        ksize=3,
                        p=1.0,
                    ),
                    dict(
                        type="MedianBlur",
                        ksize=3,
                        p=1.0,
                    ),
                ],
                p=0.2,
            ),
            dict(
                type="RandomBrightnessContrast",
                brightness_limit=(-0.2, 0.2),
                contrast_limit=(-0.0, 0.0),
                p=0.5,
            ),
            dict(
                type="ShiftScaleRotate",
                shift_limit=(-0.1, 0.1),
                scale_limit=(0.8, 1.2),
                rotate_limit=(-10, 10),
                interpolation=1,
                border_mode=0,
                p=0.6,
            ),

            dict(
                type='RandomResizedCrop',
                height=320,
                width=800,
                scale=(0.8, 1.2),
                ratio=(1.7, 2.7),
                p=0.6,
            ),
            dict(
                type="ToTensor",
                to_yuv=False,
            ),
        ],
    ),
    sampler=dict(type=torch.utils.data.DistributedSampler),
    batch_size=batch_size_per_gpu,
    pin_memory=True,
    shuffle=True,
    num_workers=data_num_workers,
    collate_fn=collate_2d,
)

因为最终跑在 BPU 上的模型使用的是 YUV444 的图像输入,而一般的训练图像输入都采用 RGB 的形式, 所以HAT提供 BgrToYuv444 的数据增强来将 RGB 转到 YUV444 的格式。 为了优化训练过程,HAT使用了 batch_processor ,可将一些增强处理放在 batch_processor 中优化训练:

def loss_collector(outputs: dict):
    losses = []
    for _, loss in outputs.items():
        losses.append(loss)
    return losses

train_batch_processor = dict(
    type="BasicBatchProcessor",
    need_grad_update=True,
    loss_collector=loss_collector,
    batch_transforms=[
        dict(type="BgrToYuv444", rgb_input=True),
        dict(
            type="TorchVisionAdapter",
            interface="Normalize",
            mean=128.0,
            std=128.0,
        ),
    ],
)

其中 loss_collector 是一个获取当前批量数据的 loss 的函数。

验证集的数据转换相对简单很多,如下所示:

val_data_loader = dict(
    type=torch.utils.data.DataLoader,
    dataset=dict(
        type="CuLaneDataset",
        data_path=val_data_path,
        to_rgb=True,
        transforms=[
            dict(
                type="FixedCrop",
                size=(0, 270, 1640, 320),
            ),
            dict(
                type="Resize",
                img_scale=(320, 800),
                multiscale_mode='value',
                keep_ratio=False,
            ),
            dict(
                type="ToTensor",
                to_yuv=False,
            ),
        ],
    ),
    sampler=dict(type=torch.utils.data.DistributedSampler),
    batch_size=batch_size_per_gpu,
    pin_memory=True,
    shuffle=False,
    num_workers=data_num_workers,
    collate_fn=collate_2d,
)
val_batch_processor = dict(
    type="BasicBatchProcessor",
    need_grad_update=False,
    loss_collector=None,
    batch_transforms=[
        dict(type="BgrToYuv444", rgb_input=True),
        dict(
            type="TorchVisionAdapter",
            interface="Normalize",
            mean=128.0,
            std=128.0,
        ),
    ],
)

10.4.13.2.3. 训练策略

CuLane 数据集上训练浮点模型使用 Cosine 的学习策略配合 Warmup,以及对 weight 的参数施加L2 norm。 configs/lane_pred/ganet/ganet_mixvargenet_culane.py 文件中的 float_trainercalibration_trainerint_trainer 分别对应浮点、量化、定点模型的训练策略。 下面为 float_trainer 训练策略示例:

import torch
base_lr = 0.01
num_epochs = 240

float_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=model,
    data_loader=train_data_loader,
    optimizer=dict(
        type=torch.optim.Adam,
        params={"weight": dict(weight_decay=4e-5)},
        lr=base_lr,
    ),
    batch_processor=train_batch_processor,
    stop_by="epoch",
    num_epochs=num_epochs,
    device=None,
    sync_bn=True,
    callbacks=[
        stat_callback,
        loss_show_update,
        dict(
            type="CosLrUpdater",
            warmup_len=1,
            warmup_by="epoch",
            step_log_interval=10,
        ),
        val_callback,
        ckpt_callback,
    ],
    train_metrics=[
        dict(type="LossShow"),
    ],
    val_metrics=[
        dict(type="CulaneF1Score"),
    ],
)

10.4.13.2.4. 量化训练

关于量化训练中的关键步骤,比如准备浮点模型、算子替换、插入量化和反量化节点、设置量化参数以及算子的融合等,请阅读 量化感知训练 章节的内容。 这里主要讲一下 HAT 的车道线检测中如何定义和使用量化模型。

在模型准备的好情况下,包括量化已有的一些模块完成之后,HAT在训练脚本中统一使用下面的脚本将浮点模型映射到定点模型上来。

model.fuse_model()
model.set_qconfig()
horizon.quantization.prepare_qat(model, inplace=True)

量化训练的整体策略可以直接沿用浮点训练的策略,但学习率和训练长度需要适当调整。 因为有浮点预训练模型,所以量化训练的学习率 Lr 可以很小, 一般可以从 0.001 或 0.0001 开始,并可以搭配 StepLrUpdater 做1-2次 scale=0.1Lr 调整; 同时训练的长度不用很长。此外 weight decay 也会对训练结果有一定影响。

GaNet 示例模型的量化训练策略可见 configs/lane_pred/ganet/ganet_mixvargenet_culane.py 文件。

10.4.13.2.5. 模型检查编译和仿真上板精度验证

对于HAT来说,量化模型的意义在于可以在 BPU 上直接运行。 因此,对于量化模型的检查和编译是必须的。前文提到的 compile_perf 脚本也可以让用户定义好量化模型之后,先检查能否在 BPU 上正常运行, 并可通过 align_bpu_validation 脚本获取模型上板精度。用法同前文。