模型导出:从训练框架到部署的桥梁
模型训练完了,怎么把它送到推理引擎手里?这一步,说白了就是模型导出。我做了这么多年模型部署,见过太多人在这一步翻车。今天咱们就聊聊 PyTorch 和 TensorFlow 这两大主流框架的导出方式。
你想想看,训练框架里跑得好好的模型,到了部署环境可能连 Python 都没有。所以我们需要一个中间表示——既保留模型的计算图,又去掉训练相关的包袱。
PyTorch 模型导出:TorchScript 与 ONNX
PyTorch 的导出方式,我个人习惯分成两条路:TorchScript 和 ONNX。前者是 PyTorch 自家的,后者是跨框架的。
torch.jit.trace 与 torch.jit.script
先说 torch.jit.trace。它的原理很简单:给模型一个输入,跑一遍前向,把执行路径记录下来。嗯,这里要注意——它只记录实际执行到的代码路径。
核心区别:trace 是追踪执行路径,script 是编译整个模型代码。
# trace 方式
import torch
class MyModel(torch.nn.Module):
def forward(self, x, use_dropout=False):
if use_dropout:
x = torch.dropout(x, 0.5, train=False)
return x
model = MyModel()
example_input = torch.randn(1, 3, 224, 224)
# 注意:这里 use_dropout 是 False
traced_model = torch.jit.trace(model, example_input)
# 问题来了:如果调用时 use_dropout=True
# traced_model 不会执行 dropout 分支!
我在项目中遇到过类似的问题。有一次导出一个带条件分支的模型,trace 出来的结果在特定输入下直接崩了。排查了半天,才发现是 trace 没记录到那个分支。
那 torch.jit.script 呢?它会分析你的 Python 代码,把整个控制流都编译进去。代价是——有些 Python 语法它不支持,比如动态的字典操作。
# script 方式
@torch.jit.script
def my_script_fn(x: torch.Tensor, n: int) -> torch.Tensor:
result = torch.zeros_like(x)
for i in range(n):
result += x * i
return result
# 这个就能正确处理循环了
scripted_fn = my_script_fn
我的建议:能用 trace 就用 trace,它简单可靠。遇到控制流复杂的模型,再考虑 script。如果 script 报语法错误,可以尝试把复杂逻辑拆成子模块。
torch.onnx.export:跨框架的桥梁
ONNX 是模型部署的通用语言。PyTorch 导出 ONNX 的核心函数就是 torch.onnx.export。
import torch.onnx
model = torchvision.models.resnet18(pretrained=True)
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model, # 模型
dummy_input, # 示例输入
"resnet18.onnx", # 输出路径
export_params=True, # 导出参数
opset_version=11, # ONNX opset 版本
do_constant_folding=True, # 常量折叠优化
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
这里有个关键参数——opset_version。我刚开始做 ONNX 导出时,习惯用最新的 opset。后来发现,目标推理引擎可能不支持那么新的算子。比如 TensorRT 8.x 对 opset 13 支持得最好,opset 17 就有不少算子不兼容。
我曾经踩过的坑:导出 ONNX 时用了 opset 17,结果在 TensorRT 里报算子不支持。后来降到 opset 13,一切正常。建议先查目标引擎支持的 opset 版本,再决定导出参数。
动态轴(dynamic_axes)也很重要。如果你的模型需要处理不同 batch size 的输入,一定要设置这个。否则导出的 ONNX 会固定 batch size 为 1。
TensorFlow 模型导出:三种主流格式
TensorFlow 的导出方式比 PyTorch 复杂一些。我把它归纳为三种:SavedModel、Frozen Graph、TF-TRT。
SavedModel:TF 的标准格式
SavedModel 是 TensorFlow 官方推荐的导出格式。它包含了模型的计算图、权重、以及 serving 相关的签名信息。
import tensorflow as tf
# 构建模型
model = tf.keras.Sequential([
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dense(10, activation='softmax')
])
# 训练...(省略)
# 导出 SavedModel
model.save('saved_model/my_model', save_format='tf')
# 或者用 tf.saved_model.save
tf.saved_model.save(model, 'saved_model/my_model_v2')
SavedModel 的好处是完整——它把模型的训练状态、优化器参数都保存了。但部署时我们通常只需要推理,所以会去掉训练相关的部分。
实际经验:SavedModel 在 TF Serving 里用得最多。如果你用 TensorFlow Serving 部署,直接导出 SavedModel 就行,省事。
Frozen Graph:轻量级部署格式
Frozen Graph 是把计算图和权重合并成一个文件。它去掉了训练相关的节点,体积更小,加载更快。
import tensorflow as tf
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
# 加载 SavedModel
loaded = tf.saved_model.load('saved_model/my_model')
infer = loaded.signatures['serving_default']
# 转换为 frozen graph
frozen_func = convert_variables_to_constants_v2(infer)
graph_def = frozen_func.graph.as_graph_def()
# 保存
with tf.io.gfile.GFile('model_frozen.pb', 'wb') as f:
f.write(graph_def.SerializeToString())
我记得有一次在嵌入式设备上部署模型,SavedModel 加载要 3 秒,换成 Frozen Graph 后只要 0.5 秒。原因很简单——省去了解析训练节点的开销。
适用场景:Frozen Graph 特别适合资源受限的环境,比如手机端、边缘设备。它也是 TensorFlow Lite 转换的常用中间格式。
TF-TRT:NVIDIA 的加速方案
TF-TRT 是 TensorFlow 与 TensorRT 的集成方案。它可以在 TensorFlow 内部调用 TensorRT 的优化器,对模型进行层融合、精度校准等操作。
import tensorflow as tf
from tensorflow.python.compiler.tensorrt import trt_convert as trt
# 配置 TF-TRT 参数
converter = trt.TrtGraphConverterV2(
input_saved_model_dir='saved_model/my_model',
precision_mode='FP16', # FP16 精度
max_workspace_size_bytes=1 << 30, # 1GB 工作空间
maximum_cached_engines=100
)
# 转换
converter.convert()
# 保存
converter.save('trt_model')
TF-TRT 的好处是——你不需要把模型导出成 ONNX 再转 TensorRT。直接在 TensorFlow 里就能享受 TensorRT 的加速。但代价是灵活性受限,有些自定义算子可能不支持。
注意:TF-TRT 的精度模式选择要谨慎。FP16 能带来 2 倍左右的加速,但某些模型精度会下降。我建议先用 FP32 跑一遍 baseline,再对比 FP16 的结果。
三种导出方式的对比
| 格式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| SavedModel | TF Serving、训练后继续训练 | 完整保存所有信息 | 体积大、加载慢 |
| Frozen Graph | 移动端、边缘设备、TFLite | 体积小、加载快 | 无法继续训练 |
| TF-TRT | NVIDIA GPU 推理加速 | 自动优化、集成方便 | 依赖 GPU、算子受限 |
知识体系总览
下面这张图展示了模型导出的整体流程和各个格式之间的关系:
从这张图可以看得很清楚:无论你用 PyTorch 还是 TensorFlow,最终都要经过中间表示层,才能到达目标部署环境。我个人建议,团队里最好统一一种中间格式,比如 ONNX,这样不管前端用什么框架训练,后端都能对接。
好了,模型导出这块就聊到这儿。记住一点:导出不是终点,而是部署的起点。选对导出方式,后面能省很多事。
公众号:蓝海资料掘金营,微信deep3321