// 导入模型
#include "sine_model.h"

// 正弦波参数
constexpr int led_pin = 2;
constexpr float pi = 3.14159265;
constexpr float freq = 0.5;
constexpr float period = (1 / freq) * 1000000;

// TensorFlow Lite 全局变量
namespace {
  tflite::ErrorReporter* error_reporter = nullptr;
  const tflite::Model* model = nullptr;
  tflite::MicroInterpreter* interpreter = nullptr;
  TfLiteTensor* model_input = nullptr;
  TfLiteTensor* model_output = nullptr;

  // Tensor 内存池
  constexpr int kTensorArenaSize = 5 * 1024;
  uint8_t tensor_arena[kTensorArenaSize];
}

void setup() {

  // 初始化日志
  static tflite::MicroErrorReporter micro_error_reporter;
  error_reporter = &micro_error_reporter;

  // 加载模型
  model = tflite::GetModel(sine_model);

  // 注册模型使用的算子
  static tflite::MicroMutableOpResolver micro_mutable_op_resolver;
  micro_mutable_op_resolver.AddBuiltin(
      tflite::BuiltinOperator_FULLY_CONNECTED,
      tflite::ops::micro::Register_FULLY_CONNECTED(),
      1, 3);

  // 创建解释器
  static tflite::MicroInterpreter static_interpreter(
      model,
      micro_mutable_op_resolver,
      tensor_arena,
      kTensorArenaSize,
      error_reporter);

  interpreter = &static_interpreter;

  // 分配 Tensor 内存
  interpreter->AllocateTensors();

  // 获取输入输出 Tensor
  model_input = interpreter->input(0);
  model_output = interpreter->output(0);
}

void loop() {

  // 生成输入数据
  unsigned long timestamp = micros() % (unsigned long)period;
  float x_val = ((float)timestamp * 2 * pi) / period;

  // 写入模型输入
  model_input->data.f[0] = x_val;

  // 执行推理
  interpreter->Invoke();

  // 获取模型输出
  float y_val = model_output->data.f[0];

  // 输出到 LED
  int brightness = (int)(255 * y_val);
  analogWrite(led_pin, brightness);
}

整体流程:

一:加载模型
    ↓
二:创建解释器
    ↓
三:分配 Tensor Arena 内存
    ↓
四:不断获取时间
    ↓
五:把时间转换成 x
    ↓
六:送入神经网络
    ↓
七:得到 y = sin(x)
    ↓
八:控制 LED 亮度

#一:加载模型

加载模型最关键的代码只有两步:

第一步:包含模型文件

#include "sine_model.h"

这个文件通常长这样:

const unsigned char sine_model[] = {
    0x20, 0x00, 0x00, ...
};

const int sine_model_len = 2488;

第二步:加载模型

model = tflite::GetModel(sine_model);

这里的:

sine_model  就是模型数组的地址

GetModel()干了什么?

作用:

解析模型
读取网络结构
读取权重
建立Model对象

返回:

const tflite::Model* #指针

接下来为什么检查版本? 

if (model->version() != TFLITE_SCHEMA_VERSION)
{
    error_reporter->Report(
        "Model version does not match Schema");
}

作用:

检查:

模型版本
=
解释器版本

例如:

模型: 3.0
解释器: 4.0

可能无法运行。

所以先检查。


model 里面到底有什么?

可以简单理解为:

model
├── 网络结构
├── 权重参数
├── 输入信息
├── 输出信息
└── 算子信息

例如:

Input(1)
    ↓
Dense(16)
    ↓
Dense(16)
    ↓
Dense(1)

这些信息都在里面。


后面为什么要创建解释器?

仅仅:

model = tflite::GetModel(...)

只是把模型解析出来。

还不能运行。

必须创建:

MicroInterpreter

如果用一句话概括:

GetModel()并没有把模型“加载到内存里”,
它只是把 Flash 中的 tflite 二进制数据解释成 TensorFlow Lite 能认识的 Model 结构,
后面由 MicroInterpreter 使用这个 Model 来完成推理。

#二:创建解释器

创建解释器的代码

// 创建 TensorFlow Lite Micro 解释器
static tflite::MicroInterpreter static_interpreter(
    model,                      // 模型
    micro_mutable_op_resolver,  // 已注册的算子
    tensor_arena,               // Tensor内存池地址
    kTensorArenaSize,           // Tensor内存池大小
    error_reporter);            // 错误报告器
                                // 这些参数的内容都是提前写好的,这里只传入即可
 
// 保存解释器指针,方便后续调用
interpreter = &static_interpreter;

解释器是什么?

可以把它理解成:

模型(Model)      = 图纸
解释器(Interpreter) = 工人

仅有模型:

model = tflite::GetModel(sine_model);

只是拿到了神经网络结构和权重。

并不会运行。


解释器负责:

读取模型
    ↓
申请Tensor内存
    ↓
执行各层计算
    ↓
输出结果

即:

interpreter->Invoke();

