C++利用LibTorch调用pytorch 模型

由于python的易用性,深度学习模型多是在python框架下进行训练的,如TensorFlow,pytorch等。而由于硬件设备的限制,有时候其部署可能需要基于C++/C的平台。比如在我们的项目中,语义分割网络是在pytorch下训练的,而分割结果基于应用的后处理部分是在C下面实现的,那怎么才能把这两种平台下的东西结合起来一起运行呢?我想到的方法有以下几种:

  • 将pytorch模型训练好后转成caffe的模型,然后利用caffe的接口在C++下面实现模型的推理应用;

  • 将C部分的代码打包编译成一个动态连接库dll,然后在python框架下调用该dll实现c下面的功能;

  • 利用pytorch的C++版本LibTorch实现pytorch模型的调用。
    本文主要记录最后一种方法。

LibTorch 的下载及使用

LibTorch 是pytorch的C++版本,在pytorch版本1.0后就有了。在官网通过如下选择,就可以得到下载链接。


image-20200304141339075.png

下载链接里有release版本和debug版本,建议两个版本都下载,两者主要是对应的dll和lib不一样,debug版本还提供了pdb,可以帮助定位错误位置。将release版本解压后,得到一个LibTorch的文件夹,再将debug版本解压,将其中的lib文件夹改名为lib_debug,同样放在之前release版本解压的LibTorch文件夹,这样就release和debug版本都可以使用了。

下载的LibTorch中提供了cmakelist,在linux平台可以利用cmake来使用它。而如果在Windows平台利用vs,只需要和一般的第三方库使用一样,在对应的工程中添加正确的AdditionalIncludeDirectories,AdditionalLibraryDirectories,AdditionalDependencies等就可以了。我在实验时,将lib里边所有的lib文件都加入到AdditionalDependencies了。程序运行的时候还需要把对应的dll拷贝到exe所在的文件夹。我使用debug时遇到了一个编译错误,添加preprocessor _SCL_SECURE_NO_WARNINGS就好了。

使用流程

利用LibTorch来调用pytorch模型的流程大致是这样的:

  1. pytorch训练好模型
  2. 将模型序列化并存成pt文件
  3. 在C中利用LibTorch的接口进行正向推演

pytorch模型序列化

第一步我们就不介绍了,我们从第二步开始。模型的序列化是利用Torch Script来完成的。TorchScript是一种从PyTorch代码创建可序列化和可优化模型的方法。用TorchScript编写的任何代码都可以从Python进程中保存并加载到没有Python依赖关系的进程中。对于一个已经训练好的pytorch模型,官方提供两种方法进行Torch Script的转换:tracing和annotation。

Tracing

Tracing的方法还是很简单的,参见如下示例代码:

import torch
import torchvision

# An instance of your model.
model = torchvision.models.resnet18()

# An example input you would normally provide to your model's forward() method.
example = torch.rand(1, 3, 224, 224)

# Use torch.jit.trace to generate a torch.jit.ScriptModule via tracing.
traced_script_module = torch.jit.trace(model, example)

Annotation

tracing适用于大多数网络,如果你的网络的forward方法中对input有逻辑判断,比如input的size为一个值时走向一个分支,而为另一值时走向另一个分支,那么只能用annotation进行转换。比如如下的网络:

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

利用annotation来将上述网络模型转成Torch Script可以按如下代码:

my_module = MyModule(10,20)
sm = torch.jit.script(my_module)

annotation的方法我并没有测试,我使用的模型用tracing就已经足够了。

序列化

序列化的意思是指将上述Torch Script描述的模型存成一个文件。

traced_script_module.save("traced_resnet_model.pt")

C++中的正向推演

#include <torch/script.h> // One-stop header.

#include <iostream>
#include <memory>

