使用 ESP
针对该分类问题,我们使用了
Kaggle 手势识别数据集
中的一个开源数据集。原始数据集包括 10 个类别,我们只使用了其中 6 个。这些类别更容易识别,且日常生活中更有用,如下表所示。我们的数据集较原数据集还有一处关于图像大小的区别,在原始数据集中,图像的大小为 (240, 640),但为了方便起见,我们将数据集的大小调整为 (96, 96)。本文中使用的数据集可以在
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 的更多细节
如果您需要转载本教程,您可以在此
获得数据,并在您的工作环境中开放数据。
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)
我们为此分类问题创建了一个基本的卷积神经网络 (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:CNN 模型
该模型运行了 5 个 epochs,最终准确率为 99% 左右。
history=model.fit(X_train, y_train, epochs=5, batch_size=64, verbose=1, validation_data=(X_test, y_test))
将训练好的模型保存为分层数据格式 (.h5)。您可以点击
,了解关于如何保存 Keras 模型的更多内容。
model.save('handrecognition_model.h5')
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 格式。
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 仓库克隆
git clone --recursive https://github.com/espressif/esp-dl.git
为了运行 ESP-DL 提供的优化器,以 Window 系统为例,我们需要找到并将以下文件放入 pychram - IDE 的工作目录中。
calibrator.pyd
calibrator_acc.pyd
evaluator.pyd
optimizer.py
中生成的校准数据集和在
中保存的 ONNX 格式模型放在一起。您的工作目录应该是这样的。
您可以按照下面的步骤生成优化后的模型和量化参数。
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.pickle'
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:优化模型的输出结果
这一步并不是模型格式转化的必要步骤,如您希望评估优化后模型的性能,您可以使用以下代码。
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)
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 的信息。
模型部署是最后的关键步骤。在这里,我们将在
微控制器上运行模型并得到结果。
Visual Studio Code
在 ESP32-S3 上部署模型。
3.1 ESP-IDF 项目的层次结构
首先,您需要在 VSCode 中创建一个基于 ESP-IDF 标准的新项目。您可以观看
,了解如何基于 ESP32 系列芯片创建 VSCode 项目。
中生成的 .cpp 和 .hpp 文件复制到当前的工作目录中。
再次,请您将所有依赖的组件添加到工作目录下的组件文件夹中。
最后,sdkconfig 文件是
ESP-WHO 示例
中的默认文件,您可以在
项目目录如下图所示:
├── CMakeLists.txt
├── components
│ ├── esp-dl
├── dependencies.lock
│ ├── app_main.cpp
│ └── CMakeLists.txt
│ ├── 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 不是本教程必须的项目。
我们将在 “model_define.hpp” 文件中定义模型,您可以依照下面的步骤进行操作。
首先导入所有相关的库。接下来您需要知道模型的具体结构,您可以使用开源工具
结束时优化生成的 ONNX 模型。您可以在
#pragma once
#include
#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;
接下来是定义每个层。
我们一般不认为输入是一个独立的层,在这里我们没有对其作定义。
除了输出层,所有层都定义为私有层。
在建立模型时,请按照前面
中定义的顺序放置各层。
class HANDRECOGNITION : public Model
Conv2D
MaxPool2D
Conv2D
MaxPool2D
Conv2D
MaxPool2D
Reshape
Conv2D
Conv2D
Softmax
3.2.3 初始化层
在定义了各层之后,我们需要初始化每个层的权重、偏置激活函数和形状。我们可以逐层进行检查。
在详述细节前,让我们先在 Netron 里打开模型,导入该模型的目的是获得一些初始化的参数。
插图 5:优化后的模型视图
图中的第一层是 reshape 层(输入没有被作为单独的层)。这一层的输入是 (96, 96, 1),顺序是 (H, W, C),输出的形状是 (1, 96, 96),顺序变为了 (C, H, W)。在之前 1.3 节的训练代码中,我们没有加入 reshape 层,但优化后的 ONNX 模型出现了 reshape 层,这是因为 ONNX 要求的运算(例如卷积层)张量顺序为 (C, H, W),在 1.6 节模型转化所述的过程中会添加 reshape 层来满足要求。我们的训练平台与 ESP-DL 要求的顺序是一致的,为 (H, W, C),与输入的顺序一致,因此不需要在部署的代码里添加 reshape 层,您可以
点击此 GitHub 链接
对于二维卷积层,我们可以从前面
末尾生成的 .hpp 文件中获得该层的过滤器、偏置项和激活函数的名称。但是对于指数,我们需要检查
对于最大池化层 (max-pooling layer),我们可以使用与建立模型时相同的参数,请参见本文的
。另一种了解参数和层的方法是使用开源工具
结束时优化生成的 ONNX 模型。
对于密集层或全连接层,我们使用了二维卷积模块。我们可以从前面
末尾生成的 .hpp 文件中获得该层的过滤器、偏置项和激活函数的名称。但是对于指数,我们需要检查
输出层是一个 SoftMax 层权重,其名称可以从
HANDRECOGNITION () :
l1(Conv2D
l2(MaxPool2D
l3(Conv2D
l4(MaxPool2D
l5(Conv2D
l6(MaxPool2D
l7(Reshape
l8(Conv2D
l9(Conv2D
l10(Softmax
下一步是建立每个层。请
,了解构建每层的构建函数。
void build(Tensor
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());
最后,我们需要将这些层连接起来,并通过调用函数逐一调用它们。请
,了解每层的调用函数。
void call(Tensor
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();
在模型建立后,需要给定输入并运行模型来进行推理。您可以将生成的输入内容放在 “app_main.cpp”文件里,然后在
#include
#include
#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),详情请见
。input_exponent 可以从
生成的输出中获得其指数值。您可以把输入/测试图片的像素写在这里。
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
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)
printf("Palm: 0");
printf("I: 1");
printf("Thumb: 2");
printf("Index: 3");
printf("ok: 4");
printf("C: 5");
printf("No result");
printf("\n");
插图 6 显示了输出结果,在 ESP32-S3 上,模型的延迟时间约为 0.7 秒,每个神经元的输出和最后的预测结果都能够显示出来。
ESP32-S3-EYE
开发板设计一个模型,它可以实时捕捉图像并进行手势识别。未来您可以在
此 GitHub 页面
