4、TensorFlow Lite Micro环境搭建:TFLM源码编译、算子支持列表、内存分配器配置
好,咱们进入正题。
上一章我们把TFLM的代码仓库拉下来了,也大概扫了一眼目录结构。这一章,咱们要真刀真枪地干——把环境搭起来,把代码编译通过,然后搞清楚TFLM到底支持哪些算子,最后再聊聊那个让无数人头疼的内存分配器配置。
说实话,我当年第一次搞TFLM编译,光环境就折腾了两天。不是这缺库,就是那报错。后来摸清了套路,发现其实就那几步。今天我把这些坑都给你指出来,你照着做,半小时内肯定能跑起来。
4.1 源码编译:别怕,就三步
TFLM的编译不像你想象中那么复杂。它用的是Makefile体系,没有CMake那么绕。我个人习惯直接在Linux环境下搞,Ubuntu 20.04或22.04都行。Windows的话,建议用WSL2,省心。
第一步,安装依赖。别偷懒,缺一个库后面编译就会报错。
sudo apt-get update
sudo apt-get install build-essential git make python3 python3-pip
pip3 install numpy pybind11
第二步,进入TFLM目录,直接跑编译命令。这里我以x86平台为例,方便你本地测试。
cd tensorflow
make -f tensorflow/lite/micro/tools/make/Makefile build
嗯,就这么简单?对,就这么简单。第一次编译会下载一些依赖,比如CMSIS、FlatBuffers这些。网速慢的话,可能要等个十来分钟。
第三步,验证编译结果。编译完成后,在 gen/linux_x86_64_default/bin/ 目录下会生成一堆测试程序。随便跑一个看看:
./gen/linux_x86_64_default/bin/hello_world_test
看到输出 ~~~ALL TESTS PASSED~~~ 了吗?恭喜你,环境搭好了。
make -f tensorflow/lite/micro/tools/make/Makefile TARGET=cortex_m_generic TARGET_ARCH=cortex-m4
别问我为什么强调这个——我有个同事,编译了三天都没过,最后发现忘了指定TARGET_ARCH,默认编译的是x86版本。
4.2 算子支持列表:TFLM到底能干啥?
你可能会问:TFLM支持多少算子?跟完整的TensorFlow Lite比,差多少?
说实话,差不少。TFLM的目标是微控制器,资源有限,所以它只支持最核心的算子。我整理了一份清单,你一看就明白。
| 算子类别 | 支持的算子 | 说明 |
|---|---|---|
| 基础数学 | ADD, SUB, MUL, DIV, NEG | 加减乘除,都是量化版本 |
| 激活函数 | RELU, RELU6, TANH, SIGMOID, SOFTMAX | 注意:没有LeakyReLU |
| 卷积相关 | CONV_2D, DEPTHWISE_CONV_2D, MAX_POOL_2D, AVE_POOL_2D | 只支持2D,不支持3D卷积 |
| 全连接 | FULLY_CONNECTED | 最常用的算子之一 |
| 形状操作 | RESHAPE, TRANSPOSE, PAD, STRIDED_SLICE | 用于调整张量形状 |
| 归一化 | SOFTMAX, LOG_SOFTMAX | 分类任务必备 |
| 其他 | CONCATENATION, SPLIT, GATHER, SLICE | 数据拼接和切片 |
看到这个表,你可能会想:就这些?够用吗?
我跟你讲,够用。我在项目中做过语音唤醒、手势识别、异常检测,用的就是这些算子。关键是,TFLM的算子都是经过极致优化的,特别是针对ARM Cortex-M系列,有CMSIS-NN加速,跑起来效率很高。
怎么查看当前TFLM版本支持哪些算子?很简单,直接看源码:
# 在 tensorflow/lite/micro/kernels/ 目录下
ls *.cc | grep -v test
每个.cc文件对应一个算子实现。比如 conv.cc 就是卷积,fully_connected.cc 就是全连接。一目了然。
4.3 内存分配器配置:别让内存成为瓶颈
好,到了最核心的部分。TFLM的内存管理,说白了就两个东西:Tensor Arena和MicroAllocator。
你想想看,MCU上RAM就那么几十KB到几百KB,模型权重、中间结果、输入输出,全都要挤在这点空间里。怎么分配?怎么避免溢出?
TFLM默认使用 MicroAllocator,它把内存分成三块:
- 持久内存(Persistent Memory):存放模型权重、常量张量。这部分生命周期跟模型一样长。
- 临时内存(Temporary Memory):存放中间计算结果。每次推理都会重新分配。
- 输入输出内存(Input/Output Memory):存放模型的输入和输出张量。
配置方式很简单,就一行代码:
// 分配一块内存,大小根据你的模型来定
constexpr int kTensorArenaSize = 100 * 1024; // 100KB
uint8_t tensor_arena[kTensorArenaSize];
// 创建解释器
static tflite::MicroMutableOpResolver<10> resolver;
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, kTensorArenaSize);
这里有个关键问题:kTensorArenaSize 设多大?
设小了,推理时内存不够,直接崩。设大了,MCU上RAM不够用。我一般这样估算:
- 先看模型文件大小,比如模型是50KB。
- 再加上中间结果的大小,这个要看模型结构。卷积层越多,中间结果越大。
- 最后留20%的余量。
举个例子:一个50KB的模型,中间结果大概需要30KB,那么总内存至少需要 (50 + 30) * 1.2 ≈ 96KB。取整,设100KB。
Serial.print("Used bytes: ");
Serial.println(interpreter.arena_used_bytes());
跑一次推理,看实际用了多少内存。然后根据这个值调整 kTensorArenaSize。我每次移植新模型,都会先跑这个,心里才有底。
还有一个配置项,你可能也会用到——MicroMutableOpResolver 的模板参数。这个参数指定了模型里最多有多少种算子。比如你的模型用了卷积、全连接、Softmax三种算子,那就设成3:
static tflite::MicroMutableOpResolver<3> resolver;
设小了,注册算子时会报错。设大了,浪费内存。我建议你直接看模型文件里的算子列表,数一下有多少种,再加1作为余量。
kTensorArenaSize 设得特别大,比如512KB。如果你的MCU有1MB RAM,那没问题。但如果你的MCU只有128KB RAM,这么搞直接溢出。记住:嵌入式开发,内存是稀缺资源,能省则省。
4.4 实战:跑通一个最小示例
光说不练假把式。咱们来写一个完整的示例,把上面讲的东西串起来。
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "tensorflow/lite/micro/micro_log.h"
// 模型数据(这里假设你已经有了一个量化后的.tflite模型)
extern const unsigned char g_model[];
extern const int g_model_len;
// 内存分配
constexpr int kTensorArenaSize = 100 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
void setup() {
// 1. 加载模型
const tflite::Model* model = tflite::GetModel(g_model);
// 2. 注册算子
static tflite::MicroMutableOpResolver<10> resolver;
// 这里可以手动注册,也可以用 AllOpsResolver 自动注册所有算子
// 3. 创建解释器
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, kTensorArenaSize);
// 4. 分配张量内存
TfLiteStatus allocate_status = static_interpreter.AllocateTensors();
if (allocate_status != kTfLiteOk) {
MicroPrintf("AllocateTensors() failed");
return;
}
// 5. 获取输入输出指针
float* input = static_interpreter.input(0)->data.f;
float* output = static_interpreter.output(0)->data.f;
MicroPrintf("Setup done! Arena used: %d bytes",
static_interpreter.arena_used_bytes());
}
void loop() {
// 填充输入数据
// ...
// 运行推理
static_interpreter.Invoke();
// 处理输出
// ...
delay(1000);
}
这段代码,你直接复制到Arduino或者STM32工程里,改一下模型数据,就能跑起来。
嗯,到这里,TFLM的环境搭建就讲完了。你可能会觉得内容有点多,但说实话,真正动手做一遍,比看十遍都管用。下一章,咱们要讲模型转换和量化——这可是把模型塞进MCU的关键一步,别错过。