模型部署
// 导入模型
#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 = µ_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
↓
输出最终预测数字
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)