使用onnx c++部署pytorch神经网络模型全流程

本文详细介绍了如何将PyTorch模型转换为ONNX格式,并在C++环境中使用ONNXRuntime进行推理。过程中提到了ONNX对特定操作的支持限制,以及在C++中运行ONNX模型时的注意事项,包括输入输出处理和内存管理。此外,还分享了其他博主的经验链接,并对比了ONNX库与Libtorch在精度上的差异。
摘要由CSDN通过智能技术生成

简介

Open Neural Network Exchange(ONNX,开放神经网络交换)格式,是一个用于表示深度学习模型的标准,可使模型在不同框架之间进行转移。如pytorch模型转换为caffe模型,python模型c++调用等等。
从我的理解来看,ONNX实现了一些神经网络的算子,通过保存计算图,即N节点到N+1节点的维度变换,kernel尺寸,从而推算出这一步进行的是什么操作,调用算子进行推理。
(更详细的源码理解,之后有空再学习一下)

步骤

项目中需要将pytorch模型部署在C++系统里。因此做了一些使用onnx库实现该步骤的工作,在这里记录整理一下。

具体的实现步骤主要有两个部分:

  1. python环境中,将pytorch模型推理过程记录为onnx模型计算图,保存为后缀.onnx文件。
  2. c++环境中,使用C++ onnxruntime库调用刚才保存的onnx文件,实现推理。

python

import torch
import network

# ================ 生成 ==========================
# 生成假输入,只需要尺寸一致即可,因为onnx只保存计算图
dummy_input1 = torch.randn(1, 1, 224, 224)
dummy_input2 = torch.randn(1, 1, 60, 60)
dummy_input3 = torch.randn(1, 1, 256)
# 实例化神经网络,假设有一个三输入,三输出的网络
net = network()
# 生成onnx模型
torch.onnx.export(net,
                  (dummy_input1, dummy_input2, dummy_input3),
                  "net.onnx", 
                   export_params=True,        # 是否保存训练好的参数在网络中
                   opset_version=10,          # ONNX算子版本
                   do_constant_folding=True,  # 是否不保存常数输出(优化选项)
                   input_names = ['input0', 'input1', 'input2'],   
                   output_names = ['output0', 'output1', 'output2'])

# ================  验证 ==============================
import onnxruntime
import numpy as np
onnx_session = onnxruntime.InferenceSession('net.onnx')
# 因为此时已经不是使用torch进行推理了,所以输入不再是tensor
input0 = np.random.randn(1, 1, 224, 224)
input1 = np.random.randn(1, 1, 60, 60)
input2 = np.random.randn(1, 1, 256)
input_name0 = onnx_session.get_inputs()[0].name
input_name1 = onnx_session.get_inputs()[1].name
input_name2 = onnx_session.get_inputs()[2].name
output_name0 = onnx_session.get_outputs()[0].name
output_name1 = onnx_session.get_outputs()[1].name
output_name2 = onnx_session.get_outputs()[2].name
# 使用Onnx模型推理
res = onnx_session.run([output_name0, output_name1, output_name2],
                       {input_name0: input0, 
                        input_name1: input1,
                        input_name2: input2})
output0 = res[0]
output1 = res[1]
output2 = res[2]

c++

#include <iostream>
#include <assert.h>
#include <vector>
#include <onnxruntime_cxx_api.h>

int main(int argc, char* argv[])
{
	//设置为VERBOSE,方便控制台输出时看到是使用了cpu还是gpu执行
    Ort::Env env(ORT_LOGGING_LEVEL_VERBOSE, "test");
    Ort::SessionOptions session_options;
    // 使用五个线程执行op,提升速度
    session_options.SetIntraOpNumThreads(5);
    // 第二个参数代表GPU device_id = 0,注释这行就是cpu执行
    OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0);
    // ORT_ENABLE_ALL: To Enable All possible opitmizations
    session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
    
#ifdef _WIN32
    const wchar_t* model_path = L"net.onnx";
#else
    const char* model_path = "net.onnx";