真正干活的是解释器。


创建解释器时传入的参数

📌1. model

model

前面加载好的模型。

例如:

Input
  ↓
Dense
  ↓
Dense
  ↓
Output

解释器需要知道网络长什么样。


📌2. micro_mutable_op_resolver

作用:

你的模型会用到哪些算子(Operator)

例如(手动注册需要使用的算子):

resolver.AddBuiltin(
    tflite::BuiltinOperator_FULLY_CONNECTED,
    tflite::ops::micro::Register_FULLY_CONNECTED()
);

因为,TensorFlow Lite Micro 不会把所有算子都编译进去,因为 MCU 内存太小。 


📌3. tensor_arena(内存的首地址)和 kTensorArenaSize(内存大小)

代码位置

constexpr int kTensorArenaSize = 5 * 1024;
uint8_t tensor_arena[kTensorArenaSize];

这里创建了:

5 × 1024 = 5120 字节   5KB 内存

为什么需要它?

模型文件(.tflite)只保存:

权重
偏置
网络结构

但是推理时还需要存放:

输入张量
输出张量
中间层结果
算子工作区

这些数据都放在:

tensor_arena

运行时的内存布局

tensor_arena
┌───────────────┐
│ Input Tensor  │
├───────────────┤
│ Dense1 Output │
├───────────────┤
│ Dense2 Output │
├───────────────┤
│ Output Tensor │
├───────────────┤
│ Scratch Buffer│
└───────────────┘

📌4. error_reporter

error_reporter

错误输出接口。

例如:

AllocateTensors failed

会打印到串口。


创建完成后

interpreter = &static_interpreter;     #让全局指针指向解释器

以后直接通过interpreter 操作模型

interpreter->AllocateTensors();
interpreter->Invoke();
interpreter->input(0);
interpreter->output(0);

实际上等价于:

static_interpreter.AllocateTensors();
static_interpreter.Invoke();
static_interpreter.input(0);
static_interpreter.output(0);

#三:分配 Tensor Arena 内存

// 从 tensor_arena 中为模型的张量分配内存
TfLiteStatus allocate_status = interpreter->AllocateTensors();

if (allocate_status != kTfLiteOk) {
    error_reporter->Report("AllocateTensors() failed");
    while (1);
}

先理解什么是 Tensor

神经网络运行时,数据会不断流动:

输入
 ↓
第一层
 ↓
第二层
 ↓
输出

例如:

x = 1.57

进入网络后:

输入Tensor
 ↓
隐藏层Tensor
 ↓
输出Tensor

这些过程数据都需要内存存放。


AllocateTensors() 做什么

执行:

interpreter->AllocateTensors();

时,

解释器会查看模型结构(例如):

Input(1)
 ↓
Dense(8)
 ↓
Dense(1)

它会计算:

输入需要多少字节
中间层需要多少字节
输出需要多少字节

然后在:

tensor_arena

里划分空间。

类似:

tensor_arena

┌────────────┐
│ Input      │
├────────────┤
│ Hidden     │
├────────────┤
│ Output     │
├────────────┤
│ Scratch    │
└────────────┘

分配前是什么样

刚创建时:

MicroInterpreter interpreter(...);

只是知道:

模型在哪
arena在哪
有哪些算子

但是:

Input Tensor
Output Tensor
中间Tensor

都还没有地址。

类似:

Input Tensor
地址:未知

Output Tensor
地址:未知

AllocateTensors 后

执行:

AllocateTensors();

之后:

Input Tensor
地址:0x3FFC0000

Output Tensor
地址:0x3FFC0040

Hidden Tensor
地址:0x3FFC0080

都确定了。

所以后面才能:

interpreter->input(0);     //返回的就是内存的地址

interpreter->output(0);    //返回的就是内存的地址

一句话理解

interpreter->AllocateTensors();

作用就是:

根据模型结构,在 tensor_arena 内存池中给输入、输出和中间计算结果划分内存空间,并把所有 Tensor 的地址建立好。

没有这一步,模型根本不知道数据该放在哪里,也无法执行 Invoke()


#四:获取输入数据

    File file = SD.open("/digit_28x28.bmp");//变量(容器)长什么样,只由前面的类型决定,变量名只是一个名字

    if (!file)
    {
        Serial.println("Open Failed");
        delay(2000);
        return;
    }

    uint8_t pixels[784];

    file.seek(1078);
    file.read(pixels, 784);
    file.close();

#五:输入数据预处理

    float in_scale = model_input->params.scale;
    int32_t in_zp  = model_input->params.zero_point;

    //通过循环,对一张图的所有点做处理
    for (int row = 0; row < 28; row++)
    {
        for (int col = 0; col < 28; col++)
        {
            int bmp_idx;
            float real_val;

            //反行列存储的bmp图像数据
            bmp_idx = (27 - row) * 28 + col;
            real_val = pixels[bmp_idx] / 255.0f;

            // 量化
            int32_t q = (int32_t)roundf(real_val / in_scale) + in_zp;

            q = max(-128, min(127, q));

            model_input->data.int8[row * 28 + col] =(int8_t)q;
        }
    }

