3. 模型转换与量化基础:从训练到STM32部署
说实话,很多做嵌入式AI的朋友第一次接触模型量化时,都觉得这玩意儿挺玄乎。我当年也一样——明明在PC上跑得好好的模型,一转到STM32上要么精度掉得厉害,要么干脆跑不起来。后来踩的坑多了,才慢慢摸清楚这里面的门道。
这一章,咱们就聊聊模型怎么从TensorFlow训练出来,再一步步变成能在STM32上跑的TFLite模型。重点放在量化上——说白了,就是怎么把模型“瘦身”又“减智”,但还不能让它变傻。
3.1 TensorFlow模型训练与导出
先说说训练。我个人习惯用Keras API搭模型,简洁直观。训练完成后,模型通常保存为两种格式:SavedModel和HDF5。但咱们的目标是TFLite Micro,所以得走SavedModel这条路。
关键点:SavedModel是TensorFlow推荐的模型存储格式,它包含了模型的计算图、权重和训练配置。TFLite转换器对SavedModel的支持最完善。
下面是一个简单的训练和导出流程:
import tensorflow as tf
# 构建一个简单的分类模型
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(16, (3,3), activation='relu', input_shape=(32,32,3)),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
# 训练...
model.fit(x_train, y_train, epochs=10)
# 导出为SavedModel
model.save('saved_model/my_model')
导出后,你会得到一个文件夹,里面包含assets、variables和saved_model.pb。嗯,这里要注意:别手欠去改里面的文件结构,TFLite转换器认的就是这个格式。
3.2 模型转换:从SavedModel到TFLite
有了SavedModel,下一步就是用TFLiteConverter把它转成.tflite文件。这个过程我称之为“模型过安检”——转换器会检查模型结构,优化算子,最后打包成FlatBuffer格式。
# 基本转换(浮点模型)
converter = tf.lite.TFLiteConverter.from_saved_model('saved_model/my_model')
tflite_model = converter.convert()
# 保存
with open('model_float32.tflite', 'wb') as f:
f.write(tflite_model)
这样转出来的模型是FP32精度的,在STM32上跑?想都别想——内存根本扛不住。所以咱们得量化。
3.3 量化:让模型瘦下来
量化的本质,就是用更少的比特数来表示模型的权重和激活值。FP32转INT8,存储直接缩到1/4,计算量也大幅下降。但代价是精度损失——你想想看,原来用32位表示的数,现在只用8位,信息肯定有丢失。
| 量化方式 | 权重类型 | 激活类型 | 是否需要校准数据 | 精度损失 |
|---|---|---|---|---|
| 动态范围量化 | INT8 | FP32 | 否 | 较小 |
| 全整数量化 | INT8 | INT8 | 是 | 中等 |
| 量化感知训练 | INT8 | INT8 | 是 | 最小 |
3.4 动态范围量化
这是最简单的量化方式。转换时,权重被量化到INT8,但推理时激活值还是用FP32计算。说白了就是“半量化”——省了存储,没省计算。
converter = tf.lite.TFLiteConverter.from_saved_model('saved_model/my_model')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()
我在项目里试过,这种方式对精度影响很小,通常掉0.5%以内。但问题是,STM32没有FP32加速单元,跑起来还是慢。所以这招只适合内存紧张但算力还行的场景。
3.5 全整数量化(INT8)
这才是嵌入式AI的“正餐”。权重和激活值全部量化到INT8,推理时全部用整数运算。STM32的CMSIS-NN库就是为这个优化的。
但有个前提:你需要提供校准数据集。转换器会用这些数据统计激活值的分布,算出最佳的缩放因子和零点。
def representative_dataset():
for i in range(100):
yield [x_train[i:i+1].astype(np.float32)]
converter = tf.lite.TFLiteConverter.from_saved_model('saved_model/my_model')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_int8_model = converter.convert()
我曾经踩过的坑:校准数据集的数量不是越多越好。我试过用5000张图片做校准,结果精度反而比用100张还差。后来发现,校准集要覆盖模型的实际使用场景,而不是盲目堆数量。一般100-200张就够了。
3.6 量化感知训练(QAT)
如果全整数量化后精度掉得太多,那就得上QAT了。原理很简单:在训练过程中模拟量化效果,让模型学会“忍受”量化误差。
QAT的训练流程和普通训练差不多,只是在模型中插入了一些伪量化节点。这些节点在前向传播时模拟量化-反量化过程,反向传播时依然用浮点梯度更新权重。
import tensorflow_model_optimization as tfmot
# 在模型上应用QAT
quantize_model = tfmot.quantization.keras.quantize_model
qat_model = quantize_model(model)
# 重新编译和训练
qat_model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
qat_model.fit(x_train, y_train, epochs=5)
# 导出时直接转换
converter = tf.lite.TFLiteConverter.from_keras_model(qat_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_qat_model = converter.convert()
我个人的经验是:QAT能让精度损失从3-5%降到1%以内。但代价是训练时间变长,而且需要调参。如果你模型本身很大,QAT的训练时间可能翻倍。
3.7 量化模型在STM32上的部署
模型转好了,怎么放到STM32上?流程其实不复杂:
- 模型转C数组:用xxd或TFLite内置工具把.tflite文件转成C语言的头文件。
- 配置TFLite Micro运行时:在STM32工程中集成TFLite Micro的源码。
- 加载模型:用tflite::MicroMutableOpResolver注册算子,用tflite::GetModel获取模型指针。
- 分配张量内存:设置Tensor Arena大小——这个很关键,我后面会专门讲。
- 运行推理:填充输入,调用Interpreter->Invoke(),读取输出。
小技巧:在STM32上调试量化模型时,可以先在PC上用TFLite Python API跑一遍同样的输入,对比输出结果。如果差异很大,八成是量化参数没设对,而不是STM32的问题。
3.8 本章知识体系
下面这张图把整个流程串起来了,从训练到部署一目了然:
这张图把整个流程串起来了。你可以看到,从训练到部署,量化是中间最关键的一环。选哪种量化方式,取决于你的精度要求和硬件资源。