#endif

    Ort::Session session(env, model_path, session_options);
    // 获得模型有多少个输入和输出,因为是三输入三输出网络,那么input和output数量都为3
    Ort::AllocatorWithDefaultOptions allocator;
    size_t num_input_nodes = session.GetInputCount();
    size_t num_output_nodes = session.GetOutputCount();
    
    std::vector<const char*> input_node_names(num_input_nodes);
    std::vector<const char*> output_node_names(num_output_nodes);
    std::vector<std::vector<int64_t>> input_node_dims_vector;
    std::vector<std::vector<int64_t>> output_node_dims_vector;
    std::vector<int64_t> input_node_dims_sum;
    std::vector<int64_t> output_node_dims_sum;
    int64_t input_node_dims_sum_all{ 1 };
    int64_t output_node_dims_sum_all{ 1 };
    
    // 获取所有输入层信息
    for (int i = 0; i < num_input_nodes; i++) {
        // 得到输入节点的名称 char*
        char* input_name = session.GetInputName(i, allocator);
        input_node_names[i] = input_name;
        
        Ort::TypeInfo type_info = session.GetInputTypeInfo(i);
        auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
        // 得到输入节点的数据类型
        ONNXTensorElementDataType type = tensor_info.GetElementType();
        
		// 得到输入节点的输入维度 std::vector<int64_t>
        input_node_dims = tensor_info.GetShape();
        input_node_dims_vector.emplace_back(input_node_dims);
        int64_t sums{ 1 };
        // 得到输入节点的输入维度和,后面要使用 int64_t
        for (int j = 0; j < input_node_dims.size(); j++) {
            sums *= input_node_dims[j]);
        }
        input_node_dims_sum.emplace_back(sums);
        input_node_dims_sum_all *= sums;
    }
	// 迭代所有输出层信息
    for (int i = 0; i < num_output_nodes; i++) {
        // 得到输出节点的名称 char*
        char* output_name = session.GetOutputName(i, allocator);
        output_node_names[i] = output_name;
        
        Ort::TypeInfo type_info = session.GetOutputTypeInfo(i);
        auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
        // 得到输出节点的数据类型
        ONNXTensorElementDataType type = tensor_info.GetElementType();
        
		// 得到输出节点的输入维度 std::vector<int64_t>
        output_node_dims = tensor_info.GetShape();
        output_node_dims_vector.emplace_back(output_node_dims);
         int64_t sums{ 1 };
        // 得到输出节点的输入维度和,后面要使用 int64_t
        for (int j = 0; j < output_node_dims.size(); j++) {
            sums *= output_node_dims[j]);
        }
        output_node_dims_sum.emplace_back(sums);
        output_node_dims_sum_all *= sums;
    }
	
	// 假设输入为三个 std::vector<std::vector<float>> inputs 创建输入tensor (假设输入为1*1*224*224)
	// 第二个参数代表输入数据 float*
    // 第三个参数代表输入节点的总尺寸 int64_t (1*1*224*224)
    // 第四个参数代表输入节点的尺寸数据 std::vector<int64_t> (vector(1, 1, 224, 224))
    // 最后一个参数代表输入节点的尺寸维度数目 size_t (4)
	std::vector<Ort::Value> ort_inputs;
	auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    for (size_t i = 0; i < num_input_nodes; i++) {
        Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, inputs[i].data(), 
        input_node_dims_sum[i], input_node_dims[i].data(), input_node_dims[i].size());
        assert(input_tensor.IsTensor());
        ort_inputs.emplace_back(input_tensor);
    }
   
    // 推理
    // 第一个参数代表运行配置
    // 第二个参数代表输入节点的名称集合
    // 第三个参数代表输入Tensor地址
    // 第四个参数代表输入节点的数目
    // 第五个参数代表输出节点的名称集合
    // 最后一个参数代表输出节点的数目
    std::vector<Ort::Value> output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), &input_tensor, num_input_nodes, output_node_names.data(), num_output_nodes);
    assert(output_tensors.size() == 3 && output_tensors[0].IsTensor() && output_tensors[1].IsTensor() && output_tensors[2].IsTensor());
    
    // 获取输出
    float* output0 = output_tensors[0].GetTensorMutableData<float>();
    float* output1 = output_tensors[1].GetTensorMutableData<float>();
    float* output2 = output_tensors[2].GetTensorMutableData<float>();
}

  1. 不支持Conv4d操作 -> 将Conv4d改成函数,不使用torch.nn.conv继承的那一套。
  2. 只支持单变量输入,输入list,tuple都会被解析成单变量输入。
  3. 不支持大于1的步长的切片赋值操作,如x[::2] = 1(但是onnxruntime1.6测试又是可以的,存疑)
  4. 不支持torch.nn.functional.grid_sample函数 -> 用mmvs.ops.point_sample.bilinear_grid_sample替代
  5. 不支持分支操作,如TopK中,K取1024,如果输入小于1024就会报错。
  6. C++ onnx的Session和Value类都是不支持移动构造和复制构造的,必须在最外层的函数去创建和使用。不能包装在子函数里,std::move也不行。
  7. pytorch对于index的类型是int64,但是必须要把这个值转成float32类型的,C++中的Onnxruntime才能输出正确值,不然会输出NAN!!!所以遇到索引之后的部分只能改写了(官网说onnx opset12及以后之后已经支持Int64输出了,不知道是不是版本的原因,onnxruntime=1.5.2不可以, opset=12, onnxruntime-python=1.6.0是可以的 )

