友情提示:
- 阅读本文需要您已经掌握Pytorch的Python用法,并掌握C++语言。
- 推荐使用Ubuntu/Mac系统实验(cmake可以自动找到已安装的opencv)。
- 本实验需要已安装好opencv和pytorch 1.0,C++编译环境(Ubuntu需要g++,Mac需要XCode)和cmake。
Pytorch 1.0已经于近日推出,其中一个亮点功能是支持将python训练的模型导出到C++进行推理。相比于目前流行的caffe训练模型+opencv dnn模块推理,pytorch从Python训练到C++部署提供了一体化的方案,可谓攻城狮的福音。
Python导出模型
根据Pytorch官网教程,我们先导出残差网络Resnet18的模型和预训练权重:
# coding=utf-8
import torch
import torchvision
from torchvision import transforms
from PIL import Image
import json
import cv2
# 初始化模型
model = torchvision.models.resnet18(pretrained=True)
model.eval() #将模型置为推理状态
# 随机生成一个输入张量
example = torch.rand(1, 3, 224, 224)
# 利用跟踪数据流的方法生成导出模型
traced_script_module = torch.jit.trace(model, example)
output = traced_script_module(torch.ones(1, 3, 224, 224))
print output.shape
print output[0, :5]
traced_script_module.save("model.pt")
这样在当前目录下会生成一个model.pt
文件,包含了模型定义和权重。
Python测试模型
- 首先准备测试数据。在当前目录创建data目录,里面放入网上随便找的猫的图像,命名为
cat.jpg
。 - 下载1000类标签文件https://github.com/raghakot/keras-vis/blob/master/resources/imagenet_class_index.json,放在当前目录。
- 创建并执行以下Python代码:
import torch
import torchvision
from torchvision import transforms
import numpy as np
from PIL import Image
import json
import cv2
# An instance of your model.
model = torchvision.models.resnet18(pretrained=True)
######
## test with an image
with open('imagenet_class_index.json') as f:
labels = json.load(f)
#print labels
img = cv2.imread('data/cat.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
#cv2.imshow('img', img)
#cv2.waitKey(0)
img = cv2.resize(img, (224, 224), img)
t = torch.from_numpy(img).float() / 255.0
t[:, :, 0] = (t[:, :, 0] - 0.485) / 0.229
t[:, :, 1] = (t[:, :, 1] - 0.456) / 0.224
t[:, :, 2] = (t[:, :, 2] - 0.406) / 0.225
t = t.unsqueeze(0)
t = t.permute(0, 3, 1, 2)
model.eval()
output = model.forward(t)
output = output[0].detach().numpy()
pred = output.argmax()
pred_label = labels[str(pred)]
print pred, pred_label
将输出:
285 [u'n02124075', u'Egyptian_cat']
表明输入图像的类别是285号“埃及猫”。
C++调用导出模型
- 创建以下test_resnet.cpp源码
#include <torch/script.h> // One-stop header.
#include <iostream>
#include <memory>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, const char* argv[]) {
if (argc != 2) {
std::cerr << "usage: example-app <path-to-exported-script-module>\n";
return -1;
}
// 加载导出的模型和权重
std::shared_ptr<torch::jit::script::Module> module = torch::jit::load(argv[1]);
assert(module != nullptr);
std::cout << "Model loaded!\n";
// 利用OpenCV读取图像
Mat image_bgr, tmp_image, image;
image_bgr = imread("../data/cat.jpg");
// 将OpenCV默认的BGR通道顺序转换为RGB通道顺序
cvtColor(image_bgr, tmp_image, COLOR_BGR2RGB);
// 缩放为模型可以接受的 224x224
resize(tmp_image, tmp_image, cv::Size(224, 224), 0, 0, CV_INTER_LINEAR);
// 将像素值从[0, 255]缩放到 [0,1]区间
tmp_image.convertTo(image, CV_32F, 1.0 / 255, 0);
///////////// 关键:将图像转换为 Tensor 输入
// 定义张量,维度为 B x H x W x C
std::vector<int64_t> sizes = {1, image.rows, image.cols, 3};
at::TensorOptions options(at::kFloat);
// 从OpenCV的Mat将数据转换为 Tensor
at::Tensor tensor_image = torch::from_blob(image.data, at::IntList(sizes), options);
// 调整张量的通道顺序为 BxCXHXW
tensor_image = tensor_image.permute({0, 3, 1, 2});
// 将张量放入vector中准备输入 (因此模型可以接受多个输入)
std::vector<torch::jit::IValue> inputs;
inputs.emplace_back(tensor_image);
// 推理
at::Tensor result = module->forward(inputs).toTensor();
// 将输出张量转换为vector
auto my_tensor = result;
std::vector<float> vec(my_tensor.data<float>(), my_tensor.data<float>() + my_tensor.numel());
// 找到vector里数值最大的对应的下标,这就是分类的类别编号
int pred = -1;
double maxValue = -9999;
for(int i = 0; i < vec.size(); ++i) {
double v = vec[i];
if (v > maxValue) {
maxValue = v;
pred = i;
//cout << " i , maxValue = " << pred << ", " << maxValue << endl;
}
}
cout << "pred = " << pred << endl;
}
这段程序里比较难找的是将OpenCV的Mat转换为Tensor的那一段。官网教程里缺失了这样的例子。
- 创建CMakeList.txt准备编译:
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(custom_ops)
find_package(Torch REQUIRED)
find_package(OpenCV REQUIRED)
message(STATUS "OpenCV library status:")
message(STATUS " version: ${OpenCV_VERSION}")
message(STATUS " libraries: ${OpenCV_LIBS}")
message(STATUS " include path: ${OpenCV_INCLUDE_DIRS}")
add_executable(test_resnet test_resnet.cpp)
target_link_libraries(test_resnet "${TORCH_LIBRARIES}" "${OpenCV_LIBS}")
set_property(TARGET test_resnet PROPERTY CXX_STANDARD 11)
- 利用cmake编译
mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/Library/Python/2.7/site-packages/torch ..
make
注意 cmake后面的参数要替换为本机torch lib的安装路径。
- 执行编译成功的程序
./test_resnet ../model.pt
将输出
Model loaded! pred = 285
此分类结果与Python测试的结果一致。
参考
[1] https://pytorch.org/tutorials/advanced/cpp_export.html
[2] https://pytorch.org/cppdocs/api/classat_1_1_tensor.html#exhale-class-classat-1-1-tensor
[3] https://github.com/pytorch/pytorch/issues/14330