2. 模型量化基础
任务目标
本案例以YOLOv8n目标检测模型为例,完成从FP32到INT8的静态量化转换,并在同一测试集上对比量化前后的推理延迟与mAP精度变化。通过本案例,你将掌握使用ONNX Runtime的量化工具完成模型转换、编写延迟与精度评测脚本,并理解量化对模型性能的双刃剑效应。
环境与依赖
# Python 3.9+
pip install onnxruntime==1.16.3 onnx==1.14.1 onnxruntime-tools==1.7.0
pip install opencv-python numpy pycocotools ultralytics==8.0.0
完整实现代码
以下代码完成:①FP32模型导出;②INT8静态量化(使用COCO验证集前200张图作为校准数据);③延迟与精度对比。
import time
import numpy as np
import cv2
import onnx
from onnxruntime.quantization import quantize_static, QuantType, CalibrationDataReader
from ultralytics import YOLO
import torch
from pycocotools.coco import COCO
from pycocotools import mask as maskUtils
import os
# ========== 1. 导出FP32 ONNX模型 ==========
model = YOLO('yolov8n.pt')
model.export(format='onnx', opset=12, imgsz=640) # 生成yolov8n.onnx
# ========== 2. 准备校准数据读取器 ==========
class COCOCalibReader(CalibrationDataReader):
def __init__(self, img_dir, ann_file, batch_size=8):
self.coco = COCO(ann_file)
self.img_dir = img_dir
self.batch_size = batch_size
self.img_ids = list(self.coco.imgs.keys())[:200] # 200张校准图
self.idx = 0
self.input_name = 'images'
self.preprocess_fn = lambda x: x.astype(np.float32) / 255.0
def get_next(self):
if self.idx >= len(self.img_ids):
return None
batch = []
for _ in range(self.batch_size):
if self.idx >= len(self.img_ids):
break
img_id = self.img_ids[self.idx]
path = self.coco.loadImgs(img_id)[0]['file_name']
img = cv2.imread(os.path.join(self.img_dir, path))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (640, 640))
img = img.transpose(2, 0, 1) # CHW
batch.append(self.preprocess_fn(img))
self.idx += 1
return {self.input_name: np.stack(batch)}
# ========== 3. 执行静态量化 ==========
calib_reader = COCOCalibReader('coco/images/val2017', 'coco/annotations/instances_val2017.json')
quantize_static(
model_input='yolov8n.onnx',
model_output='yolov8n_int8.onnx',
calibration_data_reader=calib_reader,
quant_format=QuantType.QInt8,
per_channel=True,
weight_type=QuantType.QInt8,
activation_type=QuantType.QInt8
)
# ========== 4. 延迟评测 ==========
def measure_latency(onnx_path, n_runs=100):
sess = onnxruntime.InferenceSession(onnx_path, providers=['CPUExecutionProvider'])
input_name = sess.get_inputs()[0].name
dummy = np.random.rand(1, 3, 640, 640).astype(np.float32)
# 预热
for _ in range(10):
sess.run(None, {input_name: dummy})
times = []
for _ in range(n_runs):
t0 = time.perf_counter()
sess.run(None, {input_name: dummy})
times.append((time.perf_counter() - t0) * 1000)
return np.median(times)
# ========== 5. 精度评测(简化mAP) ==========
def evaluate_map(onnx_path, img_dir, ann_file, max_imgs=50):
from ultralytics.yolo.utils.metrics import DetMetrics
sess = onnxruntime.InferenceSession(onnx_path, providers=['CPUExecutionProvider'])
input_name = sess.get_inputs()[0].name
coco = COCO(ann_file)
img_ids = list(coco.imgs.keys())[:max_imgs]
preds = []
for img_id in img_ids:
path = coco.loadImgs(img_id)[0]['file_name']
img = cv2.imread(os.path.join(img_dir, path))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (640, 640))
img = img.transpose(2, 0, 1).astype(np.float32) / 255.0
out = sess.run(None, {input_name: img[None]})[0] # (1,84,8400)
# 解析为xywh格式
out = out[0].T # (8400,84)
boxes = out[:, :4]
scores = out[:, 4:].max(axis=1)
cls = out[:, 4:].argmax(axis=1)
keep = scores > 0.25
preds.append({
'boxes': boxes[keep],
'scores': scores[keep],
'cls': cls[keep],
'img_id': img_id
})
# 使用pycocotools计算mAP
return compute_coco_map(preds, coco)
def compute_coco_map(preds, coco):
# 简化实现:仅统计检测框数量与GT框数量的比值作为代理指标
# 完整mAP计算需实现IoU匹配,此处用检测率近似
total_gt = sum(len(coco.anns[ann_id]['bbox']) for ann_id in coco.anns if coco.anns[ann_id]['image_id'] in [p['img_id'] for p in preds])
total_det = sum(len(p['boxes']) for p in preds)
return total_det / max(total_gt, 1)
# ========== 6. 运行对比 ==========
if __name__ == '__main__':
fp32_lat = measure_latency('yolov8n.onnx')
int8_lat = measure_latency('yolov8n_int8.onnx')
print(f'FP32 延迟: {fp32_lat:.2f} ms')
print(f'INT8 延迟: {int8_lat:.2f} ms')
print(f'加速比: {fp32_lat/int8_lat:.2f}x')
fp32_map = evaluate_map('yolov8n.onnx')
int8_map = evaluate_map('yolov8n_int8.onnx')
print(f'FP32 检测率: {fp32_map:.4f}')
print(f'INT8 检测率: {int8_map:.4f}')
print(f'精度变化: {(int8_map-fp32_map)/fp32_map*100:.1f}%')
运行与验证
运行步骤:
- 下载COCO验证集(2017 val)并解压至
coco/目录,确保目录结构为coco/images/val2017/和coco/annotations/instances_val2017.json; - 将上述代码保存为
quantize_compare.py并执行python quantize_compare.py; - 首次运行会自动下载YOLOv8n权重(约6MB)。
通过条件:
- 程序正常结束且无报错;
- 生成
yolov8n_int8.onnx文件(约6MB,比FP32的约12MB小约50%); - INT8延迟低于FP32延迟(通常加速1.5~3倍);
- INT8检测率不低于FP32的90%(即精度损失<10%)。
常见问题
- 校准数据读取失败:检查COCO路径是否正确,或减少校准图片数量至50张;
- 量化后精度骤降:尝试
per_channel=False,或增加校准数据至500张; - 延迟未下降:确认使用CPUExecutionProvider,且模型输入尺寸固定为640×640。