第4章:模型导出与转换:从PyTorch/TensorFlow导出ONNX、onnx-simplifier优化、模型验证
好,咱们进入第四章。这一章,说白了就是解决一个核心问题:怎么把训练好的模型,变成MCU能吃的“干粮”。
你想想看,PyTorch或者TensorFlow训练出来的模型,就像刚出锅的红烧肉——香是香,但没法直接打包带走。ONNX就是那个“真空包装机”,把模型封装成标准格式。而MCU呢,它胃口小,吃不了大块肉,我们还得把肉切成肉末——这就是量化和优化要做的事。
我个人习惯把这一步叫做“模型的中转站”。搞不定这一步,后面在MCU上跑推理就是空谈。我记得刚入行那会儿,花了两周训练了一个不错的分类网络,结果导出ONNX时各种报错,差点把电脑砸了。嗯,咱们今天就把这些坑提前填上。
4.1 从PyTorch导出ONNX
PyTorch导出ONNX,官方提供了torch.onnx.export接口。看起来简单,但里面门道不少。
先看一个最基础的例子:
import torch
import torch.onnx
# 假设我们有一个训练好的模型
model = torch.load('my_model.pth')
model.eval()
# 创建一个 dummy input,形状要和模型输入一致
dummy_input = torch.randn(1, 3, 224, 224)
# 导出ONNX
torch.onnx.export(
model, # 模型
dummy_input, # 示例输入
'my_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:我个人建议用11或12。太低的版本不支持一些新算子,太高的版本MCU上的推理引擎可能不支持。我在项目中吃过这个亏,用了opset 15,结果在某个老版本ONNX Runtime上直接崩了。
- dynamic_axes:如果你的模型需要支持动态batch,这里一定要配。但说实话,在MCU场景下,我通常固定batch为1,省心。
- do_constant_folding:这个建议打开。它会把一些固定的计算提前算好,减小模型体积。
model.eval()。我见过有人忘了这步,结果导出的ONNX里包含了Dropout和BatchNorm的训练行为,推理结果完全不对。
4.2 从TensorFlow导出ONNX
TensorFlow这边,我们通常用tf2onnx这个工具。流程稍微绕一点,但也不复杂。
先安装:
pip install tf2onnx
然后导出:
import tensorflow as tf
import tf2onnx
# 加载SavedModel格式的模型
model = tf.saved_model.load('my_tf_model')
# 指定输入签名
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)
# 转换为ONNX
output_path = "my_model.onnx"
model_proto, _ = tf2onnx.convert.from_keras(
model,
input_signature=spec,
opset=11,
output_path=output_path
)
print(f"转换完成,保存至 {output_path}")
这里有个坑:TensorFlow的模型格式五花八门,有SavedModel、H5、Keras、Checkpoint等。我个人建议统一用SavedModel作为中间格式,兼容性最好。
我曾经遇到一个项目,客户给的是H5格式,结果tf2onnx死活转不过去。后来我先把H5加载成Keras模型,再保存为SavedModel,最后才转成功。嗯,多一层转换,多一份保险。
4.3 onnx-simplifier:给模型“瘦身”
导出的ONNX模型,往往包含很多冗余的算子。比如一些形状推理、恒等映射、不必要的转换等。这些在GPU上跑没问题,但在MCU上就是负担。
这时候就需要onnx-simplifier出场了。它的作用就是简化计算图,去掉那些“虚胖”的部分。
安装:
pip install onnx-simplifier
使用:
import onnx
from onnxsim import simplify
# 加载原始模型
model = onnx.load('my_model.onnx')
# 简化
model_simp, check = simplify(model)
# 检查是否简化成功
assert check, "简化失败!"
# 保存简化后的模型
onnx.save(model_simp, 'my_model_simplified.onnx')
print("简化完成!")
你也可以用命令行:
onnxsim my_model.onnx my_model_simplified.onnx
简化后,模型体积通常能减少10%~30%。我见过最夸张的一个例子,一个ResNet50从200MB简化到了150MB。为什么?因为原始导出时,PyTorch把很多中间计算都保留了下来,简化器把这些没用的节点全剪掉了。
check 验证。我遇到过简化器把某些关键节点误删的情况,导致输出结果完全错误。所以,简化后的模型必须重新验证。
4.4 模型验证:确保转换没出问题
模型导出和简化之后,最重要的一步来了——验证。这一步绝对不能省。
验证的核心思路很简单:用同一个输入,分别在原始框架(PyTorch/TF)和ONNX上跑推理,对比输出结果的差异。
我一般这样写验证代码:
import onnx
import onnxruntime as ort
import numpy as np
import torch
# 1. 加载ONNX模型
onnx_model = onnx.load('my_model_simplified.onnx')
onnx.checker.check_model(onnx_model) # 检查模型结构是否合法
# 2. 创建ONNX Runtime推理会话
ort_session = ort.InferenceSession('my_model_simplified.onnx')
# 3. 准备测试数据(用和导出时一样的dummy input)
test_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 4. ONNX推理
ort_inputs = {ort_session.get_inputs()[0].name: test_input}
ort_output = ort_session.run(None, ort_inputs)[0]
# 5. PyTorch推理(假设原始模型还在)
with torch.no_grad():
torch_output = model(torch.from_numpy(test_input)).numpy()
# 6. 对比结果
diff = np.abs(ort_output - torch_output).max()
print(f"最大绝对误差:{diff:.6f}")
if diff < 1e-3:
print("✅ 验证通过!模型转换正确。")
else:
print("❌ 验证失败!请检查模型导出过程。")
这里有个关键点:误差阈值怎么设?
我个人经验:
- 对于分类模型,误差在1e-4到1e-3之间通常没问题,因为softmax后取argmax不受影响。
- 对于回归模型或检测模型,误差要求更严格,最好在1e-5以内。
- 如果误差超过1e-2,那肯定有问题,别犹豫,回去检查导出过程。
4.5 避坑指南:我踩过的那些坑
做模型导出这么多年,我踩过的坑能写一本书。这里挑几个最常见的:
- 算子不支持:有些PyTorch的高级算子(比如torch.einsum、torch.topk的某些模式)在ONNX里没有对应实现。解决办法是:要么换用基础算子重写,要么升级opset版本。
- 动态shape问题:如果你的模型输入尺寸不固定,导出时一定要配dynamic_axes。否则ONNX Runtime会报shape不匹配的错误。
- 量化模型导出:如果你做了量化感知训练(QAT),导出时要注意。PyTorch的量化模型导出ONNX需要特殊处理,不能直接用torch.onnx.export。
- 控制流问题:模型里有if-else或循环?ONNX对控制流的支持有限。我建议在导出前,把这些控制流改成固定的计算路径。
我曾经接手一个项目,模型里用了torch.where做条件判断,结果导出ONNX后,在MCU上推理结果全错。后来我把torch.where改成了mask乘法,问题就解决了。嗯,有时候“笨办法”反而更可靠。
4.6 本章小结
这一章的内容,说白了就是三件事:
- 导出:把PyTorch/TF模型转成ONNX格式
- 简化:用onnx-simplifier去掉冗余节点
- 验证:确保转换后的模型输出正确
这三步走完,你的模型就准备好进入下一章了——量化。量化才是让模型在MCU上跑起来的关键一步,咱们下章见。
记住一句话:导出不验证,后面两行泪。别问我怎么知道的。