3、模型序列化基础:ONNX格式介绍、PyTorch模型导出为ONNX、TensorFlow模型导出为ONNX

好,咱们进入第三章。说实话,模型序列化这块,是很多同学在落地部署时踩的第一个坑。你模型训得再好,导不出来,或者导出来精度不对,那前面全白干。

ONNX,全称是 Open Neural Network Exchange。说白了,它就是深度学习模型的“通用语言”。你想想看,PyTorch 有自己的一套,TensorFlow 也有自己的一套,各家框架各说各话。ONNX 就是那个翻译官,让不同框架的模型能互相串门。

我个人习惯,把 ONNX 看作是模型部署的“中间态”。训练用 PyTorch,部署用 TensorRT,中间这个桥梁就是 ONNX。它不参与训练,也不直接做推理加速,但它决定了你的模型能不能顺利跑到目标硬件上。

核心要点:ONNX 是一种中间表示格式,它定义了一套标准化的算子集和计算图描述方式。你的模型最终能不能被 TensorRT 优化,很大程度上取决于 ONNX 导出的质量。

3.1 ONNX 格式的核心概念

ONNX 文件里到底装了什么?我拆开给你看。

  • 计算图(Graph):描述了模型的所有计算节点和数据流向。每个节点就是一个算子,比如 Conv、Relu、MatMul。
  • 权重参数(Initializer):模型训练好的权重和偏置,以张量形式存储。
  • 输入输出(Input/Output):定义了模型的输入张量名称、数据类型、形状,以及输出张量的信息。
  • 元数据(Metadata):比如模型版本、作者、框架来源等辅助信息。

嗯,这里要注意:ONNX 的算子版本很重要。不同版本的 ONNX 支持的算子不一样。我在项目中遇到过,导出的 ONNX 模型在旧版本 TensorRT 上跑不了,就是因为用了高版本才有的算子。

小技巧:导出 ONNX 时,建议指定 opset_version=11 或 13。这两个版本兼容性最好,TensorRT 支持也最完善。别追新,稳定第一。

3.2 PyTorch 模型导出为 ONNX

PyTorch 导出 ONNX 用的是 torch.onnx.export 这个接口。看起来简单,但里面的门道不少。

先看一个标准流程:

import torch
import torchvision.models as models

# 1. 加载模型,切换到 eval 模式
model = models.resnet18(pretrained=True)
model.eval()

# 2. 构造一个 dummy 输入
dummy_input = torch.randn(1, 3, 224, 224)

# 3. 导出 ONNX
torch.onnx.export(
    model,                    # 模型
    dummy_input,              # 示例输入
    "resnet18.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 导出完成!")

这里我重点说几个坑:

  • eval 模式必须开:我见过有人忘了 model.eval(),结果导出的 ONNX 里包含了 Dropout 和 BatchNorm 的训练行为,推理结果完全不对。
  • dummy_input 的形状要对:它决定了 ONNX 模型的输入形状。如果你后续要支持动态 batch,记得配 dynamic_axes。
  • do_constant_folding:这个建议打开。它会把一些固定计算提前算好,减少推理时的计算量。

避坑指南:我曾经导出一个带 if-else 控制流的模型,结果 ONNX 死活导不出来。后来发现 PyTorch 的 torch.onnx.export 默认不支持动态控制流。解决办法是用 torch.jit.script 先转成 TorchScript,再导出 ONNX。

导出后怎么验证?我一般用 ONNX Runtime 跑一遍,对比 PyTorch 的输出:

import onnx
import onnxruntime as ort
import numpy as np

# 检查 ONNX 模型结构
onnx_model = onnx.load("resnet18.onnx")
onnx.checker.check_model(onnx_model)

# 用 ONNX Runtime 推理
ort_session = ort.InferenceSession("resnet18.onnx")
input_name = ort_session.get_inputs()[0].name
output_name = ort_session.get_outputs()[0].name

# 构造输入
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