#七:运行推理

    if (interpreter->Invoke() != kTfLiteOk)
    {
        Serial.println("Invoke Failed");
        delay(2000);
        return;
    }

解释:

前面已经把输入数据放进去了:

model_input->data.int8[row * 28 + col] =(int8_t)q;

例如:

x_val = 1.57

此时数据已经在输入 Tensor 里,但模型还没开始计算。


执行:

interpreter->Invoke();

后,TensorFlow Lite Micro 会:

读取输入 从Tensor
      ↓
执行神经网络各层
      ↓
计算输出结果
      ↓
写入输出 到Tensor

#八:获取模型输出

前面执行:

interpreter->Invoke();

之后,神经网络已经完成计算。

计算结果会被写入:

model_output

也就是:

model_output->data.int8

对于 MNIST 模型,一般有:

10个输出值
↓
分别对应数字 0~9

例如:

0 → 数字0的输出
1 → 数字1的输出
2 → 数字2的输出
...
9 → 数字9的输出

但是这里有一个问题:

模型使用的是 INT8 量化,因此:

model_output->data.int8[i]

得到的是:

INT8量化值

而不是训练时常见的:

0.0 ~ 1.0

所以还需要进行:

反量化

#九:输出反量化

反量化代码:

float out_scale = model_output->params.scale;
int32_t out_zp  = model_output->params.zero_point;

这里获取模型输出 Tensor 的两个量化参数:

scale
zero_point

INT8 反量化公式:

真实值 = (量化值 - zero_point) × scale

对应代码:

float val =
    (model_output->data.int8[i] - out_zp) * out_scale;

例如:

INT8输出值 = 100
zero_point = -128
scale = 0.0039

那么:

真实值
= (100 - (-128)) × 0.0039
≈ 0.889

所以:

INT8
 ↓
反量化
 ↓
float

#十:遍历所有类别

MNIST 有 10 个类别:

0 1 2 3 4 5 6 7 8 9

因此遍历:

for (int i = 0; i < 10; i++)

每循环一次,就处理一个数字类别。

完整代码:

for (int i = 0; i < 10; i++)
{
    float val =
        (model_output->data.int8[i] - out_zp) * out_scale;

    Serial.print(i);
    Serial.print(" : ");
    Serial.println(val, 6);
}

例如串口可能输出:

All Scores:

0 : 0.0012
1 : 0.0004
2 : 0.0021
3 : 0.9815
4 : 0.0007
5 : 0.0032
6 : 0.0010
7 : 0.0041
8 : 0.0020
9 : 0.0015

这表示:

模型分别对 0~9 这10个类别给出了一个输出值。

#十一:寻找最大输出值

最终需要知道:

哪个类别输出最大?

先定义:

int maxIndex = 0;

表示:

当前最大值对应的类别

再初始化:

float maxVal =
    (model_output->data.int8[0] - out_zp) * out_scale;

表示:

先把第0类作为当前最大值

然后在循环中比较:

if (val > maxVal)
{
    maxVal = val;
    maxIndex = i;
}

逻辑:

当前类别输出 > 当前最大值
        ↓
更新最大值
        ↓
记录对应类别

例如:

0 → 0.0012
1 → 0.0004
2 → 0.0021
3 → 0.9815  ← 最大
4 → 0.0007
...

最终:

maxIndex = 3
maxVal   = 0.9815

#十二:得到最终预测结果

输出:

Serial.print("\nPrediction = ");
Serial.println(maxIndex);

例如:

Prediction = 3

表示:

模型预测结果 = 数字3

所以:

maxIndex

就是最终预测类别。


#十三:输出最大分数

代码:

Serial.print("Confidence = ");
Serial.println(maxVal, 6);

例如:

Confidence = 0.981500

这里的 maxVal 是:

10个输出值中最大的那个值

对于使用 Softmax 的分类模型,可以把它理解为:

预测类别的概率

例如:

Prediction = 3
Confidence = 0.9815

可以理解为:

模型认为输入图片是数字3
对应概率约为98.15%

但需要注意:

maxVal ≠ 永远都是概率

只有模型输出经过 Softmax 等概率归一化处理时,才适合直接理解成概率。

更准确地说:

maxIndex → 预测类别
maxVal   → 最大输出分数

#十四:完整的推理结果流程

到这里,整个 MNIST 推理过程就完整了:

BMP图片
    ↓
读取784个像素
    ↓
图像预处理
    ↓
归一化
    ↓
INT8量化
    ↓
写入输入Tensor
    ↓
interpreter->Invoke()
    ↓
神经网络计算
    ↓
得到INT8输出Tensor
    ↓
INT8反量化
    ↓
得到10个类别输出值
    ↓
寻找最大值
    ↓
得到maxIndex
    ↓
输出最终预测数字
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