模型转换与格式解析:从训练框架到Tengine的最后一公里

模型训练好之后,离真正跑在边缘设备上还有一段路要走。这段路,就是模型转换与格式解析。我见过不少团队,模型精度刷得挺高,一到部署就卡在格式问题上——ONNX导出报错、算子不兼容、量化后精度掉得离谱。说白了,模型转换不是简单的「另存为」,而是一场格式与算子的精确对接。

这一章,我们就来拆解这件事。

常见模型格式:ONNX、Caffe、TensorFlow

先聊聊目前主流的几种模型格式。你想想看,深度学习框架这么多,PyTorch、TensorFlow、Caffe、PaddlePaddle……每个框架都有自己的序列化方式。模型部署的第一步,就是把这些五花八门的格式统一起来。

格式 所属框架 特点 部署友好度
ONNX 微软/社区 开放标准,框架间互转首选 ★★★★★
Caffe Model Caffe prototxt + caffemodel,结构清晰 ★★★★☆
TensorFlow SavedModel TensorFlow 包含计算图和权重,体积较大 ★★★☆☆
TensorFlow Lite TensorFlow 专为移动端设计,支持量化 ★★★★★

ONNX 是目前最通用的中间格式。我个人习惯把PyTorch模型先转ONNX,再喂给Tengine。为什么?因为ONNX的算子集定义得比较规范,Tengine对ONNX的支持也最成熟。我在项目中遇到过一个问题:用PyTorch直接导出的ONNX,里面有个Resize算子的坐标模式跟Tengine预期的不一样,导致推理结果全错。后来加了个opset_version=11的参数才搞定。

Caffe 格式虽然老,但在工业界存量很大。它的.prototxt文件可读性极强,网络结构一目了然。如果你手头有Caffe模型,Tengine可以直接吃进去,不需要中间转一手。

TensorFlow 的SavedModel就比较「重」了。它把计算图和变量分开存放,目录结构复杂。我建议先用TF的tf2onnx工具转成ONNX,再走Tengine的转换流程。直接转TF模型,有时候会遇到控制流算子(比如tf.while_loop)不支持的情况。

我的建议: 无论原始框架是什么,优先考虑「先转ONNX,再转Tengine」这条路径。ONNX就像模型界的通用语,能帮你避开很多框架特有的坑。

Tengine的tmfile格式:专为边缘推理设计

Tengine自己定义了一种模型格式,叫tmfile。它不是随便拍脑袋搞出来的,而是针对边缘设备的资源限制做了专门优化。

tmfile的核心设计理念就两条:

  • 紧凑:去掉所有冗余信息,只保留推理必需的数据
  • 内存友好:支持直接内存映射加载,不需要额外解析

我对比过同一个MobileNetV2模型,ONNX格式大概14MB,转成tmfile后只有11MB。别小看这3MB,在Flash只有几十兆的嵌入式设备上,每一KB都很珍贵。

tmfile的文件结构大致如下:

tmfile
├── 文件头 (magic number + 版本号 + 校验和)
├── 算子表 (所有用到的算子类型ID)
├── 张量表 (每个张量的名称、形状、数据类型)
├── 网络结构 (节点连接关系)
└── 权重数据 (二进制,按算子顺序排列)

嗯,这里要注意:tmfile的权重数据是连续存储的。这意味着加载时只需要一次mmap,就能把所有权重映射到内存里,省掉了反复fread的开销。我在RK3399上做过测试,tmfile的加载速度比直接读ONNX快了将近3倍。

避坑提醒: tmfile的版本跟Tengine运行时版本必须匹配。我曾经因为Tengine升级到2.0,忘了重新转换模型,结果加载时直接段错误。每次更新Tengine库,记得重新跑一遍模型转换。

模型转换工具:tm_convert_tool

Tengine提供了一个命令行工具——tm_convert_tool,用来把各种格式的模型转成tmfile。这个工具是部署流程中的关键一环。

