模型转换与格式解析:从训练框架到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)不支持的情况。
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倍。
模型转换工具: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,因为量化模拟本身会引入噪声,学习率太大容易震荡。
本章知识体系
下面这张图概括了模型转换与格式解析的核心流程:
从这张图可以看得很清楚:无论原始模型是什么格式,最终都要经过tm_convert_tool的转换,生成tmfile。然后根据部署需求,选择PTQ或QAT进行量化。我个人更倾向于QAT,虽然训练时间长一点,但精度有保障。
模型转换这件事,说难不难,说简单也不简单。核心就是三点:格式要统一、算子要兼容、量化要谨慎。把这三点吃透了,模型部署的「最后一公里」就不会卡壳。