2. ONNX基础
2.1 ONNX是什么:不止是“模型文件”
ONNX(Open Neural Network Exchange)本质上是一个开放、跨框架的模型中间表示标准。它不负责训练,也不直接执行推理,而是定义了一套统一的计算图描述格式,让模型能够在不同框架(PyTorch、TensorFlow、PaddlePaddle等)和不同推理引擎(ONNX Runtime、TensorRT、OpenVINO)之间自由流转。你可以把它理解为深度学习的“通用语言”——训练框架负责“写诗”,ONNX负责“翻译”,推理引擎负责“朗诵”。
对于CPU推理优化而言,ONNX的价值在于:它剥离了训练框架的运行时开销,将模型固化为静态的计算图,从而让推理引擎能够对图结构进行深度优化(算子融合、内存规划、指令调度等)。这也是为什么在工业部署中,ONNX几乎成为模型交换的事实标准。
2.2 ONNX模型结构:计算图、节点与张量
一个完整的ONNX模型文件(通常以.onnx为后缀)内部是一个序列化的Protobuf结构,其核心逻辑模型由以下几部分组成:
- 计算图(Graph):模型的主体,描述数据从输入到输出的完整流动过程。
- 节点(Node):图中的每个算子(Conv、Relu、MatMul等)对应一个节点,节点包含算子类型(op_type)、输入输出张量名、属性(attribute,如卷积的kernel_shape)。
- 张量(Tensor):数据以张量形式在节点间流动,每个张量有名称、数据类型(float32、int64等)和形状(shape)。
- 初始器(Initializer):存储模型的权重和偏置,它们作为常量输入挂在图上,不随推理过程改变。
- 元数据(Metadata):包括模型版本、算子集版本(opset)、生产者信息等,用于兼容性判断。
下面这张图展示了一个简化ONNX模型的结构,帮助你直观理解数据如何从输入经过多个算子节点流向输出:
从图中可以看到,ONNX模型本质上是一个有向无环图(DAG)。数据从输入张量出发,依次经过Conv、Relu、MaxPool等节点,每个节点消费上游张量并产生下游张量,最终汇聚到输出。初始器(权重)不参与数据流动的主线,而是作为“旁路”挂载到对应算子节点上。这种结构清晰、无循环依赖的特点,使得推理引擎可以方便地对整个图进行静态分析和优化。
2.3 导出ONNX模型:PyTorch与TensorFlow实战
不同框架导出ONNX的方式略有差异,但核心思路一致:将动态的训练图“固化”为静态的推理图。下面以最常见的PyTorch为例,展示标准导出流程:
import torch
import torchvision.models as models
# 1. 加载模型并设为评估模式
model = models.resnet18(pretrained=True)
model.eval()
# 2. 构造一个示例输入(batch=1, 3通道, 224x224)
dummy_input = torch.randn(1, 3, 224, 224)
# 3. 导出ONNX
torch.onnx.export(
model, # 要导出的模型
dummy_input, # 示例输入
"resnet18.onnx", # 输出文件路径
export_params=True, # 导出权重
opset_version=17, # ONNX算子集版本
do_constant_folding=True, # 常量折叠优化
input_names=["input"], # 输入张量名
output_names=["output"], # 输出张量名
dynamic_axes={ # 动态维度(可选)
"input": {0: "batch_size"},
"output": {0: "batch_size"}
}
)
print("ONNX模型导出成功!")
对于TensorFlow(Keras)模型,导出方式如下:
import tensorflow as tf
import tf2onnx
# 加载Keras模型
model = tf.keras.applications.MobileNetV2(weights="imagenet")
model.compile()
# 转换为ONNX
spec = (tf.TensorSpec((1, 224, 224, 3), tf.float32, name="input"),)
onnx_model = tf2onnx.convert.from_keras(model, input_signature=spec, opset=17)
# 保存为.onnx文件
with open("mobilenetv2.onnx", "wb") as f:
f.write(onnx_model.SerializeToString())
print("转换完成!")
model.eval()(PyTorch)或确保模型处于推理模式,否则BatchNorm、Dropout等层会因训练/推理行为差异导致导出结果错误。同时,opset_version建议选择推理引擎支持的版本,ONNX Runtime通常支持较新的opset。
2.4 加载与验证ONNX模型
导出ONNX后,第一步是使用官方工具验证模型的完整性,然后加载到推理引擎中。ONNX Runtime是最常用的跨平台推理引擎,加载方式非常简洁:
import onnx
import onnxruntime as ort
import numpy as np
# 1. 验证模型结构
onnx_model = onnx.load("resnet18.onnx")
onnx.checker.check_model(onnx_model)
print("模型结构验证通过!")
# 2. 创建推理会话
# 指定CPUExecutionProvider,确保使用CPU推理
session = ort.InferenceSession(
"resnet18.onnx",
providers=["CPUExecutionProvider"]
)
# 3. 查看输入输出信息
for inp in session.get_inputs():
print(f"输入: {inp.name}, 形状: {inp.shape}, 类型: {inp.type}")
for out in session.get_outputs():
print(f"输出: {out.name}, 形状: {out.shape}, 类型: {out.type}")
# 4. 构造输入并推理
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
outputs = session.run(None, {"input": input_data})
print(f"推理输出形状: {outputs[0].shape}")
加载ONNX模型时,providers参数决定了使用哪个执行后端。在CPU环境下,指定CPUExecutionProvider即可;如果系统装有NVIDIA GPU,也可以添加CUDAExecutionProvider,ONNX Runtime会自动选择最优设备。
2.5 常见误区与复盘
| 误区 | 后果 | 正确做法 |
|---|---|---|
忘记调用model.eval() | BatchNorm统计量使用训练值,推理结果错误 | 导出前显式切换到评估模式 |
| 使用动态输入形状但不声明dynamic_axes | 模型只能接受固定尺寸输入,部署灵活性差 | 在dynamic_axes中声明可变维度 |
| opset版本设置过低 | 新算子无法导出,或导出后算子集不兼容 | 根据推理引擎支持范围选择opset(通常≥15) |
忽略do_constant_folding | 模型包含冗余计算节点,推理效率降低 | 导出时开启常量折叠,简化计算图 |
| 加载时不指定providers | 可能默认使用CPU,但未显式声明不利于性能调优 | 明确指定CPUExecutionProvider或GPU Provider |
onnx-simplifier等工具对模型进行简化和兼容性处理。
掌握ONNX模型的结构与导入导出流程,是后续进行CPU推理优化的基础。只有理解了模型在ONNX格式下的组织方式,才能更有效地利用ONNX Runtime的图优化、算子融合、线程调度等高级特性,为模型在CPU上的高效运行打下坚实基础。