#Lidar融合sparse-bev感知模型
这篇教程主要是告诉大家如何利用HAT在自动驾驶数据集 Nuscenes 上训练一个Lidar融合感知模型,包括浮点、量化和定点模型。
下文以配置 configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py 为例介绍如何配置并训练Lidar融合多任务感知模型。
bev_sparse_lidar_fusion_henet_tinym_nuscenes 是一个多模态自动驾驶感知模型,接受两种 camera 和 lidar 两种模态的输入,输出动态要素3d检测框。
#训练流程
如果你只是想简单的把 bev_sparse_lidar_fusion_henet_tinym_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_lmdbtrain_lmdb和val_lmdb就是打包之后的训练数据集和验证数据集,也是网络最终读取的数据集。
#模型训练
数据集准备好之后,就可以开始训练浮点型的lidar融合多任务感知模型 bev_sparse_lidar_fusion_henet_tinym_nuscenes 了。
首先可以使用以下命令评估模型的计算量:
python3 tools/calops.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py如果你想要复现整个模型浮点和定点训练流程,你需要:
python3 tools/train.py --stage "float" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/train.py --stage "calibration" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/train.py --stage "qat" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py以上命令分别完成camera输入的浮点模型预训练,浮点模型训练和定点模型的训练,其中定点模型的训练需要以训练好的浮点模型为基础,具体内容请阅读 量化感知训练(QAT) 章节的内容。
#导出定点模型
完成量化训练后,便可以开始导出定点模型。可以通过下面命令来导出:
python3 tools/export_hbir.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py#模型验证
在完成训练之后,可以得到训练完成的浮点、量化或定点模型。和训练方法类似,我们可以用相同方法来对训好的模型做指标验证,得到为 Float , Calibration 和 Qat 的指标,分别为浮点和量化的指标。
python3 tools/predict.py --stage "float" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/predict.py --stage "calibration" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/predict.py --stage "qat" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py和训练模型时类似,--stage 后面的参数为 float、 calibration 和 qat 时,分别可以完成对训练好的浮点模型、校准模型和量化模型的验证。
定点模型精度验证也可使用下面命令,但需要注意是必须要先导出hbir:
python3 tools/predict.py --stage "int_infer" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py#模型推理和结果可视化
HAT 提供了 infer_hbir.py 脚本提供了对定点模型的推理结果进行可视化展示,以下命令会从数据集获取数据保存在 "./demo" 文件夹,然后可视化在 ${save_path} 文件夹。
python3 tools/infer_hbir.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py --save-path ${save_path} --use-dataset#定点模型检查和编译
在HAT中集成的量化训练工具链主要是为了地平线的计算平台准备的,因此,对于量化模型的检查和编译是必须的。
我们在HAT中提供了模型检查的接口,可以在定义好量化模型之后,先检查能否在 BPU 上正常运行:
python3 tools/model_checker.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py在模型训练完成后,可以通过 compile_perf_hbir 脚本将量化模型编译成可以上板运行的 hbm 文件,同时该工具也能预估在 BPU 上的运行性能:
python3 tools/compile_perf_hbir.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py以上就是从数据准备到生成量化可部署模型的全过程。
#训练细节
在这个说明中,我们对模型训练需要注意的一些事项进行说明,主要为 config 的一些相关设置。
#模型构建
模型定义相关的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={},
quantize=True,
use_4dim=True,
use_conv=True,
hw_reverse=True,
),
scatter=dict(
type="PointPillarScatter",
num_input_features=64,
use_horizon_pillar_scatter=True,
quantize=True,
),
backbone=dict(
type="HENet",
in_channels=64,
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="MMFPN",
in_strides=[2, 4, 8, 16, 32],
in_channels=[64, 64, 128, 192, 384],
fix_out_channel=256,
out_strides=[4, 8, 16, 32],
),
)
model = dict(
type="SparseBevFusionOE",
compiler_model=False,
lidar_net=lidar_network,
lidar_level_idx=[1, 2],
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="MMFPN",
in_strides=[2, 4, 8, 16, 32],
in_channels=[64, 64, 128, 192, 384],
fix_out_channel=256,
out_strides=[4, 8, 16, 32],
),
depth_branch=dict( # for auxiliary supervision only
type="DenseDepthNetOE",
embed_dims=embed_dims,
num_depth_layers=num_depth_layers,
loss_weight=0.2,
),
head=dict(
type="SparseBEVOEHead",
enable_dn=True,
level_index=[2],
cls_threshold_to_reg=0.05,
instance_bank=dict(
type="MemoryBankOE",
num_anchor=384,
embed_dims=embed_dims,
num_memory_instances=384,
anchor=anchor_file,
num_temp_instances=128,
confidence_decay=0.6,
),
anchor_encoder=dict(
type="SparseBEVOEEncoder",
pos_embed_dims=128,
size_embed_dims=32,
yaw_embed_dims=32,
vel_embed_dims=64,
vel_dims=3,
),
num_single_frame_decoder=num_single_frame_decoder,
operation_order=[
"lidar_deformable",
"ffn",
"norm",
"refine",
]
* num_single_frame_decoder
+ [
"temp_interaction",
"interaction",
"norm",
"lidar_deformable",
"ffn",
"norm",
"refine",
]
* (num_decoder - num_single_frame_decoder),
ffn=dict(
type="AsymmetricFFNOE",
in_channels=embed_dims * 2,
pre_norm=True,
embed_dims=embed_dims,
feedforward_channels=embed_dims * 4,
num_fcs=2,
ffn_drop=drop_out,
),
deformable_model=dict(
type="DeformableFeatureAggregationLiFOE",
fuse_module=dict(
type="InstanceFuseModule",
input_channels=[256, 256],
fuse_channel=256,
),
embed_dims=embed_dims,
num_groups=num_groups,
num_levels=num_levels,
num_cams=6,
attn_drop=0.15,
use_camera_embed=True,
residual_mode="cat",
point_cloud_range=point_cloud_range,
kps_generator=dict(
type="SparseBEVOEKeyPointsGenerator",
num_pts=8,
),
),
refine_layer=dict(
type="SparseBEVOERefinementModule",
embed_dims=embed_dims,
num_cls=num_classes,
refine_yaw=True,
),
target=dict(
type="SparseBEVOETarget",
num_dn_groups=5,
num_temp_dn_groups=3,
dn_noise_scale=[2.0] * 3 + [0.5] * 7,
max_dn_gt=32,
add_neg_dn=True,
cls_weight=2.0,
box_weight=0.25,
reg_weights=[2.0] * 3 + [0.5] * 3 + [0.0] * 4,
cls_wise_reg_weights={
CLASSES.index("traffic_cone"): [
2.0,
2.0,
2.0,
1.0,
1.0,
1.0,
0.0,
0.0,
1.0,
1.0,
],
},
),
cls_allow_reverse=[CLASSES.index("barrier")],
loss_cls=dict(
type="FocalLoss",
loss_name="cls",
num_classes=num_classes + 1,
gamma=2.0,
alpha=0.25,
loss_weight=2.0,
),
loss_reg=dict(type="L1Loss", loss_weight=0.25),
loss_cns=dict(type="CrossEntropyLoss", use_sigmoid=True),
loss_yns=dict(type="GaussianFocalLoss"),
decoder=dict(type="SparseBEVOEDecoder"),
reg_weights=[2.0] * 3 + [1.0] * 7,
),
)
其中,lidar_network 定义了处理lidar输入的encoder,model 给出了完整的模型定义。model 下面的 type 表示定义的模型名称,剩余的变量表示模型的其他组成部分。
这样定义模型的好处在于我们可以很方便的替换我们想要的结构。
#数据增强
跟 model 的定义一样,数据增强的流程是通过在config配置文件中定义 data_loader 和 val_data_loader 这两个dict来实现的,分别对应着训练集和验证集的处理流程。
例如训练集的定义为:
train_dataset = dict(
type="NuscenesBevDataset",
data_path=os.path.join(data_rootdir, "train_lmdb"),
transforms=[
dict(type="MultiViewsImgResize", scales=(0.40, 0.47)),
dict(type="MultiViewsImgCrop", size=(256, 704), random=False),
dict(type="MultiViewsImgFlip"),
dict(type="MultiViewsImgRotate", rot=(-5.4, 5.4)),
dict(type="BevBBoxRotation", rotation_3d_range=(-0.3925, 0.3925)),
dict(type="MultiViewsPhotoMetricDistortion", use_pil=False),
dict(
type="MultiViewsGridMask",
use_h=True,
use_w=True,
rotate=1,
offset=False,
ratio=0.5,
mode=1,
prob=0.7,
),
dict(
type="MultiViewsImgTransformWrapper",
transforms=[
dict(type="PILToTensor"),
dict(type="BgrToYuv444", rgb_input=True),
dict(type="Normalize", mean=128, std=128),
],
),
],
with_bev_bboxes=False,
with_ego_bboxes=False,
with_bev_mask=False,
with_lidar_bboxes=True,
need_lidar=True,
num_split=2,
num_sweeps=9,
load_dim=5,
use_dim=[0, 1, 2, 3, 4],
)#训练策略
为了训练一个精度高的模型,好的训练策略是必不可少的。对于每一个训练任务而言,相应的训练策略同样都定义在其中的config文件中,从 float_trainer 这个变量就可以看出来。
雷达融合sparse模型的 float_trainer 的配置如下, 我们加载了单camera输入的 sparse bev 模型作为预训练。
float_trainer = dict(
type="distributed_data_parallel_trainer",
model=model,
model_convert_pipeline=dict(
type="ModelConvertPipeline",
qat_mode="fuse_bn",
converters=[
dict(
type="LoadCheckpoint",
checkpoint_path=os.path.join(
pretrain_ckpt_dir, "float-checkpoint-best.pth.tar"
),
allow_miss=True,
ignore_extra=True,
ignore_tensor_shape=True,
verbose=True,
),
dict(
type="LoadCheckpoint",
checkpoint_path=os.path.join(
pretrain_ckpt_dir, "float-checkpoint-best.pth.tar"
),
state_dict_update_func=partial(
update_state_dict_by_add_prefix, prefix="lidar_net."
),
allow_miss=True,
ignore_extra=True,
verbose=True,
ignore_tensor_shape=True,
),
],
),
data_loader=data_loader,
optimizer=dict(
type=torch.optim.AdamW,
params={
"backbone": dict(lr=3e-4),
},
eps=1e-8,
betas=(0.9, 0.999),
lr=3e-4,
weight_decay=0.001,
),
batch_processor=batch_processor,
num_steps=num_steps,
stop_by="step",
# num_epochs=100,
device=None,
callbacks=[
stat_callback,
loss_show_update,
# dict(type="ExponentialMovingAverage"),
grad_callback,
dict(
type="CosineAnnealingLrUpdater",
warmup_len=500,
warmup_by="step",
warmup_lr_ratio=1.0 / 3,
step_log_interval=500,
update_by="step",
min_lr_ratio=1e-3,
),
val_callback,
ckpt_callback,
],
train_metrics=dict(
type="LossShow",
),
sync_bn=True,
val_metrics=[val_nuscenes_metric],
)
float_trainer 从大局上定义了我们的训练方式,包括使用多卡分布式训练(distributed_data_parallel_trainer),模型训练的epoch次数,以及优化器的选择。
同时 callbacks 中体现了模型在训练过程中使用到的小策略以及您想实现的操作,包括学习率的变换方式(CosineAnnealingLrUpdater),在训练过程中验证模型的指标(Validation),以及保存(Checkpoint)模型的操作。
当然,如果你有自己希望模型在训练过程中实现的操作,也可以按照这种dict的方式添加。
通过上面的介绍,你应该对config文件的功能有了一个比较清楚的认识。然后通过前面提到的训练脚本,就可以训练一个高精度的纯浮点的检测模型。当然训练一个好的检测模型不是我们最终的目的,它只是做为一个pretrain为我们后面训练定点模型服务的。
#量化模型训练
当我们有了纯浮点模型之后,就可以开始训练相应的定点模型了。和浮点训练的方式一样,我们只需要通过运行下面的脚本就可以得到伪量化模型了:
python3 tools/train.py --stage "calibration" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/train.py --stage "qat" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py可以看到,我们的配置文件没有改变,只改变了 stage 的类型, 其中calibration流程可以为QAT的量化训练提供一个更好的初始化参数。
我们使用模版配置了 Float2Calibration 和 Float2QAT 将模型分别转换为calibration和qat模型,具体的calibration 和 QAT 的config为:
int16_max_point_muls = [
f"head.layers.{layer}.point_mul" for layer in range(3, 39, 7)
]
int16_max_reciprocal_op = [
f"head.layers.{layer}.reciprocal_op" for layer in range(3, 39, 7)
]
int16_add_ops = [f"head.layers.{layer}.add2" for layer in range(6, 35, 7)]
float32_ops = [
f"head.layers.{layer}.cls_layers" for layer in range(6, 35, 7)
] + [f"head.layers.{layer}.quality_layers" for layer in range(6, 35, 7)]
def load_ckpt(ckpt):
if ckpt.startswith("http"):
import io
import requests
ckpt = requests.get(ckpt).content
ckpt = torch.load(io.BytesIO(ckpt), map_location="cpu")
else:
ckpt = torch.load(ckpt, map_location="cpu")
return ckpt
table_dir = "tmp_models/bev_sparse_lidar_fusion_henet_tinym_nuscenes/quant_analysis"
sensitive_setting_dict = {
"output_classification_L1_sensitive_ops.pt": 20,
"output_prediction_L1_sensitive_ops.pt": 20,
}
sensitivity_templates = []
for k, v in sensitive_setting_dict.items():
sensitive_table = load_ckpt(os.path.join(table_dir, k))
sensitivity_templates.append(
SensitivityTemplate(
sensitive_table,
topk_or_ratio=v,
)
)
int16_inputs = [
"head.anchor_encoder.pos_fc.0",
"head.anchor_encoder.size_fc.0",
"head.anchor_encoder.yaw_fc.0",
"head.anchor_encoder.vel_fc.0",
] + [f"head.layers.{i}.camera_encoder.0" for i in range(3, 39, 7)]
q_template = [ # 2. 模板,仅关注 dtype,按次序生效,因此模板的顺序很重要
# 2.1. 基础配置部分
ModuleNameTemplate({"": qint8}), # 全局 feat fp16
MatmulDtypeTemplate( # gemm single int8 input
input_dtypes=[qint8, qint8],
# prefix=[""], # 名字和 torch.nn.Module.named_module() 返回的一致
),
ConvDtypeTemplate( # gemm int8 input
input_dtype=qint8,
weight_dtype=qint8, # 成块的 weight 在这里配置
# prefix=[""], # 名字和 torch.nn.Module.named_module() 返回的一致
),
ModuleNameTemplate(
{
m: {"dtype": qint8, "threshold": 1.0} for m in ["backbone.quant"]
}, # quant int8,固定 scale 方式 1
# force=True, # 可以选择强制生效,不会被其他模板修改
),
ModuleNameTemplate(
{
m: {"dtype": qint16, "threshold": 1.1}
for m in int16_max_point_muls
}, # quant int8,固定 scale 方式 1
),
ModuleNameTemplate(
{
m: {"dtype": qint16, "threshold": 11}
for m in int16_max_reciprocal_op
}, # quant int8,固定 scale 方式 1
),
ModuleNameTemplate(
{
"head.fc_after": {"dtype": qint16, "threshold": 50},
"head.fc_before": {"dtype": qint16, "threshold": 5},
"head.mat_quant_stub": {"output": qint16},
"head.instance_bank.anchor_quant_stub": {
"dtype": qint16,
"threshold": 60,
},
"head.instance_bank.anchor_cat": {"output": qint16},
},
),
ModuleNameTemplate(
{m: {"input": qint16, "output": qint8} for m in int16_inputs},
),
*sensitivity_templates,
ModuleNameTemplate(
{m: None for m in float32_ops}, # quant int8,固定 scale 方式 1
freeze=True,
), # 全局 feat fp32
]
calibration_qconfig_setter = QconfigSetter(
reference_qconfig=get_qconfig( # 1. 主要用于获取 observer
observer=(observer_v2.MSEObserver)
# observer=(observer_v2.HistogramObserver)
),
templates=q_template,
enable_optimize=True, # False, True, 或与旧模板一致的兼容配置(改成 ENUM?)
save_dir=os.path.join(
ckpt_dir, "./qconfig_setting"
), # qconfig 保存路径,有默认值,用户可以改
custom_qconfig_mapping=None, # 是否提供兜底的配置方式?
)
qat_qconfig_setter = QconfigSetter(
reference_qconfig=get_qconfig( # 1. 主要用于获取 observer
observer=(observer_v2.MinMaxObserver), fix_scale=True
),
templates=q_template,
enable_optimize=True, # False, True, 或与旧模板一致的兼容配置(改成 ENUM?)
save_dir=os.path.join(
ckpt_dir, "./qconfig_setting"
), # qconfig 保存路径,有默认值,用户可以改
custom_qconfig_mapping=None, # 是否提供兜底的配置方式?
)
calibration_trainer = dict(
type="Calibrator",
model=model,
model_convert_pipeline=dict(
type="ModelConvertPipeline",
qat_mode="fuse_bn",
qconfig_params=dict(
activation_calibration_observer="mse",
),
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="Float2Calibration",
convert_mode=convert_mode,
example_data_loader=calibration_data_loader,
qconfig_setter=calibration_qconfig_setter,
),
],
),
data_loader=calibration_data_loader,
batch_processor=calibration_batch_processor,
num_steps=calibration_step,
device=None,
callbacks=[
ckpt_callback,
val_callback,
],
log_interval=calibration_step / 10,
val_metrics=[val_nuscenes_metric],
)
float2qat = dict(
type="Float2QAT",
convert_mode=convert_mode,
example_data_loader=calibration_data_loader,
state="train",
qconfig_setter=qat_qconfig_setter,
)
qat_model = copy.deepcopy(model)
qat_model["head"]["enable_dn"] = False
qat_trainer = dict(
type="distributed_data_parallel_trainer",
model=qat_model,
model_convert_pipeline=dict(
type="ModelConvertPipeline",
qat_mode="fuse_bn",
converters=[
float2qat,
dict(
type="LoadCheckpoint",
checkpoint_path=os.path.join(
ckpt_dir, "calibration-checkpoint-last.pth.tar"
),
allow_miss=True,
ignore_extra=True,
),
],
),
data_loader=data_loader,
optimizer=dict(
type=torch.optim.AdamW,
eps=1e-8,
betas=(0.9, 0.999),
params={
"backbone": dict(lr=5e-6),
},
lr=5e-6,
weight_decay=1e-3,
),
batch_processor=batch_processor,
num_steps=num_steps * 0.4,
stop_by="step",
device=None,
callbacks=[
stat_callback,
loss_show_update,
# bn_callback,
grad_callback,
dict(
type="StepDecayLrUpdater",
lr_decay_id=[int(num_steps * 0.4)],
step_log_interval=500,
),
val_callback,
ckpt_callback,
],
train_metrics=dict(
type="LossShow",
),
val_metrics=[val_nuscenes_metric],
)
#训练策略不同
正如我们之前所说,量化训练其实是在纯浮点训练基础上的finetue。因此量化训练的时候,我们的初始学习率相比float小很多,
训练的epoch次数也大大减少,最重要的是 model 定义的时候,我们的 pretrained 需要设置成已经训练出来的纯浮点模型的地址。
做完这些简单的调整之后,就可以开始训练我们的量化模型了。
关于量化训练中的关键步骤,比如准备浮点模型、算子替换、插入量化和反量化节点、设置量化参数以及算子的融合等,请阅读 量化感知训练(QAT) 章节的内容。
