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的流程画了张图,方便你理解:

模型转换到NNAPI流程图 TensorFlow/PyTorch 训练好的模型 ONNX格式 跨框架中间表示 自定义格式 私有/遗留格式 TFLite Converter 量化/优化 onnx2tf ONNX→TFLite 自定义解析器 二进制解析+映射 .tflite 模型文件 统一格式,包含权重和计算图 Android NNAPI 推理

从图上你能看到,不管原始模型是什么格式,最终都要汇聚到.tflite这个统一格式上,再交给NNAPI去执行。我个人习惯在转换过程中多做几次验证——每转一步就推理一次,确保精度没有明显下降。

11.5 实战中的避坑指南

最后分享几个我踩过的坑:

  • 动态shape问题:NNAPI不支持动态输入尺寸。如果你的模型输入是可变大小的,要么固定输入尺寸,要么用多个模型分别处理不同尺寸。我曾经因为没注意这个,在线上出了事故。
  • 算子兼容性:不是所有TFLite算子都能被NNAPI加速。转换后可以用NNAPI DelegategetSupportedOperations()方法检查哪些算子跑在NPU上。
  • 量化校准数据:做INT8量化时,校准数据集要能代表真实场景。我用过随机噪声做校准,结果模型精度掉了5个点,后来换成真实数据就好了。

总结一下:模型转换的核心是格式统一和精度保持。TFLite是终点,ONNX是桥梁,自定义格式需要自己搭桥。每一步转换都要验证,别偷懒。

好了,这一章的内容就这些。模型转换这块,多动手试试就熟了。


公众号:蓝海资料掘金营,微信deep3321