esp32示例程序(使用 ESP-DL 深度学习库基于 ESP32-S3 实现手势识别)
人工智能改变了计算机与现实世界交互的方式 。过去 ,人们通过微小的低功率设备和传感器获取数据 ,并传输至云端进行决策 。这样的方式在设备连接性 、成本和数据隐私方面带来了一定挑战。相对地,边缘人工智能是在物理设备上另一种处理数据的方式 ,它无需在设备和云之间来回发送数据 ,改善了信息延迟 、提高了安全性、减少了对带宽的要求和功耗水平 。
乐鑫推出了深度学习开发库 ESP-DL ,您可以使用它在乐鑫的 AI SoC ESP32-S3 上部署您自己的高性能深度学习模型 。在这篇文章中 ,我们将为您介绍如何使用 ESP-DL 在 ESP32-S3 上部署深度学习模型 。本文仅用于开发者学习使用 ,其中模型并非商业化项目 。
目录
本文的主要内容为:
深度学习模型开发 ESP-DL 格式 深度学习模型部署 未来的计划使用 ESP-DL 前的准备工作
在深入了解 ESP-DL 之前 ,我们假设读者已经具备了以下知识储备:
构建和训练神经网络的知识(点此了解深度学习的相关基础知识) 能够配置乐鑫物联网开发框架 ESP-IDF release 4.4 版本的开发环境(更多细节请阅读如何搭建 ESP-IDF 开发环境或 ESP-IDF 工具链说明) 具备基本的 C 和 C++ 编程语言知识 具备模型训练后量化的知识1. 模型开发
为了简单起见 ,我们以分类问题为例进行阐述 。我们开发了一个简单的深度学习模型 ,并对 6 种不同的手势进行了分类 。尽管有许多可以直接使用的开源预训练模型 ,但对于这个演示,我们从零开始建立模型 ,以更好地讲解模型中的每一层 。
注:我们使用了 Google Co-lab 进行模型开发 。
1.1 数据集
针对该分类问题 ,我们使用了 Kaggle 手势识别数据集中的一个开源数据集 。原始数据集包括 10 个类别,我们只使用了其中 6 个。这些类别更容易识别 ,且日常生活中更有用 ,如下表所示 。我们的数据集较原数据集还有一处关于图像大小的区别,在原始数据集中 ,图像的大小为 (240, 640) ,但为了方便起见 ,我们将数据集的大小调整为 (96, 96) 。本文中使用的数据集可以在这里找到。
手势
所用标签
手掌
0
I
1
拇指
2
食指
3
OK
4
C
5
表格 1:手势分类
1.2 测试/训练分离
我们需要将数据集分为测试和训练数据集 。这些数据集是我们原始数据集的子集 ,训练数据集用于训练模型 、测试数据集用于测试模型的性能 。校准数据集在模型量化阶段用于校准 ,您可以从训练集和测试集中抽选一部分作为校准数据集。生成以上数据集的过程是相同的 ,我们使用了 train_test_split 以实现此目标 。
from sklearn.model_selection import train_test_split ts = 0.3 # Percentage of images that we want to use for testing. X_train, X_test1, y_train, y_test1 = train_test_split(X, y, test_size=ts, random_state=42) X_test, X_cal, y_test, y_cal = train_test_split(X_test1, y_test1, test_size=ts, random_state=42)注:点击这里 ,了解关于 train_test_split 的更多细节
如果您需要转载本教程 ,您可以在此 GitHub 页面获得数据 ,并在您的工作环境中开放数据 。
import pickle with open(X_test.pkl, rb) as file: X_test = pickle.load(file) with open(y_test.pkl, rb) as file: y_test = pickle.load(file) with open(X_train.pkl, rb) as file: X_train = pickle.load(file) with open(y_train.pkl, rb) as file: y_train = pickle.load(file)1.3 建立模型
我们为此分类问题创建了一个基本的卷积神经网络 (Convolution Neural Network, CNN) 。它由 3 个卷积层组成,然后是最大池和全连接层 ,输出层有 6 个神经元 。您可以点击这里 ,了解创建 CNN 的更多内容 。以下是用于建立 CNN 的代码 。
import tensorflow as tf from tensorflow import keras from keras.models import Sequential from keras.layers.convolutional import Conv2D, MaxPooling2D from keras.layers import Dense, Flatten, Dropout print(tf.__version__) model = Sequential() model.add(Conv2D(32, (5, 5), activation=relu, input_shape=(96, 96, 1))) model.add(MaxPooling2D((2, 2))) model.add(Dropout(0.2)) model.add(Conv2D(64, (3, 3), activation=relu)) model.add(MaxPooling2D((2, 2))) model.add(Dropout(0.2)) model.add(Conv2D(64, (3, 3), activation=relu)) model.add(MaxPooling2D((2, 2))) model.add(Flatten()) model.add(Dense(128, activation=relu)) model.add(Dense(6, activation=softmax)) model.compile(optimizer=adam,loss=sparse_categorical_crossentropy,metrics=[accuracy]) model.summary()1.4 训练模型
该模型运行了 5 个 epochs,最终准确率为 99% 左右 。
history=model.fit(X_train, y_train, epochs=5, batch_size=64, verbose=1, validation_data=(X_test, y_test)) 插图 2:训练结果
1.5 保存模型
将训练好的模型保存为分层数据格式 (.h5) 。您可以点击这里 ,了解关于如何保存 Keras 模型的更多内容 。
model.save(handrecognition_model.h5)1.6 模型转化
ESP-DL 使用开放式神经网络交换 (ONXX) 格式的模型。您可以点击这里 ,了解 ONXX 是如何工作的 。为了与 ESP-DL 兼容,请使用下方代码将训练的 .h5 格式的模型转换为 ONXX 格式 。
model = tf.keras.models.load_model("/content/handrecognition_model.h5") tf.saved_model.save(model, "tmp_model") !python -m tf2onnx.convert --saved-model tmp_model --output "handrecognition_model.onnx" !zip -r /content/tmp_model.zip /content/tmp_model最后 ,下载 H5 格式的模型 、ONNX 格式的模型和模型检查点 ,以供将来使用。
from google.colab import files files.download("/content/handrecognition_model.h5") files.download("/content/handrecognition_model.onnx") files.download("/content/tmp_model.zip")2. ESP-DL 格式
当模型的 ONNX 格式准备就绪 ,您可以按照以下步骤将您的模型转换为 ESP-DL 格式 。
注:我们使用 Pychram IDE 进行 ESP-DL 格式转换 。
2.1 格式转换要求
首先 ,您需要成功搭建环境 ,并安装正确的模块版本 ,否则将出现错误。您可以点击这里 ,阅读关于 ESP-DL 格式转换要求的更多信息 。
模块
安装方法
Python == 3.7
Numba == 0.53.1
pip install Numba==0.53.1
ONNX == 1.9.0
pip install ONNX==1.9.0
ONNX Runtime == 1.7.0
pip install ONNXRuntime==1.7.0
ONNX Optimizer == 0.2.6
pip install ONNXOptimizer==0.2.6
表格 2:所需的模块和特定版本
接下来 ,您需要下载 ESP-DL ,并从 GitHub 仓库克隆 ESP-DL 。
git clone --recursive https://github.com/espressif/esp-dl.git2.2 优化与量化
为了运行 ESP-DL 提供的优化器,以 Window 系统为例 ,我们需要找到并将以下文件放入 pychram - IDE 的工作目录中 。
calibrator.pyd calibrator_acc.pyd evaluator.pyd optimizer.py接下来 ,您需要将在 1.2 节中生成的校准数据集和在 1.5 节中保存的 ONNX 格式模型放在一起 。您的工作目录应该是这样的 。
插图 3:工作目录
您可以按照下面的步骤生成优化后的模型和量化参数 。
2.2.1 导入库
from optimizer import * from calibrator import * from evaluator import *2.2.2 加载 ONNX 模型
onnx_model = onnx.load("handrecognition_model.onnx")2.2.3 优化 ONNX 模型
optimized_model_path = optimize_fp_model("handrecognition_model.onnx")2.2.4 加载校准数据集
with open(X_cal.pkl, rb) as f: (test_images) = pickle.load(f) with open(y_cal.pkl, rb) as f: (test_labels) = pickle.load(f) calib_dataset = test_images[0:1800:20] pickle_file_path = handrecognition_calib.pickle2.2.5 校准
model_proto = onnx.load(optimized_model_path) print(Generating the quantization table:) calib = Calibrator(int16, per-tensor, minmax) # calib = Calibrator(int8, per-channel, minmax) calib.set_providers([CPUExecutionProvider]) # Obtain the quantization parameter calib.generate_quantization_table(model_proto,calib_dataset, pickle_file_path) # Generate the coefficient files for esp32s3 calib.export_coefficient_to_cpp(model_proto, pickle_file_path, esp32s3, ., handrecognition_coefficient, True)如果一切正常,这时您可以在路径中生成两个扩展名为 .cpp 和 .hpp 的文件 ,如下图 。
注:稍后您还将用到这个输出结果 ,建议先截图保存 。
插图 4:优化模型的输出结果
2.3 评估
这一步并不是模型格式转化的必要步骤,如您希望评估优化后模型的性能 ,您可以使用以下代码 。
print(Evaluating the performance on esp32s3:) eva = Evaluator(int16, per-tensor, esp32s3) eva.set_providers([CPUExecutionProvider]) eva.generate_quantized_model(model_proto, pickle_file_path) output_names = [n.name for n in model_proto.graph.output] providers = [CPUExecutionProvider] m = rt.InferenceSession(optimized_model_path, providers=providers) batch_size = 64 batch_num = int(len(test_images) / batch_size) res = 0 fp_res = 0 input_name = m.get_inputs()[0].name for i in range(batch_num): # int8_model [outputs, _] = eva.evalaute_quantized_model(test_images[i * batch_size:(i + 1) * batch_size], False) res = res + sum(np.argmax(outputs[0], axis=1) == test_labels[i * batch_size:(i + 1) * batch_size]) # floating-point model fp_outputs = m.run(output_names, {input_name: test_images[i * batch_size:(i + 1) * batch_size].astype(np.float32)}) fp_res = fp_res + sum(np.argmax(fp_outputs[0], axis=1) == test_labels[i * batch_size:(i + 1) * batch_size]) print(accuracy of int8 model is: %f % (res / len(test_images))) print(accuracy of fp32 model is: %f % (fp_res / len(test_images)))注:请点击这里 ,了解更多关于 ESP-DL API 的信息。
3. 模型部署
模型部署是最后的关键步骤 。在这里 ,我们将在 ESP32-S3 微控制器上运行模型并得到结果 。
注:我们使用 Visual Studio Code 在 ESP32-S3 上部署模型。
3.1 ESP-IDF 项目的层次结构
首先 ,您需要在 VSCode 中创建一个基于 ESP-IDF 标准的新项目 。您可以观看此视频 ,或阅读本文档 ,了解如何基于 ESP32 系列芯片创建 VSCode 项目 。 其次 ,您需要将前面 2.2 节中生成的 .cpp 和 .hpp 文件复制到当前的工作目录中。 再次 ,请您将所有依赖的组件添加到工作目录下的组件文件夹中 。 最后 ,sdkconfig 文件是 ESP-WHO 示例中的默认文件,您可以在 GitHub 仓库中找到它 。项目目录如下图所示:
├── CMakeLists.txt ├── components │ ├── esp-dl ├── dependencies.lock ├── main │ ├── app_main.cpp │ └── CMakeLists.txt ├── model │ ├── handrecognition_coefficient.cpp │ ├── handrecognition_coefficient.hpp │ └── model_define.hpp ├── partitions.csv ├── sdkconfig ├── sdkconfig.defaults ├── sdkconfig.defaults.esp32 ├── sdkconfig.defaults.esp32s2 └── sdkconfig.defaults.esp32s3注:ESP-WHO 不是本教程必须的项目 。
3.2 模型定义
我们将在 “model_define.hpp ” 文件中定义模型 ,您可以依照下面的步骤进行操作 。
3.2.1 导入库
首先导入所有相关的库 。接下来您需要知道模型的具体结构 ,您可以使用开源工具 Netron 查看前面 2.2 节结束时优化生成的 ONNX 模型 。您可以在这里查看 ESP-DL 目前支持的所有库 。
#pragma once #include <stdint.h> #include "dl_layer_model.hpp" #include "dl_layer_base.hpp" #include "dl_layer_max_pool2d.hpp" #include "dl_layer_conv2d.hpp" #include "dl_layer_reshape.hpp" #include "dl_layer_softmax.hpp" #include "handrecognition_coefficient.hpp" using namespace dl; using namespace layer; using namespace handrecognition_coefficient;3.2.2 定义层
接下来是定义每个层 。
我们一般不认为输入是一个独立的层,在这里我们没有对其作定义 。 除了输出层 ,所有层都定义为私有层。 在建立模型时 ,请按照前面 1.3 节中定义的顺序放置各层 。 class HANDRECOGNITION : public Model<int16_t> { private: Conv2D<int16_t> l1; MaxPool2D<int16_t> l2; Conv2D<int16_t> l3; MaxPool2D<int16_t> l4; Conv2D<int16_t> l5; MaxPool2D<int16_t> l6; Reshape<int16_t> l7; Conv2D<int16_t> l8; Conv2D<int16_t> l9; public: Softmax<int16_t> l10; // output layer3.2.3 初始化层
在定义了各层之后,我们需要初始化每个层的权重 、偏置激活函数和形状 。我们可以逐层进行检查。
在详述细节前 ,让我们先在 Netron 里打开模型 ,导入该模型的目的是获得一些初始化的参数 。
插图 5:优化后的模型视图
3.2.4 构建层
下一步是建立每个层 。请点击这里 ,了解构建每层的构建函数 。
void build(Tensor<int16_t> &input) { this->l1.build(input); this->l2.build(this->l1.get_output()); this->l3.build(this->l2.get_output()); this->l4.build(this->l3.get_output()); this->l5.build(this->l4.get_output()); this->l6.build(this->l5.get_output()); this->l7.build(this->l6.get_output()); this->l8.build(this->l7.get_output()); this->l9.build(this->l8.get_output()); this->l10.build(this->l9.get_output()); }3.2.5 调用层
最后 ,我们需要将这些层连接起来 ,并通过调用函数逐一调用它们。请点击这里 ,了解每层的调用函数 。
void call(Tensor<int16_t> &input) { this->l1.call(input); input.free_element(); this->l2.call(this->l1.get_output()); this->l1.get_output().free_element(); this->l3.call(this->l2.get_output()); this->l2.get_output().free_element(); this->l4.call(this->l3.get_output()); this->l3.get_output().free_element(); this->l5.call(this->l4.get_output()); this->l4.get_output().free_element(); this->l6.call(this->l5.get_output()); this->l5.get_output().free_element(); this->l7.call(this->l6.get_output()); this->l6.get_output().free_element(); this->l8.call(this->l7.get_output()); this->l7.get_output().free_element(); this->l9.call(this->l8.get_output()); this->l8.get_output().free_element(); this->l10.call(this->l9.get_output()); this->l9.get_output().free_element(); } };3.3 模型运行
在模型建立后 ,需要给定输入并运行模型来进行推理 。您可以将生成的输入内容放在 “app_main.cpp ”文件里,然后在 ESP32-S3 上运行模型。
3.3.1 导入库
#include <stdio.h> #include <stdlib.h> #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "dl_tool.hpp" #include "model_define.hpp"3.3.2 输入声明
我们训练好的模型的输入大小为 (96, 96, 1) ,详情请见 1.3 节 。input_exponent 可以从 2.2.5 节生成的输出中获得其指数值 。您可以把输入/测试图片的像素写在这里 。
int input_height = 96; int input_width = 96; int input_channel = 1; int input_exponent = -7; __attribute__((aligned(16))) int16_t example_element[] = { //add your input/test image pixels };3.3.3 设置输入参数
每个输入的像素将根据上面声明的 input_exponent 进行调整 。
extern "C" void app_main(void) { Tensor<int16_t> input; input.set_element((int16_t *)example_element).set_exponent(input_exponent).set_shape({input_height,input_width,input_channel}).set_auto_free(false);3.3.4. 调用模型
通过调用 forward 函数并传入输入来调用模型 。延迟时间 (Latency) 用于计算 ESP32-S3 运行神经网络的时间 。
HANDRECOGNITION model; dl::tool::Latency latency; latency.start(); model.forward(input); latency.end(); latency.print("\nSIGN", "forward");3.3.5. 监测输出
输出来自公共层 ,例如 l10,您可以在终端打印结果 。
float *score = model.l10.get_output().get_element_ptr(); float max_score = score[0]; int max_index = 0; for (size_t i = 0; i < 6; i++) { printf("%f, ", score[i]*100); if (score[i] > max_score) { max_score = score[i]; max_index = i; } } printf("\n"); switch (max_index) { case 0: printf("Palm: 0"); break; case 1: printf("I: 1"); break; case 2: printf("Thumb: 2"); break; case 3: printf("Index: 3"); break; case 4: printf("ok: 4"); break; case 5: printf("C: 5"); break; default: printf("No result"); } printf("\n"); }插图 6 显示了输出结果 ,在 ESP32-S3 上 ,模型的延迟时间约为 0.7 秒,每个神经元的输出和最后的预测结果都能够显示出来 。
插图 6:输出结果
4. 未来的计划
接下来 ,我们将为 ESP32-S3-EYE 开发板设计一个模型 ,它可以实时捕捉图像并进行手势识别 。未来您可以在此 GitHub 页面查看相关代码。
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!