深度学习模型移植嵌入式设备教程

简单介绍了深度学习模型移植嵌入式设备的方法

这里以ASR模型和RK3588S为例,使用的推理框架是ONNXRuntime,其中交叉编译是在Ubutu22.04上完成的

目前主流的推理框架包括NCNN,MNN,TNN,ONNXRuntime

其中NCNN,TNN,MNN三者均针对嵌入式进行了优化,其在推理速度上有一定的优势,但是其支持的算子没有ONNXRuntime那么全面,因此可能转换时出现错误,此时就需要修改模型或者自行针对缺少的算子进行实现,因此这里选择ONNXRuntime

如果你的模型是很标准的CNN结构如Resnet,VGG等或者在其上修改并不多,那么选择NCNN/TNN/MNN即可

步骤

Step1:

准备训练好的模型,利用Pytorch自带的onnx转换工具将其转为onnx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import onnx
import torch

input_sample = torch.randn((1, 1, 80, 1000)) # 模型输入
model.to_onnx("model.onnx", # 保存名称
input_sample, # 输入
export_params=True, # 常量折叠
opset_version=13, # 算子版本
input_names=["input"], # 输入名称
output_names=["output"], # 输出名称
dynamic_axes={ # 如果输入的长度不确定,则需要在这里指定
"input": {0: 'batch_size', 3: "seq_len"},
"output": {0: 'batch_size', },
})
# 转换完以后可以使用下列语句对ONNX模型进行检查
model = onnx.load("model.onnx")
onnx.checker.check_model(model)

如果你的模型不是很标准的结构,比如有许多自定义的结构,那么这一步有可能会报错,此时就需要去修改模型以确保能被转换为ONNX模型

同时ONNX模型也是很多其他推理框架的中间模型,此时可以通过https://convertmodel.com进行转换

Step2:

准备好对应的交叉编译工具,由于我们的目标是64位的,因此这里使用的是aarch64-none-linux-gnu

下载交叉编译工具链解压后将其放到任意位置,随后编辑.bashrc文件

在文件最后加上

1
export PATH="your toolchain path:$PATH"

随后新开一个端口,或者使用source .bashrc刷新配置

之后检查交叉编译工具链是否正确

1
aarch64-none-linux-gnu-gcc -v

如果出现了对应的版本号则说明工具链安装正确

Step3:

下载ONNXRuntime,这里下载对应的版本就行(此时为了方便调试,可以同时下载x64和aarch64两种)

下载后就得到了ONNXRuntime的动态库,同时如果官方给出的里面没有你所需要的版本,则需要下载源码自行编译

将下载好的文件解压,并放置到工程目录下方便后续使用

Step4:

编写C++程序以实现对ONNX模型的调用,这里给出C++程序的基本框架,该框架只对ONNX模型进行了测试,以便确定能跑通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cassert>
#include "onnxruntime_cxx_api.h"

int main() {
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, ""); // 创建环境
Ort::SessionOptions session_options; // 创建配置
session_options.SetInterOpNumThreads(1); // 设置线程数
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED); // 设置图优化等级,这里是EXTENDED等级
char* model_path="model.onnx"; // 模型名称

Ort::Session session(env, model_path, session_options); //创建session

size_t num_input_nodes = session.GetInputCount();
std::vector<const char*> input_node_name = {"input"}; // 输入名称,此处要和导出部分一致
std::vector<const char*> output_node_name = {"output"}; // 输出名称,此处要和导出部分一致
std::vector<int64_t> input_node_dims = {1, 1, 80, 250}; // 输入数据形状
size_t input_tensor_size = 80*250; // 输入数据总大小
vector<float> input_tensor_values(input_tensor_size); // 构建输入数据,注意这里应该是一维vector
// 给输入数据初始值
for(size_t i = 0;i<input_tensor_size;i++){
input_tensor_values[i] = (float) i / (input_tensor_size + 1);
}

auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); // 配置存储

// 创建tensor,注意模型的输入必须是tensor而不是vector
// 第一个参数是memory_info,第二个是输入数据指针,第三个是输入数据大小,第四个是输入数据形状指针,第五个是输入数据形状大小
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 4);
assert(input_tensor.IsTensor()); // 断言

auto output_tensors = session.Run(Ort::RunOptions{nullptr}, input_node_name.data(), &input_tensor, 1, output_node_name.data(), 1); // 执行推理

long int *floatarr = output_tensors.front().GetTensorMutableData<long int>(); // 转换输出,由于我的pytorch模型输出的就是long int格式,如果输出的是浮点,这里应该是float

// 打印结果
for (int i = 0; i < 60; i++) {
std::cout << floatarr[i] << '\n';
}

return 0;
}

编写完C++后,由于我使用的是Clion作为IDE,因此还需要编写CMakeList.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.22)
project(onnx_test)

set(CMAKE_CXX_STANDARD 11)

include_directories(your onnxruntime path/include)

link_directories(your onnxruntime path/lib)

add_executable(onnx_test main.cpp)

target_link_libraries(onnx_test onnxruntime)

之后就可以在本地编译,进行测试

在本地测试通过后还需要进行交叉编译到目标平台

此时的CMakeList.txt需要进行修改,这里为了方便进行测试所以设置了条件编译,此时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cmake_minimum_required(VERSION 3.22)
project(onnx_test)

set(CMAKE_CXX_STANDARD 11)

set(CROSS_COMPILE 0)