# 推理
ort_outputs = ort_session.run([output_name], {input_name: input_data})

# 对比 PyTorch 输出
with torch.no_grad():
    torch_outputs = model(torch.from_numpy(input_data))

# 检查误差
diff = np.abs(ort_outputs[0] - torch_outputs.numpy()).max()
print(f"最大误差:{diff:.6f}")

如果误差在 1e-5 以内,基本没问题。超过 1e-3 就要警惕了,可能是算子精度问题。

3.3 TensorFlow 模型导出为 ONNX

TensorFlow 导出 ONNX 稍微麻烦一点。因为 TensorFlow 的图结构更复杂,而且有 SavedModel、Keras H5、Checkpoint 等多种格式。

我推荐用 tf2onnx 这个工具。它专门干这个事,而且社区活跃,踩过的坑都填得差不多了。

先安装:

pip install tf2onnx

然后看代码:

import tensorflow as tf
import tf2onnx

# 1. 加载 Keras 模型
model = tf.keras.applications.MobileNetV2(weights='imagenet')
model.summary()

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

# 3. 导出 ONNX
output_path = "mobilenetv2.onnx"
model_proto, _ = tf2onnx.convert.from_keras(
    model,
    input_signature=spec,
    opset=13,
    output_path=output_path
)

print(f"ONNX 模型已保存到 {output_path}")

如果是 SavedModel 格式,用这个:

import tf2onnx

# 加载 SavedModel
model = tf.saved_model.load("path/to/saved_model")

# 获取推理函数
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)

# 转换
tf2onnx.convert.from_function(
    model.signatures['serving_default'],
    input_signature=spec,
    output_path="model.onnx"
)

这里有个关键点:TensorFlow 的 NHWC 格式和 ONNX 的 NCHW 格式不一样。你想想看,ONNX 默认用 NCHW,但 TensorFlow 习惯用 NHWC。如果不做处理,导出的 ONNX 模型在推理时可能会出问题。

我的经验:在导出前,先用 tf.transpose 把输入从 NHWC 转成 NCHW。或者在 tf2onnx 的转换参数里加上 --inputs-as-nchw,让工具自动处理。

验证方法和 PyTorch 类似,用 ONNX Runtime 跑一遍,对比 TensorFlow 的输出:

import onnxruntime as ort
import numpy as np

ort_session = ort.InferenceSession("mobilenetv2.onnx")
input_name = ort_session.get_inputs()[0].name

# 构造输入(注意是 NCHW 格式)
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

# ONNX Runtime 推理
ort_outputs = ort_session.run(None, {input_name: input_data})

# TensorFlow 推理(需要转回 NHWC)
tf_input = np.transpose(input_data, (0, 2, 3, 1))
tf_output = model.predict(tf_input)

# 对比
diff = np.abs(ort_outputs[0] - tf_output).max()
print(f"最大误差:{diff:.6f}")

3.4 导出后的常见问题与排查

导出 ONNX 不是终点,而是起点。我整理了几个高频问题:

问题现象 可能原因 解决方案
ONNX 模型无法加载 算子版本不兼容 降低 opset_version,或升级 ONNX 库
推理结果偏差大 模型未切换到 eval 模式 导出前加 model.eval()
动态 batch 不生效 dynamic_axes 配置错误 检查输入输出名称是否匹配
TensorFlow 导出失败 使用了自定义算子 用 tf2onnx 的 custom_ops 参数注册
ONNX 文件过大 包含了不必要的节点 用 onnx-simplifier 简化模型

总结一下:ONNX 导出这件事,说白了就是“对齐”。对齐框架的算子、对齐数据格式、对齐输入输出。只要这三样对齐了,后面 TensorRT 的加速才能顺风顺水。

下一章,我们会深入 ONNX 模型的优化技巧,包括如何剪枝、如何合并算子、如何消除冗余节点。这些都是为 TensorRT 加速铺路的硬功夫。