4、TensorFlow Lite Micro实战:框架介绍、模型转换流程、在MCU上部署一个简单的分类模型
好,咱们进入实战环节了。前面几章讲了那么多理论,说实话,不亲手跑一遍代码,总觉得心里没底。这一章我就带大家走一遍完整的流程——从TensorFlow Lite Micro框架是什么,到怎么把模型转成MCU能吃的格式,最后在开发板上跑一个分类模型。
我个人习惯是,先搞清楚工具链再动手。所以咱们先聊聊TFLM这个框架。
4.1 TensorFlow Lite Micro 框架介绍
TensorFlow Lite Micro,简称TFLM,是Google专门为微控制器设计的推理引擎。说白了,它就是TFLite的“瘦身版”。
为什么需要单独搞一个Micro版本?你想想看,普通的TFLite依赖标准C++库和操作系统,动不动就要几百KB的RAM。MCU上呢?可能总共就几十KB。所以TFLM做了几件事:
- 去掉了动态内存分配——所有内存都在编译时确定
- 精简了算子集——只保留最常用的操作,比如卷积、全连接、池化
- 没有文件系统依赖——模型数据直接以C数组形式嵌入代码
- 纯C++实现——不依赖操作系统,裸机也能跑
我在项目中遇到过一个问题:一开始用了完整的TFLite库,结果编译出来200多KB,MCU根本放不下。换成TFLM之后,直接降到30KB左右。嗯,差距就是这么大。
核心要点:TFLM的运行时环境只有几千行代码,非常适合资源受限的嵌入式设备。它支持ARM Cortex-M系列、ESP32、RISC-V等主流架构。
4.2 模型转换流程
咱们在PC上训练好的模型通常是Keras的.h5文件或者PyTorch的.pth文件。MCU不认识这些格式。所以需要经过一个转换流程。
整个流程分三步走:
- 训练模型——用Keras或PyTorch训练一个分类模型
- 转成TFLite格式——用TensorFlow的转换器生成.tflite文件
- 量化并生成C数组——把浮点模型转成int8,然后导出为C语言头文件
我给大家看一个具体的转换代码。假设我们有一个简单的二分类模型,输入是32x32的灰度图:
import tensorflow as tf
# 1. 加载训练好的Keras模型
model = tf.keras.models.load_model('my_model.h5')
# 2. 转换为TFLite格式
converter = tf.lite.TFLiteConverter.from_keras_model(model)
# 3. 设置量化——这一步很关键
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_model = converter.convert()
# 4. 保存为.tflite文件
with open('model_quantized.tflite', 'wb') as f:
f.write(tflite_model)
# 5. 生成C数组
import binascii
with open('model_quantized.tflite', 'rb') as f:
hex_data = binascii.hexlify(f.read()).decode('utf-8')
c_array = 'unsigned char model_tflite[] = {\n'
for i in range(0, len(hex_data), 2):
c_array += '0x' + hex_data[i:i+2] + ', '
if (i // 2) % 16 == 15:
c_array += '\n'
c_array += '};\n'
c_array += 'unsigned int model_tflite_len = ' + str(len(hex_data)//2) + ';'
with open('model_data.h', 'w') as f:
f.write(c_array)
避坑指南:我曾经在量化这一步吃过亏。如果不提供representative_dataset,转换器会使用默认的校准数据,导致量化后的模型精度暴跌。一定要用真实的验证集数据做校准,哪怕只有几十张图片也行。
量化为什么重要?说白了,MCU没有FPU(浮点运算单元)或者FPU性能很弱。用int8计算比float32快4倍以上,内存占用也减少75%。
4.3 在MCU上部署分类模型
好,模型文件已经转成C数组了。接下来就是把它烧到MCU里跑起来。
我以STM32F407为例,给大家演示部署流程。这个芯片有192KB RAM,跑一个简单的分类模型绰绰有余。
4.3.1 工程结构
一个典型的TFLM工程包含这些文件:
project/
├── src/
│ ├── main.cpp # 主程序
│ ├── model_data.h # 模型C数组
│ └── tensorflow/ # TFLM核心库
├── include/
│ └── tensorflow/ # 头文件
└── Makefile
4.3.2 推理代码实现
这是核心部分。我直接贴代码,大家跟着看:
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/system_setup.h"
#include "model_data.h" // 包含模型数据
// 定义全局变量
constexpr int kTensorArenaSize = 50 * 1024; // 50KB的Tensor Arena
uint8_t tensor_arena[kTensorArenaSize];
void setup() {
// 初始化TFLM
tflite::InitializeTarget();
// 创建算子解析器
static tflite::AllOpsResolver resolver;
// 创建解释器
static tflite::MicroInterpreter interpreter(
tflite::GetModel(model_tflite), // 模型数据
resolver,
tensor_arena,
kTensorArenaSize
);
// 分配张量内存
TfLiteStatus allocate_status = interpreter.AllocateTensors();
if (allocate_status != kTfLiteOk) {
// 内存分配失败处理
while(1);
}
// 获取输入输出张量指针
TfLiteTensor* input = interpreter.input(0);
TfLiteTensor* output = interpreter.output(0);
}
void loop() {
// 1. 准备输入数据(假设从ADC读取)
int8_t* input_data = interpreter.input(0)->data.int8;
for (int i = 0; i < 1024; i++) { // 32x32=1024
input_data[i] = read_adc_value(); // 读取传感器数据
}
// 2. 执行推理
TfLiteStatus invoke_status = interpreter.Invoke();
if (invoke_status != kTfLiteOk) {
// 推理失败处理
return;
}
// 3. 获取分类结果
int8_t* output_data = interpreter.output(0)->data.int8;
int predicted_class = 0;
int8_t max_value = output_data[0];
for (int i = 1; i < 2; i++) { // 二分类
if (output_data[i] > max_value) {
max_value = output_data[i];
predicted_class = i;
}
}
// 4. 根据结果执行动作
if (predicted_class == 0) {
// 类别0:开灯
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
} else {
// 类别1:关灯
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}
delay(100); // 100ms推理一次
}
注意:Tensor Arena的大小要根据模型来定。我建议先设大一点,比如50KB,然后看实际用了多少。可以在代码里加一句printf("arena used: %d\n", interpreter.arena_used_bytes());来查看。用多少设多少,别浪费宝贵的RAM。
4.3.3 性能优化技巧
在实际项目中,我总结了几条经验:
- 算子融合:把ReLU和卷积融合成一个算子,减少内存访问次数
- 内存复用:TFLM内部已经做了张量复用,但输入输出缓冲区可以自己管理
- 降低推理频率:不是每毫秒都需要推理,100ms一次对大多数场景足够了
- 使用DMA传输数据:如果输入来自ADC或摄像头,用DMA可以减少CPU占用
我记得有一次做手势识别项目,推理一次要80ms,感觉太慢了。后来发现是输入数据拷贝花了大量时间。改成DMA传输后,推理时间降到了30ms。嗯,有时候瓶颈不在计算,而在数据搬运。
4.4 实战总结
这一章咱们走完了从模型转换到MCU部署的全流程。我再说几个关键点:
| 步骤 | 关键动作 | 常见坑 |
|---|---|---|
| 模型转换 | 量化到int8 | 忘记提供校准数据集 |
| 生成C数组 | 用binascii转hex | 数组太大导致编译失败 |
| 部署推理 | 分配Tensor Arena | Arena太小导致分配失败 |
| 结果处理 | 解析输出张量 | 量化后的输出需要反量化 |
最后说一句,TFLM虽然好用,但不是万能的。如果你的模型有自定义算子,或者网络结构特别复杂,可能得自己实现算子。不过对于大多数分类任务,TFLM内置的算子已经够用了。
下一章咱们会讲更高级的话题——如何在低功耗场景下做持续推理,以及如何用RTOS管理推理任务。到时候见。