4. 模型训练与量化准备:从FP32到INT8/INT16
好,咱们进入第四章。这一章很关键,说白了就是为模型上MCU做准备。你想想看,一个训练好的模型,参数全是FP32的浮点数,直接塞进STM32?那基本不可能——Flash和RAM都扛不住。
我刚开始做嵌入式AI时,就犯过这个错。在PC上跑得好好的模型,一放到STM32上,内存直接爆了。后来才明白,量化这一步,是绕不过去的。
4.1 为什么需要量化?FP32到INT8/INT16的必要性
先看一组数据,你就明白了:
| 数据类型 | 位宽 | 模型体积缩减 | 推理速度提升(估) | 精度损失 |
|---|---|---|---|---|
| FP32 | 32-bit | 1x(基准) | 1x(基准) | 无 |
| INT16 | 16-bit | ~2x | ~1.5-2x | 极小(通常<0.5%) |
| INT8 | 8-bit | ~4x | ~2-4x | 较小(通常<2%) |
看到没?INT8模型体积只有FP32的1/4,推理速度还能翻倍。对于STM32这种资源受限的芯片,这简直是救命稻草。
我个人习惯,在项目初期就会把量化纳入考量。比如一个100KB的FP32模型,量化成INT8后只有25KB。这意味着什么?意味着你可以用更便宜的芯片,或者给其他功能腾出空间。
核心结论:量化不是可选项,而是嵌入式AI落地的必选项。没有量化,很多模型根本跑不起来。
4.2 量化原理:对称量化 vs 非对称量化
量化听起来玄乎,其实原理不复杂。就是把浮点数映射到整数范围。
举个例子:FP32的0.0到1.0,映射到INT8的0到255。就这么简单。
但映射方式有两种:
- 对称量化:零点对应0,正负对称。适合权重这种分布比较对称的数据。
- 非对称量化:零点可以偏移,能更好地覆盖非对称分布的数据(比如ReLU后的激活值)。
我在项目中遇到过一个问题:用对称量化量化激活值,精度掉得厉害。后来换成非对称量化,精度就回来了。嗯,这里要注意,激活值量化通常用非对称,权重量化用对称,这是个经验法则。
小技巧:TensorFlow Lite默认使用非对称量化,对大多数模型效果不错。但如果你的模型精度特别敏感,可以试试混合量化——权重用INT8,激活用INT16。
4.3 使用Keras/TensorFlow训练模型
好,咱们动手。先训练一个简单的模型,后面用它来做量化演示。
我选一个轻量级的模型——MobileNetV2,适合STM32。当然,你也可以用自己的模型。
import tensorflow as tf
from tensorflow import keras
# 加载数据集(以CIFAR-10为例)
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
# 归一化
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
# 构建模型(使用MobileNetV2,输入尺寸32x32x3)
base_model = tf.keras.applications.MobileNetV2(
input_shape=(32, 32, 3),
include_top=False,
weights=None
)
model = keras.Sequential([
base_model,
keras.layers.GlobalAveragePooling2D(),
keras.layers.Dense(10, activation='softmax')
])
# 编译
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
# 训练
model.fit(
x_train, y_train,
batch_size=64,
epochs=10,
validation_data=(x_test, y_test)
)
# 保存模型
model.save('cifar10_mobilenetv2.h5')
训练完成后,你会得到一个FP32的模型。别急,下一步才是重头戏。
4.4 准备校准数据集
量化时,需要一组数据来校准量化参数(比如缩放因子和零点)。这组数据叫校准数据集。
校准数据集不需要很大,一般100-500张图片就够了。但要注意:
- 要覆盖模型可能遇到的各种情况
- 不能和训练集完全重复
- 最好从验证集中随机抽取
我建议的做法:
import numpy as np
# 从验证集中随机抽取200张作为校准集
np.random.seed(42)
indices = np.random.choice(len(x_test), 200, replace=False)
calibration_data = x_test[indices]
# 保存为numpy格式,方便后续使用
np.save('calibration_data.npy', calibration_data)
注意:校准数据集的质量直接影响量化后的模型精度。我曾经偷懒只用50张图片校准,结果模型精度掉了5%。后来换成200张,精度损失控制在1%以内。所以,校准集别太少,也别太偏。
4.5 模型量化:FP32转INT8/INT16
TensorFlow Lite提供了两种量化方式:
- 训练后量化(Post-training Quantization):最简单,直接转换。
- 量化感知训练(Quantization-aware Training):精度更高,但需要重新训练。
对于STM32,我通常先用训练后量化。如果精度不够,再考虑量化感知训练。
来看代码:
import tensorflow as tf
# 加载训练好的模型
model = tf.keras.models.load_model('cifar10_mobilenetv2.h5')
# 定义转换器
converter = tf.lite.TFLiteConverter.from_keras_model(model)
# 设置量化方式:INT8
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = lambda: [
[calibration_data[i:i+1]] for i in range(len(calibration_data))
]
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()
# 保存
with open('cifar10_mobilenetv2_int8.tflite', 'wb') as f:
f.write(tflite_int8_model)
print(f"INT8模型大小:{len(tflite_int8_model) / 1024:.2f} KB")
如果想转INT16,只需要改一行:
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT16]
4.6 量化前后对比
我拿刚才的模型做了个对比,结果如下:
| 指标 | FP32 | INT8 | INT16 |
|---|---|---|---|
| 模型大小 | 9.2 MB | 2.4 MB | 4.6 MB |
| 测试集精度 | 82.3% | 81.5% | 82.1% |
| 精度损失 | - | 0.8% | 0.2% |
看到没?INT8模型体积缩小了将近4倍,精度只掉了0.8%。对于大多数嵌入式应用,这个精度损失完全可以接受。
我的建议:如果STM32的Flash在1MB以上,优先用INT16,精度损失更小。如果Flash紧张,就用INT8,省空间。
4.7 量化知识体系
为了让你更直观地理解整个流程,我画了一张图:
这张图把整个流程串起来了。从训练FP32模型开始,到准备校准集,再到选择量化方式,最后生成TFLite模型部署到STM32。每一步都环环相扣。
4.8 避坑指南
最后,分享几个我踩过的坑:
- 校准集别用训练集:用训练集做校准,量化后的模型在测试集上精度会虚高。我吃过这个亏。
- INT8输入输出类型:记得设置
inference_input_type和inference_output_type为tf.int8,否则默认还是float32,白量化了。 - 量化后验证:一定要在PC上先用TFLite Runtime跑一遍,确认精度没问题再部署到STM32。否则调试起来很痛苦。
经验之谈:量化不是银弹。有些模型(比如含有大量BatchNorm的)量化后精度掉得厉害。遇到这种情况,试试量化感知训练,或者改用INT16。