3. ONNX模型序列化与反序列化:从文件到内存的完整旅程
好,咱们今天聊点实在的。模型训练完了,怎么把它存起来?部署的时候又怎么读进来?这背后就是序列化和反序列化。说白了,序列化就是把内存里的模型对象变成一串字节流,反序列化就是反过来。ONNX 用的是 Google 的 Protobuf 来做这件事,我当年第一次接触 Protobuf 时还觉得挺新鲜,后来发现它确实是工业级序列化的不二之选。
3.1 onnx.load 与 onnx.save:最常用的两个函数
先看最直观的用法。ONNX 官方提供了两个高层 API,几乎覆盖了 90% 的场景。
import onnx
# 加载模型
model = onnx.load('model.onnx')
print(f"模型 IR 版本: {model.ir_version}")
print(f"算子集版本: {model.opset_import[0].version}")
# 保存模型
onnx.save(model, 'model_copy.onnx')
嗯,这里有个细节。我刚开始用的时候,以为 onnx.load 只是简单读文件,后来踩过坑才发现——它背后做了模型校验。如果文件损坏或者版本不对,它会直接抛异常。我个人习惯在加载后立刻调用 onnx.checker.check_model(model) 做二次确认。
onnx.load。我见过有人直接把加密字节流传进去,结果报了一堆乱码错误。
3.2 Protobuf 序列化原理:ONNX 的骨架
ONNX 模型本质上是一个 Protobuf 消息。你想想看,一个深度学习模型可能有几百层、几万个参数,怎么高效地存成二进制?Protobuf 给出了答案。
Protobuf 的核心思想是:先定义数据结构(.proto 文件),然后编译器生成对应的序列化代码。ONNX 的 proto 文件在官方仓库里,定义了 ModelProto、GraphProto、NodeProto 等核心类型。
// 简化版的 ONNX proto 定义
message ModelProto {
int64 ir_version = 1;
repeated OperatorSetIdProto opset_import = 2;
string producer_name = 3;
string producer_version = 4;
string domain = 5;
int64 model_version = 6;
string doc_string = 7;
GraphProto graph = 8;
repeated StringStringEntryProto metadata_props = 14;
}
message GraphProto {
repeated NodeProto node = 1;
string name = 2;
repeated TensorProto initializer = 5;
repeated ValueInfoProto input = 11;
repeated ValueInfoProto output = 12;
repeated ValueInfoProto value_info = 13;
}
为什么用 Protobuf 而不是 JSON?我做过对比测试:一个 200MB 的 ResNet 模型,Protobuf 序列化后只有 200MB 左右(因为权重本身就是二进制),而 JSON 序列化后体积膨胀 3-5 倍,加载时间更是慢 10 倍以上。Protobuf 的二进制格式天生适合存储张量数据。
3.3 模型文件完整性校验:别让坏模型坑了你
我曾经在客户现场部署时,遇到一个诡异的问题:模型在本地跑得好好的,传到服务器上就报错。排查了半天,发现是文件传输过程中丢了一个字节。从那以后,我养成了给每个 ONNX 模型加校验和的习惯。
常用的校验方式有两种:
- MD5/SHA256 哈希校验:在模型分发时附带哈希值,接收方计算对比。
- ONNX 内置校验:使用
onnx.checker模块。
import hashlib
import onnx
def verify_model_integrity(model_path, expected_hash=None):
"""校验模型文件完整性"""
# 计算文件哈希
sha256_hash = hashlib.sha256()
with open(model_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256_hash.update(chunk)
actual_hash = sha256_hash.hexdigest()
if expected_hash and actual_hash != expected_hash:
raise ValueError(f"模型哈希不匹配!期望 {expected_hash},实际 {actual_hash}")
# ONNX 结构校验
model = onnx.load(model_path)
onnx.checker.check_model(model)
print(f"模型完整性校验通过,SHA256: {actual_hash[:16]}...")
return model
3.4 模型版本兼容性处理:向前兼容的学问
ONNX 的版本兼容性是个大坑。我见过太多人因为版本不匹配,模型加载失败。ONNX 有多个版本维度:
| 版本类型 | 说明 | 常见问题 |
|---|---|---|
| IR 版本 | 模型中间表示的版本号 | 高版本 IR 可能不被低版本 ONNX 运行时支持 |
| 算子集版本 | 每个算子(如 Conv、Relu)的版本 | 旧版本算子在新运行时可能被弃用 |
| ONNX 运行时版本 | 实际执行推理的引擎版本 | 不同运行时对同一算子的实现可能有差异 |
怎么处理?我总结了一套「三步走」策略:
- 导出时锁定版本:在训练脚本中显式指定 opset 版本,不要用默认值。
- 加载时检查版本:读取模型的
ir_version和opset_import,与当前环境对比。 - 运行时适配:如果版本不匹配,尝试使用
onnx.version_converter进行转换。
import onnx
from onnx import version_converter
def load_with_version_check(model_path, target_opset=18):
"""带版本检查的模型加载"""
model = onnx.load(model_path)
# 检查当前算子集版本
current_opset = model.opset_import[0].version
print(f"当前算子集版本: {current_opset}, 目标版本: {target_opset}")
if current_opset < target_opset:
print("版本过低,尝试升级...")
model = version_converter.convert_version(model, target_opset)
elif current_opset > target_opset:
print("版本过高,可能存在兼容性问题,建议降级或更新运行时")
# 这里可以尝试降级,但降级可能丢失某些特性
onnx.checker.check_model(model)
return model
3.5 实战:完整的序列化与反序列化流程
最后,我把这些知识点串起来,给你一个完整的实战示例。这个流程我在多个项目中验证过,基本能覆盖 95% 的场景。
import onnx
import hashlib
import tempfile
import os
def safe_onnx_serialize(model, output_path, metadata=None):
"""安全的 ONNX 模型序列化"""
# 1. 添加元数据
if metadata:
for key, value in metadata.items():
meta = model.metadata_props.add()
meta.key = key
meta.value = str(value)
# 2. 保存模型
onnx.save(model, output_path)
# 3. 计算校验和
sha256 = hashlib.sha256()
with open(output_path, 'rb') as f:
sha256.update(f.read())
# 4. 保存校验和到同目录
hash_path = output_path + '.sha256'
with open(hash_path, 'w') as f:
f.write(sha256.hexdigest())
print(f"模型已保存至: {output_path}")
print(f"校验和已保存至: {hash_path}")
return sha256.hexdigest()
def safe_onnx_deserialize(model_path, expected_hash=None):
"""安全的 ONNX 模型反序列化"""
# 1. 校验完整性
if expected_hash:
sha256 = hashlib.sha256()
with open(model_path, 'rb') as f:
sha256.update(f.read())
if sha256.hexdigest() != expected_hash:
raise ValueError("模型完整性校验失败")
# 2. 加载模型
model = onnx.load(model_path)
# 3. 结构校验
onnx.checker.check_model(model)
# 4. 版本兼容性检查
print(f"IR 版本: {model.ir_version}")
print(f"算子集版本: {model.opset_import[0].version}")
return model
# 使用示例
if __name__ == "__main__":
# 创建一个简单的模型用于演示
import onnx.helper as helper
import onnx.numpy_helper as numpy_helper
import numpy as np
# 构建一个简单的加法模型
X = helper.make_tensor_value_info('X', onnx.TensorProto.FLOAT, [None, 3])
Y = helper.make_tensor_value_info('Y', onnx.TensorProto.FLOAT, [None, 3])
Z = helper.make_tensor_value_info('Z', onnx.TensorProto.FLOAT, [None, 3])
node = helper.make_node('Add', ['X', 'Y'], ['Z'])
graph = helper.make_graph([node], 'add_graph', [X, Y], [Z])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid('', 18)])
# 序列化
with tempfile.NamedTemporaryFile(suffix='.onnx', delete=False) as f:
temp_path = f.name
hash_val = safe_onnx_serialize(model, temp_path, {'author': 'AI安全课程'})
# 反序列化
loaded_model = safe_onnx_deserialize(temp_path, hash_val)
print("序列化与反序列化流程完成!")
# 清理
os.remove(temp_path)
os.remove(temp_path + '.sha256')
嗯,到这里,ONNX 模型的序列化与反序列化就讲完了。你可能会问:这些代码在实际项目中够用吗?我的回答是:够用,但要根据你的部署环境做调整。比如在嵌入式设备上,你可能需要自己实现一个轻量级的 Protobuf 解析器,因为完整的 Protobuf 库太大了。这些进阶内容,我们后面的章节会详细展开。