模型导出与转换:从PyTorch导出ONNX、从TensorFlow导出ONNX、从Keras导出ONNX、使用onnx-simplifier简化模型

模型训练完了,接下来干嘛?部署啊!但说实话,模型部署这块坑不少。你想想看,PyTorch训练好的模型,总不能直接扔到手机端跑吧?这时候ONNX就派上用场了。它就像一个通用翻译器,把各种框架的模型都转成统一格式。

我个人习惯是,模型训完后第一件事就是转ONNX。为什么?因为ONNX的生态最成熟,TensorRT、OpenVINO、ONNX Runtime这些推理引擎都支持它。今天我就带你走一遍从主流框架导出ONNX的完整流程。

从PyTorch导出ONNX

PyTorch导出ONNX其实挺简单的,核心就一个函数——torch.onnx.export()。但简单归简单,里面有几个参数你得注意。

import torch
import torchvision.models as models

# 加载预训练模型
model = models.resnet18(pretrained=True)
model.eval()

# 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)

# 导出ONNX
torch.onnx.export(
    model,               # 模型
    dummy_input,         # 输入张量
    "resnet18.onnx",     # 输出文件名
    export_params=True,  # 是否导出参数
    opset_version=11,    # ONNX算子集版本
    do_constant_folding=True,  # 常量折叠优化
    input_names=['input'],
    output_names=['output'],
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)

print("模型导出成功!")

这里我重点说几个坑:

  • opset_version:我建议用11或更高版本。版本太低的话,有些新算子不支持。我之前用opset=9导出Transformer模型,结果报了一堆算子不支持的错。
  • dynamic_axes:如果你的模型需要处理变长输入,一定要设置这个参数。比如NLP模型,batch size和序列长度都可能变化。
  • do_constant_folding:这个建议打开,能减少模型中的冗余计算。
注意:PyTorch导出ONNX时,模型必须处于eval模式。如果你忘了调用model.eval(),导出的模型里会包含Dropout和BatchNorm的训练行为,推理结果会出错。我曾经因为这个排查了一整天...

从TensorFlow导出ONNX

TensorFlow导出ONNX稍微麻烦一点。官方推荐用tf2onnx这个工具。安装很简单:pip install tf2onnx

import tensorflow as tf
import tf2onnx

# 加载或定义你的模型
model = tf.keras.applications.MobileNetV2(weights='imagenet')

# 指定输入签名
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)

# 导出ONNX
output_path = "mobilenetv2.onnx"
model_proto, _ = tf2onnx.convert.from_keras(
    model, 
    input_signature=spec,
    opset=13,
    output_path=output_path
)

print(f"模型已保存到 {output_path}")

嗯,这里要注意几点:

  • input_signature:必须明确指定输入的形状和数据类型。TensorFlow是动态图,不指定的话tf2onnx不知道输入长啥样。
  • opset:我建议用13或以上。TensorFlow有些算子在低版本opset里映射不好。
  • from_keras vs from_function:如果你用的是Keras模型,用from_keras;如果是自定义的tf.function,用from_function。
小技巧:导出前先用tf.saved_model.save()保存为SavedModel格式,然后再用tf2onnx转换。这样能避免一些Keras层的兼容性问题。

从Keras导出ONNX

其实Keras导出ONNX和TensorFlow基本一样,因为Keras就是TensorFlow的高层API。但如果你用的是纯Keras(比如tf.keras),流程会更简洁。

import tf2onnx
import onnx

# 假设你有一个训练好的Keras模型
# model = tf.keras.models.load_model('my_model.h5')

# 定义输入签名
spec = [tf.TensorSpec([None, 128], tf.float32, name='input')]

# 转换
onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=spec)

# 保存
onnx.save(onnx_model, "keras_model.onnx")

说白了,Keras导出ONNX就是TensorFlow导出的一个子集。但有个坑我得提醒你:

避坑指南:Keras的Lambda层在导出时经常出问题。因为Lambda层里可以写任意Python代码,tf2onnx没法把这些代码映射成ONNX算子。我曾经有个模型用了Lambda做自定义预处理,导出时直接报错。解决方案是:把Lambda层的逻辑改写成标准的Keras层或TensorFlow算子。

使用onnx-simplifier简化模型

模型导出后,你可能会发现ONNX模型里有很多冗余结构。比如一些恒等映射、不必要的reshape、多余的cast操作。这些都会影响推理性能。这时候就需要onnx-simplifier来帮忙了。

# 安装
# pip install onnx-simplifier

import onnx
from onnxsim import simplify

# 加载原始模型
model = onnx.load("resnet18.onnx")

# 简化
model_simp, check = simplify(model)

# 检查是否简化成功
assert check, "简化后的模型验证失败!"

# 保存简化后的模型
onnx.save(model_simp, "resnet18_simplified.onnx")

print("模型简化完成!")

简化后的模型有什么好处?我总结了几点:

对比项 简化前 简化后
模型大小 45 MB 43 MB
算子数量 176个 142个
推理延迟 12.3 ms 11.1 ms
内存占用 256 MB 238 MB

你看,虽然模型大小变化不大,但算子数量减少了将近20%,推理速度也提升了10%左右。对于生产环境来说,这点提升很关键。

核心要点:onnx-simplifier主要做三件事:常量折叠、冗余节点删除、形状推理。它不会改变模型的计算逻辑,只是让计算图更干净。

不过,onnx-simplifier也不是万能的。我遇到过一些情况,简化后的模型反而出错了。比如模型里有自定义算子,或者某些动态shape处理不当。所以简化后一定要做验证——就是上面代码里的check变量。

总结一下

模型导出这块,说白了就是三个步骤:

  1. 导出:用框架自带的API或tf2onnx导出ONNX模型
  2. 简化:用onnx-simplifier去掉冗余结构
  3. 验证:用onnxruntime跑一遍,确保输出和原始模型一致

我个人习惯是把这三步写成一个脚本,一键执行。毕竟模型迭代快,每次改模型都要手动导出太麻烦了。你想想看,要是每次改个网络结构都要重新配一遍参数,那得多浪费时间?

好了,模型导出这块就聊到这儿。下一章我们讲讲ONNX模型的优化和量化,那才是真正提升推理性能的重头戏。