模型文件格式解析:ONNX、SavedModel、PyTorch .pt
做模型加密和安全部署,第一步不是写代码,而是搞清楚你手里拿的到底是个什么东西。我见过太多团队,加密方案设计得天花乱坠,结果连模型文件里藏着什么关键信息都没搞明白——说白了,连门都没找对。
今天咱们就来拆解三种最常见的模型格式:ONNX、TensorFlow SavedModel、PyTorch .pt。我会结合自己踩过的坑,带你看看这些文件内部到底长什么样,以及怎么把元数据安全地提取出来。
ONNX 格式:跨平台的“通用语言”
ONNX 是我个人最推荐的中间格式。为什么?因为它把模型结构、权重、甚至训练时的超参数都打包进了一个文件里。你想想看,一个 .onnx 文件,本质上就是一个 Protocol Buffers 序列化的数据。
文件结构其实很简单:
- 模型图(Graph):定义了所有算子节点和它们之间的连接关系
- 权重张量(Initializer):所有训练好的参数,以 TensorProto 形式存储
- 元数据(Metadata):包括模型版本、作者、描述、opset 版本等
- 输入输出信息:每个 tensor 的名称、数据类型、形状
我在项目中遇到过一件事:有个同事导出的 ONNX 模型死活跑不通推理,查了两天,最后发现是 opset 版本不匹配。嗯,这里要注意——ONNX 的 opset 版本决定了你支持哪些算子,跨版本部署时一定要检查。
核心要点:ONNX 文件是自包含的,这意味着你不需要原始训练框架就能加载它。这对安全部署来说是个巨大优势——你可以完全隔离训练环境。
来看看怎么用 Python 提取 ONNX 的元数据:
import onnx
model = onnx.load("model.onnx")
# 提取元数据
metadata = {
"ir_version": model.ir_version,
"producer_name": model.producer_name,
"producer_version": model.producer_version,
"domain": model.domain,
"model_version": model.model_version,
"doc_string": model.doc_string
}
# 提取输入输出信息
for input_tensor in model.graph.input:
print(f"输入: {input_tensor.name}, 形状: {input_tensor.type.tensor_type.shape}")
for output_tensor in model.graph.output:
print(f"输出: {output_tensor.name}, 形状: {output_tensor.type.tensor_type.shape}")
我的小技巧:提取元数据时,别忘了检查 model.graph.initializer 里的权重信息。有时候攻击者会在这里藏一些“后门张量”,我曾在一次安全审计中发现过这种情况。
TensorFlow SavedModel:目录结构里的秘密
SavedModel 跟 ONNX 不一样,它不是一个文件,而是一个目录。我第一次接触时也觉得奇怪——为什么不用单个文件?后来才明白,这是为了支持更灵活的部署场景。
典型的 SavedModel 目录结构:
saved_model/
├── saved_model.pb # 模型图定义(MetaGraphDef)
├── variables/
│ ├── variables.index # 变量索引文件
│ └── variables.data-00000-of-00001 # 权重数据
└── assets/ # 外部资源文件(可选)
说实话,这个结构在安全部署时有点麻烦。因为你要保护的不只是一个文件,而是一整个目录。我曾经犯过一个错误:只加密了 .pb 文件,忘了处理 variables 目录——结果模型加载时直接报错。
避坑指南:加密 SavedModel 时,必须把整个目录作为一个整体来处理。我曾经只加密了 .pb 文件,结果部署时 variables 目录里的权重完全暴露了——那叫一个尴尬。
提取元数据的方法:
import tensorflow as tf
# 加载 SavedModel
loaded = tf.saved_model.load("saved_model/")
# 提取签名信息
signatures = loaded.signatures
for name, sig in signatures.items():
print(f"签名: {name}")
for input_name, input_spec in sig.structured_inputs.items():
print(f" 输入: {input_name}, 类型: {input_spec.dtype}, 形状: {input_spec.shape}")
for output_name, output_spec in sig.structured_outputs.items():
print(f" 输出: {output_name}, 类型: {output_spec.dtype}, 形状: {output_spec.shape}")
# 提取变量信息
for var in loaded.variables:
print(f"变量: {var.name}, 形状: {var.shape}, dtype: {var.dtype}")
PyTorch .pt 文件:最灵活的“黑盒子”
PyTorch 的 .pt 文件,说白了就是一个 Python 的 pickle 序列化对象。这意味着它可以是任何东西——模型状态字典、完整模型对象、甚至是一个包含多个组件的字典。
我见过最离谱的情况:有人把整个训练脚本都 pickle 进了 .pt 文件里。你想想看,这有多危险?攻击者完全可以反序列化后执行任意代码。
常见的 .pt 文件内容:
| 存储方式 | 内容 | 安全性 |
|---|---|---|
| state_dict | 仅权重参数(推荐) | 高 |
| 完整模型 | 模型结构 + 权重 | 中 |
| 自定义字典 | 可包含任意 Python 对象 | 低 |
安全建议:永远不要加载来源不明的 .pt 文件。pickle 反序列化可以执行任意代码——这不是开玩笑的。我建议在安全部署时,只使用 state_dict 格式,并且用 torch.jit.script 或 torch.onnx.export 转换成更安全的格式。
提取元数据的正确姿势:
import torch
# 安全加载:只加载 state_dict
checkpoint = torch.load("model.pt", map_location="cpu", weights_only=True)
# 提取元数据
if "metadata" in checkpoint:
print("模型元数据:", checkpoint["metadata"])
# 提取模型结构信息(如果有)
if "model_config" in checkpoint:
print("模型配置:", checkpoint["model_config"])
# 提取训练信息
if "optimizer_state_dict" in checkpoint:
print("优化器状态已保存,包含梯度信息")
# 查看所有键
print("文件包含的键:", list(checkpoint.keys()))
个人经验:我习惯在训练脚本里就把元数据写进 checkpoint:
torch.save({
'model_state_dict': model.state_dict(),
'metadata': {
'model_name': 'ResNet50',
'input_shape': [1, 3, 224, 224],
'num_classes': 1000,
'training_date': '2024-01-15',
'framework_version': torch.__version__
}
}, "model.pt")
这样部署时一眼就能看出模型的基本信息,不用再去猜。
三种格式对比:选哪个更安全?
| 特性 | ONNX | SavedModel | .pt |
|---|---|---|---|
| 文件形式 | 单个 .onnx 文件 | 目录(多个文件) | 单个 .pt 文件 |
| 序列化方式 | Protocol Buffers | Protobuf + 自定义格式 | Pickle |
| 安全性 | 高(结构化数据) | 中(需保护整个目录) | 低(pickle 风险) |
| 元数据提取 | 简单(API 直接支持) | 中等(需解析签名) | 灵活(但需小心) |
| 跨平台性 | 优秀 | 良好 | 差(依赖 PyTorch) |
我个人建议:如果要做安全部署,优先考虑 ONNX 格式。它结构清晰、自包含、没有 pickle 的安全隐患。如果必须用 PyTorch,至少转成 TorchScript 再部署——别直接拿 .pt 文件上生产环境。
好了,这一章的内容就到这里。下一章我们会深入讨论如何对这些模型文件进行加密——包括哪些部分必须加密、哪些可以保留明文、以及加密后如何保证推理性能不受影响。到时候我会分享一个我实际用过的加密方案,保证让你少走弯路。