4. ONNX模型加载与推理:使用ONNX Runtime加载模型、创建Session、执行推理
好,咱们进入正题。前面几章我们把ONNX模型的结构、转换、可视化都聊了一遍,现在终于到了最核心的一步——让模型真正跑起来。
说白了,加载模型、创建Session、执行推理,这三步就是ONNX Runtime的“三板斧”。我刚开始接触ONNX Runtime时,觉得这玩意儿不就是个推理引擎嘛,能有多复杂?结果在实际项目里踩了不少坑。嗯,今天我把这些经验都揉碎了讲给你听。
4.1 加载模型:从文件到内存
加载模型,就是把.onnx文件读进内存。这一步看似简单,但有个细节很多人会忽略——模型文件的完整性校验。
我个人习惯在加载前先检查文件大小,至少确保文件不是0字节。你想想看,如果模型文件在传输过程中损坏了,你后面花再多时间调优都是白搭。
# 加载模型前的安全检查
import os
import onnxruntime as ort
model_path = "resnet50.onnx"
if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件 {model_path} 不存在")
if os.path.getsize(model_path) == 0:
raise ValueError("模型文件为空,请检查文件完整性")
# 正式加载
session = ort.InferenceSession(model_path)
print("模型加载成功!")
os.path.getsize()检查文件大小。曾经有个项目,模型文件从服务器下载时网络中断,生成了一个只有几百字节的残损文件,排查了半天才发现是文件没下完整。
4.2 创建Session:配置推理环境
Session是ONNX Runtime的核心对象。它负责管理模型的计算图、内存分配、执行引擎等。创建Session时,你可以指定运行在CPU还是GPU上,以及各种优化选项。
在ARM平台上,我们通常用CPU推理。但别小看CPU推理,配置得当的话,性能差距能到30%以上。
# 创建Session的基本方式
session = ort.InferenceSession("resnet50.onnx")
# 带配置的Session创建
import onnxruntime as ort
# 设置Session选项
sess_options = ort.SessionOptions()
# 设置线程数(ARM平台建议设为物理核心数)
sess_options.intra_op_num_threads = 4
sess_options.inter_op_num_threads = 2
# 启用内存优化
sess_options.enable_mem_pattern = True
sess_options.enable_cpu_mem_arena = True
# 设置日志级别(生产环境建议设为WARNING)
sess_options.log_severity_level = 2 # 0:VERBOSE, 1:INFO, 2:WARNING, 3:ERROR
# 创建Session
session = ort.InferenceSession("resnet50.onnx", sess_options)
intra_op_num_threads:单个算子内部的并行线程数。我建议设为CPU物理核心数,比如树莓派4B是4核,就设4。inter_op_num_threads:算子之间的并行线程数。通常设2就够了,设太多反而会因为线程切换导致性能下降。enable_mem_pattern:内存预分配模式。开启后能减少推理时的内存分配开销,ARM平台建议开启。
4.3 获取模型输入输出信息
创建Session后,第一件事不是急着推理,而是搞清楚模型的输入输出长什么样。这一步我吃过亏——有一次模型输入是NCHW格式,我传了NHWC的数据,结果推理结果全错,排查了整整一天。
# 获取模型输入输出信息
input_name = session.get_inputs()[0].name
input_shape = session.get_inputs()[0].shape
input_type = session.get_inputs()[0].type
output_name = session.get_outputs()[0].name
output_shape = session.get_outputs()[0].shape
print(f"输入名称: {input_name}")
print(f"输入形状: {input_shape}")
print(f"输入类型: {input_type}")
print(f"输出名称: {output_name}")
print(f"输出形状: {output_shape}")
| 属性 | 说明 | 常见值 |
|---|---|---|
| name | 输入/输出张量的名称 | "input", "output", "data" |
| shape | 张量的形状,可能包含动态维度 | [1, 3, 224, 224] |
| type | 张量的数据类型 | "tensor(float)", "tensor(int64)" |
4.4 执行推理:让模型跑起来
终于到了最激动人心的环节——执行推理。在ONNX Runtime中,推理就是调用run()方法,传入输出名称和输入数据。
# 准备输入数据(假设是图像分类任务)
import numpy as np
# 模拟一张224x224的RGB图像
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 执行推理
outputs = session.run([output_name], {input_name: input_data})
# 获取结果
result = outputs[0]
print(f"推理结果形状: {result.shape}")
print(f"推理结果: {result}")
这里有个细节:run()的第一个参数是输出名称列表,如果你传None,会返回所有输出。我个人习惯明确指定输出名称,这样代码更清晰,也避免不必要的计算。
4.5 完整推理流程示例
把上面的步骤串起来,就是一个完整的推理流程。我在项目中通常封装成一个函数,方便复用。
import numpy as np
import onnxruntime as ort
def onnx_inference(model_path, input_data):
"""
ONNX模型推理函数
:param model_path: 模型文件路径
:param input_data: 输入数据,numpy数组
:return: 推理结果
"""
# 1. 创建Session
sess_options = ort.SessionOptions()
sess_options.intra_op_num_threads = 4
sess_options.log_severity_level = 2
session = ort.InferenceSession(model_path, sess_options)
# 2. 获取输入输出信息
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
# 3. 执行推理
outputs = session.run([output_name], {input_name: input_data})
return outputs[0]
# 使用示例
if __name__ == "__main__":
# 加载模型
model_path = "resnet50.onnx"
# 准备输入数据
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 执行推理
result = onnx_inference(model_path, input_data)
print("推理完成,结果形状:", result.shape)
input_data的数据类型是float64,而模型期望的是float32。ONNX Runtime不会自动做类型转换,类型不匹配时会报错或者默默做隐式转换,导致性能骤降。所以,务必确保输入数据的dtype和模型要求一致。
4.6 性能调优小贴士
在ARM平台上,推理性能是重中之重。这里分享几个我实测有效的优化点:
- 预热推理:第一次推理通常较慢,因为要分配内存、加载缓存。建议先跑一次“假推理”做预热。
- 批量推理:如果有多张图片要处理,尽量合并成一个batch,能显著提升吞吐量。
- 内存复用:如果反复推理相同形状的数据,可以复用输入输出缓冲区,减少内存分配开销。
# 预热推理示例
def warm_up(session, input_name, output_name, input_shape):
"""预热推理,减少首次推理的延迟"""
dummy_input = np.random.randn(*input_shape).astype(np.float32)
for _ in range(3):
session.run([output_name], {input_name: dummy_input})
print("预热完成")
# 使用
warm_up(session, input_name, output_name, [1, 3, 224, 224])
好了,这一章的内容就到这里。加载模型、创建Session、执行推理,这三步看起来简单,但每个环节都有值得深挖的细节。下一章我们会聊如何对ONNX模型进行优化,让它在ARM平台上跑得更快。到时候见!