4、模型格式与转换:ONNX作为中间格式、PyTorch/TF模型转ONNX、ONNX转TensorRT/OpenVINO、量化与精度损失

模型格式转换,说白了就是给模型「换衣服」。你训练好的 PyTorch 或 TensorFlow 模型,边缘设备根本不认识。你得把它变成设备能懂的格式。

我个人习惯把 ONNX 当作中间桥梁。为什么?因为它生态好,几乎所有推理引擎都支持。我在项目中遇到过好几次,团队用不同框架训练模型,最后全靠 ONNX 统一收口。

ONNX 作为中间格式

ONNX 全称 Open Neural Network Exchange,微软和 Meta 联合搞的。它的核心价值就一句话:一次转换,到处推理

你想想看,如果每个框架都直接对接 TensorRT、OpenVINO、TFLite,那得写多少适配代码?ONNX 帮你把 N 对 N 的问题,简化成了 N 对 1 对 N。

ONNX 的核心优势:

  • 算子标准统一,跨框架兼容
  • 支持静态图和动态图导出
  • 内置量化、优化工具链
  • 社区活跃,更新及时

嗯,这里要注意:ONNX 不是万能的。有些 PyTorch 的自定义算子,ONNX 不支持。我踩过这个坑,后来老老实实改模型结构。

PyTorch 模型转 ONNX

PyTorch 转 ONNX 用 torch.onnx.export。代码不长,但参数有讲究。

import torch
import torchvision.models as models

model = models.resnet18(pretrained=True)
model.eval()

dummy_input = torch.randn(1, 3, 224, 224)

torch.onnx.export(
    model,
    dummy_input,
    "resnet18.onnx",
    export_params=True,
    opset_version=11,
    do_constant_folding=True,
    input_names=['input'],
    output_names=['output'],
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)

这里我重点说几个参数:

  • opset_version:算子集版本。我建议用 11 或更高,太低有些新算子不支持。
  • dynamic_axes:动态轴。如果你要支持可变 batch size,必须配这个。
  • do_constant_folding:常量折叠。能优化掉一些固定计算,建议打开。

我的经验:导出前一定要调用 model.eval()。我见过有人忘了这步,结果导出的 ONNX 里还带着 dropout 和 BN 的训练状态,推理结果全乱套。

TensorFlow 模型转 ONNX

TF 转 ONNX 用 tf2onnx 工具。支持 SavedModel、Keras H5、Checkpoint 等多种格式。

import tf2onnx
import tensorflow as tf

model = tf.keras.applications.MobileNetV2(weights='imagenet')
model.save('mobilenetv2_savedmodel')

# 命令行方式
# python -m tf2onnx.convert --saved-model mobilenetv2_savedmodel --output mobilenetv2.onnx

# Python API 方式
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"))
output_path = "mobilenetv2.onnx"

model_proto, _ = tf2onnx.convert.from_keras(
    model,
    input_signature=spec,
    opset=13,
    output_path=output_path
)

说实话,TF 转 ONNX 比 PyTorch 麻烦一点。主要是 TF 的算子太「灵活」,有些动态控制流 ONNX 根本接不住。我曾经遇到一个 TF 模型里用了 tf.while_loop,转 ONNX 直接报错。最后只能重写模型。

避坑指南:TF 2.x 的 Keras 模型导出时,注意 input_shape 要明确指定。我遇到过 batch size 没写,结果导出的 ONNX 推理时 batch 维度被固定成 1。

ONNX 转 TensorRT

TensorRT 是 NVIDIA 的推理优化引擎。ONNX 转 TensorRT,核心是 trtexec 工具或 Python API。

# 命令行方式
trtexec --onnx=resnet18.onnx \
        --saveEngine=resnet18.engine \
        --fp16 \
        --workspace=1024 \
        --minShapes=input:1x3x224x224 \
        --optShapes=input:8x3x224x224 \
        --maxShapes=input:16x3x224x224

# Python API 方式
import tensorrt as trt

logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, logger)

