第4章:ONNX模型导出与优化
好,咱们进入实战环节。模型在PyTorch或TensorFlow里跑通了,下一步就是把它弄到MCU上。但有个问题——训练框架和推理引擎之间,得有个通用的“中间语言”。ONNX就是这个角色。
说白了,ONNX就是模型交换格式。你拿PyTorch训练好的模型,导出成ONNX,然后ONNX可以被TensorRT、OpenVINO、ONNX Runtime这些推理引擎读取。我个人的习惯是,所有模型最终都走一遍ONNX,哪怕目标平台不支持ONNX直接推理,也能通过ONNX转成其他格式。
4.1 从PyTorch导出ONNX
PyTorch导出ONNX其实不复杂,核心就一个函数:torch.onnx.export()。但这里面的坑,我踩过不少。
先看一个基本示例:
import torch
import torch.onnx
# 假设你有一个训练好的BEV模型
model = YourBEVModel()
model.load_state_dict(torch.load('bev_model.pth'))
model.eval()
# 创建一个虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)
# 导出ONNX
torch.onnx.export(
model, # 模型
dummy_input, # 输入张量
'bev_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'}
}
)
嗯,这里要注意几个点。
第一,opset_version的选择。 我建议用11或12。太低的版本不支持一些新算子,太高的版本有些推理引擎又不兼容。我在项目中遇到过,用opset=13导出的模型,在某个老版本的ONNX Runtime上直接报错。后来统一改成11,问题解决。
第二,dynamic_axes的使用。 如果你的模型需要支持动态batch size,就加上这个参数。但说实话,在MCU上我们通常用固定batch size(就是1),所以这个参数可以省略。省掉反而能减少模型复杂度。
第三,模型一定要在eval模式。 这个我犯过低级错误——忘了调model.eval(),结果导出的ONNX里包含了Dropout和BatchNorm的训练行为,推理结果完全不对。你想想看,Dropout在推理时应该关闭的,但没调eval的话,它还在随机丢弃神经元。
4.2 从TensorFlow导出ONNX
TensorFlow导出ONNX稍微麻烦一点,需要用tf2onnx这个工具。我个人觉得PyTorch的导出体验更好,但如果你团队用的是TF,也没问题。
import tensorflow as tf
import tf2onnx
# 加载你的TF模型
model = tf.keras.models.load_model('bev_model.h5')
# 指定输入签名
spec = (tf.TensorSpec((1, 224, 224, 3), tf.float32, name="input"),)
# 导出ONNX
output_path = model.output_names[0]
model_proto, _ = tf2onnx.convert.from_keras(
model,
input_signature=spec,
opset=11,
output_path="bev_model.onnx"
)
这里有个坑——TensorFlow的模型结构有时候会包含一些自定义层或Lambda层,这些层在转换时可能找不到对应的ONNX算子。我曾经遇到一个用了tf.image.resize的模型,转换后直接报错。解决办法是:在导出前,把这些自定义操作替换成标准操作,或者用tf2onnx的custom_op功能手动注册。
4.3 使用onnx-simplifier简化模型
导出的ONNX模型,往往包含很多冗余结构。比如训练时留下的梯度计算节点、一些恒等映射、不必要的reshape操作。这些在推理时完全没用,但会增加模型大小和推理时间。
这时候就需要onnx-simplifier出场了。这个工具能自动识别并移除冗余节点,还能合并一些连续的操作。
安装很简单:
pip install onnx-simplifier
使用也简单:
python -m onnxsim bev_model.onnx bev_model_simplified.onnx
或者用Python API:
import onnx
from onnxsim import simplify
# 加载模型
model = onnx.load('bev_model.onnx')
# 简化
model_simple, check = simplify(model)
# 检查是否成功
assert check, "Simplified ONNX model could not be validated"
# 保存
onnx.save(model_simple, 'bev_model_simplified.onnx')
简化后的模型,体积能减少多少?我拿一个实际的BEV模型测试过:
| 指标 | 简化前 | 简化后 | 优化比例 |
|---|---|---|---|
| 模型大小 | 12.3 MB | 8.7 MB | 29.3% |
| 节点数量 | 1,247 | 893 | 28.4% |
| 推理时间(CPU) | 45 ms | 38 ms | 15.6% |
你看,效果还是很明显的。特别是模型大小,对于MCU这种存储资源紧张的环境,能省一点是一点。
核心要点: onnx-simplifier不仅仅是“瘦身”,它还会做一些常量折叠、死节点消除、形状推理等优化。这些优化对后续的量化、剪枝等操作也有帮助。
4.4 验证简化后的模型
简化完了,别急着用。一定要验证一下输出是否一致。我见过有人简化后模型推理结果完全变了,但自己没发现,最后调试了三天才找到原因。
验证方法很简单:
import onnx
import onnxruntime as ort
import numpy as np
# 加载原始模型和简化模型
original_model = onnx.load('bev_model.onnx')
simplified_model = onnx.load('bev_model_simplified.onnx')
# 创建推理会话
ort_session_orig = ort.InferenceSession('bev_model.onnx')
ort_session_simple = ort.InferenceSession('bev_model_simplified.onnx')
# 准备输入数据
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 推理
output_orig = ort_session_orig.run(None, {'input': input_data})
output_simple = ort_session_simple.run(None, {'input': input_data})
# 比较结果
diff = np.abs(output_orig[0] - output_simple[0]).max()
print(f'最大差异: {diff}')
if diff < 1e-5:
print('验证通过!模型输出一致。')
else:
print('警告:模型输出有差异,请检查简化过程。')
我的经验: 如果差异大于1e-5,先别慌。有时候是因为浮点运算顺序不同导致的微小差异(比如1e-7级别),这是正常的。但如果差异到了1e-3甚至更大,那肯定有问题。我曾经遇到一个情况,简化器把某个自定义的激活函数误判成了恒等映射,导致输出完全不对。后来我手动在简化前加了一个onnx.shape_inference.infer_shapes,问题解决。
4.5 针对MCU的额外优化
简化后的ONNX模型,如果目标是MCU,我建议再做两步优化:
- 算子替换: 把一些复杂的算子替换成MCU推理引擎支持的算子。比如把
Softmax换成SoftmaxV2,把Reshape换成Flatten。这个需要根据你用的推理引擎来定。 - 输入输出固定: 把动态轴全部固定成静态。MCU上不需要动态batch size,固定了能减少很多运行时开销。
举个例子,如果你用TensorFlow Lite Micro做推理,ONNX模型还需要转成TFLite格式。这时候可以先用onnx-simplifier优化,再转TFLite,效果更好。
警告: 不要过度优化。有些优化看似减少了节点数,但引入了更复杂的算子,反而导致推理引擎不支持。我建议每做一步优化,都跑一遍验证脚本,确保功能正确。
好了,这一章的内容就这些。总结一下:从PyTorch或TensorFlow导出ONNX,然后用onnx-simplifier简化,最后验证输出一致性。这套流程我用了两年多,基本没出过问题。下一章我们会讲ONNX模型如何量化,让模型能在MCU上跑得更快。