简单介绍了深度学习模型移植嵌入式设备的方法
这里以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', }, })
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); char* model_path="model.onnx";
Ort::Session session(env, model_path, session_options);
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); 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); 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; }
|
编写完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
|
#include "wav.h" #include <iostream> #include "fbank.h" #include <cassert> #include "onnxruntime_cxx_api.h"
int main(){
wav::WavReader reader;
wenet::FeatureConfig config = wenet::FeatureConfig(80, 16000);
wenet::Fbank fbank_(config.num_bins, config.sample_rate, config.frame_length, config.frame_shift);
reader.Open("1.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'; reader.normalized(); std::vector<std::vector<float>> feats; std::vector<float> waves; waves.insert(waves.end(), data, data + number_samples);
int frame_number = fbank_.Compute(waves, &feats);
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]); } }
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);
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; 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步差不多
至此移植深度学习模型至嵌入式上也就完成了
参考
- WeNet
- ONNXRuntime