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这些。网速慢的话,可能要等个十来分钟。

我的经验: 如果你在公司内网,或者网络环境不好,建议提前把依赖包下载好。我曾经在客户现场,网络差到连GitHub都连不上,最后只能手动把依赖包拷进去。那叫一个狼狈。

第三步,验证编译结果。编译完成后,在 gen/linux_x86_64_default/bin/ 目录下会生成一堆测试程序。随便跑一个看看:

./gen/linux_x86_64_default/bin/hello_world_test

看到输出 ~~~ALL TESTS PASSED~~~ 了吗?恭喜你,环境搭好了。

注意: 如果你编译的是ARM Cortex-M版本,需要指定目标架构。比如:
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不支持的算子,比如LSTM、GRU这些循环神经网络算子,编译时会报错。我曾经有个项目,模型里用了LSTM做时序预测,结果移植到MCU上才发现不支持。最后只能把模型拆成多个小模型,用状态机来模拟。那段时间,我做梦都在改代码。

怎么查看当前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不够用。我一般这样估算:

  1. 先看模型文件大小,比如模型是50KB。
  2. 再加上中间结果的大小,这个要看模型结构。卷积层越多,中间结果越大。
  3. 最后留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的关键一步,别错过。