ONNX 部署 PyTorch 模型
- ONNX RUNTIME 官网主页
- ONNX RUNTIME 官网文档
- 官方托管 Github 仓库: Microsoft/onnxruntime
- ONNX Model Zoo: 预训练的 onnx 模型库
- onnx/models# 目标检测与图像分割 onnx 模型
- (可选)将模型从 PyTorch 导出到 ONNX 并使用 ONNX Runtime 运行
- torch.onnx - 官方文档
Others
ONNX C++ API 说明
- 对每个输入节点需要创建一个 - Ort::Value类型的变量, 该变量可以是具有动态维度的张量. 例如可以一次性输入 1 张图片, 也可以一次性输入 batch_size=100 张图片.
- 创建 - Ort::Value类型的变量需要通过- Ort::Value::CreateTensor函数来创建, 该函数有几种重载形式, 对于如下这种 CreateTensor 形式而言
| 1 | static Ort::Value Ort::Value::CreateTensor<T>(const OrtMemoryInfo* info, T* p_data, size_t p_data_element_count, const int64_t* shape, size_t shape_len); | 
创建张量就需要将具体的数据组织成一维数组, 将首地址传入 T* p_data,
 数据元素总数传入 size_t p_data_element_count,
 张量形状组织成一维数组, 首地址传入 const int64_t* shape,
 张量总维数传入size_t shape_len.
例如:
要将 100 张 32*64 的 RGB 图片创建为张量, 那么原始数据形状可能为 [100,3,32,64], 需要将其变成一维的数组std::vector<float> p_data(100*3*32*64), 然后将数组首地址p_data 传入. 那么元素总数就是 p_data_element_count=100*3*32*64=614400. 为了让ONNX RUNTIME 明确数据的原始格式, 需要创建 std::vector<int64_t> shape = {100,3,32,64}, 然后将shape 作为维度数组的首地址传入. 最后该数据原本是 4 维的, 因此shape_len=4.
- 如果是多输入的, 那么首先创建多个 Ort::Value类型的变量, 然后创建std::vector<Ort::Value> ort_inputs, 再通过移动构造将这些变量压入vector容器中.
| 1 | ort_inputs.push_back(std::move(Tensor_1)); | 
这样, 形式上可以将任意多个 Ort::Value 变量在内存中放在一起了.
- 在进行模型推理时, 调用 session.Run()函数. 该函数有 三种重载版本 .
 第一种是只需要给输入变量预分配内存, 将输出变量通过函数返回值返回;
 第二种需要给输入输出变量都预分配内存, 没有返回值;
 第三种将输入输出进行IoBinding&, 没有返回值.