if(CROSS_COMPILE)
set(CMAKE_SYSTEM_NAME Linux)
set (CMAKE_SYSTEM_PROCESSOR aarch64)

set(CMAKE_C_COMPILER aarch64-none-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-none-linux-gnu-g++)

include_directories(your onnxruntime_aarch64 path/include)

link_directories(your onnxruntime_aarch64 path/lib)

else()
include_directories(your onnxruntime_x64 path/include)

link_directories(your onnxruntime_x64 path/lib)

endif()

add_executable(onnx_test main.cpp)

target_link_libraries(onnx_test onnxruntime)

同时由于我在Clion中设置交叉编译失败了,因此需要手动编译,在工程目录下执行:

1
2
3
4
mkdir build
cd build
cmake ..
make

随后便可以得到编译后的程序onnx_test,将其传到目标板子上,同时还需要将onnxruntime_aarch64/lib下的文件,传输到目标开发版的/usr/lib目录下

在目标板子上新建test文件夹,其下有可执行文件onnx_test和ONNX模型model.onnx

1
2
chmod +x onnx_test	# 赋予可执行权限
./onnx_test # 执行

在看到输出结果后,就说明移植成功

Step5:

在针对ONNX模型的移植测试通过后,需要进一步完善程序,才能实现ASR

由于我们的ASR模型输入的数据是音频的Fbank特征,因此还需要移植Fbank特征提取程序,这里我移植了WeNet的程序

同时由于python程序中对音频和Fbank特征进行了归一化和标准化,因此也需要对其进行实现

不同的深度学习模型这一部分应该有很大的区别,这里给出最后的主程序以供参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//
// Created by qyk on 23-1-8.
//
#include "wav.h"
#include <iostream>
#include "fbank.h"
#include <cassert>
#include "onnxruntime_cxx_api.h"

int main(){

wav::WavReader reader; // 创建reader

wenet::FeatureConfig config = wenet::FeatureConfig(80, 16000); // 创建Fbank参数,80维mel,采样率16k

wenet::Fbank fbank_(config.num_bins, config.sample_rate, config.frame_length,
config.frame_shift); //创建fabnk提取器


reader.Open("1.wav");
//读取wav数据
const float* data=reader.data();
int number_samples = reader.num_samples();
int sample_rate = reader.sample_rate();
std::cout << "wav time:" << (float)number_samples / sample_rate << '\n';
//wav归一化
reader.normalized();

std::vector<std::vector<float>> feats; // 创建Fbank特征vector
std::vector<float> waves; // 创建音频vector
waves.insert(waves.end(), data, data + number_samples); // 将音频数据写入其中


//计算Fbank, 返回Fbank特征帧数, feats[frame_number, fbank_dim]
int frame_number = fbank_.Compute(waves, &feats);

// 对feats进行转置,得到feats_T
std::vector<std::vector<float>> feats_T(feats[0].size());
for(int i=0; i<frame_number; i++){
for(int j=0; j<feats[0].size(); j++){
feats_T[j].push_back(feats[i][j]);
}
}

//Fbank标准化
for(int i=0; i<80; ++i) {
double sum = std::accumulate(std::begin(feats_T[i]), std::end(feats_T[i]), 0.0);
double mean = sum / feats_T[i].size();
double std = 0.0;
for(int j=0; j<feats_T[i].size(); j++){
std = std + pow(feats_T[i][j]-mean,2);
}
std = sqrt(std/feats_T[i].size()-1);
for(int j=0; j<feats_T[i].size(); j++){
feats_T[i][j] = (feats_T[i][j] - mean)/(1e-10+std);
}
}

Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "");
Ort::SessionOptions session_options;
session_options.SetInterOpNumThreads(1);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
char* model_path="model.onnx";

Ort::Session session(env, model_path, session_options);
// Ort::AllocatorWithDefaultOptions allocator;

size_t num_input_nodes = session.GetInputCount();
std::vector<const char*> input_node_name = {"input"};
std::vector<const char*> output_node_name = {"output"};
std::vector<int64_t> input_node_dims = {1, 1, 80, frame_number};
size_t input_tensor_size = 80*frame_number;

std::vector<float> input_tensor_values(input_tensor_size);
size_t count=0;
// 创建输入vector,将feast_T转为一维向量,以便后面构建tensor
for(size_t i = 0; i<80; i++){
for(size_t j = 0; j<frame_number; j++){
input_tensor_values[count] = feats_T[i][j];
count++;
}
}

auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);

Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 4);

assert(input_tensor.IsTensor());

auto output_tensors = session.Run(Ort::RunOptions{nullptr}, input_node_name.data(), &input_tensor, 1, output_node_name.data(), 1);

long int *floatarr = output_tensors.front().GetTensorMutableData<long int>();

for (int i = 0; i < 60; i++) {
std::cout << floatarr[i] << '\n';
}

return 0;

}

同时由于移植的Fbank提取是多个文件在一个文件夹中,因此也需要在该文件夹中新建CMakeList.txt,并填写以下内容

1
add_library(utils STATIC wav.cpp fft.cpp fbank.cpp)

同时在项目主文件夹中的CMakeList.txt也需要加上

1
2
3
4
5
include_directories(./ ./utils)

add_subdirectory(utils)

target_link_libraries(main utils onnxruntime)

此后便可编译运行,而交叉编译部分第4步差不多

至此移植深度学习模型至嵌入式上也就完成了

参考

  1. WeNet
  2. ONNXRuntime