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。我习惯分三步走:
- 保存原始模型:用
tf.saved_model.save导出,保留完整计算图。 - 执行转换:指定输入输出张量,设置量化参数。
- 对比输出:用同一组输入数据,分别跑原始模型和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的转换工具是ncnn2onnx和onnx2ncnn。我建议你走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::Mat的from_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这些量化方式,怎么测?怎么保证精度不掉?到时候细说。