ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

模型部署

2026/8/14 13:14:44 拓冰建站 浏览量
模型部署 // 导入模型 #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. modelmodel前面加载好的模型。例如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_reportererror_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_pointINT8 反量化公式真实值 (量化值 - 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 ↓ 输出最终预测数字