RIOT 中的 uTensor MNIST 手写数字识别示例:从 TensorFlow 模型训练到 MCU 端推理
2026/9/20 23:32:47 网站建设 项目流程
  • 物联网
  • 嵌入式
  • 操作系统
  • 实时系统

【免费下载链接】RIOT

RIOT - The friendly OS for IoT

项目地址:https://gitcode.com/GitHub_Trending/riot/RIOT
点击查看免费下载

导读

本文围绕 RIOT 仓库 tests/pkg/utensor/README.md 展开,系统讲解如何在一个嵌入式 IoT 操作系统 RIOT 上运行 uTensor 深度学习推理引擎:用 TensorFlow 训练一个 MLP(多层感知机)模型,在 MNIST 数据集上完成手写数字识别,并通过utensor-cli工具把模型与权重转换为 C++ 代码,静态烧写进固件后在 MCU 上执行推理。读完本文,你将掌握该示例的构建运行方法、如何更换待识别数字、以及如何重新训练模型并重新生成 C++ 模型文件的完整工作流。

示例概览:在 MCU 上跑一个神经网络

uTensor 是面向 MCU 的轻量级深度学习推理框架。本示例应用展示了一条完整的"端到端"链路:

  1. 使用 TensorFlow 在 MNIST 数据集上训练一个 MLP(Multi-Layer Perceptron)模型,用于识别图像中的手写数字(0–9);
  2. 通过utensor-cli将训练好的模型(Protocol Buffer 格式的.pb文件)转换为 C++ 源码(模型结构 + 权重);
  3. 将 C++ 模型文件作为 RIOT 的外部模块(external module)编译进固件;
  4. 把一张 MNIST 测试集图片以二进制 blob 形式嵌入固件,在设备上运行推理并打印预测的标签。

该应用的代码源自一个公开的博客示例(Hackster.io 上的 "Simple Neural Network on MCUs"),由 RIOT 团队(Inria)适配进仓库,版权信息见 main.cpp 头部注释。

应用运行时输出如下:

Simple MNIST end-to-end uTensor cli example (device) Predicted label: 7

仓库自带的自动化测试 tests/01-run.py 正是通过child.expect_exact("Predicted label: 7")来校验该默认输出,确保端到端推理链路可用。

快速构建与运行

在具备 RIOT 工具链(make+ 对应架构的交叉编译器)的机器上,进入示例目录并指定目标板卡即可编译并打开串口终端:

make BOARD=<board of your choice> all term

例如使用native板卡或任意 RAM/Flash 充足的开发板。term目标会编译完成后直接打开终端连接设备的串口,便于观察推理输出。

内存约束:并非所有板卡都能跑

神经网络的权重文件 deep_mlp_weight.hpp 约 650 KB(包含所有层权重),加上 uTensor 运行时,对板卡的 RAM/Flash 有一定要求。仓库在 Makefile.ci 中显式声明了BOARD_INSUFFICIENT_MEMORY列表(如bluepill-stm32f103c8nucleo-f103rbmicrobitsamd10-xministm32f0discovery等数十块小内存板卡),CI 会跳过这些内存不足的板卡。选择板卡时建议优先选用内存充足的型号(例如nucleo-f746zgesp32-wroom-32等),否则可能链接失败。

同时,Makefile 中通过CFLAGS += -DTHREAD_STACKSIZE_MAIN=THREAD_STACKSIZE_LARGEmain线程分配更大的栈空间,以满足 uTensor 推理时的栈需求。

输入数字从哪来:blob 机制

待识别的手写数字图片存放在应用目录下的digit二进制文件中(28×28 像素、每像素以 float32 存储,共 28×28×4 = 3136 字节)。构建时,它通过 RIOT 构建系统的blob 机制(Makefile 中的BLOBS += digit)被自动转换为一个 C 数组digit,以静态数据的方式嵌入固件镜像。

在 main.cpp 中可以看到完整的推理主流程:

alignas(float) #include "blob/digit.h" // 嵌入的 MNIST 测试图片(float32 数组) #include "deep_mlp.hpp" // utensor-cli 生成的模型文件 #include "tensor.hpp" // uTensor 张量类 int main() { puts("Simple MNIST end-to-end uTensor cli example (device)\n"); // 创建上下文,推理在该上下文中执行 Context ctx; // blob 以字节存储,借助 alignas(float) 保证对齐后转为 float 指针 float *digit_as_float = (float *)(uintptr_t)digit; // 把输入图片包装成张量:{1, digit_len >> 2},digit_len 为字节数,>>2 得到 float 个数(784) auto input_x = new WrappedRamTensor<float>({1, digit_len >> 2}, digit_as_float); // 将输入张量的所有权移交给 context,并注册模型计算图 get_deep_mlp_ctx(ctx, input_x); // 取输出张量引用并触发推理 S_TENSOR pred_tensor = ctx.get("y_pred:0"); ctx.eval(); // 读取预测标签(one-hot 向量中的最大索引)并打印 uint8_t pred_label = *(pred_tensor->read<int>(0, 0)); printf("Predicted label: %d\r\n", pred_label); return 0; }

关键点解析:

  • get_deep_mlp_ctx(ctx, input_x)utensor-cli生成(声明见 deep_mlp.hpp),其实现 deep_mlp.cpp 内部通过ctx.add(...)ctx.push(new MatMulOp()/new QuantizedMatMulOp()...)等调用把整个计算图(占位符、reshape、量化、矩阵乘法、Softmax 等算子)注册进Context
  • 输出张量名为y_pred:0,对应 README 中utensor-cli convert命令的--output-nodes=y_pred
  • ctx.eval()触发整张计算图的执行。

