第4章:模型导出与量化——从FP32到INT8的实战之路

模型导出和量化,说白了就是把训练好的“大胖子”模型,压缩成能在芯片上跑起来的“精干小伙”。我刚开始接触这块时,也觉得不就是转个格式嘛,结果踩了不少坑。今天咱们就把PyTorch/TensorFlow模型导出为ONNX、INT8量化原理、校准数据集制作、量化精度损失分析这几个环节,一个一个掰开揉碎了讲。

4.1 模型导出:PyTorch/TensorFlow → ONNX

ONNX(Open Neural Network Exchange)是模型交换的中间格式。你想想看,训练用PyTorch,部署用TensorRT,中间总得有个“翻译官”。ONNX就是这个角色。

4.1.1 PyTorch导出ONNX

PyTorch导出ONNX主要用 torch.onnx.export。我个人习惯在导出前先做一次推理验证,确保模型是正常的。

import torch
import torch.onnx

# 假设model是你的ISP模型,dummy_input是示例输入
model.eval()
dummy_input = torch.randn(1, 3, 640, 640)  # batch=1, 3通道, 640x640

torch.onnx.export(
    model,
    dummy_input,
    "isp_model.onnx",
    export_params=True,        # 导出训练好的参数
    opset_version=11,          # 推荐11或更高
    do_constant_folding=True,  # 常量折叠优化
    input_names=['input'],
    output_names=['output'],
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)
print("ONNX导出完成")
我的经验:opset_version别选太低。我曾在opset=9上折腾半天,发现某些算子不支持,后来升到11就解决了。建议直接用11或13。

4.1.2 TensorFlow导出ONNX

TensorFlow导出ONNX需要借助 tf2onnx 工具。流程稍微绕一点,但原理一样。

import tf2onnx
import tensorflow as tf

# 加载你的TensorFlow模型
model = tf.saved_model.load("path/to/saved_model")

# 指定输入签名
spec = (tf.TensorSpec((None, 640, 640, 3), tf.float32, name="input"),)

# 导出ONNX
output_path = "isp_model.onnx"
model_proto, _ = tf2onnx.convert.from_keras(
    model, input_signature=spec, opset=13, output_path=output_path
)
print(f"ONNX模型已保存至: {output_path}")
注意:TensorFlow的NHWC格式和ONNX的NCHW格式经常打架。导出前确认你的数据排布,否则推理结果全是错的。我曾经因为这个排查了整整两天。

4.2 INT8量化原理:为什么是INT8?

FP32模型在AI芯片上跑,功耗高、带宽大。INT8量化就是把FP32的权重和激活值,映射到INT8的范围内(-128到127)。

核心公式很简单:

量化: Q = round(R / S) + Z
反量化: R = (Q - Z) * S

其中:
R = 真实浮点值
Q = 量化后的整数值
S = 缩放因子 (scale)
Z = 零点偏移 (zero_point)

你想想看,FP32有4个字节,INT8只有1个字节。模型体积直接缩小4倍,推理速度提升2-4倍。在ISP场景下,这个加速效果非常明显。

4.2.1 对称量化 vs 非对称量化

类型 特点 适用场景 我的建议
对称量化 Z=0,计算简单 权重分布对称(如经过BN的权重) 权重推荐用对称
非对称量化 Z≠0,精度更高 激活值分布不对称(如ReLU输出) 激活推荐用非对称
核心要点:量化不是简单的截断,而是寻找最优的S和Z,让量化后的信息损失最小。说白了,就是让INT8的128个台阶,尽可能覆盖FP32的数值范围。

4.3 校准数据集制作:量化前的关键一步

量化需要“校准数据集”来确定S和Z。这个数据集不需要很大,但必须能代表真实数据分布。

我一般这样制作:

  1. 从训练集中随机抽取:500-1000张图片,覆盖不同光照、场景
  2. 预处理对齐:确保预处理方式和训练时完全一致
  3. 保存为特定格式:推荐使用numpy数组或TFRecord,方便快速加载
  4. 验证多样性:检查是否包含极端情况(过曝、欠曝、噪声大等)
import numpy as np
import cv2
import os

def make_calibration_dataset(image_dir, output_path, size=(640, 640)):
    """
    制作校准数据集
    """
    images = []
    for fname in os.listdir(image_dir)[:500]:  # 取500张
        img = cv2.imread(os.path.join(image_dir, fname))
        img = cv2.resize(img, size)
        img = img.astype(np.float32) / 255.0  # 归一化到[0,1]
        images.append(img)
    
    calib_data = np.stack(images, axis=0)  # [N, H, W, C]
    np.save(output_path, calib_data)
    print(f"校准数据集已保存: {calib_data.shape}")
    return calib_data
避坑指南:我曾经只用100张纯白天场景的图片做校准,结果模型在夜景下精度暴跌。校准数据集一定要包含各种场景,尤其是边缘情况。

4.4 量化精度损失分析:到底损失了多少?

量化一定会损失精度,关键是要控制在可接受范围内。我通常用以下指标评估:

4.4.1 常用评估指标

指标 计算公式 可接受范围 说明
PSNR 10 * log10(MAX² / MSE) 下降 < 0.5 dB 图像质量常用
SSIM 结构相似性 下降 < 0.02 人眼感知相关
余弦相似度 cos(FP32输出, INT8输出) > 0.99 特征层对比

4.4.2 精度损失常见原因

  • 权重分布不均匀:某些层数值范围过大,量化后信息丢失严重
  • 激活值有异常点:个别极大值拉高了整个量化范围
  • 校准数据集不匹配:校准数据分布和实际推理数据分布不一致
  • 敏感层未特殊处理:某些层(如第一层、最后一层)对量化特别敏感
我曾经踩过的坑:有一次量化后PSNR掉了2个dB,排查了半天,发现是某个卷积层的权重范围是[-0.01, 0.01],量化到INT8后几乎全变成了0。解决方案是对该层单独做per-channel量化,或者保留FP32。

4.5 知识结构总览

下面这张图总结了本章的核心流程,从模型导出到量化部署,每一步都环环相扣。

模型导出与量化流程 模型导出与INT8量化流程 PyTorch模型 torch.onnx.export TensorFlow模型 tf2onnx.convert ONNX中间格式 模型交换标准 校准数据集 500-1000张代表性图片 INT8量化原理 对称/非对称量化 量化参数计算 S和Z的确定 权重量化 per-tensor / per-channel 激活量化 校准数据统计 精度损失评估 PSNR / SSIM / 余弦相似度 INT8部署模型 校准数据集是关键 覆盖各种场景 精度损失 < 0.5dB 可接受范围

4.6 实战建议与避坑总结

最后,我把这些年积累的经验浓缩成几条建议:

  • 导出ONNX后一定要验证:用onnxruntime跑一遍,对比输出是否一致。我见过太多导出后算子不对齐的情况。
  • 量化前先做敏感层分析:逐层量化,找出哪些层对精度影响最大。这些层可以考虑保留FP32。
  • 校准数据集宁多勿少:500张是底线,1000张更稳妥。场景覆盖越全,量化后泛化能力越好。
  • 量化后做全量测试:不要只看几个指标,要在完整测试集上跑一遍。我遇到过量化后PSNR没掉,但某个特定场景全黑的情况。
记住:量化不是终点,而是部署优化的起点。一个好的量化模型,应该是在精度和速度之间找到最佳平衡点。这个平衡点,需要你根据实际芯片和业务场景去调。