4、ONNX模型优化:使用onnx-simplifier简化模型,使用onnxruntime进行推理验证
好,咱们继续往下走。模型训练完了,转成ONNX了,但你以为这就完事了?
说实话,没那么简单。我见过太多人,模型转出来就直接部署,结果推理速度慢得离谱,或者干脆跑不起来。问题出在哪?
出在模型本身。你训练出来的模型,里面有很多冗余操作——比如一些恒等映射、多余的reshape、不必要的cast。这些在训练时无所谓,但在推理时就是累赘。
所以这一章,咱们就聊聊怎么给ONNX模型「瘦身」,以及怎么验证它还能正常工作。
4.1 为什么要简化ONNX模型?
先说说我个人的经历。之前做一个道路分割项目,模型转成ONNX后,用Netron一看,好家伙,整整300多个节点。但仔细一分析,里面至少有50个节点是多余的——比如连续的Transpose、Identity节点,还有那些训练框架自动插入的Gather操作。
这些节点有什么用?
说白了,一点用没有。它们只是训练框架为了计算图方便而留下的「痕迹」。推理时,这些操作只会增加计算开销,甚至导致某些推理后端报错。
所以,简化模型的核心目的就两个:
- 减少计算量:去掉冗余节点,让推理更快
- 提高兼容性:某些推理引擎对复杂图结构支持不好,简化后更稳定
重要:简化模型不会改变模型的数学输出。它只是把计算图中的「弯路」拉直了。
4.2 onnx-simplifier:模型瘦身利器
onnx-simplifier这个工具,说白了就是帮你自动检测并移除冗余节点。它的原理其实不复杂:
- 用ONNX Runtime跑一遍模型,记录每个节点的输出值
- 分析哪些节点是「恒等映射」(输入等于输出)
- 把这些节点从图中移除,重新连接上下游
嗯,听起来挺聪明,对吧?
4.2.1 安装与基本使用
安装很简单,一行命令:
pip install onnx-simplifier
然后就可以用了。我建议你写个Python脚本,而不是用命令行,因为这样更容易控制:
import onnx
from onnxsim import simplify
# 加载原始模型
model_path = "deeplabv3.onnx"
simplified_path = "deeplabv3_simplified.onnx"
# 简化模型
model = onnx.load(model_path)
model_simplified, check = simplify(model)
if check:
print("简化成功!")
onnx.save(model_simplified, simplified_path)
else:
print("简化失败,请检查模型结构")
这里有个细节要注意:simplify()函数返回两个值,第一个是简化后的模型,第二个是一个布尔值,表示简化是否成功。我建议你一定检查这个返回值。
小技巧:如果简化失败,可以试试设置skip_fuse_bn=True。我在一个MobileNetV3模型上遇到过这个问题,跳过了BN融合就好了。
4.2.2 简化效果对比
咱们来看看实际效果。以我之前做的一个轻量化分割模型为例:
| 指标 | 简化前 | 简化后 | 变化 |
|---|---|---|---|
| 节点数 | 287 | 203 | -29.3% |
| 模型大小 | 4.2 MB | 3.8 MB | -9.5% |
| 推理时间(CPU) | 23 ms | 18 ms | -21.7% |
你看,节点数减少了近30%,推理时间也快了20%以上。这还是在CPU上,如果在NPU或者GPU上,效果会更明显。
4.3 使用ONNX Runtime验证推理
模型简化完了,怎么知道它还能正常工作?
当然是用ONNX Runtime跑一遍推理,跟原始模型对比结果。
4.3.1 安装ONNX Runtime
pip install onnxruntime
如果你有GPU,可以安装GPU版本:
pip install onnxruntime-gpu
4.3.2 编写推理验证脚本
我个人习惯写一个完整的验证脚本,包括加载模型、准备输入数据、运行推理、对比结果:
import onnxruntime as ort
import numpy as np
from PIL import Image
# 加载简化后的模型
session = ort.InferenceSession("deeplabv3_simplified.onnx")
# 获取输入输出信息
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
input_shape = session.get_inputs()[0].shape
print(f"输入名称: {input_name}")
print(f"输入形状: {input_shape}")
print(f"输出名称: {output_name}")
# 准备输入数据(假设输入是1x3x256x256)
input_data = np.random.randn(1, 3, 256, 256).astype(np.float32)
# 运行推理
outputs = session.run([output_name], {input_name: input_data})
# 输出结果形状
print(f"输出形状: {outputs[0].shape}")
注意:输入数据的类型一定要是np.float32,否则ONNX Runtime会报错。我之前就因为这个踩过坑,排查了半天才发现是数据类型不对。
4.3.3 对比简化前后的输出
光能跑还不够,还得验证简化后的模型输出跟原始模型一致。我通常的做法是:
# 加载原始模型和简化模型
session_orig = ort.InferenceSession("deeplabv3.onnx")
session_simp = ort.InferenceSession("deeplabv3_simplified.onnx")
# 使用相同的输入
input_data = np.random.randn(1, 3, 256, 256).astype(np.float32)
# 分别推理
output_orig = session_orig.run(None, {input_name: input_data})
output_simp = session_simp.run(None, {input_name: input_data})
# 计算差异
diff = np.abs(output_orig[0] - output_simp[0]).max()
print(f"最大差异: {diff:.6f}")
if diff < 1e-5:
print("✅ 简化前后结果一致!")
else:
print("❌ 差异过大,请检查模型")
一般来说,最大差异在1e-5以内就算正常。如果差异太大,说明简化过程可能出了问题。
4.4 常见问题与避坑指南
做模型简化时,有几个坑我帮你提前踩过了:
- 动态输入形状:如果你的模型支持动态batch或动态分辨率,简化时可能会出问题。我建议先固定输入形状再简化。
- 自定义算子:如果模型里有自定义算子,onnx-simplifier可能不认识,会跳过这些节点。这时候需要手动检查。
- 量化模型:量化后的ONNX模型简化要格外小心,因为量化参数可能被误删。我曾经把一个量化模型简化后,精度直接掉到0.3...
我的建议:简化后一定要用真实数据跑一遍推理,对比输出结果。不要只看节点数减少了就以为万事大吉。
4.5 本章小结
好,咱们来捋一捋这章的核心内容:
- ONNX模型里有很多冗余节点,需要简化
- onnx-simplifier可以自动检测并移除这些节点
- 简化后要用ONNX Runtime验证推理结果
- 注意动态形状、自定义算子、量化模型这些特殊情况
下一章,咱们会聊怎么把简化后的ONNX模型转换成TensorRT引擎,在GPU上跑出飞一样的速度。到时候见!