基本用法很简单:

tm_convert_tool -f onnx -i model.onnx -o model.tmfile
tm_convert_tool -f caffe -i deploy.prototxt -m model.caffemodel -o model.tmfile
tm_convert_tool -f tflite -i model.tflite -o model.tmfile

参数说明:

  • -f:指定输入格式(onnx / caffe / tflite / mxnet / darknet)
  • -i:输入模型文件路径
  • -m:Caffe格式需要额外指定权重文件
  • -o:输出tmfile路径

转换过程中,工具会打印每一层的转换状态。如果遇到不支持的算子,会报错并提示算子名称。这时候就需要手动处理了——要么替换算子,要么写自定义算子插件。

实际案例: 有一次我转一个YOLOv5s模型,转换工具报错说Focus算子不支持。查了一下,Tengine当时还没有实现这个算子。解决方案是把Focus层拆成Slice + Concat的组合,手动修改ONNX图后再转。折腾了半天,但跑起来效果一样。

转换完成后,建议用tm_benchmark工具验证一下:

tm_benchmark -m model.tmfile -r 10

这个命令会跑10次推理,打印每层的耗时和总耗时。如果结果跟原始框架对不上,八成是转换过程中某个算子出了问题。

量化感知训练基础:让模型提前适应低精度

模型转换的最后一步,往往跟量化有关。边缘设备上,FP32推理太慢、太耗电,INT8量化几乎是必选项。

量化有两种主流方式:

  • 训练后量化(Post-Training Quantization, PTQ):模型训练完,用校准集统计激活值的范围,然后直接转INT8。优点是简单,缺点是精度可能掉得厉害。
  • 量化感知训练(Quantization-Aware Training, QAT):在训练过程中模拟量化误差,让模型权重主动适应低精度表示。精度通常比PTQ高1-3个百分点。

QAT的原理其实不复杂。你在训练图里插入FakeQuantize节点,这些节点在前向传播时模拟量化和反量化的过程,让模型「感受」到量化带来的误差。反向传播时,梯度绕过这些节点直接更新权重。训练结束后,去掉FakeQuantize节点,导出INT8模型。

在Tengine里做QAT,我推荐用PyTorch的torch.quantization模块:

import torch
import torch.quantization as quant

# 定义模型
model = MyModel()
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
model = quant.prepare_qat(model, inplace=True)

# 正常训练
for epoch in range(num_epochs):
    for data, target in train_loader:
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

# 转换到量化状态
model = quant.convert(model, inplace=True)
torch.onnx.export(model, dummy_input, "model_qat.onnx")

嗯,这里有个关键点:QAT训练时,学习率要调小一些。我一般设为原来的1/10,因为量化模拟本身会引入噪声,学习率太大容易震荡。

经验之谈: QAT不是万能的。如果模型本身精度就不高(比如Top-1只有60%),量化后大概率掉到50%以下。先保证浮点模型精度足够好,再考虑量化。我通常要求浮点模型至少比目标精度高2-3个百分点,给量化留出余量。

本章知识体系

下面这张图概括了模型转换与格式解析的核心流程:

模型转换与格式解析知识体系 ONNX 模型 Caffe 模型 TensorFlow 模型 tm_convert_tool tmfile 格式 训练后量化 (PTQ) 量化感知训练 (QAT) INT8 tmfile INT8 tmfile (QAT) QAT 训练时需调小学习率,建议为原学习率的 1/10

从这张图可以看得很清楚:无论原始模型是什么格式,最终都要经过tm_convert_tool的转换,生成tmfile。然后根据部署需求,选择PTQ或QAT进行量化。我个人更倾向于QAT,虽然训练时间长一点,但精度有保障。

模型转换这件事,说难不难,说简单也不简单。核心就是三点:格式要统一、算子要兼容、量化要谨慎。把这三点吃透了,模型部署的「最后一公里」就不会卡壳。