2、基础知识:TFLite Micro推理流程、输出张量格式、常见模型输出(检测框、类别、分数)解析
TFLite Micro(简称 TFLM)是 TensorFlow Lite 针对微控制器(MCU)环境设计的推理引擎,其核心目标是在资源极度受限的硬件上(通常为几十 KB 到几百 KB 的 RAM,Flash 存储不超过 1MB)高效运行机器学习模型。与移动端或服务器端不同,TFLM 没有操作系统支撑,无法动态分配大块内存,也不支持完整的 C++ 标准库。因此,理解其推理流程和输出张量格式,是进行自定义后处理(如 NMS、阈值过滤、分类映射)的前提。本章将系统拆解 TFLM 从“模型加载”到“输出张量解析”的完整链路,并重点剖析检测类模型(如 MobileNet SSD、YOLO 系列轻量版)的输出结构。
上图展示了 TFLM 推理的完整链路。从模型获取到输出张量,再到后处理,每个环节都有其特定的数据格式和约束。下面我们将逐一深入解析。
2.1 TFLite Micro 推理流程核心原理
TFLM 的推理流程可以概括为四个阶段:模型加载与解释器创建、张量内存分配、输入数据填充、执行推理(Invoke)。与标准 TensorFlow Lite 不同,TFLM 没有文件系统访问能力,模型通常以 C 字节数组的形式编译进固件。
关键流程清单(以代码视角):
// 1. 模型以 C 数组形式声明
extern const unsigned char model_tflite[] = { ... };
// 2. 创建解释器(需要预先分配 Tensor Arena)
static tflite::MicroMutableOpResolver<10> resolver;
static tflite::MicroInterpreter interpreter(
model_tflite, resolver, tensor_arena, kTensorArenaSize);
// 3. 分配张量内存
TfLiteStatus allocate_status = interpreter.AllocateTensors();
// 4. 获取输入张量并填充数据
TfLiteTensor* input = interpreter.input(0);
// 将图像/信号数据按量化参数映射到 input->data.uint8 或 int8
// 5. 执行推理
TfLiteStatus invoke_status = interpreter.Invoke();
// 6. 获取输出张量
TfLiteTensor* output_boxes = interpreter.output(0);
TfLiteTensor* output_classes = interpreter.output(1);
TfLiteTensor* output_scores = interpreter.output(2);
理解这个流程的关键在于:Tensor Arena 是预先分配的静态内存池,所有中间张量都在这块内存中复用。这意味着输出张量的内存地址在 AllocateTensors() 之后是固定的,后处理代码可以直接访问这些地址,但必须注意在下次 Invoke() 之前完成数据拷贝或处理,否则数据会被覆盖。
2.2 输出张量格式深度解析
检测类模型(如 SSD MobileNet V2、EfficientDet-Lite)的输出张量通常有三个,但具体维度取决于模型训练时的配置和 TFLite 转换器的参数。下表列出了最常见的三种输出格式:
| 输出张量 | 常见维度 | 数据类型 | 含义说明 |
|---|---|---|---|
| 检测框 (Boxes) | [1][N][4] 或 [N][4] | float32 / int8 / uint8 | N 为最大检测数量(如 10、20、50)。每个框由 4 个值组成,通常为 [y_min, x_min, y_max, x_max],坐标已归一化到 [0,1] 范围。 |
| 类别 (Classes) | [1][N] 或 [N] | float32 / int8 / uint8 | 每个检测框对应的类别索引(整数)。注意:在量化模型中,该值可能是浮点表示,需要取整。 |
| 分数 (Scores) | [1][N] 或 [N] | float32 / int8 / uint8 | 每个检测框的置信度分数,范围 [0,1]。量化模型中需要反量化到浮点。 |
2.2.1 量化输出张量的反量化
TFLM 在 MCU 上广泛使用 8 位量化(int8 或 uint8)来减少内存占用和计算量。输出张量如果是量化类型,必须通过 scale 和 zero_point 参数进行反量化,才能得到真实的浮点值。反量化公式为:
real_value = (quantized_value - zero_point) * scale
在代码中,可以通过 output->params.scale 和 output->params.zero_point 获取这两个参数。例如,对于分数张量,量化后的 int8 值 42,若 scale=0.01,zero_point=-128,则真实分数为 (42 - (-128)) * 0.01 = 1.7,这显然超出了 [0,1] 范围,说明该检测框的分数无效(可能被填充为默认值)。
常见误区:很多开发者直接使用 output->data.uint8 中的原始值进行阈值比较,而忽略了反量化。这会导致阈值过滤完全失效。例如,设定阈值 0.5,但量化后的分数可能是 0 到 255 之间的整数,直接比较必然出错。务必先反量化再比较。
2.3 常见模型输出解析与案例拆解
以 SSD MobileNet V2 模型为例,该模型在 COCO 数据集上训练,可检测 80 类物体。经过 TFLite 转换后,输出张量通常为:
- 输出 0(Boxes):形状 [1][10][4],表示最多 10 个检测框。
- 输出 1(Classes):形状 [1][10],每个值对应类别索引(1-80,0 为背景)。
- 输出 2(Scores):形状 [1][10],每个检测框的置信度。
注意:输出张量的顺序在转换时可能不同。有些模型将 scores 放在第一个输出,boxes 放在最后。因此,在编写后处理代码前,必须通过 interpreter.outputs() 或模型元数据确认每个索引对应的实际含义。
案例:基于 TFLM 的人体检测后处理
假设我们有一个在 ESP32-S3 上运行的人体检测模型,输入为 96x96 灰度图,输出为 [1][5][4] 的 boxes、[1][5] 的 classes 和 [1][5] 的 scores。我们的目标是过滤出置信度大于 0.6 的检测框,并只保留类别为“person”(索引 1)的框。
初始现象:直接使用所有输出框绘制,发现画面中出现了大量重叠框和低置信度误检。
分析过程:
- 首先检查输出张量的量化参数,确认 scores 需要反量化。
- 打印前 5 个原始输出值,发现 scores 的量化值在 [-128, 127] 之间,反量化后大部分在 0.1~0.9 之间。
- 发现 classes 输出虽然是 int8,但部分值大于 80,说明这些是无效填充框(通常填充为 -1 或 0)。
实际操作步骤:
// 反量化 scores
float score = (tensor_scores->data.int8[i] - score_zero_point) * score_scale;
// 过滤低置信度
if (score < 0.6f) continue;
// 过滤非 person 类别
int class_id = tensor_classes->data.int8[i] - class_zero_point;
if (class_id != 1) continue;
// 反量化 boxes(坐标归一化)
float y_min = (boxes[4*i] - box_zero_point) * box_scale;
float x_min = (boxes[4*i+1] - box_zero_point) * box_scale;
float y_max = (boxes[4*i+2] - box_zero_point) * box_scale;
float x_max = (boxes[4*i+3] - box_zero_point) * box_scale;
// 映射到图像像素坐标
int pixel_y_min = (int)(y_min * image_height);
int pixel_x_min = (int)(x_min * image_width);
// ... 绘制或进一步处理
结果对比:过滤前有 5 个框,过滤后仅剩 2 个有效框,误检率降低 60%,且没有漏检。
风险与代价:阈值设置过高(如 0.8)可能导致漏检;设置过低(如 0.3)则可能引入噪声。需要根据实际场景调整。
经验提醒:在 MCU 上调试输出张量时,建议通过串口打印原始量化值和反量化后的浮点值,对比验证。不要假设模型输出一定是浮点类型,务必检查 tensor->type 字段。
2.4 常见误区与总结复盘
在 TFLM 后处理开发中,以下误区最为常见:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 忽略输出张量顺序 | 将 boxes 当作 scores 使用,导致逻辑混乱 | 通过 interpreter.output(i) 逐个打印维度,确认顺序 |
| 未处理量化参数 | 阈值过滤失效,坐标严重偏移 | 始终使用 scale 和 zero_point 反量化 |
| 假设输出框数量固定有效 | 将填充的无效框(score=0 或 class=-1)当作真实检测 | 先检查 score 是否大于 0,再检查 class 是否在有效范围内 |
| 坐标未归一化 | 绘制框位置错误 | 确认模型输出是归一化坐标(0~1),再乘以图像宽高 |
| 在 Invoke 后延迟处理输出 | 下次推理覆盖了输出数据 | 在 Invoke 后立即拷贝或处理输出数据 |
总结复盘:掌握 TFLM 推理流程的核心在于理解“静态内存分配”和“量化数据流”。输出张量的解析不是简单的数组读取,而是一个涉及数据类型转换、参数映射和有效性判断的工程过程。建议在项目初期就编写一个通用的“输出张量调试工具函数”,打印每个输出的维度、类型、scale、zero_point 以及前几个原始值,这能极大加速后续后处理算法的开发与调试。