在C++中部署python深度学习-学习笔记

文章目录

  • 一、简介
  • 二、思路
  • 三、深度学习部署平台和模型部署框架
    • 3.1 部署平台
    • 3.2 部署框架
  • 四、基于TorchScript的PyTorch模型部署
    • 4.1 TorchScript
      • 1.Tracing应用
      • 2.Script应用
    • 4.2 Libtorch
    • 4.3 基于C++的PyTorch模型部署完整流程
    • 4.4 本人的实际使用过程
      • 第一步:将模型导出成torchscript
      • 第二步:下载libtorch并在VS中配置libtorch环境
      • 第三步:编写代码并写输入输出
      • 第四步:Bug
  • 五、基于ONNX的PyTorch模型部署
    • 5.1 ONNX和ONNXRuntime简介
    • 5.2 实现步骤
    • 5.3 和libtorch的区别
  • 参考链接

持续更新中…

一、简介

工业界与学术界最大的区别在于工业界的模型需要落地部署,学界更多的是关心模型的精度要求,而不太在意模型的部署性能。一般来说,我们用深度学习框架训练出一个模型之后,使用Python就足以实现一个简单的推理演示了。但在生产环境下,Python的可移植性和速度性能远不如C++。所以对于深度学习算法工程师而言,Python通常用来做idea的快速实现以及模型训练,而用C++作为模型的生产工具。

方案一:C++调用python
Python 提供了一套 C API库,使得开发者能很方便地从C/ C++ 程序中调用 Python 模块。参考我另一篇博客:C++调用Python(混合编程)函数整理总结
方案二:C++部署深度学习
即本博客内容。

不同方案类型和优缺点比较:
在C++中部署python深度学习-学习笔记_第1张图片

二、思路

模型转换
移动端部署
服务器端部署

三、深度学习部署平台和模型部署框架

3.1 部署平台

目前主流的深度学习部署平台包含GPU、CPU、ARM。

3.2 部署框架

模型部署框架则有英伟达推出的TensorRT,谷歌的Tensorflow和用于ARM平台的tflite,开源的caffe,百度的飞浆,腾讯的NCNN。

其中基于GPU和CUDA的TensorRT在服务器,高性能计算,自动驾驶等领域有广泛的应用。

平台和框架的对应:
在C++中部署python深度学习-学习笔记_第2张图片
在C++中部署python深度学习-学习笔记_第3张图片

四、基于TorchScript的PyTorch模型部署

目前PyTorch能够完美的将Python和C++结合在一起。实现PyTorch模型部署的核心技术组件就是TorchScript和libtorch。
PyTorch官方教程:官方教程
基于PyTorch的深度学习算法工程化流程大体如下图所示:
在C++中部署python深度学习-学习笔记_第4张图片

4.1 TorchScript

TorchScript可以视为PyTorch模型的一种中间表示,TorchScript表示的PyTorch模型可以直接在C++中进行读取。PyTorch在1.0版本之后都可以使用TorchScript的方式来构建序列化的模型。TorchScript提供了TracingScript两种应用方式。

1.Tracing应用

示例如下:

