模型部署
// 导入模型 #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 ↓ 输出最终预测数字

相关新闻

ZonyLrcToolsX歌词下载完全攻略:一条命令批量补齐本地音乐库

ZonyLrcToolsX歌词下载完全攻略:一条命令批量补齐本地音乐库

ZonyLrcToolsX歌词下载完全攻略:一条命令批量补齐本地音乐库 【免费下载链接】ZonyLrcToolsX ZonyLrcToolsX 是一个能够方便地下载歌词的小软件。 项目地址: https://gitcode.com/gh_mirrors/zo/ZonyLrcToolsX 你有没有过这样的经历:从网盘里导出…

2026/10/31 8:22:23 阅读更多 →
AI编程助手商业模式解析:从免费到订阅,数据如何驱动价值

AI编程助手商业模式解析:从免费到订阅,数据如何驱动价值

1. 从“免费午餐”到“付费订阅”:AI编程工具的商业模式演进 最近和几个开发朋友聊天,大家不约而同地提到了一个现象:前两年还觉得“真香”的Github Copilot,现在用起来感觉越来越“肉疼”了。一方面是订阅费确实不便宜&#xff0…

2026/10/29 8:44:37 阅读更多 →
深入理解 GPU 共享内存的 Bank Conflict(存储体冲突)

深入理解 GPU 共享内存的 Bank Conflict(存储体冲突)

深入理解 GPU 共享内存的 Bank Conflict(存储体冲突) 前言 如果你写过 CUDA 程序,尤其是做过矩阵乘法、卷积、归约(reduction)这类需要高性能的算子,那么"Bank Conflict"这个词你一定绕不开。它…

2026/10/30 23:06:23 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/31 0:19:11 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/30 9:20:34 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/30 10:19:21 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/31 0:59:28 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/30 10:19:39 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/30 9:21:37 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/30 17:32:24 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/30 10:28:40 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/31 0:09:01 阅读更多 →