3、模型格式与转换测试:TFLite、ONNX、NCNN格式验证、转换工具的正确性测试

模型训练出来,只是万里长征走了一半。另一半,就是把它塞进嵌入式设备里跑起来。这一步,卡住了无数人。

我见过太多团队,模型在PC上跑得飞起,一上板子就崩。或者精度从95%掉到60%。问题出在哪?十有八九,是模型格式转换这关没把好。

今天我们就聊聊这个。TFLite、ONNX、NCNN,这三个格式,基本覆盖了主流嵌入式场景。怎么验证它们?怎么保证转换工具没坑?我把自己踩过的雷,一个一个说给你听。

3.1 为什么转换测试这么重要?

说白了,不同框架有不同算子。PyTorch的算子,ONNX不一定支持。ONNX支持的,TFLite可能又砍了一刀。转换过程,本质上是个「翻译」工作。翻译错了,意思就变了。

我曾经接手过一个项目,模型在TensorFlow里精度92%。转成TFLite后,直接掉到78%。查了两天,发现是一个量化参数传错了。嗯,从那以后,我每次转换完,第一件事就是做精度对齐测试。

核心原则:转换后的模型,输出必须和原始模型一致。误差允许范围,通常控制在1e-5以内。超过这个数,就要警惕了。

3.2 TFLite格式验证

TFLite是Google主推的移动端格式。它支持量化,支持委派(Delegate),在ARM设备上表现不错。但它的算子支持列表,其实比TensorFlow窄很多。

3.2.1 转换工具的正确性测试

转换工具主要是tf.lite.TFLiteConverter。我习惯分三步走:

  1. 保存原始模型:用tf.saved_model.save导出,保留完整计算图。
  2. 执行转换:指定输入输出张量,设置量化参数。
  3. 对比输出:用同一组输入数据,分别跑原始模型和TFLite模型,比对结果。
import tensorflow as tf
import numpy as np

# 1. 加载原始模型
model = tf.keras.models.load_model('original_model.h5')

# 2. 转换
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()

# 3. 保存
with open('model.tflite', 'wb') as f:
    f.write(tflite_model)

# 4. 推理对比
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

input_data = np.random.randn(1, 224, 224, 3).astype(np.float32)

# 原始模型输出
original_output = model.predict(input_data)

# TFLite输出
interpreter.set_tensor(interpreter.get_input_details()[0]['index'], input_data)
interpreter.invoke()
tflite_output = interpreter.get_tensor(interpreter.get_output_details()[0]['index'])

# 计算误差
diff = np.max(np.abs(original_output - tflite_output))
print(f'最大误差: {diff}')

小技巧:如果误差大于1e-3,先检查输入输出的数据类型。TFLite默认用float32,但量化后可能变成uint8。类型不匹配,误差会很大。

3.2.2 常见踩坑点

  • 动态形状不支持:TFLite要求输入形状固定。我遇到过模型输入是None,转换直接报错。解决办法是显式指定input_shapes
  • 自定义算子:如果模型里有自定义层,TFLite不认识。要么注册算子,要么替换成标准算子。
  • 量化校准数据:做INT8量化时,需要提供校准数据集。数据分布不对,量化后精度会崩。我曾经用ImageNet的100张图做校准,结果模型在真实场景下完全不能用。后来改用目标场景的500张图,问题解决。

3.3 ONNX格式验证

ONNX是微软和Facebook推的开放格式。它的优势是生态广,PyTorch、TensorFlow、MXNet都能转。但问题也在这——框架太多,兼容性参差不齐。

3.3.1 转换工具链

PyTorch转ONNX,用torch.onnx.export。TensorFlow转ONNX,用tf2onnx。我建议你每次转换后,都用onnxruntime跑一遍推理,和原始模型做对比。

import torch
import onnx
import onnxruntime
import numpy as np

# 1. 导出ONNX
model = torch.load('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'}})

# 2. 验证ONNX模型结构
onnx_model = onnx.load('model.onnx')
onnx.checker.check_model(onnx_model)

# 3. 用onnxruntime推理
ort_session = onnxruntime.InferenceSession('model.onnx')
ort_inputs = {ort_session.get_inputs()[0].name: dummy_input.numpy()}
ort_outputs = ort_session.run(None, ort_inputs)

# 4. 对比
with torch.no_grad():
    torch_output = model(dummy_input).numpy()