class MyModel(torch.nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = torch.nn.Linear(4, 4)
 
 
    def forward(self, x, h):
        new_h = torch.tanh(self.linear(x) + h)
        return new_h, new_h
 
 
# 创建模型实例 
my_model = MyModel()
# 输入示例
x, h = torch.rand(3, 4), torch.rand(3, 4)
# torch.jit.trace方法对模型构建TorchScript
traced_model = torch.jit.trace(my_model, (x, h))
# 保存转换后的模型
traced_model.save('model.pt')

将model.pt在您的工作目录中生成一个文件。我们现在正式离开了Python的领域,并准备跨越到C ++领域。
在这段代码中,我们先是定义了一个简单模型并创建模型实例,然后给定输入示例,Tracing方法最关键的一步在于使用torch.jit.trace方法对模型进行TorchScript转化。我们可以获得转化后的traced_model对象获得其计算图属性和代码属性。
计算图属性:

print(traced_model.graph)
graph(%self.1 : __torch__.torch.nn.modules.module.___torch_mangle_1.Module,
      %input : Float(3, 4),
      %h : Float(3, 4)):
  %19 : __torch__.torch.nn.modules.module.Module = prim::GetAttr[name="linear"](%self.1)
  %21 : Tensor = prim::CallMethod[name="forward"](%19, %input)
  %12 : int = prim::Constant[value=1]() # /var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0
  %13 : Float(3, 4) = aten::add(%21, %h, %12) # /var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0
  %14 : Float(3, 4) = aten::tanh(%13) # /var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0
  %15 : (Float(3, 4), Float(3, 4)) = prim::TupleConstruct(%14, %14)
  return (%15)

代码属性:

print(traced_cell.code)
def forward(self,
    input: Tensor,
    h: Tensor) -> Tuple[Tensor, Tensor]:
  _0 = torch.add((self.linear).forward(input, ), h, alpha=1)
  _1 = torch.tanh(_0)
  return (_1, _1)

这样我们就可以将整个模型都保存到硬盘上了,并且经过这种方式保存下来的模型可以加载到其他其他语言环境中。

2.Script应用

TorchScript的另一种实现方式是Script的方式,可以算是对Tracing方式的一种补充。当模型代码中含有if或者for-loop等控制流程序时,使用Tracing方式是无效的,这时候可以采用Script方式来进行实现TorchScript。实现方法跟Tracing差异不大,关键在于把jit.tracing换成jit.script方法。
示例如下:

scripted_model = torch.jit.script(MyModel)
scripted_model.save('model.pt')

有if等控制流的模型示例:

import torch
 
class MyModule(torch.nn.Module):
    def __init__(self, N, M):
        super(MyModule, self).__init__()
        self.weight = torch.nn.Parameter(torch.rand(N, M))
 
    def forward(self, input):
        if input.sum() > 0:
          output = self.weight.mv(input)
        else:
          output = self.weight + input
        return output

因为forward此模块的方法使用依赖于输入的控制流,所以它不适合跟踪。
除了Tracing和Script之外,我们也可以混合使用这两种方式,这里不做详述。总之,TorchScript为我们提供了一种表示形式,可以对代码进行编译器优化以提供更有效的执行。

4.2 Libtorch

在Python环境下对训练好的模型进行转换之后,我们需要C++环境下的PyTorch来读取模型并进行编译部署。这种C++环境下的PyTorch就是libtorch。因为libtorch通常用来作为PyTorch模型的C++接口,libtorch也称之为PyTorch的C++前端。

要在C ++中加载序列化的PyTorch模型,您的应用程序必须依赖于PyTorch C ++ API - 也称为LibTorch。LibTorch发行版包含一组共享库,头文件和CMake构建配置文件。虽然CMake不是依赖LibTorch的要求,但它是推荐的方法,并且将来会得到很好的支持。

我们可以直接从PyTorch官网下载已经编译好的libtorch安装包,当然也可以下载源码自行进行编译。这里需要注意的是,安装的libtorch版本要与Python环境下的PyTorch版本一致。
下载地址:[深度学习][libtorch]​Windows上Libtorch下载地址
官网只能下载最新版本。
在C++中部署python深度学习-学习笔记_第5张图片
安装好libtorch后可简单测试下是否正常。
比如我们用TorchScript转换一个预训练模型,示例如下:

import torch
import torchvision.models as models
vgg16 = models.vgg16()
example = torch.rand(1, 3, 224, 224).cuda() 
model = model.eval()
traced_script_module = torch.jit.trace(model, example)
output = traced_script_module(torch.ones(1,3,224,224).cuda())
traced_script_module.save('vgg16-trace.pt')
print(output)

输出为:

tensor([[ -0.8301, -35.6095, 12.4716]], device='cuda:0',
        grad_fn=<AddBackward0>)

然后切换到C++环境,编写CmakeLists文件如下:

cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR)
project(libtorch_test)
find_package(Torch REQUIRED)
message(STATUS "Pytorch status:")
message(STATUS "libraries: ${TORCH_LIBRARIES}")
add_executable(libtorch_test test.cpp)
target_link_libraries(libtorch_test "${TORCH_LIBRARIES}")
set_property(TARGET libtorch_test PROPERTY CXX_STANDARD 11)

继续编写test.cpp代码如下:

