模型文件格式解析: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 文件上生产环境。

好了,这一章的内容就到这里。下一章我们会深入讨论如何对这些模型文件进行加密——包括哪些部分必须加密、哪些可以保留明文、以及加密后如何保证推理性能不受影响。到时候我会分享一个我实际用过的加密方案,保证让你少走弯路。