11、模型转换与格式支持:TensorFlow Lite模型(.tflite)、ONNX模型转换、自定义模型格式适配
模型转换这件事,说白了就是让不同框架训练出来的模型,能在Android的NNAPI上跑起来。我刚开始接触这块时,也踩过不少坑——明明在PC上推理得好好的模型,一搬到手机上就各种报错。后来才明白,模型格式转换不是简单的文件后缀改名,里面门道多着呢。
11.1 TensorFlow Lite模型(.tflite)的生成与优化
TensorFlow Lite是目前Android上最主流的移动端模型格式。我个人习惯用TF2.x训练模型,然后转成.tflite。转换过程其实不复杂,但有几个关键点要注意。
11.1.1 基本转换流程
先看一个最基础的转换示例:
import tensorflow as tf
# 加载训练好的模型
model = tf.keras.models.load_model('my_model.h5')
# 转换器
converter = tf.lite.TFLiteConverter.from_keras_model(model)
# 执行转换
tflite_model = converter.convert()
# 保存
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
嗯,这里要注意:如果你的模型用了自定义层,转换前得先注册。我曾经因为忘了这步,折腾了整整一下午。
11.1.2 量化:让模型更轻量
为什么要量化?你想想看,手机上内存和电量都有限。FP32的模型参数,转成INT8后体积直接缩到四分之一,推理速度也能快不少。
量化方式对比:
| 量化类型 | 精度损失 | 体积缩减 | 适用场景 |
|---|---|---|---|
| 动态范围量化 | 较小 | 约4倍 | 通用场景 |
| FP16量化 | 几乎无损 | 约2倍 | GPU推理 |
| INT8全整数量化 | 中等 | 约4倍 | CPU/边缘设备 |
代码实现动态范围量化:
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()
我的经验:如果模型对精度要求极高(比如医疗影像),建议先用FP16量化试试。INT8虽然压缩率高,但有些层会掉点严重。
11.2 ONNX模型转换:跨框架的桥梁
ONNX(Open Neural Network Exchange)是个好东西。它让PyTorch训练的模型也能跑到Android上。我有个项目就是用PyTorch训练的目标检测模型,通过ONNX转成NNAPI可用的格式。
11.2.1 PyTorch → ONNX → TFLite
这个链路我走过好几遍,流程大概是:
# 第一步:PyTorch导出ONNX
import torch
import torch.onnx
model = torch.load('pytorch_model.pth')
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "model.onnx",
input_names=['input'],
output_names=['output'],
dynamic_axes={'input': {0: 'batch_size'},
'output': {0: 'batch_size'}})
# 第二步:ONNX转TFLite(需要onnx2tf工具)
# 命令行:onnx2tf -i model.onnx -o tflite_model
避坑指南:我曾经在转换一个带BatchNormalization层的模型时,发现ONNX导出的节点顺序和TFLite期望的不一致。解决办法是在导出ONNX时设置training=False,并且用torch.no_grad()包裹。
11.2.2 ONNX支持的算子限制
不是所有PyTorch算子都能转成ONNX。我整理了一个常用算子支持表:
| 算子类型 | 支持情况 | 常见问题 |
|---|---|---|
| Conv2D、MaxPool、ReLU | 完全支持 | 无 |
| BatchNormalization | 支持(需注意模式) | 训练/推理模式混淆 |
| Reshape、Transpose | 支持 | 动态shape需额外处理 |
| 自定义算子 | 不支持 | 需用ONNX自定义算子扩展 |
11.3 自定义模型格式适配
有时候你会遇到一些非标准的模型格式,比如公司内部自研的推理引擎格式,或者从某些老旧框架导出的模型。这时候就得自己写适配器了。
11.3.1 自定义格式解析器
我做过一个项目,模型文件是二进制的自定义格式,里面包含了权重、偏置和网络结构描述。解析思路是这样的:
public class CustomModelParser {
public static Model parse(byte[] data) {
// 1. 解析头部信息(魔数、版本号)
ByteBuffer buffer = ByteBuffer.wrap(data);
int magic = buffer.getInt();
if (magic != 0xCUSTOM) {
throw new IllegalArgumentException("无效的模型格式");
}
// 2. 读取层数
int layerCount = buffer.getInt();
List<Layer> layers = new ArrayList<>();
// 3. 逐层解析
for (int i = 0; i < layerCount; i++) {
int layerType = buffer.getInt();
int weightSize = buffer.getInt();
float[] weights = new float[weightSize];
for (int j = 0; j < weightSize; j++) {
weights[j] = buffer.getFloat();
}
layers.add(new Layer(layerType, weights));
}
return new Model(layers);
}
}
我的建议:写自定义解析器时,一定要先搞清楚模型文件的二进制布局。我习惯先用Python写个解析脚本验证,确认无误后再移植到Android上。
11.3.2 适配NNAPI的ANeuralNetworksModel
解析完自定义格式后,需要把它映射到NNAPI的模型结构。核心是构建操作数(operand)和操作(operation):
// 创建NNAPI模型
ANeuralNetworksModel* model = nullptr;
ANeuralNetworksModel_create(&model);
// 添加输入张量
uint32_t inputDim[] = {1, 224, 224, 3};
ANeuralNetworksModel_addOperand(model, /* 操作数类型 */);
ANeuralNetworksModel_setOperandValue(model, inputOperand, inputData, dataSize);
// 添加卷积操作
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_CONV_2D,
inputCount, inputOperands,
outputCount, outputOperands);
// 标识输入输出
ANeuralNetworksModel_identifyInputsAndOutputs(model, inputCount, inputIndexes,
outputCount, outputIndexes);
// 编译
ANeuralNetworksCompilation* compilation;
ANeuralNetworksCompilation_create(model, &compilation);
ANeuralNetworksCompilation_finish(compilation);
注意:NNAPI的操作数类型和自定义格式中的类型可能不一致。比如自定义格式用float32,但NNAPI可能要求用TENSOR_FLOAT32。转换时一定要做类型映射,否则会报ANEURALNETWORKS_BAD_DATA错误。
11.4 模型转换的整体流程
我把整个模型转换到NNAPI的流程画了张图,方便你理解:
从图上你能看到,不管原始模型是什么格式,最终都要汇聚到.tflite这个统一格式上,再交给NNAPI去执行。我个人习惯在转换过程中多做几次验证——每转一步就推理一次,确保精度没有明显下降。
11.5 实战中的避坑指南
最后分享几个我踩过的坑:
- 动态shape问题:NNAPI不支持动态输入尺寸。如果你的模型输入是可变大小的,要么固定输入尺寸,要么用多个模型分别处理不同尺寸。我曾经因为没注意这个,在线上出了事故。
- 算子兼容性:不是所有TFLite算子都能被NNAPI加速。转换后可以用
NNAPI Delegate的getSupportedOperations()方法检查哪些算子跑在NPU上。 - 量化校准数据:做INT8量化时,校准数据集要能代表真实场景。我用过随机噪声做校准,结果模型精度掉了5个点,后来换成真实数据就好了。
总结一下:模型转换的核心是格式统一和精度保持。TFLite是终点,ONNX是桥梁,自定义格式需要自己搭桥。每一步转换都要验证,别偷懒。
好了,这一章的内容就这些。模型转换这块,多动手试试就熟了。
公众号:蓝海资料掘金营,微信deep3321