4. 模型优化与导出(下):剪枝、蒸馏、C数组与量化验证
上一节我们把模型训练跑通了,也做了基础的量化。但说实话,那个模型直接扔到MCU上,大概率跑不动——要么太大,要么太慢。这一节,我们来聊聊怎么把模型“削”到能在单片机上跑的程度。
我个人习惯把模型优化分成两步:结构瘦身和数据压缩。剪枝和蒸馏属于前者,量化和转C数组属于后者。咱们一个一个来。
4.1 模型剪枝:把不重要的连接砍掉
剪枝这个概念,说白了就是“删掉那些对结果影响不大的权重”。你想想看,一个训练好的神经网络,很多权重其实接近于0,留着它们只会增加计算量。
我在项目中遇到过最典型的例子:一个手势识别模型,原始大小1.2MB,剪掉40%的权重后精度只掉了0.3%,但模型体积直接砍半。嗯,这就是剪枝的价值。
4.1.1 剪枝的两种方式
| 类型 | 做法 | 适用场景 |
|---|---|---|
| 非结构化剪枝 | 把绝对值小的权重直接置0 | 配合稀疏矩阵库(如CMSIS-NN的稀疏支持) |
| 结构化剪枝 | 整通道或整层删除 | 硬件加速友好,适合MCU |
对于MCU部署,我建议优先考虑结构化剪枝。为什么?因为非结构化剪枝虽然压缩率高,但产生的稀疏矩阵在Cortex-M上加速效果有限。CMSIS-NN虽然支持稀疏,但实际加速比不如结构化剪枝来得直接。
4.1.2 剪枝实操(TensorFlow示例)
import tensorflow_model_optimization as tfmot
# 定义剪枝参数
pruning_params = {
'pruning_schedule': tfmot.sparsity.keras.PolynomialDecay(
initial_sparsity=0.2, # 初始稀疏度
final_sparsity=0.5, # 目标稀疏度
begin_step=0,
end_step=1000
)
}
# 给模型套上剪枝包装
pruned_model = tfmot.sparsity.keras.prune_low_magnitude(
model, **pruning_params
)
# 重新训练(微调)
pruned_model.fit(x_train, y_train, epochs=5)
# 导出时去掉剪枝包装
final_model = tfmot.sparsity.keras.strip_pruning(pruned_model)
注意最后一步strip_pruning,很多人会忘。如果不做这一步,导出的模型里还带着剪枝的辅助变量,体积反而更大。我曾经踩过这个坑,折腾了半天才发现。
4.2 知识蒸馏:大模型教小模型
蒸馏的思路很有意思——用一个大的教师模型去指导一个小学生模型训练。教师模型已经学会了数据的分布规律,它输出的“软标签”比原始硬标签包含更多信息。
举个例子:手写数字识别,硬标签说“这是7”,但教师模型可能会输出“90%是7,8%是1,2%是9”。这个分布信息对学生模型来说,比单纯的“7”要有用得多。
4.2.1 蒸馏的损失函数
蒸馏的损失由两部分组成:
- 硬损失:学生输出与真实标签的交叉熵
- 软损失:学生输出与教师输出的KL散度
控制两者权重的参数叫alpha,我一般设0.7,也就是70%靠教师指导,30%靠真实标签。还有一个温度参数T,温度越高,软标签的分布越平滑。
关键参数速查:
- alpha = 0.7(软损失权重)
- T = 4(温度,常用范围2~8)
- 学生模型参数量 = 教师模型的1/5 ~ 1/10
4.3 转换为C数组:xxd工具的使用
模型优化完了,接下来要把它变成MCU能吃的格式。TFLite模型是二进制文件,不能直接烧进单片机。我们需要把它转成C语言的数组。
这里我推荐用xxd工具,Linux和macOS自带,Windows可以用Git Bash里的版本。用法极其简单:
# 把tflite模型转成C头文件
xxd -i model.tflite > model_data.h
生成的model_data.h里会包含一个unsigned char数组和它的长度。比如:
unsigned char model_tflite[] = {
0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33,
// ... 后面是模型的所有字节
};
unsigned int model_tflite_len = 123456;
model.tflite,数组名就是model_tflite。如果文件名有特殊字符,记得手动改一下变量名,否则C编译器会报错。
在MCU代码里引用这个头文件,然后传给TFLite Micro的解释器就行:
#include "model_data.h"
// 加载模型
const tflite::Model* model = tflite::GetModel(model_tflite);
4.4 验证量化模型的精度损失
模型转成C数组之前,还有一件重要的事要做——验证量化后的精度。量化一定会带来精度损失,关键是要确认这个损失在可接受范围内。
4.4.1 量化前后对比流程
我一般按这个步骤来:
- 用原始浮点模型跑一遍测试集,记录精度
- 用量化后的TFLite模型跑同样的测试集,记录精度
- 计算精度差值
在Python里可以用TFLite的Python API来跑量化模型:
import tensorflow as tf
# 加载量化模型
interpreter = tf.lite.Interpreter(model_path="model_quant.tflite")
interpreter.allocate_tensors()
# 获取输入输出张量
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# 逐样本推理
accuracy = 0
for i in range(len(test_images)):
interpreter.set_tensor(input_details[0]['index'], test_images[i])
interpreter.invoke()
output = interpreter.get_tensor(output_details[0]['index'])
# 计算精度...
4.4.2 精度损失的容忍标准
| 应用场景 | 可接受精度损失 | 说明 |
|---|---|---|
| 关键词唤醒 | < 2% | 用户对误唤醒很敏感 |
| 手势识别 | < 3% | 有一定容错空间 |
| 异常检测 | < 1% | 漏报代价高 |
| 简单分类(5类以内) | < 5% | 类别少,特征明显 |
如果量化后精度掉得太多,可以试试量化感知训练(Quantization-Aware Training, QAT)。这个方法在训练时就模拟量化误差,让模型学会适应低精度。代价是训练时间增加30%~50%,但精度通常能挽回1~2个百分点。
4.5 本章知识结构总览
下面这张图把整个流程串起来了,从训练好的模型到最终烧进MCU的C数组:
从图上可以看到,剪枝、蒸馏、量化这三条路可以组合使用。我个人的推荐组合是:结构化剪枝(砍30%)+ 量化(INT8),这个组合在大多数MCU项目里都能达到体积和精度的平衡点。