4、模型转换与量化:将训练好的模型(PyTorch/TF)转换为TFLite/ONNX格式,以及INT8量化实战

模型训练完了,在电脑上跑得挺欢。但你要把它塞进一个只有几百KB内存的MCU里?嗯,这里就有故事了。

我刚开始做边缘端部署时,犯过一个低级错误——直接把PyTorch的.pth文件往STM32里拷。结果可想而知,芯片根本不认识这玩意儿。后来才明白,模型从训练框架到推理引擎,中间必须经过一道「翻译」工序。这就是模型转换。

而量化,说白了就是给模型「瘦身」。把32位浮点数变成8位整数,模型体积直接缩到四分之一,推理速度翻倍。代价呢?精度会掉一点点。但在我做过的项目中,大部分场景下这点精度损失完全能接受。

4.1 为什么需要模型转换与量化

你想想看,PyTorch和TensorFlow是训练框架,它们的设计目标是灵活、可微分、能反向传播。而TFLite和ONNX是推理格式,它们的目标是轻量、高效、能跑在特定硬件上。

这两者的基因完全不同。所以我们需要一座桥。

核心原因有三点:

  • 硬件兼容性:MCU、NPU、TPU这些设备只认特定的模型格式
  • 推理效率:训练框架有大量冗余代码,推理框架做了极致优化
  • 资源限制:边缘设备的内存和算力有限,必须压缩模型

我记得有个项目,模型在GPU上推理只要5ms,但转到Cortex-M4上后,一次推理花了3秒。后来做了INT8量化加模型剪枝,才压到200ms以内。这就是量化的价值。

4.2 PyTorch模型转ONNX

ONNX是个好东西。它像一个通用翻译器,能把PyTorch、TF、PaddlePaddle的模型都转成同一种中间格式。然后你再从ONNX转到目标平台的格式。

我个人习惯先用ONNX做一次中转,因为它的生态最成熟,踩坑的人也最多,网上资料好找。

下面是一个典型的PyTorch转ONNX示例:

import torch
import torch.onnx

# 假设你有一个训练好的模型
model = torch.load('model.pth')
model.eval()

# 创建一个虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)

# 导出ONNX
torch.onnx.export(
    model,               # 模型
    dummy_input,         # 输入张量
    'model.onnx',        # 输出文件名
    export_params=True,  # 是否导出参数
    opset_version=11,    # ONNX算子集版本
    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 13导出的模型,在老旧NPU上跑不了。一般选11或12最稳妥。

4.3 TensorFlow模型转TFLite

TF转TFLite相对简单,因为都是Google自家的东西。但要注意,TF1和TF2的转换API完全不同。现在主流都用TF2了。

我曾经在TF1时代被坑过——SavedModel和GraphDef两种格式搞混,折腾了一整天。后来学乖了,统一用SavedModel。

import tensorflow as tf

# 加载SavedModel模型
model = tf.saved_model.load('saved_model_dir')

# 创建转换器
converter = tf.lite.TFLiteConverter.from_saved_model('saved_model_dir')

# 如果不做量化,直接转换
tflite_model = converter.convert()

# 保存TFLite模型
with open('model.tflite', 'wb') as f:
    f.write(tflite_model)

print("TFLite转换成功!")

这里有个坑:如果你的模型里有自定义算子,TFLite默认不支持。需要注册自定义算子,或者用TF Select算子。嗯,这个后面会细讲。

4.4 INT8量化实战

量化才是重头戏。INT8量化分两种:训练后量化量化感知训练

训练后量化最简单,模型训练完直接转。适合大部分场景。量化感知训练则是在训练过程中模拟量化效果,精度更高,但需要改训练代码。

我个人建议:先试训练后量化。如果精度掉得太多,再上量化感知训练。别一上来就搞复杂的。

4.4.1 TFLite INT8量化

import tensorflow as tf

# 加载模型
converter = tf.lite.TFLiteConverter.from_saved_model('saved_model_dir')

# 设置量化方式为INT8
converter.optimizations = [tf.lite.Optimize.DEFAULT]

# 提供代表性数据集(用于校准)
def representative_dataset():
    for _ in range(100):
        data = tf.random.normal([1, 224, 224, 3])
        yield [data]

converter.representative_dataset = representative_dataset

# 指定目标类型为INT8
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]

# 设置输入输出类型
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8

# 转换
tflite_quant_model = converter.convert()

# 保存
with open('model_quant.tflite', 'wb') as f:
    f.write(tflite_quant_model)

print("INT8量化模型生成成功!")

警告:代表性数据集一定要用真实数据的分布。我见过有人用随机数做校准,结果量化后的模型精度直接崩了。校准集最好从训练集里抽100-500张。

4.4.2 ONNX转INT8量化

ONNX的量化工具链稍微复杂一些。我常用的是ONNX Runtime的量化工具:

from onnxruntime.quantization import quantize_dynamic, QuantType

# 动态量化(不需要校准数据)
quantize_dynamic(
    'model.onnx',
    'model_quant.onnx',
    weight_type=QuantType.QInt8
)

print("ONNX动态量化完成!")

如果需要静态量化(精度更高),需要校准数据:

from onnxruntime.quantization import quantize_static, QuantType
from onnxruntime.quantization import CalibrationMethod

# 静态量化需要校准数据
calibration_data = ...  # 你的校准数据集

quantize_static(
    'model.onnx',
    'model_quant.onnx',
    calibration_data_reader=calibration_data,
    quant_format=QuantType.QInt8,
    per_channel=True,
    calibration_method=CalibrationMethod.MinMax
)

4.5 量化后的精度验证

量化完别急着部署。先跑一遍验证集,看看精度掉了多少。

我一般定个标准:Top-1准确率下降不超过1%就算合格。如果超过2%,就要考虑用量化感知训练了。

量化方法 模型大小 推理速度 精度损失 适用场景
FP32(未量化) 100% 1x 0% 服务器端
FP16 50% 1.5-2x ~0.1% GPU/高端NPU
INT8(训练后量化) 25% 3-4x 0.5-2% 大部分边缘设备
INT8(量化感知训练) 25% 3-4x 0.1-0.5% 精度敏感场景

经验之谈:对于分类模型,INT8量化通常只掉0.5%左右。但对于目标检测模型,尤其是小目标检测,量化后精度可能掉3-5%。这时候我建议用量化感知训练,或者在关键层保留FP32。

4.6 常见坑与避坑指南

做模型转换和量化这几年,我踩过的坑能写一本书。挑几个典型的说说:

  • 算子不支持:有些PyTorch的高级算子(如torch.einsum)ONNX不支持。解决办法是用基础算子重写,或者注册自定义算子。
  • 动态shape问题:边缘设备通常要求固定输入尺寸。如果你的模型支持动态batch,记得在导出时固定下来。
  • 量化后精度骤降:我曾经遇到一个模型,量化后准确率从95%掉到60%。查了半天,发现是某个层的激活值分布太广,量化时信息丢失严重。后来加了clip操作才解决。
  • 端侧推理速度没提升:INT8量化理论上快4倍,但如果你用的MCU没有硬件INT8加速,实际速度可能只快1.5倍。嗯,这个要提前查芯片手册。

最后说一句:模型转换和量化不是一锤子买卖。你可能需要反复尝试不同的量化策略、校准数据集大小、算子选择。别怕麻烦,这是嵌入式AI工程师的日常。