int main(int argc, const char* argv[]) {
  if (argc != 2) {
    std::cerr << "usage: example-app <path-to-exported-script-module>\n";
    return -1;
  }


  torch::jit::script::Module module;
  try {
    // Deserialize the ScriptModule from a file using torch::jit::load().
    module = torch::jit::load(argv[1]);
  }
  catch (const c10::Error& e) {
    std::cerr << "error loading the model\n";
    return -1;
  }
    
  // Simple tests of the model
  std::vector<torch::jit::IValue> inputs;
  inputs.push_back(torch::ones({1, 3, 224, 224}));

  // Execute the model and turn its output into a tensor.
  at::Tensor output = module.forward(inputs).toTensor();

  std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';

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

自己训练的模型的实际操作

下面以我们训练的语义分割网络为例,介绍如何将自己的模型在C++中跑起来。

在实际的操作中,也是遇到了一些问题的。

GPU及DataParallel的问题

第一个问题是我们之前的模型训练是在GPU(相信应该都是这样的)中进行的,并且使用了DataParallel,在序列化时,如下代码是正确的,可以与示例代码做下比较。

device = torch.device('cuda')
model = get_model(args_in)
model = torch.nn.DataParallel(model, device_ids=[0])
model.load_state_dict(torch.load(args_in.test_model_path))
model.to(device)
# use evaluation mode to ignore dropout, etc
model.eval()

# The tracing input need not to be the same size as the forward case.
example = torch.rand(1, 3, 1080, 1920).to(device)

# Use torch.jit.trace to generate a torch.jit.ScriptModule via tracing.
traced_script_module = torch.jit.trace(model.module, example)

traced_script_module.save("traced_model.pt")

对于GPU训练的模型,需要将模型和tracing用的tensor通过to(device)或者.cuda()转到GPU上,如第5,10行。对于利用DataParallel训练的模型,需要在trace时使用model.module,如第13行。

关于DataParallel多说一句,如果希望正向的时候不需要像第3行那样将model再包一层,在训练save model的时候应该按如下

torch.save(model.module.state_dict(), save_path)

这样存的model就不需要第3行代码,而且第13行的.module也不需要了。

附上因为DataParallel没弄对在pycharm中遇到的错误

RuntimeError: hasSpecialCase INTERNAL ASSERT FAILED at ..\torch\csrc\jit\passes\alias_analysis.cpp:300, please report a bug to PyTorch. We don't have an op for aten::to but it isn't a special case. (analyzeImpl at ..\torch\csrc\jit\passes\alias_analysis.cpp:300)

网络输出是Tuple的问题

我们的网络输出是一个tuple而不是一个tensor,于是在C++调用的时候总是crash,用了debug版本的LibTorch,才发现问题。官方提到LibTorch这种方式需要网络的输出是一个tuple或者tensor,那如果输出的是tuple,在C++端代码应该按如下修改

torch::Tensor result = module.forward(input).toTuple()->elements()[0].toTensor();

图像的前处理

在pytorch模型的训练过程中,我们一般会对图像进行一些前处理,比如

transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
            ])

在LibTorch中,可以这样做

  tensor_image = tensor_image.toType(torch::kFloat);
  tensor_image = tensor_image.div(255);
  // Normalization
  tensor_image[0][0] = tensor_image[0][0].sub_(0.485).div_(0.229);
  tensor_image[0][1] = tensor_image[0][1].sub_(0.456).div_(0.224);
  tensor_image[0][2] = tensor_image[0][2].sub_(0.406).div_(0.225);

最后贴上我们利用opencv读视频,然后对每一帧运行语义分割正向的代码。

  // module for forward process
  torch::jit::script::Module module;
  try {
    // Deserialize the ScriptModule from a file using torch::jit::load().
    module = torch::jit::load("traced_model.pt");
  } catch (const c10::Error &e) {
    std::cerr << "error loading the model\n";
  }
  torch::DeviceType device = torch::kCUDA;
  module.to(device);

  // opencv windows
  cv::namedWindow("Test", 0);
  cvMoveWindow("Test", 0, 0);
  
  cv::VideoCapture  t_video_in(videoPath);
  long nbFrames = static_cast<long>(t_video_in.get(CV_CAP_PROP_FRAME_COUNT));

  for (long f = 0; f < nbFrames; f++) {
    cv::Mat image, input;
    t_video_in >> image;
    cv::cvtColor(image, input, CV_BGR2RGB);

    // run semantic segmentation to get label image
    torch::Tensor tensor_image = torch::from_blob(input.data, { 1, input.rows, input.cols, 3 }, torch::kByte);
    tensor_image = tensor_image.permute({ 0, 3, 1, 2 });
    tensor_image = tensor_image.toType(torch::kFloat);
    tensor_image = tensor_image.div(255);
    // Normalization
    tensor_image[0][0] = tensor_image[0][0].sub_(0.485).div_(0.229);
    tensor_image[0][1] = tensor_image[0][1].sub_(0.456).div_(0.224);
    tensor_image[0][2] = tensor_image[0][2].sub_(0.406).div_(0.225);

    tensor_image = tensor_image.to(torch::kCUDA);
    torch::Tensor result = module.forward({ tensor_image }).toTuple()->elements()[0].toTensor();
    torch::Tensor pred = result.argmax(1);
    pred = pred.squeeze();
    pred = pred.to(torch::kU8);
    pred = pred.to(torch::kCPU);

    cv::Mat label(cv::Size(image.cols,image.rows), CV_8U, pred.data_ptr());
    cv::imshow("Test", label);

    cv::waitKey(1);
  }
  t_video_in.release();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容