#include "torch/script.h"
#include "torch/torch.h"
#include 
#include 
using namespace std;
 
 
int main(int argc, const char* argv[]){
    if (argc != 2) {
        std::cerr << "usage: example-app \n";
        return -1;
    }
 
 
    // 读取TorchScript转化后的模型
    torch::jit::script::Module module;
    try {
        module = torch::jit::load(argv[1]);
    }
 
 
    catch (const c10::Error& e) {
        std::cerr << "error loading the model\n";
        return -1;
    }
 
 
    module->to(at::kCUDA);
    assert(module != nullptr);
    std::cout << "ok\n";
 
 
    // 构建示例输入
    std::vector<torch::jit::IValue> inputs;
    inputs.push_back(torch::ones({1, 3, 224, 224}).to(at::kCUDA));
 
 
    // 执行模型推理并输出tensor
    at::Tensor output = module->forward(inputs).toTensor();
    std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';}

编译test.cpp并执行,输出如下。对比Python环境下的的运行结果,可以发现基本是一致的,这也说明当前环境下libtorch安装没有问题。

ok
-0.8297, -35.6048, 12.4823
[Variable[CUDAFloatType]{1,3}]

我们构建示例应用程序所需的最后一件事是LibTorch发行版。您可以随时从PyTorch网站的下载页面获取最新的稳定版本。如果下载并解压缩最新存档,则应收到具有以下目录结构的文件夹:

libtorch/
  bin/
  include/
  lib/
  share/
  • 该lib/文件夹包含您必须链接的共享库,
  • 该include/文件夹包含程序需要包含的头文件,
  • 该share/文件夹包含必要的CMake配置,以启用find_package(Torch)上面的简单命令。

最后一步是构建应用程序。为此,假设我们的示例目录布局如下:

example-app/
  CMakeLists.txt
  example-app.cpp

我们现在可以运行以下命令从example-app/文件夹中构建应用程序 :

mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
make

/path/to/libtorch应该是解压缩的LibTorch发行版的完整路径。如果一切顺利,它将看起来像这样:

root@4b5a67132e81:/example-app# mkdir build
root@4b5a67132e81:/example-app# cd build
root@4b5a67132e81:/example-app/build# cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Configuring done
-- Generating done
-- Build files have been written to: /example-app/build
root@4b5a67132e81:/example-app/build# make
Scanning dependencies of target example-app
[ 50%] Building CXX object CMakeFiles/example-app.dir/example-app.cpp.o
[100%] Linking CXX executable example-app
[100%] Built target example-app

如果我们提供ResNet18我们之前为生成的example-app二进制文件创建的序列化模型的路径,我们应该得到友好的“ok”奖励:

root@4b5a67132e81:/example-app/build# ./example-app model.pt
ok

4.3 基于C++的PyTorch模型部署完整流程

第一步:

通过torch.jit.trace方法将PyTorch模型转换为TorchScript,示例如下:

import torch
from torchvision.models import resnet18
model =resnet18()
example = torch.rand(1, 3, 224, 224)
tracing.traced_script_module = torch.jit.trace(model, example)

第二步:
将TorchScript序列化为.pt模型文件。

traced_script_module.save("traced_resnet_model.pt")

第三步:
在C++中导入序列化之后的TorchScript模型,为此我们需要分别编写包含调用程序的cpp文件、配置和编译用的CMakeLists.txt文件。
CMakeLists.txt文件示例内容如下:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(custom_ops)
find_package(Torch REQUIRED)
add_executable(example-app example-app.cpp)
target_link_libraries(example-app "${TORCH_LIBRARIES}")
set_property(TARGET example-app PROPERTY CXX_STANDARD 14)

包含模型调用程序的example-app.cpp示例编码如下:

#include  // torch头文件.
#include #include 
 
 
int main(int argc, const char* argv[]) {
  if (argc != 2) {
    std::cerr << "usage: example-app \n";
    return -1;
  }
 
 
  torch::jit::script::Module module;
  try {
    // 反序列化:导入TorchScript模型
    module = torch::jit::load(argv[1]);
  }
 
 
  catch (const c10::Error& e) {
    std::cerr << "error loading the model\n";
    return -1;
  }
  std::cout << "ok\n";
  }

两个文件编写完成之后便可对其执行编译:

mkdir example_test
cd example_test
cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
cmake --example_test . --config Release

第四步:

给example-app.cpp添加模型推理代码并执行:

std::vector<torch::jit::IValue> inputs;
inputs.push_back(torch::ones({1, 3, 224, 224}));
// 执行推理并将模型转化为Tensor
output = module.forward(inputs).toTensor();
std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';

以上便是C++中部署PyTorch模型的全过程。

4.4 本人的实际使用过程

第一步:将模型导出成torchscript

不管是 LibTorch 用的 .pt 格式模型,还是 OpenCV DNN 和 ONNX Runtime 用的 .onnx 模型,都是需要在 Python 中将 PyTorch 模型导出得到的 (不能直接用 .pth 模型)。在模型导出前必须执行 torch_model.eval() 或者 torch_model.train(False) 将模型转为推理模式,因为像 dropout 或 batchnorm 之类的操作在推理模式和训练模式下的行为是不同的。

python代码(CPU版本)

import torch
from torch import nn

from model.model import MattingDGF


class MattingDGF_TorchScriptWrapper(nn.Module):
    """
    The purpose of this wrapper is to hoist all the configurable attributes to the top level.
    So that the user can easily change them after loading the saved TorchScript model.
    这个包装器的目的是将所有可配置的属性提升到顶层。
    这样用户可以在加载保存的 TorchScript 模型后轻松更改它们。

    Example:
        model = torch.jit.load('torchscript.pth')
        model.backbone_scale = 0.25
        model.refine_mode = 'sampling'
        model.refine_sample_pixels = 80_000
        pha, fgr = model(src, bgr)[:2]
    """

    def __init__(self, *args, **kwargs):
        super().__init__()
        self.model = MattingDGF(*args, **kwargs)
        # Hoist the attributes to the top level.
        self.downsample_ratio = self.model.downsample_ratio

    def forward(self, src):
        # Reset the attributes.
        self.model.downsample_ratio = self.downsample_ratio
        return self.model(src)

    def load_state_dict(self, *args, **kwargs):
        return self.model.load_state_dict(*args, **kwargs)


if __name__ == "__main__":
    model_backbone = 'mobilenetv2'
    model_checkpoint = '../TrainedModel-V3/GSK-V3-3.pth'
    precision = 'float32'
    output = '../TrainedModel-V3-TorchScript/GSK-V3-3-torchscript-cpu-float32.pth'

    model = MattingDGF_TorchScriptWrapper(model_backbone).eval()
    # 在cpu上进行推理的意思吗?
    model.load_state_dict(torch.load(model_checkpoint, map_location='cpu'))
    for p in model.parameters():
        p.requires_grad = False

    if precision == 'float16':
        model = model.half()

    model = torch.jit.script(model)
    model.save(output)

python代码(GPU版本)

import torch
from torch import nn

from model.model import MattingDGF


class MattingDGF_TorchScriptWrapper(nn.Module):
    """
    The purpose of this wrapper is to hoist all the configurable attributes to the top level.
    So that the user can easily change them after loading the saved TorchScript model.
    这个包装器的目的是将所有可配置的属性提升到顶层。
    这样用户可以在加载保存的 TorchScript 模型后轻松更改它们。

    Example:
        model = torch.jit.load('torchscript.pth')
        model.backbone_scale = 0.25
        model.refine_mode = 'sampling'
        model.refine_sample_pixels = 80_000
        pha, fgr = model(src, bgr)[:2]
    """

    def __init__(self, *args, **kwargs):
        super().__init__()
        self.model = MattingDGF(*args, **kwargs)
        # Hoist the attributes to the top level.
        self.downsample_ratio = self.model.downsample_ratio

    def forward(self, src):
        # Reset the attributes.
        self.model.downsample_ratio = self.downsample_ratio
        return self.model(src)

    def load_state_dict(self, *args, **kwargs):
        return self.model.load_state_dict(*args, **kwargs)


if __name__ == "__main__":
    model_backbone = 'mobilenetv2'
    model_checkpoint = '../TrainedModel-V3/GSK-V3-3.pth'
    precision = 'float32'
    output = '../TrainedModel-V3-TorchScript/GSK-V3-3-torchscript-gpu-float32.pth'
    device = torch.device('cuda')

    model = MattingDGF_TorchScriptWrapper(model_backbone).to(device).eval()
    # 在cpu上进行推理的意思吗?
    # gpu
    model.load_state_dict(torch.load(model_checkpoint, map_location=device))
    for p in model.parameters():
        p.requires_grad = False

    if precision == 'float16':
        model = model.half()

    model = torch.jit.script(model)
    model.save(output)

用python测试是否导出正确

import torch
from PIL import Image
from torchvision import transforms as T
import cv2

# 加载torchscript模型
model = torch.jit.load('../TrainedModel-V3-TorchScript/GSK-V3-3-torchscript-cpu-float32.pth')

# 读入输入数据
images_src = 'E:/green_screen_keying/test_set/test2.png'
with Image.open(images_src) as img:
    img = img.convert('RGB')
transforms = T.Compose([T.Resize((1080, 1920)), T.ToTensor()])
device = 'cpu'  # 要和转换时的保持一致
# src = img.to(device, non_blocking=True)
src = transforms(img)
src = torch.unsqueeze(src, 3).permute(3, 0, 1, 2)  # [B,C,H,W]

pred_pha_hr, pred_fgr_hr, pred_pha_lr, pred_fgr_lr, pred_err_lr = model(src)

# 转换回numpy格式
# 返回com
tgt_bgr = torch.tensor([1.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)
com = pred_fgr_hr * pred_pha_hr + tgt_bgr * (1 - pred_pha_hr)
com = com.cpu().permute(2, 3, 1, 0).squeeze(3).numpy()
com = cv2.cvtColor(com, cv2.COLOR_BGR2RGB)
com = cv2.resize(com, (400, 400)) # * 255

# 返回pha
pha = pred_pha_hr.cpu().permute(2, 3, 1, 0).squeeze(3).numpy()
pha = cv2.resize(pha, (400, 400)) # * 255

cv2.imshow("img-show", pha)
cv2.waitKey(0)
cv2.destroyAllWindows()

第二步:下载libtorch并在VS中配置libtorch环境

需要对应torch的版本下载libtorch,见链接[深度学习][libtorch]​Windows上Libtorch下载地址

VS中配置环境见链接:Windows下使用C++调用pytorch模型教程(VS工程)

配置gpu使用
libtorch的GPU使用
在GPU上运行模型需注意,image数据和模型必须都在GPU上,不能在CPU上,容易报错。

C++测试代码如下所示:

#include  // One-stop header.

#include 
#include 

int main() {
    torch::jit::script::Module module;
    try {
        // Deserialize the ScriptModule from a file using torch::jit::load().
        module = torch::jit::load("E:/green_screen_keying/deep-learning-V3-main/deep-learning-V3-main/TrainedModel-V3-TorchScript/GSK-V3-3-torchscript-cpu-float32.pth");
    }
    catch (const c10::Error& e) {
        std::cerr << "error loading the model\n";
        return -1;
    }

    std::cout << "ok\n";
}

输出ok即可。

第三步:编写代码并写输入输出

其中需要注意的是导出RGB图片时可能和内存有关系,得分别导出每个通道再合并才可以。
C++代码:
cpu版本

#include  // One-stop header.
#include 
#include 
#include 
#include 
using namespace cv;
using namespace std;

int main() {
    //加载模型
    torch::jit::script::Module model;
    try {
        // Deserialize the ScriptModule from a file using torch::jit::load().
        model = torch::jit::load("E:/green_screen_keying/deep-learning-V3-main/deep-learning-V3-main/TrainedModel-V3-TorchScript/GSK-V3-3-torchscript-cpu-float32.pth");
    }
    catch (const c10::Error& e) {
        cerr << "error loading the model\n";
        return -1;
    }

    cout << "model load ok\n";

    //导入一张图像并将其转换为tensor
    Mat img = imread("E:/green_screen_keying/test_set/test2.png");
    cvtColor(img, img, CV_BGR2RGB); // convert to RGB
    img.convertTo(img, CV_32FC3, 1.0f / 255.0f); //normalization
    int img_h = img.rows;
    int img_w = img.cols;
    int depth = img.channels();
    auto input_tensor = torch::from_blob(img.data, { 1, img_h, img_w, depth }); //opencv format H*W*C
    input_tensor = input_tensor.permute({ 0, 3, 1, 2 }); //pytorch format N*C*H*W

    //模型处理
    vector<torch::jit::IValue> inputs;
    inputs.push_back(input_tensor.to(torch::kCPU));
    //at::Tensor output = model.forward(inputs).toTensor();
    //有多个返回值
    vector<at::Tensor> dataOutputAll = model.forward(inputs).toTensorVector();

    //导出结果,即tensor转opencv
    torch::Tensor pha_tensor = dataOutputAll[0];
    cout << pha_tensor.sizes() << endl;
    torch::Tensor fgr_tensor = dataOutputAll[1];
    cout << fgr_tensor.sizes() << endl;
    torch::Tensor pha_lr = dataOutputAll[2];
    torch::Tensor fgr_lr = dataOutputAll[3];
    torch::Tensor err_lr = dataOutputAll[4];
    //合成
    torch::Tensor tgt_bgr = torch::ones({ 1, 3,1,1 });
    torch::Tensor com_tensor = fgr_tensor * pha_tensor + tgt_bgr * (1 - pha_tensor);

    //s1:sequeeze去掉多余维度,(1,C,H,W)->(C,H,W);s2:permute执行通道顺序调整,(C,H,W)->(H,W,C)
    
    /*pha_tensor = pha_tensor.squeeze(0).detach().permute({1, 2, 0});
    pha_tensor = pha_tensor.mul(255).clamp(0, 255).to(torch::kU8); //s3:*255,转uint8
    pha_tensor = pha_tensor.to(torch::kCPU); //迁移至CPU
    Mat phaImg(img_h, img_w, CV_8UC1, pha_tensor.data_ptr()); // 将Tensor数据拷贝至Mat
    resize(phaImg, phaImg, Size(400, 400));
    */
    
    com_tensor = com_tensor.squeeze(0).detach().permute({ 1, 2, 0 });
    cout << "com_tensor:" << com_tensor.sizes() << endl;
    com_tensor = com_tensor.mul(255).clamp(0, 255).to(torch::kU8); //s3:*255,转uint8
    com_tensor = com_tensor.to(torch::kCPU); //迁移至CPU[190,265,3]
    torch::Tensor r_value =  com_tensor.index({ "...", 0 });//获取tensor的第i个通道的值
    Mat rImg(img_h, img_w, CV_8UC1, r_value.data_ptr()); // 将Tensor数据拷贝至Mat
    torch::Tensor g_value = com_tensor.index({ "...", 1 });//获取tensor的第i个通道的值
    Mat gImg(img_h, img_w, CV_8UC1, g_value.data_ptr()); // 将Tensor数据拷贝至Mat
    torch::Tensor b_value = com_tensor.index({ "...", 2 });//获取tensor的第i个通道的值
    Mat bImg(img_h, img_w, CV_8UC1, b_value.data_ptr()); // 将Tensor数据拷贝至Mat

    Mat comImg;
    //vectormv;
    Mat mv[3];
    mv[0] = bImg;
    mv[1] = gImg;
    mv[2] = rImg;
    merge(mv,3, comImg);
    
    //imshow("pha", phaImg);
    //waitKey(0);
    imshow("com", comImg);
    waitKey(0);
    img.release();
    //phaImg.release();
    rImg.release();
    gImg.release();
    bImg.release();
    comImg.release();

    return 0;
}

gpu版本

#include  // One-stop header.
#include 
#include 
#include 
#include 
#include //cuda相关函数头文件

using namespace cv;
using namespace std;

int main() {
    //加载模型
    auto device = torch::kCUDA;
    torch::jit::script::Module model;
    if (torch::cuda::is_available())
    {
        cout << "use cuda...\n";
        //device_ = torch::kCUDA;
    }
    else {
        cout << "cuda wrong!" << endl;
    }
    try {
        // Deserialize the ScriptModule from a file using torch::jit::load().
        model = torch::jit::load("E:/green_screen_keying/deep-learning-V3-main/deep-learning-V3-main/TrainedModel-V3-TorchScript/GSK-V3-3-torchscript-gpu-float32.pth");
    }
    catch (const c10::Error& e) {
        cerr << "error loading the model\n";
        return -1;
    }

    cout << "model load ok\n";
    model.to(at::kCUDA); // 模型加载至GPU
    //导入一张图像并将其转换为tensor
    //读入视频
    /*
    VideoCapture capture;
    capture.open("E:/green_screen_keying/test_video_13/test_videos/chizi.mp4");
    if (!capture.isOpened())
    {
        printf("can not open ...\n");
        return -1;
    }
    */
    Mat img = imread("E:/green_screen_keying/test_set/test2.png");
    //Mat img;
    //while (capture.read(img)) {
        //resize(img, img, Size(400, 400));
        cvtColor(img, img, CV_BGR2RGB); // convert to RGB
        img.convertTo(img, CV_32FC3, 1.0f / 255.0f); //normalization
        int img_h = img.rows;
        int img_w = img.cols;
        int depth = img.channels();
        auto input_tensor = torch::from_blob(img.data, { 1, img_h, img_w, depth }); //opencv format H*W*C
        input_tensor = input_tensor.permute({ 0, 3, 1, 2 }); //pytorch format N*C*H*W

        //模型处理
        vector<torch::jit::IValue> inputs;
        //torch::kCPU
        inputs.push_back(input_tensor.to(at::kCUDA));
        //at::Tensor output = model.forward(inputs).toTensor();
        //有多个返回值
        vector<at::Tensor> dataOutputAll = model.forward(inputs).toTensorVector();

        //导出结果,即tensor转opencv
        torch::Tensor pha_tensor = dataOutputAll[0];
        cout << pha_tensor.sizes() << endl;
        torch::Tensor fgr_tensor = dataOutputAll[1];
        cout << fgr_tensor.sizes() << endl;
        torch::Tensor pha_lr = dataOutputAll[2];
        torch::Tensor fgr_lr = dataOutputAll[3];
        torch::Tensor err_lr = dataOutputAll[4];
        //合成
        torch::Tensor tgt_bgr = torch::ones({ 1, 3,1,1 }).to(at::kCUDA);
        torch::Tensor com_tensor = fgr_tensor * pha_tensor + tgt_bgr * (1 - pha_tensor);

        //s1:sequeeze去掉多余维度,(1,C,H,W)->(C,H,W);s2:permute执行通道顺序调整,(C,H,W)->(H,W,C)

        pha_tensor = pha_tensor.squeeze(0).detach().permute({1, 2, 0});
        pha_tensor = pha_tensor.mul(255).clamp(0, 255).to(torch::kU8); //s3:*255,转uint8
        pha_tensor = pha_tensor.to(torch::kCPU); //迁移至CPU
        Mat phaImg(img_h, img_w, CV_8UC1, pha_tensor.data_ptr()); // 将Tensor数据拷贝至Mat
        //resize(phaImg, phaImg, Size(400, 400));
        

        com_tensor = com_tensor.squeeze(0).detach().permute({ 1, 2, 0 });
        cout << "com_tensor:" << com_tensor.sizes() << endl;
        com_tensor = com_tensor.mul(255).clamp(0, 255).to(torch::kU8); //s3:*255,转uint8
        com_tensor = com_tensor.to(torch::kCPU); //迁移至CPU[190,265,3]
        torch::Tensor r_value = com_tensor.index({ "...", 0 });//获取tensor的第i个通道的值
        Mat rImg(img_h, img_w, CV_8UC1, r_value.data_ptr()); // 将Tensor数据拷贝至Mat
        torch::Tensor g_value = com_tensor.index({ "...", 1 });//获取tensor的第i个通道的值
        Mat gImg(img_h, img_w, CV_8UC1, g_value.data_ptr()); // 将Tensor数据拷贝至Mat
        torch::Tensor b_value = com_tensor.index({ "...", 2 });//获取tensor的第i个通道的值
        Mat bImg(img_h, img_w, CV_8UC1, b_value.data_ptr()); // 将Tensor数据拷贝至Mat

        Mat comImg;
        //vectormv;
        Mat mv[3];
        mv[0] = bImg;
        mv[1] = gImg;
        mv[2] = rImg;
        merge(mv, 3, comImg);

        //imshow("pha", phaImg);
        //waitKey(0);
        imshow("com", comImg);
        waitKey(0);
        img.release();
        phaImg.release();
        rImg.release();
        gImg.release();
        bImg.release();
        comImg.release();

    //}

    return 0;
}

第四步:Bug

在融合到已有C++程序时遇到了Bug:
Bug1:
at::Scalar不明确、at::Allocator不明确、torch::Scalar不明确,

  • 这些报错一般出现在和OpenCV库联用的情况
  • 因为使用 opencv 的Scalar类型,导致和Libtorch命名空间的Scalar冲突。可以根据提示在对应的文件位置添加命名空间说明
  • at::Allocator不明确
    在 \libtorch/include/ATen/detail/CUDAHooksInterface.h 第28行附近,增加
    namespace at{
    using c10::Allocator; // 添加命名空间
  • at::Scalar不明确
    在 \libtorch/include/ATen/core/TensorBody.h 第35行附近,增加
    namespace at{
    using c10::Scalar; //添加命名空间
  • torch::Scalar不明确
    在 \libtorch\include\torch\csrc\api\include\torch\linalg.h 第6行左右位置,增加
#pragma once

#include 

namespace torch {
using torch::Scalar;  //添加命名空间
namespace linalg {

在 \libtorch\include\torch\csrc\api\include\torch\nn\init.h 第8行左右位置,增加

#pragma once

#include 
#include 
#include 

namespace torch {
    using torch::Scalar;  //添加命名空间
namespace nn {
namespace init {

解决方案参考:YoloV5-LibTorch:C++中使用yolov5
Bug2:
报错:libotrch的某些文件缺少;,报错
error C2059: 语法错误:“” 等
在object.h,ivalue_inl.h中注释掉错误的地方
在C++中部署python深度学习-学习笔记_第6张图片
在C++中部署python深度学习-学习笔记_第7张图片

五、基于ONNX的PyTorch模型部署

5.1 ONNX和ONNXRuntime简介

ONNX
Open Neural Network Exchange(ONNX,开放神经网络交换)格式,是一个用于表示深度学习模型的标准,可使模型在不同框架之间进行转移。如pytorch模型转换为caffe模型,python模型c++调用等等。

在实际业务中,可以使用Pytorch或者TensorFlow训练模型,导出成ONNX格式,然后在转换成目标设备上支撑的模型格式,比如TensorRT Engine、NCNN、MNN等格式。ONNX定义了一组和环境,平台均无关的标准格式,来增强各种AI模型的可交互性,开放性较强。

ONNXRuntime
ONNXRuntime是微软推出的一款推理框架,用户可以非常便利的用其运行一个onnx模型。 ONNXRuntime支持多种运行后端包括CPU,GPU,TensorRT,DML等。

ONNX Runtime Inferencing:高性能推理引擎

  1. 可在不同的操作系统上运行,包括Windows、Linux、Mac、Android、iOS等;
  2. 可利用硬件增加性能,包括CUDA、TensorRT、DirectML、OpenVINO等;
  3. 支持PyTorch、TensorFlow等深度学习框架的模型,需先调用相应接口转换为ONNX模型;
  4. 在Python中训练,确可部署到C++/Java等应用程序中。

下面是使用ONNXRuntime的一个简单例子:

总体来看,整个ONNXRuntime的运行可以分为三个阶段,Session构造,模型加载与初始化和运行。和其他所有主流框架相同,ONNXRuntime最常用的语言是python,而实际负责执行框架运行的则是C++。

import numpy as np
import onnx
import onnxruntime as ort

image = cv2.imread("image.jpg")
image = np.expand_dims(image, axis=0)

onnx_model = onnx.load_model("resnet18.onnx")
sess = ort.InferenceSession(onnx_model.SerializeToString())
sess.set_providers(['CPUExecutionProvider'])
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name

output = sess.run([output_name], {input_name : image_data})
prob = np.squeeze(output[0])
print("predicting label:", np.argmax(prob))

5.2 实现步骤

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

  • python环境中,将pytorch模型推理过程记录为onnx模型计算图,保存为后缀.onnx文件。
  • 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 
#include 
#include 
#include 

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
        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
        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> inputs 创建输入tensor (假设输入为1*1*224*224)
	// 第二个参数代表输入数据 float*
    // 第三个参数代表输入节点的总尺寸 int64_t (1*1*224*224)
    // 第四个参数代表输入节点的尺寸数据 std::vector (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>();
}

5.3 和libtorch的区别

实际上使用Libtorch库来实现会更准确也更简单,在实验中,使用Libtorch库推算,C++代码和python代码的误差为0,但是ONNX库推算会带来1e-5左右的误差,所以ONNX库推算不太适合需要特别精确数值的任务)。

参考链接

【深度学习】基于web端和C++的两种深度学习模型部署方式
pytorch怎么使用c++调用部署模型?
C++环境下部署深度学习模型方案
使用onnx c++部署pytorch神经网络模型全流程
【ONNX】导出,载入PyTorch的ONNX模型并进行预测新手教程(Windows+Python+Pycharm+PyTorch+ONNX)
Pytorch模型C++部署

你可能感兴趣的:(深度学习,python,c++)