对于第一种版本:
| 1 | std::vector<Value> Run(const RunOptions& run_options, const char* const* input_names, const Value* input_values, size_t input_count, | 
input_names是输入节点名的字符串数组的首地址, 例如 std::vector<const char*> names = {"input_1, "input_2"}. 那么将names 传入即可.  input_values是要传入的若干个张量的首地址, 如果只传入一个张量, 那就对这个张量取地址 &Tensor_1 传入即可; 如果要传入多个张量, 那么将这多个张量放在一个 vector 中, 并将该 vector 首地址传入即可 (二者实际是一致的).input_count 是要输入张量的个数.output_names,output_count同理.
- 在对输出变量进行解码时, 首先要按照输出节点数分为多个张量, 视该张量数据类型不同, 以不同数据类型取出其中的数据.
 例如下面代码块中,type就是数据类型.1 
 2
 3
 4
 5
 6// print output node types 
 Ort::TypeInfo type_info = session.GetOutputTypeInfo(i);
 auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
 ONNXTensorElementDataType type = tensor_info.GetElementType();
 printf("Output %d : type=%d\n", i, type);
其中 ONNXTensorElementDataType 是一个枚举类型, 具体定义如下:
| 1 | // https://github.com/microsoft/onnxruntime/blob/master/include/onnxruntime/core/session/onnxruntime_c_api.h#L93 | 
对于不同的数据类型, 对应的内存块中存储的是整型变量或者浮点型变量, 应当进行不同形式的解码, 如下.
| 1 | float* boxes_ptr = output_tensors[0].GetTensorMutableData<float>(); | 
解码后得到的是一维数据, 还应当依据输出张量的维度信息进行重新组织, 得到合理的输出.
线性回归例程
这里通过一个简单的线性回归模型来说明 onnx 模型文件的保存与加载
上图所示是一个简单的线性回归模型, 用 python+pytorch 创建并训练, 真实的回归模型为 y1 = 12*x+9, y2 = 4*x+9. 通过大量的随机数进行训练使其能够稳定预测输出值. 然后通过torch.onnx 将模型输出为 model.onnx 文件, 并再次通过 python+onnxruntime 读取该模型以验证模型文件是没有问题的.
然后通过 CPP+onnxruntime_cxx_api 读取该模型文件, 并输入 batch_size=6 个数据, 从得到的 [batch_size=6,2] 个数据来看, 可以较为准确地预测输出值.
| 1 | 标量输入:x, 向量输出 y = [y1,y2] | 
- 通过 pytorch 创建模型、训练并保存 onnx 模型文件
- 通过 cpp 加载 onnx 模型文件, 并进行推理
MaskRCNN 例程
 当前有一个任务是要将 python+pytorch 编写的 maskrcnn 模型部署到 C++ 环境中, 主要路线是用 pytorch 内置的 torch.onnx 将模型输出为 *.onnx 文件, 然后再用 onnxruntime_cxx_api 导入模型进行推理.
示例图片如下: demo.jpg
 
- 调用 pytorch内置模型maskrcnn_resnet50_fpn(包括预训练参数). 使用torch.jit.script直接输出序列化模型文件model.pt.
 如果用C++版本的libtorch+torchvision也许是可以直接导入该模型文件并进行推理的, 但是torchvision的编译过程踩坑不断, 已弃疗!
- 调用 torch.onnx输出序列化模型文件model.onnx, 输出时需要提供一个示例样本, 理论上可以用随机数torch.randn, 这不影响模型导出以及后续的加载. 只是随机生成的样本可能经过模型推理后输出为空, 在python环境中也许还比较容易发现异样, 但在用C++编程加载模型并进行推理的时候如果也用随机样本, 并且输出为空, 会让人怀疑人生的!
requirements:
| 1 | > conda install -c conda-forge onnx | 
- 仍旧是用 python加载模型文件model.onnx, 并用onnxruntime进行模型推理. 原始模型推理得到的输出是batch_size个字典组成的列表. 每个字典是对应某张图片的四个输出:"boxes","labels","scores","masks". 而onnxruntime推理得到的输出似乎直接就全都是列表(待证!).
Requirements:
| 1 | > pip install onnxruntime | 
- 以上是在 python中进行模型导出与加载, 接下来要换用C++接口实现.
环境: Visual Studio 2017, Windows 10 x64.
新建 VS 工程 -> 项目 -> 管理 NuGet 程序包 (N)-> 浏览 -> 输入 onnx: 选择 Microsoft.ML.OnnxRuntime+Microsoft.ML.OnnxRuntime.MKLML 进行安装 (每新建一个工程就要安装一次). 如果要用GPU 加速, 则应该下载 GPU 版本, 并且似乎 CPU 与GPU版本不可共存. 并且 Python 环境中也要对应, 使用一个就要卸载另一个.(这个后续再去仔细研究, 暂且仅用 CPU 版本)
参考:
- 读取图片
Note: 我首先将 demo_maskrcnn.jpg resize 成了 500*667 大小的图片demo_resize.jpg, 且此后所有操作都是针对处理后的图片进行的.
| 1 | from PIL import Image | 
- ONNX C++ API调用模型进行前向推理
用 python(左) 和cpp(右)推理得到的模型输出结果依次如下图所示, 虽然有所区别, 但如果将置信度设置在 90% 以上, 则二者都检测出了图片中人物的全身. 此外, 这里试验用的模型是 pytorch预训练模型, 并没有针对此类图片进行专门训练. 测试出现偏差也情有可原.
 
     
- 
- main.cpp
- MaskRCNN_Session.cpp
- MaskRCNN_Session.h
 
- 本文例程打包下载: https://github.com/siyouluo/Storage/releases/download/v1.0.0/onnx.zip 
ONNX 部署 PyTorch 模型