目录
1、概述
2、什么是POT工具
2.1 POT两种量化算法
2.2 POT两种调用方式
3、基于POT API对YOLOv5模型进行量化
3.2 创建YOLOv5DataLoader Class
3.3 创建COCOMetric Class
3.5 定义并运行量化流水线
3.6 YOLOv5m FP32和INT8模型精度比较
4、小结
5、参考文献
作者:孙霞克 英特尔AI框架软件工程师
Ultralytics YOLOv5作为最流行的目标检测网络之一,因为其良好的工程化和文档支持,深受广大AI开发者的喜爱,也广泛地应用于工业界实践中。我们在之前的文章《基于OpenVINOTM 2022.1实现YOLOv5推理程序》及 《使用OpenVINOTM预处理API进一步提升YOLOv5推理性能》中详细介绍了:
在此基础上,本文将重点介绍如何使用OpenVINOTM 2022.1 Post-training Optimization Tool(POT)API对YOLOv5 OpenVINO FP32模型进行INT8量化,实现模型文件压缩,从而进一步提高模型推理性能。此外,我们提供了FP32和INT8模型精度计算方法,介绍了OpenVINO 性能测试工具Benchmark App的使用方法,并展示了基于OpenVINO backend的YOLOv5 INT8模型目标检测demo。
本文完整代码请参考OpenVINO notebook: 220-yolov5-accuracy-check-and-quantization。
POT通过在已训练好的模型上应用量化算法,将模型的权重和激活函数从FP32/FP16的值域映射到INT8的值域中,从而实现模型压缩,以降低模型推理所需的计算资源和内存带宽,进一步提高模型的推理性能。不同于Quantization-aware Training (QAT)方法, POT在不需要对原模型进行fine tuning的情况下进行量化,也能得到精度较好的INT8模型,因此广泛地被应用于工业界的量化实践中。
Fig.1展示了OpenVINO POT优化的主要流程, 我们可以看到使用POT工具需要以下几个要素:
Fig.1 OpenVINO POT优化的主要流程
POT提供了两种量化算法: Default Quantization和Accuracy-aware Quantization,其中
POT提供了以下两种调用方式:POT命令行方式和POT API方式,其中
因为YOLOv5模型的前后处理模块包括letterbox,Non-maximum Suppression(NMS)与OpenVINO Accuracy Checker Tool预定义的前后处理模块不完全一致,因此我们采用基于POT API调用方式,通过集成客制化DataLoader和Metric到量化流水线,来实现YOLOv5的模型INT8量化。
3.1 配置YOLOv5和OpenVINO开发环境
首先,下载YOLOv5源码,安装YOLOv5和OpenVINO的python依赖。
git clone https://github.com/ultralytics/yolov5.git -b v6.1
cd yolov5 && pip install -r requirements.txt && pip install \ openvino==2022.1.0 openvino-dev==2022.1.0
然后,通过YOLOv5提供的export.py将预训练的Pytorch模型转换为OpenVINO FP32 IR模型,Fig.3展示了模型转换的输出结果。
python export.py --weights yolov5m/yolov5m.pt --imgsz 640 \
--batch-size 1 --include openvino
Fig.3 YOLOv5 Pytorch模型转换为OpenVINO FP32 IR模型输出结果
接下来,我们按以下步骤来实现基于POT API的量化流水线:
首先,我们通过继承POT DataLoader基类来定义客制化的YOLOv5Dataloader子类。我们节选了部分代码如下,其中_init_dataloader(self)函数调用YOLOv5自定义的create_dataloader()函数读取数据集并通过letterbox改变输入图片尺寸。此外,每次调用函数__getitem__(self, item)会读取index为item的输入图片和对应的annotation,并对图片做归一化处理,最后返回item,annotation和预处理后的图片。
class YOLOv5DataLoader(DataLoader):
""" Inherit from DataLoader function and implement for YOLOv5.
"""
def _init_dataloader(self):
dataloader = create_dataloader(self._data_source['val'],
imgsz=self._imgsz, batch_size=self._batch_size,
stride=self._stride, single_cls=self._single_cls,
pad=self._pad, rect=self._rect, workers=self._workers)[0]
return dataloader
def __getitem__(self, item):
try:
batch_data = next(self._data_iter)
except StopIteration:
self._data_iter = iter(self._data_loader)
batch_data = next(self._data_iter)
im, target, path, shape = batch_data
im = im.float()
im /= 255
nb, _, height, width = im.shape
img = im.cpu().detach().numpy()
target = target.cpu().detach().numpy()
annotation = dict()
annotation['image_path'] = path
annotation['target'] = target
annotation['batch_size'] = nb
annotation['shape'] = shape
annotation['width'] = width
annotation['height'] = height
annotation['img'] = img
return (item, annotation), img
接下来我们通过继承POT Metric基类创建COCOMetric子类, 该类集成了YOLOv5原生的后处理NMS函数和计算精度的方法,可以用来计算基于COCO数据集Mean Average Precision(mAP)精度,包括[email protected]和[email protected]:0.95。其中update(self, output, target)函数有output和target两个输入,分别是模型推理结果的原始输出和输入图片对应的annotation。模型原始输出经过YOLOv5 NMS后处理后,会与annotation一起计算得到该输入图片的目标检测精度统计数据。最后,_process_stats(self, stats)以所有图片的精度统计数据stats为输入,计算得到包括[email protected]和[email protected]:0.95的模型精度。
class COCOMetric(Metric):
""" Inherit from DataLoader function and implement for YOLOv5.
"""
def _process_stats(self, stats):
mp, mr, map50, map = 0.0, 0.0, 0.0, 0.0
stats = [np.concatenate(x, 0) for x in zip(*stats)]
if len(stats) and stats[0].any():
tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=False, save_dir=None, names=self._class_names)
ap50, ap = ap[:, 0], ap.mean(1)
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
np.bincount(stats[3].astype(np.int64), minlength=self._nc)
else:
torch.zeros(1)
return mp, mr, map50, map
def update(self, output, target):
""" Calculates and updates metric value
Contains postprocessing part from Ultralytics YOLOv5 project
:param output: model output
:param target: annotations
"""
annotation = target[0]["target"]
width = target[0]["width"]
height = target[0]["height"]
shapes = target[0]["shape"]
paths = target[0]["image_path"]
im = target[0]["img"]
iouv = torch.linspace(0.5, 0.95, 10).to(self._device) # iou vector for [email protected]:0.95
niou = iouv.numel()
seen = 0
stats = []
# NMS
annotation = torch.Tensor(annotation)
annotation[:, 2:] *= torch.Tensor([width, height, width, height]).to(self._device) # to pixels
lb = []
out = output[0]
out = torch.Tensor(out).to(self._device)
out = non_max_suppression(out, self._conf_thres, self._iou_thres, labels=lb,
multi_label=True, agnostic=self._single_cls)
# Metrics
for si, pred in enumerate(out):
labels = annotation[annotation[:, 0] == si, 1:]
nl = len(labels)
tcls = labels[:, 0].tolist() if nl else [] # target class
_, shape = Path(paths[si]), shapes[si][0]
seen += 1
if len(pred) == 0:
if nl:
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
continue
# Predictions
if self._single_cls:
pred[:, 5] = 0
predn = pred.clone()
scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
# Evaluate
if nl:
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
correct = process_batch(predn, labelsn, iouv)
else:
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
self._stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
self._last_stats = stats
3.4 设置POT的量化参数
创建好YOLOv5DataLoader和COCOMetric类之后,我们可以用get_config()设置POT量化流水线中model,engine,dataset,metric和algorithms的参数。以下我们节选了algorithms部分的config,这里我们选择“DefaultQuantization”量化算法来快速获得最佳性能的模型。此外,针对采用non-ReLU activation的YOLOv5模型,我们通过设置"preset": "mixed"来对模型weights进行对称量化,并对模型activation进行非对称量化,从而更好地保证量化后模型的精度。
def get_config():
""" Set the configuration of the model, engine,
dataset, metric and quantization algorithm.
"""
algorithms = [
{
"name": "DefaultQuantization", # or AccuracyAwareQuantization
"params": {
"target_device": "CPU",
"preset": "mixed",
"stat_subset_size": 300
}
}
]
config["algorithms"] = algorithms
return config
接下来,我们以下面的代码逐步演示如何定义和运行量化流水线,其中包括模型加载、量化所需的DataLoader,Metric,Engine和Pipeline初始化、FP32模型精度校验、INT8模型量化、INT8模型精度校验等步骤。最终,量化生成的OpenVINO INT8 IR模型会被保存在本地。
""" Download dataset and set config
"""
config = get_config()
init_logger(level='INFO')
logger = get_logger(__name__)
save_dir = increment_path(Path("./yolov5/yolov5m/yolov5m_openvino_model/"), exist_ok=True) # increment run
save_dir.mkdir(parents=True, exist_ok=True) # make dir
# Step 1: Load the model.
model = load_model(config["model"])
# Step 2: Initialize the data loader.
data_loader = YOLOv5DataLoader(config["dataset"])
# Step 3 (Optional. Required for AccuracyAwareQuantization): Initialize the metric.
metric = COCOMetric(config["metric"])
# Step 4: Initialize the engine for metric calculation and statistics collection.
engine = IEEngine(config=config["engine"], data_loader=data_loader, metric=metric)
# Step 5: Create a pipeline of compression algorithms.
pipeline = create_pipeline(config["algorithms"], engine)
metric_results = None
# Check the FP32 model accuracy.
metric_results_fp32 = pipeline.evaluate(model)
logger.info("FP32 model metric_results: {}".format(metric_results_fp32))
# Step 6: Execute the pipeline to calculate Min-Max value
compressed_model = pipeline.run(model)
# Step 7 (Optional): Compress model weights to quantized precision
# in order to reduce the size of final .bin file.
compress_model_weights(compressed_model)
# Step 8: Save the compressed model to the desired path.
optimized_save_dir = Path(save_dir).joinpath("optimized")
save_model(compressed_model, Path(Path.cwd()).joinpath(optimized_save_dir), config["model"]["model_name"])
# Step 9 (Optional): Evaluate the compressed model. Print the results.
metric_results_i8 = pipeline.evaluate(compressed_model)
logger.info("Save quantized model in {}".format(optimized_save_dir))
logger.info("Quantized INT8 model metric_results: {}".format(metric_results_i8))
Fig.4展示了YOLOv5m FP32及INT8模型精度的比较,我们可以从图中看到,相对FP32模型,通过“DefaultQuantization”算法量化后的INT8模型的精度下降控制在1%以内,对于目标识别的网络,这种程度的精度下降一般是可以接受的,如果用户对INT8模型有更高的精度要求,建议可以尝试采用“AccuracyAwareQuantization”的算法对INT8模型做进一步迭代优化。
Fig.4 YOLOv5m FP32及INT8模型精度[email protected], [email protected]:0.95比较
3.7 OpenVINO性能测试工具Benchmark App介绍
OpenVINO提供了性能测试工具Benchmark App,方便开发者快速测试OpenVINO模型在不同的硬件平台上的性能。我们以下面的例子,简单介绍benchmark app的使用方法和相关参数,更多内容请参考Benchmark App官方文档。
benchmark_app -m \
./yolov5/yolov5m/yolov5m_openvino_model/optimized/yolov5m.xml \
-i ./yolov5/data/images/bus.jpg -d CPU -hint throughput
benchmark_app提供了Python和C++的两种版本,我们使用通过openvino-dev安装的Python版本的benchmark_app来进行性能测试,涉及到的参数包括:
开发者可以参考上述例子,针对自己的硬件平台和使用场景,选择合适的性能测试参数来对YOLOv5 FP32及INT8模型进行性能测试。
3.8 YOLOv5 INT8模型推理demo
最后,我们用以下命令行,运行基于OpenVINO backend的YOLOv5m INT8模型推理demo。
cd yolov5 && python detect.py \
--weights ./yolov5m/yolov5m_openvino_model/optimized/yolov5m.xml
Fig.5展示了推理的输入图片及INT8模型的推理结果,我们可以看到,INT8模型以较高置信度检测到了图片中的所有车和行人bounding box和label。
Fig.5 基于OpenVINO backend的推理demo的输入图片(左图)及目标检测结果(右图)
本文基于Ultralytics YOLOv5源码,将预训练的YOLOv5m Pytorch模型转换为OpenVINO FP32 Intermediate Representation (IR)模型。下一步,通过OpenVINO Post-Training Optimization Tool (POT) API来定义客制化DataLoader和Metric,从而复用YOLOv5客制化的前后处理(letterbox,Non-maximum Suppression)及精度计算等模块。采用“DefaultQuantization”的量化算法,定义和运行量化流水线对FP32模型进行INT8量化。此外,通过与FP32模型精度比较,我们发现采用“DefaultQuantization”算法量化的INT8模型已经具有较好的精度([email protected], [email protected]:0.95精度下降都小于1%)。然后,我们介绍了OpenVINO性能测试工具Benchmark App的使用方法。最后,我们通过基于OpenVINO backend的demo演示了YOLOv5m INT8模型推理的效果。