with open("resnet18.onnx", "rb") as f:
    parser.parse(f.read())

config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)  # 1GB
config.set_flag(trt.BuilderFlag.FP16)

profile = builder.create_optimization_profile()
profile.set_shape("input", (1, 3, 224, 224), (8, 3, 224, 224), (16, 3, 224, 224))
config.add_optimization_profile(profile)

engine = builder.build_serialized_network(network, config)
with open("resnet18.engine", "wb") as f:
    f.write(engine)

这里有个关键点:动态 shape 的配置。minShapes、optShapes、maxShapes 三个值必须配好。我见过有人只配了固定 shape,结果换不同分辨率图片时直接崩了。

性能对比(ResNet-18,batch=1,FP32 vs FP16):

引擎 延迟(ms) 吞吐量(fps) 显存占用(MB)
ONNX Runtime(CPU) 12.3 81
TensorRT FP32 3.1 322 245
TensorRT FP16 1.8 555 128

你看,FP16 比 FP32 快了将近一倍,显存还省一半。这就是 TensorRT 的价值。

ONNX 转 OpenVINO

OpenVINO 是 Intel 的推理引擎,主要跑在 CPU、集成显卡和 VPU 上。转换用 mo 工具(Model Optimizer)。

# 命令行方式
mo --input_model resnet18.onnx \
   --output_dir ./openvino_model \
   --data_type FP16 \
   --input_shape [1,3,224,224] \
   --mean_values [123.675,116.28,103.53] \
   --scale_values [58.395,57.12,57.375]

# 转换后生成两个文件:
# resnet18.xml  - 模型结构
# resnet18.bin  - 模型权重

OpenVINO 有个好处:对 CPU 优化极好。我曾在 Intel Xeon 上跑过,比 ONNX Runtime 快 2-3 倍。但要注意,它不支持所有 ONNX 算子。我遇到过 ScatterND 算子不支持,最后用 --disable_fusing 参数绕过去的。

我的建议:如果目标设备是 Intel CPU 或集成显卡,优先用 OpenVINO。如果是 NVIDIA GPU,用 TensorRT。别混着用,效果反而差。

量化与精度损失

量化,说白了就是把 FP32 的模型变成 INT8。模型变小了,推理变快了,但精度会掉。

我刚开始做量化时,觉得精度损失无所谓。结果有一次在安防项目里,量化后的模型把行人误检成车辆,差点出事。从那以后,我对量化精度特别敏感。

常见的量化方式有三种:

  • 训练后量化(PTQ):最简单,拿少量校准数据跑一遍,自动算量化参数。精度损失一般 1-3%。
  • 量化感知训练(QAT):训练时模拟量化效果,精度损失可以控制在 0.5% 以内。但需要重新训练,成本高。
  • 动态量化:只量化权重,激活值运行时动态量化。适合 LSTM、Transformer 这类模型。

量化精度对比(ResNet-18,ImageNet Top-1):

精度类型 Top-1 准确率 模型大小 推理速度(fps)
FP32 69.76% 44.7 MB 322
INT8(PTQ) 68.12% 11.2 MB 890
INT8(QAT) 69.31% 11.2 MB 890

你看,PTQ 掉了 1.64%,QAT 只掉了 0.45%。如果你的业务对精度要求高,别省那点训练成本,上 QAT。

避坑指南:量化校准数据一定要覆盖真实场景。我曾经用 ImageNet 的校准集量化了一个安防模型,结果部署后精度暴跌。后来发现,安防场景的图片分布和 ImageNet 完全不一样。换了真实场景的校准数据后,精度才恢复正常。

最后说一句:模型格式转换和量化,不是一锤子买卖。你很可能需要反复调参、测试、再调参。我建议你建一个自动化 pipeline,把转换、量化、精度验证串起来。这样每次改模型,跑一遍 pipeline 就知道效果。

嗯,这一节就到这里。下一节我们聊聊推理引擎的部署实战,到时候我会拿一个完整的项目案例来演示。