还有一些其他博主记录的

  • https://www.cnblogs.com/xiaxuexiaoab/p/15654972.html
  • https://blog.csdn.net/zzz_zzz12138/article/details/109138805
  • https://blog.csdn.net/JoeyChen1219/article/details/121141318

杂言

  • 有些单输入的网络(如目标检测yolo、人脸识别网络)也支持使用opencv.dnn.readNetFromOnnx实现推理)
  • 实际上使用Libtorch库来实现会更准确也更简单,在实验中,使用Libtorch库推算,C++代码和python代码的误差为0,但是ONNX库推算会带来1e-5左右的误差,所以ONNX库推算不太适合需要特别精确数值的任务)。因为也调研了一些pytorch + libtorch库实现C++调用pytorch模型推理的工作,这里也浅浅记录一下吧。
    主要的步骤也是两步:
  1. python环境下,调用torch.jit.trace或者torch.jit.script创建pt模型。
  2. c++环境下,使用libtorch调用刚才保存的pt模型进行推理。

一些有用的博客:

参考

  1. (一个有意思的内容)加密解密Onnx模型
  2. onnx转移动端模型ort部署
  3. ONNX动态输入和输出
  4. ONNX使用pytorch导出的模型进行推理
  5. PyTorch模型C++部署
  6. https://github.com/microsoft/onnxruntime/blob/master/onnxruntime/test/shared_lib/test_inference.cc
  • 16
    点赞
  • 91
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
### 回答1: 很抱歉,作为一种语言模型,我并不能提供完整的代码。但是,下面是一些参考资料和代码片段,可以帮助您开始采用ONNX格式进行语义分割的C语言编程。 首先,您需要准备一个ONNX格式的模型,这可以通过使用训练工具(如TensorFlow或PyTorch)训练得到。然后,您可以使用ONNX Runtime库,将模型加载到C程序中,并在该程序中运行推理。 以下是加载ONNX模型的C代码片段: ``` #include <onnxruntime_c_api.h> int main(int argc, char* argv[]) { OrtEnv* env; OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "test", &env); OrtSessionOptions* session_options; OrtCreateSessionOptions(&session_options); // Assuming model_path is the path to your ONNX model file const char* model_path = "model.onnx"; OrtSession* session; OrtCreateSession(env, model_path, session_options, &session); // .... Run inference here OrtReleaseSession(session); OrtReleaseSessionOptions(session_options); OrtReleaseEnv(env); return 0; } ``` 然后,您可以使用OrtRun()函数运行推理。下面是一个示例代码片段: ``` #include <onnxruntime_c_api.h> int main(int argc, char* argv[]) { OrtEnv* env; OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "test", &env); OrtSessionOptions* session_options; OrtCreateSessionOptions(&session_options); const char* model_path = "model.onnx"; OrtSession* session; OrtCreateSession(env, model_path, session_options, &session); // Prepare input tensors size_t input_tensor_size = ...; float* input_tensor_values ### 回答2: 语义分割是计算机视觉领域的一个重要任务,其目标是将图像中的每个像素分类到不同的语义类别中。采用ONNX格式的网络可以更方便地在不同平台和框架之间进行模型的转移和部署。 在C语言中,我们可以使用ONNX Runtime来加载和运行ONNX格式的模型。下面是一个简单的示例代码,演示了如何使用ONNX Runtime进行语义分割: ```c #include <stdio.h> #include <onnxruntime_c_api.h> const char* modelPath = "path/to/your/model.onnx"; const char* imagePath = "path/to/your/image.jpg"; int main() { OrtEnv* env; OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "app", &env); OrtSessionOptions* sessionOptions; OrtCreateSessionOptions(&sessionOptions); OrtSetIntraOpNumThreads(sessionOptions, 1); OrtSetSessionGraphOptimizationLevel(sessionOptions, ORT_ENABLE_ALL); OrtSession* session; OrtCreateSession(env, modelPath, sessionOptions, &session); OrtAllocator* allocator; OrtGetAllocatorWithDefaultOptions(&allocator); OrtValue* inputTensor; // 加载和预处理图像 // 这里省略了图像加载和预处理的代码 // 假设我们得到了一个输入大小为 [1, 3, H, W] 的图像张量 // 创建输入张量 size_t inputDims[] = {1, 3, H, W}; OrtCreateTensorWithDataAsOrtValue(allocator, inputDims, 4, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, inputData, inputSize, ONNX_RUNTIME_DEVICE_CPU, &inputTensor); // 运行模型 const char* inputNames[] = {"input"}; OrtValue* outputTensor; OrtRun(session, NULL, inputNames, &inputTensor, 1, &outputNames, 1, &outputTensor); // 处理输出结果 float* outputData; OrtGetTensorMutableData(outputTensor, (void**)&outputData); // 这里省略了结果后处理的代码 // 假设我们得到了一个大小为[H, W]的语义分割结果 // 打印结果 for (int h = 0; h < H; h++) { for (int w = 0; w < W; w++) { printf("%f ", outputData[h * W + w]); } printf("\n"); } // 释放资源 OrtReleaseValue(outputTensor); OrtReleaseValue(inputTensor); OrtReleaseSession(session); OrtReleaseSessionOptions(sessionOptions); OrtReleaseEnv(env); return 0; } ``` 以上代码中,我们首先创建了ONNX Runtime的环境(env)和会话选项(sessionOptions),然后使用这些对象创建了一个会话(session)。接着,我们加载和预处理了输入图像,并创建了一个输入张量(inputTensor)。然后,我们通过调用`OrtRun`函数来运行模型,并得到了一个输出张量(outputTensor)。最后,我们处理了输出结果,并释放了所有的资源。 需要注意的是,上述代码中省略了图像加载和预处理、结果后处理等操作的具体代码,这部分根据具体的需求和模型进行实现。此外,还需确保在编译时正确链接ONNX Runtime的库文件。 希望以上代码对您有所帮助! ### 回答3: ONNX 是一个开放的神经网络推理标准,可以在不同的深度学习框架间实现模型的互操作性。下面是一个使用 ONNX 格式的网络进行语义分割的 C 代码的参考: ```c #include <onnxruntime_c_api.h> int main() { OrtEnv* env; OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "ONNX_Semantic_Segmentation", &env); OrtSessionOptions* session_options; OrtCreateSessionOptions(&session_options); OrtSession* session; OrtCreateSession(env, "path/to/your/model.onnx", session_options, &session); OrtStatus* status; OrtAllocator* allocator; OrtGetAllocatorWithDefaultOptions(&allocator); int image_width = 224; // 定义输入图像的宽度 int image_height = 224; // 定义输入图像的高度 int num_channels = 3; // 定义输入图像的通道数 size_t input_tensor_size = image_width * image_height * num_channels; float* input_tensor_values = (float*)malloc(input_tensor_size * sizeof(float)); // 将输入图像的数据读入 input_tensor_values OrtMemoryInfo* memory_info; OrtCreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &memory_info); OrtValue* input_tensor; OrtCreateTensorWithDataAsOrtValue(memory_info, input_tensor_values, input_tensor_size * sizeof(float), (int64_t[3]){1, image_channels, image_width, image_height}, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &input_tensor); const char* input_names[] = {"input"}; OrtValue* input_tensors[] = {input_tensor}; const char* output_names[] = {"output"}; OrtValue* output_tensors[] = {nullptr}; // 进行推理 OrtRun(session, nullptr, input_names, input_tensors, 1, output_names, 1, output_tensors); int output_tensor_size; OrtGetTensorShapeElementCount(OrtGetTensorTypeAndShape(output_tensors[0], &status), &output_tensor_size); // 获取预测结果的数据 float* output_tensor_values; OrtGetTensorMutableData(output_tensors[0], (void**)&output_tensor_values); // 处理预测结果的数据 // 释放内存 free(input_tensor_values); OrtReleaseValue(input_tensor); OrtReleaseMemoryInfo(memory_info); OrtReleaseSession(session); OrtReleaseSessionOptions(session_options); OrtReleaseEnv(env); return 0; } ``` 上述代码使用 ONNX Runtime C API 来加载 ONNX 格式的模型进行推理。其中,你需要根据你的模型的尺寸,将输入图像的数据填充到 `input_tensor_values` 中,然后获取推理结果的数据并做相应处理。请注意,代码中的路径 `path/to/your/model.onnx` 和处理预测结果的数据部分需要根据你的实际情况进行修改。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值