- 物联网
- 嵌入式
- 操作系统
- 实时系统
【免费下载链接】RIOT
RIOT - The friendly OS for IoT
导读
本文围绕 RIOT 仓库 tests/pkg/utensor/README.md 展开,系统讲解如何在一个嵌入式 IoT 操作系统 RIOT 上运行 uTensor 深度学习推理引擎:用 TensorFlow 训练一个 MLP(多层感知机)模型,在 MNIST 数据集上完成手写数字识别,并通过utensor-cli工具把模型与权重转换为 C++ 代码,静态烧写进固件后在 MCU 上执行推理。读完本文,你将掌握该示例的构建运行方法、如何更换待识别数字、以及如何重新训练模型并重新生成 C++ 模型文件的完整工作流。
示例概览:在 MCU 上跑一个神经网络
uTensor 是面向 MCU 的轻量级深度学习推理框架。本示例应用展示了一条完整的"端到端"链路:
- 使用 TensorFlow 在 MNIST 数据集上训练一个 MLP(Multi-Layer Perceptron)模型,用于识别图像中的手写数字(0–9);
- 通过
utensor-cli将训练好的模型(Protocol Buffer 格式的.pb文件)转换为 C++ 源码(模型结构 + 权重); - 将 C++ 模型文件作为 RIOT 的外部模块(external module)编译进固件;
- 把一张 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-stm32f103c8、nucleo-f103rb、microbit、samd10-xmini、stm32f0discovery等数十块小内存板卡),CI 会跳过这些内存不足的板卡。选择板卡时建议优先选用内存充足的型号(例如nucleo-f746zg、esp32-wroom-32等),否则可能链接失败。
同时,Makefile 中通过CFLAGS += -DTHREAD_STACKSIZE_MAIN=THREAD_STACKSIZE_LARGE为main线程分配更大的栈空间,以满足 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, --index | 0 | 在 MNIST 测试集中选取的图片索引 |
-o, --output | digit | 输出二进制文件名 |
--no-plot | False | 禁用 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.pb4. 生成 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_modulesUSEPKG += utensor声明依赖 pkg/utensor 外部包,uTensor 推理引擎的源码由此获得;EXTERNAL_MODULE_DIRS += external_modules把external_modules目录加入外部模块搜索路径,其中的models子模块(含Makefile与Makefile.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
相关推荐
Caffe MNIST手写数字识别实战:从数据准备到模型训练完整流程
Caffe MNIST手写数字识别实战:从数据准备到模型训练完整流程 想要快速入门深度学习实战?Caffe框架结合MNIST手写数字识别项目是完美的起点!🎯
深度学习计算机视觉机器学习使用GGML实现MNIST手写数字识别:从训练到推理全流程解析
使用GGML实现MNIST手写数字识别:从训练到推理全流程解析 项目概述 GGML是一个专注于机器学习模型推理的轻量级库,特别适合在资源受限的环境中运行。本文将
人工智能机器学习深度学习算子库本地部署模型量化Caffe 实战:在 MNIST 上训练 LeNet 手写数字识别模型(从数据准备到训练部署全流程)
Caffe 实战:在 MNIST 上训练 LeNet 手写数字识别模型(从数据准备到训练部署全流程) 本文以 Caffe 官方 MNIST 示例为线索,完整讲解
深度学习计算机视觉
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考