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}%')

运行与验证

运行步骤:

  1. 下载COCO验证集(2017 val)并解压至coco/目录,确保目录结构为coco/images/val2017/和coco/annotations/instances_val2017.json;
  2. 将上述代码保存为quantize_compare.py并执行python quantize_compare.py;
  3. 首次运行会自动下载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。
FP32 vs INT8 量化对比流程 FP32 ONNX模型 静态量化(INT8) INT8 ONNX模型 评测指标:延迟(ms) + 检测率(mAP代理) 预期:延迟降低1.5~3x 预期:精度损失<10%