更换待识别数字

默认的digit文件包含 MNIST 测试集的第一张图片——一个手写的7。你可以用示例自带的 Python 脚本 generate_digit.py 从 MNIST 测试集中选取其他图片重新生成digit文件:

./generate_digit.py --index 1

脚本参数说明:

参数默认值含义
-i, --index0在 MNIST 测试集中选取的图片索引
-o, --outputdigit输出二进制文件名
--no-plotFalse禁用 matplotlib 弹出图片窗口

脚本会调用tf.keras.datasets.mnist.load_data()加载 MNIST 测试集,取第index张图片,将其astype('float32')后以二进制原样写入digit文件(与 blob 机制的 float32 布局一致)。每个选中的数字在脚本结束时都会以图像形式显示出来,方便与固件预测出的标签做"目测"对比。

需要注意:每次生成新的 digit 后,固件必须重新编译——图片是作为 blob 静态嵌入固件镜像的,不重新烧写固件就无法生效:

make BOARD=<board of your choice> all flash term

重新训练模型并更新 C++ 模型文件

应用默认包含一个预训练模型,存放在external_modules/models外部模块中(除 Makefile 外,所有 C++ 文件——模型结构 + 权重——均由utensor-cli从 TensorFlow 训练的模型自动生成)。如果希望训练新模型并同步更新应用中的 C++ 文件,可按以下步骤操作。

1. 安装 Python3 依赖

pip3 install --user utensor_cgen graphviz pip3 install --user tensorflow -U

注意:utensor_cgen目前仅兼容 TensorFlow 1.x,请勿使用 TensorFlow 2.x 执行下述转换流程。

2. 克隆 utensor-mnist-demo 仓库

该仓库包含在 MNIST 数据集上训练 MLP 模型的 Python 脚本:

cd /tmp git clone <uTensor/utensor-mnist-demo 仓库地址>

3. 训练 MLP 模型

cd /tmp/utensor-mnist-demo python3 /tmp/utensor-mnist-demo/tensorflow-models/deep_mlp.py

训练完成后,模型以 Protocol Buffer 格式存放在:

/tmp/utensor-mnist-demo/mnist_model/deep_mlp.pb

4. 生成 C++ 模型文件并纳入 RIOT 构建

cd $RIOTBASE/tests/pkg/utensor utensor-cli convert /tmp/utensor-mnist-demo/mnist_model/deep_mlp.pb --target utensor --output-nodes=y_pred

该命令会在当前目录(应用目录)生成模型与权重 C++ 文件(deep_mlp.cpp/deep_mlp.hpp/deep_mlp_weight.hpp),随后即可重新编译固件。生成结果需覆盖或替换 external_modules/models 目录下的同名文件。

外部模块如何接入 RIOT 构建

生成的模型 C++ 文件通过 RIOT 的外部模块机制接入构建系统。相关配置集中在应用的 Makefile:

include ../Makefile.pkg_common USEPKG += utensor # 引入 uTensor 包(pkg/utensor) # Include input data as blob BLOBS += digit # 将 digit 二进制文件作为 blob 嵌入固件 # Include C++ model and training weights code as an external module USEMODULE += models EXTERNAL_MODULE_DIRS += external_modules
  • USEPKG += utensor声明依赖 pkg/utensor 外部包,uTensor 推理引擎的源码由此获得;
  • EXTERNAL_MODULE_DIRS += external_modulesexternal_modules目录加入外部模块搜索路径,其中的models子模块(含MakefileMakefile.include)被USEMODULE += models拉入构建;
  • models/Makefile仅包含include $(RIOTBASE)/Makefile.base,声明这是一个标准模块;models/Makefile.include 则负责把头文件目录加入USEMODULE_INCLUDES,并追加CXXEXFLAGS += -Wno-unused-parameter抑制生成代码中未使用参数的告警;
  • 生成的模型代码使用了 C++(如new、模板类),因此该示例依赖 C++ 工具链支持(RIOT 的 C++ 支持)。

小结:一条可复用的 MCU 端 ML 流水线

该示例完整演示了在 RIOT 上落地一个端到端 ML 应用的标准路径:

  • 训练侧:TensorFlow(1.x)训练 MLP → 导出.pb模型;
  • 转换侧utensor-cli convert --target utensor --output-nodes=y_pred生成 C++ 模型与权重;
  • 集成侧USEPKG+USEMODULE+EXTERNAL_MODULE_DIRS接入 RIOT 构建,BLOBS机制静态嵌入输入数据;
  • 推理侧Context+WrappedRamTensor+ctx.eval()在 MCU 上完成前向计算,输出预测标签。

对于希望把更大模型(CNN、量化模型等)搬到 RIOT + uTensor 上的开发者,本文的每一步都可直接复用:更换训练脚本产出新的.pb,重新执行utensor-cli convert,再调整main.cpp中的输入输出张量名即可。

  • 物联网
  • 嵌入式
  • 操作系统
  • 实时系统

【免费下载链接】RIOT

RIOT - The friendly OS for IoT

项目地址:https://gitcode.com/GitHub_Trending/riot/RIOT
点击查看免费下载

相关推荐

上一篇:Envoy Google VRP 安全赏金计划实战:双 Envoy 边界/源站架构、威胁模型与 Docker 执行环境
下一篇:Elasticsearch Node.js客户端错误处理与重试机制:构建稳定应用的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询