声明:本文章创作目的为知识分享,请随意转载,但一定要标注本文链接,因为文章用markdown编辑器处理生成,直接复制文字会导致图片、公式、代码等出现缺失,错位等问题造成阅读上的困扰,所以请您要转载务必要标一下原文出处。
序
最近遇到个这样的问题,在一个web应用(运行于搭载Linux系统的服务器)上加入神经网络计算功能。已知该web应用是python写的,所以最简单办法就是给服务器的python装上相关的库然后导入,写新的python程序执行,但是呢又怕装了新库和某些已有的冲突,总之就是原有的python环境不好再做更改。想了一个办法,在自己电脑(Windows系统)用python把神经网络模型训练好,开一个Linux虚拟机,远程Linux虚拟机用C++编译出一个动态库,服务器上的python环境加载这个C++动态库(加载C++动态库的函数好像python环境都会有),C++动态库内部进行模型的加载和推理,在C++和python中间倒了两手,最终实现了想要的功能,中间踩了很多坑,费时一周总算搞定,特此记录一下。另外求助百度的过程中发现讨论相关问题的人很少,也是想对卡在类似问题的人有些启发(可能纯粹是没有我这么奇葩需求的吧,哈哈哈。。。百度上有C++加载python模型的需求一般都是因为要在嵌入式环境运行,或者为了加快程序执行速度的)。
在此特别感谢一下百度大模型文心一言,大部分代码都是它帮忙写的,第一次体会到AI写代码带来的便利,节省了很多学习新API函数的时间,让我能够专注于配置操作系统和各种软件。
实践环境
python训练及生成模型,运行C++IDE:
操作系统:Windows 10 家庭中文版(版本:22H2)
工具:Anaconda(conda版本:4.12.0 创建的虚拟环境中python版本:3.9.12 )
PyCharm(版本:Community Edition 2022.2.3)
Microsoft Visual Studio Community 2022 (64 位)
VMware Workstation 16 Player
加载C++动态库及模型推理:
操作系统:Ubuntu Desktop(版本Ubuntu 16.04.7 LTS)
Ubuntu自带编译器版本:gcc (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
Ubuntu自带GLIBC版本:ldd (Ubuntu GLIBC 2.23-0ubuntu11.3) 2.23
Ubuntu自带python3版本:python 3.5.2
踩坑记录
开始之前,先查询一下C++要加载python生成的神经网络模型有哪些方法,我找到两种方式:
- python提供了一些库和函数可以让C++调用,这种方式实际上C++没有脱离pyhton,C++执行相关函数会启动python解释器,C++和python解释器之间互相传递数据来执行python脚本或者C++的语句,这种方式显然不行,本质上还是python执行神经网络的加载,依然需要python安装相应的库。
- python神经网络相关的库都可以把模型导出并保存成文件,其中某些库只能用python加载模型文件,但有些库比如Pytorch提供了对应的C++版本libtorch,libtorch有C++版本的函数可以加载这些模型文件。在进一步查找中发现有些帖子提到了“onnxruntime”,很多神经网络相关的库都可以将模型保存为onnx格式,onnxruntime提供了包括C++在内多种语言的版本可以加载onnx模型,这种方式能支持很多主流的深度学习框架,所以最后采用了这种方式。
稍微补充一下,为了简单起见,下面采用的都是CPU版本的onnxruntime,如果要使用GPU版本,对于显卡型号和CUDA版本等,估计又得费一番功夫,期待看到这篇文章的各位能有兴趣尝试一下O(∩_∩)O。
首先打开Anaconda Powershell Prompt创建一个新的python环境,我把新环境起名为test_onnx,为了和我使用的环境一致,版本指定为3.9.12:
conda create -n test_onnx PYTHON=3.9.12
切换到新环境:
conda activate test_onnx
安装各种库(新环境创建有些常用库会一起下载的,这里以防万一全部执行一遍安装命令。另外我把下载地址配置为了国内镜像源,可以自行找相关资料配置一下,这样下载速度很快):
conda install numpy
conda install pandas
conda install matplotlib
conda install scikit-learn
cond install skl2onnx
conda install onnxruntime
pip install netron -i https://pypi.tuna.tsinghua.edu.cn/simple
numpy提供了矩阵运算功能,pandas可以加载很多类型的文件,matplotlib类似于MATLAB的plot()可以提供绘图功能,因为只是测试一下这样的思路是否可行我就没再下一些深度学习框架,只下了一个机器学习常用的sklearn(下载的时候叫scikit_learn)。
skl2onnx顾名思义就是的把sklearn模型转换为onnx格式的工具(外国好像习惯把to写成2,这样确实不用加下划线就可以隔开前后两个单词),onnxruntime其实就是python版本的onnxruntime,如果只需要保存模型其实不需要,但想在其他语言加载前先看一下预测结果最好还是安装一下,netron可以把模型结构在网页中画出来,我下面的demo很简单,不展示结构也无所谓,后续有需要的可以安装,另外netron使用conda安装找不到,应该是anaconda的仓库没有这个包,用pip安装可以,-i加上网址指定清华源下载,不然pip install netron直接国外下载太慢了。
下面用excel编造一些数据,我简单搞一个函数
y
=
1.1
x
1
+
2.1
x
2
+
3.1
x
3
+
4.1
x
4
y=1.1x_1+2.1x_2+3.1x_3+4.1x_4
y=1.1x1+2.1x2+3.1x3+4.1x4,用RAND()随机搞出来一些数,前面加上一列瞎编的时间,尽可能模拟实际遇到的数据形式,保存为CSV UTF-8(逗号分隔)格式,文件名我叫"data.csv",懒得上传文件,但是为了方便复现我的结果,直接把数据贴一下:
time | x1_value | x2_value | x3_value | x4_value | y_value |
---|---|---|---|---|---|
1:11:12 | 0.231391757 | 0.424127518 | 0.890998572 | 0.773326116 | 7.077931369 |
1:11:13 | 0.909232116 | 0.209825566 | 0.824914955 | 0.507029715 | 6.076847207 |
1:11:14 | 0.380178084 | 0.166423402 | 0.015073295 | 0.876885383 | 4.40964232 |
1:11:15 | 0.285301344 | 0.963457724 | 0.762371759 | 0.1552552 | 5.336991471 |
1:11:16 | 0.017423531 | 0.013711542 | 0.839705318 | 0.211723918 | 3.519114669 |
1:11:17 | 0.354440436 | 0.206380433 | 0.002707666 | 0.101631662 | 1.248366966 |
1:11:18 | 0.116008696 | 0.298023054 | 0.580074616 | 0.420236131 | 4.274657425 |
1:11:19 | 0.075660899 | 0.565839263 | 0.434440782 | 0.619645013 | 5.15880042 |
1:11:20 | 0.59203102 | 0.708645682 | 0.363499148 | 0.567508222 | 5.593021125 |
1:11:21 | 0.50629823 | 0.095088949 | 0.663844374 | 0.784424303 | 6.030672048 |
1:11:22 | 0.16456975 | 0.583424764 | 0.844684908 | 0.536180816 | 6.223083292 |
1:11:23 | 0.193205286 | 0.541811371 | 0.367766334 | 0.184423949 | 3.24654352 |
1:11:24 | 0.754875664 | 0.007903874 | 0.811785497 | 0.703014339 | 6.245855196 |
1:11:25 | 0.843490284 | 0.765858497 | 0.614582741 | 0.145985621 | 5.039889702 |
1:11:26 | 0.967238706 | 0.100044887 | 0.685977216 | 0.330525245 | 4.755739717 |
1:11:27 | 0.173049683 | 0.431119176 | 0.908428768 | 0.504700703 | 5.981106982 |
1:11:28 | 0.564857009 | 0.524199468 | 0.761847198 | 0.804755172 | 7.383384113 |
1:11:29 | 0.596083519 | 0.677699878 | 0.549462803 | 0.121534175 | 4.280486423 |
1:11:30 | 0.947996525 | 0.368910205 | 0.568715036 | 0.314892474 | 4.871583365 |
1:11:31 | 0.466681807 | 0.843056881 | 0.648055945 | 0.120741745 | 4.787784021 |
1:11:32 | 0.101161227 | 0.388510344 | 0.618128242 | 0.891083262 | 6.496787998 |
1:11:33 | 0.3771967 | 0.051390545 | 0.479569381 | 0.210505779 | 2.87257529 |
1:11:34 | 0.401774396 | 0.273418288 | 0.51759388 | 0.179744546 | 3.357623909 |
1:11:35 | 0.487554839 | 0.380141337 | 0.686731925 | 0.619396222 | 6.003000606 |
1:11:36 | 0.155196447 | 0.799631251 | 0.402855695 | 0.031452008 | 3.22774761 |
1:11:37 | 0.799143044 | 0.480533405 | 0.997579784 | 0.693353159 | 7.823422782 |
1:11:38 | 0.554700679 | 0.273459411 | 0.219175332 | 0.178255557 | 2.594726826 |
1:11:39 | 0.486189905 | 0.25205185 | 0.96550959 | 0.03577982 | 4.203894769 |
1:11:40 | 0.466503143 | 0.24467903 | 0.671226494 | 0.849280399 | 6.589831188 |
1:11:41 | 0.054350504 | 0.04515882 | 0.134553492 | 0.156385397 | 1.212915029 |
1:11:42 | 0.023507929 | 0.566842902 | 0.123712579 | 0.734104385 | 4.609565791 |
1:11:43 | 0.639463705 | 0.390654188 | 0.125601524 | 0.474525304 | 3.858702341 |
1:11:44 | 0.37870873 | 0.82847111 | 0.020729518 | 0.806560026 | 5.527526546 |
1:11:45 | 0.671088196 | 0.026713209 | 0.046829856 | 0.606746887 | 3.427129547 |
1:11:46 | 0.391438381 | 0.314436345 | 0.800251252 | 0.072241798 | 3.867868797 |
1:11:47 | 0.556095592 | 0.17439875 | 0.902020999 | 0.961929717 | 7.71811946 |
1:11:48 | 0.74880473 | 0.283432869 | 0.088321513 | 0.058110207 | 1.930942766 |
1:11:49 | 0.685885327 | 0.714106422 | 0.415757414 | 0.531256649 | 5.721097594 |
1:11:50 | 0.201695246 | 0.359582492 | 0.732434161 | 0.517236448 | 5.368203339 |
1:11:51 | 0.273549662 | 0.251068444 | 0.563577396 | 0.356913854 | 4.038585088 |
打开PyCharm,创建一个test_onnx工程,把python解释器配置为test_onnx,记得把data.csv放到生成的项目路径下,创建一个python文件test_onnx.py,这里简单用一个线性回归模型做例子,代码如下(需要解释的我都用注释的形式添加):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
import onnxruntime as rt
import netron
input_dim = 4 # 模型输入的维度,对于我上面造的函数,输入有x1到x4四个维度
datafilename = "data.csv" # 要读取的文件名,这里是相对路径(和py文件同一层级下)
value = pd.read_csv(datafilename, encoding="utf-8").values # 读取csv文件,以utf-8模式解码,.value可以获取返回结果中值的部分
data = value[1:, :] # 第0列是时间,我这里不需要,需要的话请单独处理,注意python计数从0开始而不是1
X = data[:, 1:5] # 取第1到4列,python还有个奇葩地方,1:5这样写其实是1到4.。。
X = X.astype(np.float64) # 由于第0列是字符,上一行取出来的X也会是str类型,强行转成float类型
Y = data[:, 5]
Y = Y.astype(np.float64) # 注意,如果您使用的实际数据有不能直接转float的内容,请在类型转换前单独处理,不然报错
onnx_model_file_path = "model.onnx" # 要保存的onnx模型文件名
# train_test_split函数可以划分数据集为训练集和测试集
# 输入参数分别为模型输入,模型输出,测试集的比例(0.2为20%),最后一个参数为随机种子,设置一个定值可以保证每次运行结果相同
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
model = LinearRegression() # 创建线性回归模型
model.fit(x_train, y_train) # 使用训练数据拟合模型
y_pred = model.predict(x_test) # 使用模型进行预测
mse = mean_squared_error(y_test, y_pred) # 评估模型
r2 = r2_score(y_test, y_pred) # 评估模型
# 下面两行可以打印对模型精度的评价,需要可以取消注释
# print(f"Mean Squared Error: {mse}")
# print(f"R^2 Score: {r2}")
# 虽然输入是四个float值,但onnx模型接收的输入为张量形式,FloatTensorType会创建一个float张量
initial_type = [('float_input', FloatTensorType([None, input_dim]))] # 输入还需要起个名字,我起名叫'float_input'
onx = convert_sklearn(model, initial_types=initial_type) # 将上面训练的sklearn线性回归模型转换为onnx模型
with open(onnx_model_file_path, "wb") as f:
f.write(onx.SerializeToString()) # 将onnx模型保存为文件
onnxmodel = rt.InferenceSession(onnx_model_file_path) # 加载onnx模型
input_name = onnxmodel.get_inputs()[0].name # 获取onnx模型输入名称
label_name = onnxmodel.get_outputs()[0].name # 获取onnx模型输出名称
pred_onx = onnxmodel.run([label_name], {input_name: x_test.astype(np.float32)})[0] # 运行onnx模型
# 可视化结果
fig, axs = plt.subplots(nrows=1, ncols=2) # 创建一个一行两列的子图布局
axs[0].plot(y_pred, label='sklearn_predict')
axs[0].plot(y_test, label='real_value')
axs[0].legend() # 添加图例
axs[1].plot(pred_onx, label='onnx_model_predict')
axs[1].plot(y_test, label='real_value')
axs[1].legend() # 添加图例
plt.show()
netron.start(onnx_model_file_path)
运行代码,可以看到sklearn预测的结果和onnx模型完全一致,由于我这里数据很简单,曲线是完全重合的:
上面代码里的import netron和netron.start(onnx_model_file_path)没有注释掉,在关闭弹出的曲线图后浏览器会弹出一个网页,展示模型的结构:
接下来就要把模型用C++来加载啦,我先在windows下测试过没问题。回想一下幸亏windows下面加载成功了,不然直接Linux下测试出现一堆问题,我会怀疑是不是这个方法完全走不通。
首先肯定是找onnxruntime的C++版本,网址为https://github.com/microsoft/onnxruntime/releases,写文章时最新版本为1.17.1,一打开这个网址就是这样的:
当时我就想肯定整最新的呗,网页往下拉,到这个位置点一下:
根据CPU架构下载自己的对应版本,我下载的是onnxruntime-win-x64-1.17.1。每个文件名其实看意思也能明白,后缀win是windows版本,osx是苹果系统版本,linux是Linux系统版本;后缀x64、arm64,x86是CPU不同架构;后缀带cuda和gpu的就是显卡运算的版本,不带这些的就是CPU运算版本;后缀有training的个人猜测是训练模型的版本,所以我下载的不带training的,因为只做模型推理。所以我下载的版本意思是windows平台,CPU架构为x64,只进行模型推理,版本号为1.17.1的onnxruntime库。
VS2022创建一个windows下的控制台应用,名称为test_onnxruntime:
创建完成后把刚才下载的压缩包中include文件夹和lib文件夹复制到工程路径下:
回到VS2022编辑器,在项目上点一下右键属性,展开C/C++ —> 常规,添加include文件夹路径为附加包含目录:
然后是链接器 —> 常规,添加lib文件夹路径为附加库目录:
链接器 —> 输入,附加依赖项添加onnxruntime.lib:
配置完毕,记得把之前python保存的模型文件复制过来,或者代码里直接写绝对路径为到python工程的路径,老规矩代码直接贴出来,哦对了,记得把VS2022自动帮你写的hello world代码全部删掉,我这里用try-catch形式捕获异常,如果运行结果打印一些错误信息,可以继续查有没有相关解决办法:
#include <iostream>
#include <assert.h>
#include <onnxruntime_cxx_api.h>
#include <onnxruntime_c_api.h>
int main() {
const wchar_t* model_path = L"model.onnx";
try {
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "ONNX_C_API");
std::wcout << L"Attempting to load model from: " << model_path << std::endl;
Ort::SessionOptions session_options;
// 加载模型
Ort::Session session(env, model_path, session_options);
std::wcout << L"Model loaded successfully." << std::endl;
Ort::AllocatorWithDefaultOptions allocator;
// 获取输入节点信息
size_t num_input_nodes = session.GetInputCount();
size_t num_output_nodes = session.GetOutputCount();
// 定义输入和输出节点的名称向量
std::vector<const char*> input_node_names;
std::vector<const char*> output_node_names;
// 获取输入节点信息并填充到向量中
for (size_t i = 0; i < num_input_nodes; i++) {
Ort::AllocatedStringPtr in_name = session.GetInputNameAllocated(i, allocator);
const char* in_name_cstr = in_name.get(); // 获取字符串指针
std::cout << "Input Name: " << in_name_cstr << std::endl;
input_node_names.push_back(in_name_cstr);
}
// 获取输出节点信息并填充到向量中
for (size_t i = 0; i < num_output_nodes; i++) {
Ort::AllocatedStringPtr out_name = session.GetOutputNameAllocated(i, allocator);
const char* out_name_cstr = out_name.get(); // 获取字符串指针
std::cout << "Output Name: " << out_name_cstr << std::endl;
output_node_names.push_back(out_name_cstr);
}
// 设置输入数据的维度,这里以单条数据为例
int input_dim = 4;
std::vector<int64_t> input_tensor_dims = { 1, input_dim };
size_t input_tensor_size = 1 * input_dim;
// 构造输入数据
std::vector<float> input_tensor_values(input_tensor_size);
for (unsigned int i = 0; i < input_tensor_size; i++)
{
input_tensor_values[i] = 1;
std::cout << input_tensor_values[i] << std::endl;
}
// 将输入数据转换为onnx模型需要的张量输入
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info,
input_tensor_values.data(),
input_tensor_values.size(),
input_tensor_dims.data(),
input_tensor_dims.size()
);
assert(input_tensor.IsTensor());
std::vector<Ort::Value> ort_inputs;
ort_inputs.push_back(std::move(input_tensor));
// 将输入提供给模型进行推理得到输出
auto output_tensors = session.Run(
Ort::RunOptions{ nullptr },
input_node_names.data(),
ort_inputs.data(),
ort_inputs.size(),
output_node_names.data(),
1
);
// 从输出张量获取输出数据
float* floatarr = output_tensors[0].GetTensorMutableData<float>();
std::cout << "推理结果:" << *floatarr << std::endl;
}
catch (const Ort::Exception& e) {
// 处理 Ort::Exception 异常
std::cerr << "Caught Ort::Exception: " << std::string(e.what()) << std::endl;
// 在异常描述信息中查找错误代码
size_t pos = std::string(e.what()).find("ErrorCode: ");
if (pos != std::string::npos) {
std::string error_code_str = std::string(e.what()).substr(pos + 12); // 12 是 "ErrorCode: " 的长度
int error_code = std::stoi(error_code_str);
std::cerr << "Error Code: " << error_code << std::endl;
}
// 可选:进行其他异常处理或返回错误码
return -1;
}
return 0;
}
编译一下没有报错,运行之前记得把onnxruntime.dll复制到工程路径下x64//Debug路径下,因为生成的exe会在这个路径下找onnxruntime.dll,刚才编译时使用的路径它找不到,运行结果如下:
控制台打印了模型加载成功,读取到模型输入名称为float_input,这个是python中我们定义的,输出名称这个是模型自己给出来的,下面四个1是我提供的输入,经过模型推理后结果为10.4,完全正确。
上面的代码基本都是文心一言给出的,看到运行结果的瞬间感谢了一波文心一言。研究到这,我膨胀了,朋友们,这个onnx还是蛮简单的嘛,Linux下面运行肯定也是小菜一碟!结果接下来遇到了各种版本问题,配置问题,下面才是噩梦的开始。
启动之前我的文章https://blog.csdn.net/NCEPUautomation/article/details/112556541中配置过的Ubuntu虚拟机,然后重新打开VS2022,新建一个Linux下的控制台应用程序,名称我叫做test_onnxruntime_linux:
在工程上右键添加一个cpp文件,我叫做test_onnxruntime_linux.cpp,然后回到之前的github网址,下载一个Linux下的onnxruntime,我还是下载了最新的onnxruntime-linux-x64-1.17.1.tgz,把里面的include和lib文件夹放到虚拟机下,我放在了/home/lww/testonnx路径下,和刚才类似,需要配置一下附加包含目录和附加库目录,工程上右键属性进行配置,目录配置和之前类似,附加依赖项这里有点区别要注意,先看一下这个库在Linux下叫啥:
应该是这个libonnxruntime.so了,于是在附加依赖项添加onnxruntime(这里要注意,这其实是我已经踩过的坑,VS远程编译Linux,对于这种动态库,不能包含.so后缀,前面的lib也要去掉,不能添加成libonnxruntime):
然后先直接生成一下,失败了:
到这里我是懵逼的,什么情况?第一步就失败了?然后把这两个错误复制给文心一言分析一下,它说:
于是使用命令检查一下so文件格式,结果终端说它是一个文本文件,看了一下目录里,还有一个叫libonnxruntime.so.1.17.1的文件很可疑,于是我也看了一下它的属性,结果它是个动态库!
尝试cat查看.so文件内容,结果它写着:
这下明白了!感觉像在玩解谜游戏,把libonnxruntime.so.1.17.1复制一份,重命名为了libonnxruntime.so,再次编译,这次成功了:
哈哈哈,没想到居然能成功,于是把原来的helloworld代码全部删除,上面的C++代码复制过来,新建一个文件夹testonnxmodel,把model.onnx复制进去,再把第6行做个修改,改成:
const ORTCHAR_T* model_path = "/home/lww/testonnxmodel/model.onnx"
再次编译,报了一堆错。。。
到这一步当时我卡了很久,把其中一些报错复制到github项目的提问区搜了一下,结果搜到了一个“解决办法”,把C++版本从默认的C++11换成C++14,结果报错好像少了,但还是有问题:
C++的版本更换并没有彻底解决问题,但是这次问题又明确一些了,指向了一个叫GLIBC的东西,于是把第一个错误复制求助了文心一言:
再次感谢文心一言,现在问题比较明确了,是我系统的编译套件版本太低,这里有两个思路,一个是更新GLIBC,但GLIBC更新又受限于操作系统,如果操作系统的版本太旧,更新GLIBC也无法更新到最新版本,重新配一个操作系统又是几个小时,我已经懒得再配了,于是采用另一个方法。另一个方法就是下载旧版本的onnxruntime,因为旧版本的onnxruntime编译时肯定使用的GCC版本不高。这里本来有个坑,我下载了一个特别旧版本的onnxruntime,但运行的时候程序直接捕获了一个错误,提示onnxruntime版本太低无法加载onnx模型,直接跳过这一部分,结论就是生成onnx模型的时候一定要看一下模型是什么版本的,onnxruntime版本一定要比模型版本高或者相同。然后我用python看了一下onnx模型版本,输出:ONNX Model Version: 9,这。。。9是什么版本号,所以最后我不断尝试,找到了onnxruntime1.15.0版本可以用。。。下载旧版本的onnxruntime请从之前的网址中点击这里:
回到VS2022,把C++版本改回C++11,用onnxruntime1.15.0版本的include和lib替换之前的,同样记得把so.1.15.0复制一份改成.so,这次编译成功!在这里还做了几个小改动,把几个cout改成了wcout,cout还不知道为什么print不出来。
让我们运行一下程序,好!模型加载成功!但是catch捕获到了异常Caught Ort::Exception: input name cannot be empty:
这就怪了,一样的代码怎么会这样呢?启动调试看一下发现了问题,Input Name和Output Name在变量中显示并不是float_input和variable!
当时到这里我已经不想再纠结这种问题了,估计是操作系统不同导致的编码问题,或者我使用的数据类型有问题导致出现了强制类型转换,反正只是简单测试一下,所以我简单粗暴地把有问题的输入输出名称强制赋值了一下,再把完整代码贴一下吧:
#include <iostream>
#include <assert.h>
#include <onnxruntime_cxx_api.h>
#include <onnxruntime_c_api.h>
int main() {
const ORTCHAR_T* model_path = "/home/lww/testonnxmodel/model.onnx";
try {
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "ONNX_C_API");
std::wcout << "Attempting to load model from: " << model_path << std::endl;
Ort::SessionOptions session_options;
// 加载模型
Ort::Session session(env, model_path, session_options);
std::wcout << "Model loaded successfully." << std::endl;
Ort::AllocatorWithDefaultOptions allocator;
// 获取输入节点信息
size_t num_input_nodes = session.GetInputCount();
size_t num_output_nodes = session.GetOutputCount();
// 设置输入数据的维度,这里以单条数据为例
int input_dim = 4;
std::vector<int64_t> input_tensor_dims = { 1, input_dim };
size_t input_tensor_size = 1 * input_dim;
// 构造输入数据
std::vector<float> input_tensor_values(input_tensor_size);
for (unsigned int i = 0; i < input_tensor_size; i++)
{
input_tensor_values[i] = 1;
std::wcout << input_tensor_values[i] << std::endl;
}
// 将输入数据构造成onnx模型需要的张量形式
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info,
input_tensor_values.data(),
input_tensor_values.size(),
input_tensor_dims.data(),
input_tensor_dims.size()
);
assert(input_tensor.IsTensor());
std::vector<Ort::Value> ort_inputs;
ort_inputs.push_back(std::move(input_tensor));
const char* str = "float_input";
const char* const* floatinput = &str;
const char* str2 = "variable";
const char* const* variableoutput = &str2;
// 将输入提供给模型进行推理得到输出
auto output_tensors = session.Run(
Ort::RunOptions{ nullptr },
floatinput,
ort_inputs.data(),
ort_inputs.size(),
variableoutput,
1
);
// 获取输出数据
float* floatarr = output_tensors[0].GetTensorMutableData<float>();
std::wcout << "result = " << *floatarr << std::endl;
}
catch (const Ort::Exception& e) {
// 处理 Ort::Exception 异常
std::cerr << "Caught Ort::Exception: " << std::string(e.what()) << std::endl;
// 在异常描述信息中查找错误代码
size_t pos = std::string(e.what()).find("ErrorCode: ");
if (pos != std::string::npos) {
std::string error_code_str = std::string(e.what()).substr(pos + 12); // 12 是 "ErrorCode: " 的长度
int error_code = std::stoi(error_code_str);
std::cerr << "Error Code: " << error_code << std::endl;
}
// 可选:进行其他异常处理或返回错误码
return -1;
}
return 0;
}
再次启动,成功输出结果!
应用程序加载没问题,那就基本能成了,把编译为应用程序改为编译动态库,在工程属性里打开常规。然后配置类型改为动态库(.so):
函数名改成testonnx(个人习惯,不是应用程序主函数的习惯换个名字)再编译一次,成功了,把生成的libtest_onnxruntime_linux.so放到虚拟机路径下,我放倒了/home/lww/testonnx/lib这里,然后编写python加载so文件,命名为test_run_onnx.py,放到/home/lww路径,代码如下:
import ctypes
from ctypes import *
lib = ctypes.CDLL("/home/lww/testonnx/lib/libtest_onnxruntime_linux.so")
lib.testonnx.argtypes = ()
lib.testonnx.restype = c_int
res = lib.testonnx()
在终端执行:
python3 test_run_onnx.py
结果报错了:
啊。。。为什么会找不到so文件呢,感觉就差最后一步就成功了啊,于是求助文心一言,截取一下部分回答:
感觉这个回答挺有道理的,前面我们在VS2022编译程序的时候指定了动态库的位置,但是python并不知道这个位置,所以我们仿照给出的例子,把lib文件夹添加到环境变量:
export LD_LIBRARY_PATH=/home/lww/testonnx/lib:$LD_LIBRARY_PATH
再次python加载,依然报错找不到。。。这次真的是最后一步了,难道真的不行吗。。。最后这一步又让我苦恼了半天,突然灵机一动,会不会是什么权限的问题?直接用root用户一下试试?结果又报错了:
有进展!但是执行到加载函数这儿报错了,提示找不到testonnx函数,这时候才想起来,我们编译动态库的时候没有把函数名加上C修饰!把函数开头int testonnx()改为extern “C” int testonnx(),再次编译,替换之前的文件,然后再次执行python3命令,这次加!载!成!功!
大功告成!几天的努力终于有了结果!这种方法虽然很折腾,但有个好处是最后部署的Linux环境要求不高,我们只需要把onnxruntime的so文件和自己的so文件放到Linux系统就可以,前面的建模和自己so文件编译都可以放到其他环境来完成。
相关资料参考
- https://github.com/microsoft/onnxruntime
GitHub官方onnxruntime仓库 - https://onnxruntime.ai/docs/
ONNX Runtime官方文档(英文) - https://blog.csdn.net/weixin_42156097/article/details/127321349
CSDN文章——Ubuntu20.04安装CUDA、cuDNN、onnxruntime、TensorRT - https://blog.csdn.net/CFH1021/article/details/108732114
CSDN文章——python关于onnx模型的一些基本操作 - https://zhuanlan.zhihu.com/p/524023964
知乎文章——[推理部署]🌔ONNX推理加速技术文档-杂记 - https://zhuanlan.zhihu.com/p/513777076
知乎文章——C/C++下的ONNXRUNTIME推理 - https://blog.csdn.net/csczh/article/details/126758334
CSDN文章——ONNX 运行时报错 ORT_RUNTIME_EXCEPTION Ort::Exception 未经处理的异常 - https://blog.csdn.net/weixin_42280271/article/details/130147209
CSDN文章——onnxruntime 运行过程报错“onnxruntime::Model::Model Unknown model file format version“ - https://blog.csdn.net/wohenibdxt/article/details/131220958
CSDN文章——onnxruntime c++ 推理示例(tensorRT/cuda provider) - https://blog.csdn.net/c654528593/article/details/129840187
CSDN文章——C++ onnxruntime从12升级到14遇到的问题 - https://blog.csdn.net/chen1231992/article/details/117255528
CSDN文章——问题libm.so.6: version `GLIBC_2.27‘ not found的解决方法 - https://blog.csdn.net/YY007H/article/details/136301038
CSDN文章——【pyinstaller打包记录】Linux系统打包可执行文件后,onnxruntime报警告(Init provider bridge failed) - https://blog.csdn.net/weixin_43938778/article/details/132026825
CSDN文章——【c++】c++ linux onnxruntime部署深度学习框架记录 - https://zhuanlan.zhihu.com/p/655086894
知乎文章——ONNX-Runtime一本通:综述&使用&源码分析(持续更新) - https://zhuanlan.zhihu.com/p/614677069
知乎文章——4种方法解决Linux中加载C++动态库失败的问题 - http://www.manongjc.com/detail/58-txxhnfelzgtzgrh.html
浅谈机器学习模型推理性能优化
其他补充
- 这里要明确一下概念,onnx和onnxruntime关系上很密切,但是概念上是完全不同的两个东西。
- onnx指的是我们保存的模型格式,就好像.zip和.rar格式都是压缩文件一样,skl2onnx完成的工作就类似于我们把.rar格式的压缩文件转换为了.zip格式,也就是说把特定格式的文件转换为了通用格式的文件,原来我们必须用指定软件打开文件,现在可以用任意软件打开了。这样的好处显而易见,随便换一个模型训练的库,加载它还可以用以前的程序加载。
- onnxruntime只是一个加载onnx模型的工具,其他深度学习的库既然支持把自己的模型保存为onnx模型,那本身也可以支持加载onnx模型,不过其他深度学习库一安装就是一个大包,而onnxruntime只用了加载模型的功能,所以我采用了这种方法。也就是说在skl2onnx保存onnx模型后,随便你用Pytorch,Caffe什么的,也都可以加载onnx模型。
最后最后!再次感谢文心一言,代码和BUG处理上解决了很多问题,比百度搜索好使,哈哈哈。