3、模型准备:从TensorFlow/Keras导出模型,ONNX格式转换与优化
好,咱们进入第三章。这一章很关键,说白了就是要把你训练好的模型,从TensorFlow/Keras这个“温室”里搬出来,再“整容”成ONNX格式,最后还得给它“瘦瘦身”。
我刚开始做STM32上的AI部署时,就吃过这个亏。模型在电脑上跑得飞起,一转到CubeAI里就报错。后来才发现,是模型导出和转换这一步没做好。嗯,这里面的坑,我替你们先踩过了。
3.1 为什么需要ONNX?
你想想看,TensorFlow是Google的,PyTorch是Meta的,而STM32CubeAI是ST的。它们之间怎么沟通?ONNX就是那个“通用语言”。
ONNX(Open Neural Network Exchange)说白了就是一个模型交换格式。它能把各种框架训练出来的模型,统一成一个标准格式。这样,STM32CubeAI就能读懂了。
我个人习惯是:训练用Keras,导出用ONNX,部署用CubeAI。这套流程最稳。
核心要点:ONNX是模型从训练环境到嵌入式环境的“桥梁”。没有它,你的模型就困在电脑里出不来。
3.2 从Keras导出模型
咱们先看看怎么把Keras模型保存下来。这一步其实很简单,但有个细节要注意。
3.2.1 保存为H5格式
Keras训练完,通常直接保存为.h5文件。这是最基础的做法。
# 训练完模型后
model.save('my_model.h5') # 保存整个模型(结构+权重+优化器状态)
# 或者只保存权重(更轻量)
model.save_weights('my_weights.h5')
我在项目中遇到过一个问题:保存整个模型时,如果Keras版本不一致,加载时会报错。所以我建议,如果你只是要部署,用save_weights就够了。结构单独用JSON存。
3.2.2 保存为SavedModel格式
TensorFlow 2.x推荐用SavedModel格式。它更完整,也更容易转ONNX。
# 保存为SavedModel格式
model.save('saved_model_dir', save_format='tf')
这个格式会生成一个文件夹,里面包含模型结构、权重和计算图。我个人觉得,转ONNX时用SavedModel最省心。
小技巧:如果你不确定模型是否保存完整,可以用tf.saved_model.load()加载试试。能加载成功,说明没问题。
3.3 ONNX格式转换
模型保存好了,接下来就是转成ONNX。这里要用到一个工具——tf2onnx。
3.3.1 安装tf2onnx
先装工具包:
pip install tf2onnx onnx onnxruntime
嗯,这里要注意版本兼容性。我建议用Python 3.8-3.10,太新或太旧都可能出问题。
3.3.2 转换命令
转换其实就一行命令:
python -m tf2onnx.convert \
--saved-model saved_model_dir \
--output model.onnx \
--opset 11
参数说明:
--saved-model:输入的SavedModel文件夹路径--output:输出的ONNX文件名--opset:ONNX算子集版本。我一般用11,兼容性好
如果你保存的是.h5文件,也可以用:
python -m tf2onnx.convert \
--keras-model my_model.h5 \
--output model.onnx
注意:转换时如果报错,多半是模型里有不支持的算子。比如某些自定义层、Lambda层。我曾经遇到一个用Lambda层写的自定义激活函数,转ONNX时直接卡死。解决办法是:把自定义层改成标准层,或者用Keras内置函数替代。
3.4 ONNX模型优化
模型转成ONNX后,还不能直接用。因为ONNX模型里有很多冗余信息,比如多余的节点、不必要的转换操作。我们需要给它“瘦身”。
3.4.1 使用onnx-simplifier
这个工具能自动简化ONNX模型,去掉冗余节点。
pip install onnx-simplifier
python -m onnxsim model.onnx model_simplified.onnx
简化后,模型大小通常能减少10%-30%。我有个项目,原始ONNX模型12MB,简化后只有8.5MB。对于STM32这种资源受限的设备,这很关键。
3.4.2 量化优化
量化是让模型更小的“杀手锏”。简单说,就是把模型里的浮点数(float32)变成整数(int8)。
为什么能这么做?因为嵌入式设备对精度要求没那么高。你想想看,STM32上跑个手势识别,输出是“剪刀”还是“石头”,差0.1%的精度根本感觉不到。
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType
# 加载简化后的模型
model_path = 'model_simplified.onnx'
quantized_path = 'model_quantized.onnx'
# 动态量化
quantize_dynamic(model_path, quantized_path, weight_type=QuantType.QInt8)
量化后的模型,大小能再减少75%左右。12MB的模型,量化完可能就3MB。但代价是精度会下降一点点,通常1%-3%。
经验之谈:量化前一定要做精度验证。我建议先量化,然后在测试集上跑一遍,看精度下降是否在可接受范围内。如果下降太多,可以试试只量化权重,不量化激活值。
3.4.3 算子融合
ONNX模型里,有些算子是可以合并的。比如“Conv + BatchNorm + Relu”可以融合成一个算子。这样能减少计算量。
不过,这个操作通常由STM32CubeAI在导入时自动完成。我们不需要手动做。但了解这个原理,有助于你理解为什么模型部署后速度会变快。
3.5 验证转换后的模型
模型转换完,一定要验证。不然你辛辛苦苦部署到STM32上,结果跑出来全是乱码,那可就尴尬了。
3.5.1 使用ONNX Runtime验证
import onnxruntime as ort
import numpy as np
# 加载ONNX模型
session = ort.InferenceSession('model_quantized.onnx')
# 准备输入数据(假设输入是224x224的图片)
input_data = np.random.randn(1, 224, 224, 3).astype(np.float32)
# 运行推理
outputs = session.run(None, {'input_1': input_data})
print(outputs[0].shape) # 检查输出形状
这里要注意:输入数据的形状、数据类型必须和模型要求的一致。我曾经因为输入数据是float64,模型要求float32,结果推理结果全错。查了半天才发现是类型不匹配。
3.5.2 对比Keras和ONNX的输出
最稳妥的方法,是用同一张图片,分别在Keras和ONNX上跑一次,对比输出结果。
# Keras推理
keras_output = keras_model.predict(input_data)
# ONNX推理
onnx_output = session.run(None, {'input_1': input_data})[0]
# 计算误差
error = np.abs(keras_output - onnx_output).max()
print(f'最大误差: {error:.6f}')
如果最大误差小于1e-3,说明转换没问题。如果误差很大,那就要检查模型转换过程了。
避坑指南:我曾经遇到一个模型,Keras和ONNX的输出误差达到0.5。查了半天,发现是Keras里用了tf.image.resize做预处理,而ONNX里没有这个算子。解决办法是:把预处理操作移到模型外部,或者在Keras里用标准层实现。
3.6 总结与建议
好了,这一章的内容就这些。咱们捋一下关键点:
- 保存模型:推荐用SavedModel格式,兼容性最好
- 转ONNX:用tf2onnx工具,注意算子兼容性
- 优化模型:先用onnx-simplifier简化,再考虑量化
- 验证模型:用ONNX Runtime跑一遍,对比Keras输出
我个人建议,把这套流程写成脚本。每次训练完新模型,一键执行。省时省力,还不会出错。
下一章,咱们就要把优化好的ONNX模型,导入到STM32CubeAI里了。到时候会看到,模型在STM32上到底能跑多快。
嗯,今天就到这里。有问题随时交流。