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上?流程其实不复杂:

  1. 模型转C数组:用xxd或TFLite内置工具把.tflite文件转成C语言的头文件。
  2. 配置TFLite Micro运行时:在STM32工程中集成TFLite Micro的源码。
  3. 加载模型:用tflite::MicroMutableOpResolver注册算子,用tflite::GetModel获取模型指针。
  4. 分配张量内存:设置Tensor Arena大小——这个很关键,我后面会专门讲。
  5. 运行推理:填充输入,调用Interpreter->Invoke(),读取输出。

小技巧:在STM32上调试量化模型时,可以先在PC上用TFLite Python API跑一遍同样的输入,对比输出结果。如果差异很大,八成是量化参数没设对,而不是STM32的问题。

3.8 本章知识体系

下面这张图把整个流程串起来了,从训练到部署一目了然:

模型转换与量化部署流程 TensorFlow训练 导出SavedModel TFLite转换+量化 动态范围量化 全整数量化(INT8) 量化感知训练(QAT) STM32 + TFLite Micro部署 关键指标 存储: FP32→INT8 压缩比: 4x 推理加速: 2-4x 校准数据 100-200张 覆盖实际场景

这张图把整个流程串起来了。你可以看到,从训练到部署,量化是中间最关键的一环。选哪种量化方式,取决于你的精度要求和硬件资源。