diff = np.max(np.abs(torch_output - ort_outputs[0]))
print(f'ONNX与PyTorch最大误差: {diff}')

警告:ONNX的算子版本一直在更新。你导出的模型,可能在某些旧版本推理引擎上跑不了。我建议你导出时指定opset_version,比如opset_version=11,兼容性最好。

3.3.2 精度验证策略

我个人习惯,用三组数据做验证:

测试数据 目的 样本量
随机噪声 检查数值稳定性 100组
真实样本 检查实际精度 1000组
边界样本 检查极端情况 50组

边界样本怎么找?我一般用对抗样本,或者输入全0、全1、极大值、极小值。这些数据最容易暴露问题。

3.4 NCNN格式验证

NCNN是腾讯开源的移动端推理框架。它在ARM平台上的优化做得很好,尤其对骁龙和麒麟芯片。但它的工具链相对封闭,转换过程容易出问题。

3.4.1 转换流程

NCNN的转换工具是ncnn2onnxonnx2ncnn。我建议你走ONNX中转,而不是直接从PyTorch转NCNN。原因很简单:ONNX的生态更成熟,出问题好排查。

# 1. PyTorch -> ONNX
python -m torch.onnx.export model.pth model.onnx

# 2. ONNX -> NCNN
./onnx2ncnn model.onnx model.param model.bin

# 3. 验证
./ncnn2table model.param model.bin input_data.bin output_ref.bin
./ncnn2int8 model.param model.bin model_int8.param model_int8.bin table.bin

提示:NCNN的ncnn2table工具,可以用来做量化校准。但它要求输入数据是二进制格式。我写了个小脚本,把图片转成.bin文件,方便批量处理。

3.4.2 常见问题

  • 算子不支持:NCNN的算子列表比ONNX少。遇到不支持的算子,要么替换,要么自己实现。我遇到过Resize算子,NCNN只支持最近邻插值,不支持双线性。最后我改成了Interp算子。
  • 内存对齐:NCNN对内存对齐要求严格。输入数据没对齐,推理结果会错。我建议你用ncnn::Matfrom_pixels方法加载数据,它会自动处理对齐。
  • 多输入模型:NCNN支持多输入,但输入顺序要和模型定义一致。我曾经搞反了两个输入,结果模型输出完全不对。排查了半天才发现。

3.5 自动化测试框架

手动测试一次两次还行,项目迭代起来,每次改模型都要测一遍,那就得自动化了。我写了个简单的测试框架,分享给你:

import json
import numpy as np

class ModelConverterTester:
    def __init__(self, original_model, test_data):
        self.original = original_model
        self.test_data = test_data
        self.results = {}

    def test_conversion(self, converter_func, format_name):
        """测试转换后的模型"""
        converted_model = converter_func(self.original)
        original_out = self.original.predict(self.test_data)
        converted_out = converted_model.predict(self.test_data)

        diff = np.max(np.abs(original_out - converted_out))
        self.results[format_name] = {
            'max_error': float(diff),
            'passed': diff < 1e-5
        }
        return self.results[format_name]

    def report(self):
        """生成测试报告"""
        for fmt, result in self.results.items():
            status = '✅' if result['passed'] else '❌'
            print(f'{status} {fmt}: 最大误差 {result["max_error"]:.2e}')
        return self.results

# 使用示例
tester = ModelConverterTester(original_model, test_input)
tester.test_conversion(tflite_converter, 'TFLite')
tester.test_conversion(onnx_converter, 'ONNX')
tester.test_conversion(ncnn_converter, 'NCNN')
tester.report()

建议:把这个测试框架集成到CI/CD流水线里。每次模型更新,自动跑一遍转换测试。发现问题,立刻告警。别等到上线了才发现模型是坏的。

3.6 总结

模型格式转换,看着简单,坑不少。我给你的建议是:

  • 转换后必做精度对比,误差超过1e-5就要查原因。
  • 用ONNX做中转,兼容性最好,工具链最成熟。
  • 自动化测试,别靠人工一遍遍跑。
  • 保留原始模型,方便回溯问题。

你想想看,如果模型在PC上精度95%,上板子后变成60%,这个锅谁来背?大概率是转换过程出了问题。所以,花时间把转换测试做好,后面能省很多事。

下一章,我们会聊量化测试。INT8、FP16这些量化方式,怎么测?怎么保证精度不掉?到时候细说。