本教程将完整的记录使用pytorch从模型训练到模型调用(基于Python),再通过libtorch转成C++调用(基于win32 C++控制台程序),最终集成到MFC程序中来,这样就可以完整的在Windows下走通 AI 算法建模到生产级部署的全部流程。
基本配置环境如下:
Python版本:Python 3.6.1
Pytorch版本:1.2.0
Libtorch:1.3
操作系统:Win10
编译器:VS 2015
1.算法—MNIST手写体数字识别
MNIST是非常有名的手写体数字识别数据集,非常适合用来实战讲解,因此我们也以此作为项目的开始,通过Pytorch建立算法来识别图像中的数字。MNIST由手写体数字的图片和相对应的标签组成,如下图所示:
Pytorch的torchvision库中已经自带了该数据集,可以直接下载和导入。
下面给出完整的训练代码train.py:
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import PIL
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
self.conv2_drop = nn.Dropout2d()
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
x=self.conv1(x)
x = F.relu(F.max_pool2d(x, 2))
x=self.conv2(x)
x = F.relu(F.max_pool2d(self.conv2_drop(x), 2))
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
return F.log_softmax(x, dim=1)
def train(args, model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % args.log_interval == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
def test(args, model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
def main():
# 用于训练的超参数设置
parser = argparse.ArgumentParser(description='PyTorch MNIST样例')
parser.add_argument('--batch-size', type=int, default=64,
help='input batch size for training (default: 64)')
parser.add_argument('--test-batch-size', type=int, default=1000,
help='input batch size for testing (default: 1000)')
parser.add_argument('--epochs', type=int, default=10,
help='number of epochs to train (default: 10)')
parser.add_argument('--lr', type=float, default=0.01,
help='learning rate (default: 0.01)')
parser.add_argument('--momentum', type=float, default=0.5,
help='SGD momentum (default: 0.5)')
parser.add_argument('--no-cuda', action='store_true', default=False,
help='disables CUDA training')
parser.add_argument('--seed', type=int, default=1,
help='random seed (default: 1)')
parser.add_argument('--log-interval', type=int, default=10,
help='how many batches to wait before logging training status')
args = parser.parse_args()
use_cuda = not args.no_cuda and torch.cuda.is_available() # 判断是否使用GPU训练
print(use_cuda)
torch.manual_seed(args.seed) # 固定住随机种子,使训练结果可复现
device = torch.device("cuda" if use_cuda else "cpu")
kwargs = {'num_workers': 8, 'pin_memory': True} if use_cuda else {}
# 加载训练数据
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=args.batch_size, shuffle=True, **kwargs)
#加载测试数据
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=args.test_batch_size, shuffle=True, **kwargs)
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
for epoch in range(1, args.epochs + 1):
train(args, model, device, train_loader, optimizer, epoch)
test(args, model, device, test_loader)
torch.save(model, "model.pth")# 保存模型参数
if __name__ == '__main__':
import time
start = time.time()
main()
end = time.time()
running_time = end-start
print('time cost : %.5f 秒' %running_time)
上述代码采用的模型架构比较简单,只用了2个卷积层和2个全连接层,在我的GTX 1080TI上跑了77秒,检测精度达到99%。CPU上跑了311秒,检测精度98%。训练完成后自动保存当前模型参数为model.pth文件。
接下来使用该模型参数文件进行单张图片预测,完整代码文件test.py:
import torch
import cv2
import torch.nn.functional as F
from train import Net
from torchvision import datasets, transforms
from PIL import Image
if __name__ == '__main__':
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torch.load('model.pth') # 加载模型
model = model.to(device)
model.eval() # 把模型转为test模式
img = cv2.imread("img_3.jpg",0) # 读取要预测的灰度图片
img = Image.fromarray(img)
trans = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
img = trans(img)
img = img.unsqueeze(0) # 图片扩展多一维,[batch_size,通道,长,宽],此时batch_size=1
img = img.to(device)
output = model(img)
pred = output.max(1, keepdim=True)[1]
pred = torch.squeeze(pred)
print('检测结果为:%d' % (pred.cpu().numpy()))
预测结果如下:
输入图片: 输出:2 预测正确。
到这里已经完成算法建模部分。接下来,需要将这个算法部署到生产级的环境中,也就是我们这里需要的C++环境。一般的生产环境不会采用Python(考虑速度和稳定性等因素),尤其是边缘计算等设备,因此需要在C++中实现模型的调用和推理。
2. C++调用—使用libtorch导出序列化模型
从Pytorch-1.0开始其最瞩目的功能就是生产的大力支持,推出了C++版本的生态端,包括C++前端和C++模型编译工具。对于我们来说,在想要部署深度学习应用的时候,只需要在Python端利用Pytorch进行训练,然后使用torch.jit导出我们训练好的模型,再利用C++端的Pytorch接口读取进行预测即可。由于这种方式是官方推荐和支持的,因此,相比于其它方式这种模式更加稳定可靠。官方已经替我们编译好Windows版本的libtorch,这下就节省了我们编译Pytorch的时间,可以直接拿来使用,只要稍微配置一下就可以在Windows上运行libtorch了。
2.1导出序列化模型(pth转pt)
首先我们需要将刚才的算法模型进行序列化导出,基本原理与测试单张图片类似,输入一个样例图片,然后利用训练好的模型对图片进行一次推理。稍微不同的就是,才推理的过程中使用了torch.jit函数来记录了整个推理的一个路径流,最后由torch.jit来保存这个路径流。完整代码如下:
import torch
import cv2
import torch.nn.functional as F
from train import Net
from torchvision import datasets, transforms
from PIL import Image
if __name__ == '__main__':
device = torch.device('cpu') #使用cpu进行推理
model = torch.load('model.pth') # 加载模型
model = model.to(device)
model.eval() # 把模型转为test模式
img = cv2.imread("img.jpg",0) # 读取要预测的灰度图片
img = Image.fromarray(img)
trans = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
img = trans(img)
img = img.unsqueeze(0) # 图片扩展多一维,[batch_size,通道,长,宽]
img = img.to(device)
traced_net = torch.jit.trace(model,img)
traced_net.save("model.pt")
print("模型序列化导出成功")
这里需要注意,由于我们后面安装的libtorch是cpu版本的,因此,在序列化过程中只使用cpu进行推理,否则序列化出来的模型后面C++没办法正确读取。
2.2安装opencv
在安装libtorch之前,首先需要安装OpenCV,因为在处理图像类操所时不像python版可以用PIL等额外的库,在C++下面使用OpenCV相对比较方便。下载网址:https://opencv.org/releases.html。在版本选择上推荐opencv4.1.1,因为opencv4.1之后,简化了dll和相关的lib文件,所有的依赖都集成在一个巨大的opencv_world.dll文件中,不需要在C++工程中导入n个lib和dll文件了,只需要1个即可。按照页面提示下载Windows版本即可。
下载完成后双击安装包进行解压,解压到某个固定目录。
下面打开VS2015(或者VS2017、VS2019都可以),新建一个win32控制台应用程序mnist。
在创建项目时去掉预编译头和安全开发生命周期SDL检查复选框:
最后单击“完成”实现项目创建。由于libtorch只能在64位windows上运行,因此我们需要修改项目为release x64,如下图所示:
后面所有的项目配置都按照Release x64来配置,至于调试版的Debug x64可以按照这个教程一样的配置即可。
首先生成一下项目,依次单击“生成”—“生成解决方案”,这样可以在项目下生成一个x64/release文件夹,该文件夹下生成了对应的可执行程序mnist.exe(尽管我们还一行代码没有编写)。
然后我们在前面下载的opencv中目录找到opencv\build\x64\vc14\bin文件夹,将bin文件夹中的opencv_videoio_ffmpeg411_64.dll和opencv_world411.dll(如果是debug版本则复制opencv_world411d.dll)文件到刚才项目生成的x64/release文件夹下面。
接下来在项目中配置Opencv。在vs2015菜单栏中依次选择“项目”——“mnist属性”,然后单击左侧“VC++目录”,添加相关路径,包含目录中添加:
E:\toolplace\opencv4.1.1\opencv\build\include
E:\toolplace\opencv4.1.1\opencv\build\include\opencv2
库目录中加入:
E:\toolplace\opencv4.1.1\opencv\build\x64\vc14\lib
如果是高版本VS,则可以导入vc15文件夹中的lib库。然后在“链接器”->“输入”->“附加依赖项”中添加:
opencv_world411.lib
下面在mnist.cpp中添加一段C++测试代码用于测试opencv的安装是否成功:
#include "stdafx.h"
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
int main()
{
// 读入一张图片
Mat img = imread("img.jpg");
// 创建一个名为 "图片"窗口
namedWindow("图片");
// 在窗口中显示图片
imshow("图片", img);
// 等待6000 ms后窗口自动关闭
waitKey(6000);
return 0;
}
重新生成项目,然后在x64/release目录下放置1张测试图片“img.jpg”,运行生成的软件查看图像显示是否正常。效果如下图所示:
2.3安装libtorch
首先去官网下载libtorch:https://pytorch.org/get-started/locally/。按下面的配置下载(下载release 版本):
下载完成后解压,目录结构如下:
其中,include文件夹包含了我们需要的libtorch头文件;lib文件夹中包含了libtorch的库文件(lib和dll)。与opencv类似,我们需要将头文件和lib文件引入到本项目中。
在项目“VC++目录”的“包含目录”中添加相关路径,包含目录中添加:
E:\toolplace\libtorch\include
库目录中加入:
E:\toolplace\libtorch\lib
然后在“链接器”->“输入”->“附加依赖项”中添加:
c10.lib
torch.lib
libprotobuf.lib
caffe2_detectron_ops.lib
然后保存配置即可。最后将libtorch包中的c10.dll、caffe2_detectron_ops.dll、caffe2_module_test_dynamic.dll、torch.dll、libiomp5md.dll、libiompstubs5md.dll文件拷贝到我们mnist项目的x64/Release文件夹下面。注意,我们所有第三方库都是使用的release版,如果是新手很可能需要在C++中调试程序,建议新手刚开始全部按照debug版本来配置(包括opencv和libtorch),等debug调通后再配置release版本进行产品发布。
2.4 C++中调用模型进行推理
将前面序列化导出的model.pt文件拷贝到mnist项目的x64/Release文件夹下面,这个文件就是我们最终项目进行推理需要的参数文件。完整的c++代码如下:
#include "stdafx.h"
#undef UNICODE //这个一定要加,否则会编译错误
#include <torch/script.h> // 引入libtorch头文件
#include <iostream>
#include <memory>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
int main()
{
Mat image;
image = imread("img.jpg", IMREAD_GRAYSCALE);//读取灰度图
torch::jit::script::Module module;
try {
module = torch::jit::load("model.pt"); //加载模型
}
catch (const c10::Error& e) {
std::cerr << "无法加载model.pt模型\n";
return -1;
}
std::vector<int64_t> sizes = { 1, 1, image.rows, image.cols };
at::TensorOptions options(at::ScalarType::Byte);
at::Tensor tensor_image = torch::from_blob(image.data, at::IntList(sizes), options);//将opencv的图像数据转为Tensor张量数据
tensor_image = tensor_image.toType(at::kFloat);//转为浮点型张量数据
at::Tensor result = module.forward({ tensor_image }).toTensor();//推理
auto max_result = result.max(1, true);
auto max_index = std::get<1>(max_result).item<float>();
std::cerr << "检测结果为:";
std::cout << max_index << std::endl;
waitKey(6000);
}
重新生成解决方案,然后在x64/Release文件夹下放置测试图片img.jpg,双击运行mnist.exe,程序输出结果如下:
至此,已顺利完成C++的模型调用。
尽管本教程针对手写体数字识别,但是在C++调用的代码中,已经实现好了相关的接口,这样针对其它图像类应用,例如:图像检测、图像分类、图像识别、语义分割皆可以参照本教程实现。
2.5 MFC中集成调用
本教程最终希望实现MFC下调用pytorch模型,即给程序封装一个壳,真正的能够让用户便捷的使用,对于有这方面需求的读者可以参照本节继续阅读下去,如果不需要则可以打道回府(顺便无耻的喊一声:能给个5星好评不啦)。
打开VS2015,创建一个MFC应用程序MnistMFC,创建过程如下所示:
其它部分按照默认配置即可,最后单击“完成”实现项目创建。
首先修改解决方案,改为Release X64,并生成一下项目。然后打开资源管理视图,找到工具箱,添加两个Button ,一个命名为“选择图像”,一个命名为“检测”,然后在工具箱中找到picture control控件,添加到我们的界面,并将其ID号重新命名为 IDC_SHOW 。此时界面如下图所示:
接下来按照2.2和2.3节中的部分为项目配置opencv和libtorch库引用,然后将相关dll文件复制到x64/Release文件夹下面,如下图所示:
打开项目的stdafx.h文件,在头部添加libtorch库的导入(这个非常重要!!!放在其它任何地方都会导致编译出问题):
// stdafx.h : 标准系统包含文件的包含文件,
// 或是经常使用但不常更改的
// 特定于项目的包含文件
#undef UNICODE //这个一定要加,否则会编译错误
#include <torch/script.h> // 引入libtorch头文件
#pragma once
#ifndef VC_EXTRALEAN
#define VC_EXTRALEAN // 从 Windows 头中排除极少使用的资料
#endif
#include "targetver.h"
#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // 某些 CString 构造函数将是显式的
// 关闭 MFC 对某些常见但经常可放心忽略的警告消息的隐藏
#define _AFX_ALL_WARNINGS
#include <afxwin.h> // MFC 核心组件和标准组件
#include <afxext.h> // MFC 扩展
#include <afxdisp.h> // MFC 自动化类
#ifndef _AFX_NO_OLE_SUPPORT
#include <afxdtctl.h> // MFC 对 Internet Explorer 4 公共控件的支持
#endif
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h> // MFC 对 Windows 公共控件的支持
#endif // _AFX_NO_AFXCMN_SUPPORT
#include <afxcontrolbars.h> // 功能区和控件条的 MFC 支持
#ifdef _UNICODE
#if defined _M_IX86
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_X64
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#else
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#endif
#endif
然后,依次单击菜单“项目”—“MnistMFC 属性”—“配置属性”—“C/C++”—“常规”,将SDL检查改为“否”。配置完成后重新生成解决方案查看是否能够正常编译通过,如果不行说明前面的配置有问题,请仔细核对本教程步骤再试。
在CMnistMFCDlg.h中添加opencv库的导入:
#include <iostream>
#include <memory>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
重新生成一下项目。至此,已完成两个库的配置和导入。
下面开始编写逻辑实现代码。首先将项目字符集从“使用Unicode字符集”改为“未设置”,这样就可以比较方便的处理字符串数据转换。然后为CMnistMFCDlg类添加两个类内变量:
public:
Mat m_img;//图像
BITMAPINFO *m_imgInfo;//图像颜色表信息
接下来为按钮添加响应函数,首先是“选择图像”对应的响应函数:
//选择图像
void CMnistMFCDlg::OnBnClickedButtonChoosepic()
{
// TODO: 在此添加控件通知处理程序代码
CFileDialog dlg(TRUE, _T("*.jpg"),
NULL,
OFN_ALLOWMULTISELECT | OFN_HIDEREADONLY | OFN_FILEMUSTEXIST,
_T("image Files(*.jpg;*.jpg)|*.jpg;*.jpg|All Files (*.*)|*.*||"),
NULL);
//打开文件对话框的标题名
dlg.m_ofn.lpstrTitle = _T("选择图像 ");
if (dlg.DoModal() != IDOK)
return;
CString mPath = dlg.GetPathName();
m_img = imread(mPath.GetBuffer(0), IMREAD_GRAYSCALE);//读取灰度图
//修改颜色表等信息
m_imgInfo = (BITMAPINFO*) new BYTE[sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)];
//颜色表赋值
for (int i(0); i < 256; ++i)
{
m_imgInfo->bmiColors[i].rgbBlue = i;
m_imgInfo->bmiColors[i].rgbGreen = i;
m_imgInfo->bmiColors[i].rgbRed = i;
m_imgInfo->bmiColors[i].rgbReserved = 0;
}
//头文件信息(注意由实际显示情况可得出图像原点显示在空间左下角)
m_imgInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
m_imgInfo->bmiHeader.biBitCount = 8 * m_img.channels();
m_imgInfo->bmiHeader.biHeight = -m_img.rows;
m_imgInfo->bmiHeader.biWidth = m_img.cols;
m_imgInfo->bmiHeader.biPlanes = 1;
m_imgInfo->bmiHeader.biCompression = BI_RGB;
m_imgInfo->bmiHeader.biSizeImage = m_img.channels() * m_img.cols * m_img.rows;
m_imgInfo->bmiHeader.biXPelsPerMeter = 0;
m_imgInfo->bmiHeader.biYPelsPerMeter = 0;
m_imgInfo->bmiHeader.biClrUsed = 0;
m_imgInfo->bmiHeader.biClrImportant = 0;
//开始绘图
CDC *pDC;
pDC = GetDlgItem(IDC_SHOW)->GetDC();
CRect imgCtrlRect;
GetDlgItem(IDC_SHOW)->GetClientRect(&imgCtrlRect);
pDC->SetStretchBltMode(COLORONCOLOR);
::StretchDIBits(pDC->GetSafeHdc(),
0, 0,
imgCtrlRect.Width(), imgCtrlRect.Height(),
0, 0,
m_img.cols, m_img.rows,
m_img.data,
m_imgInfo,
DIB_RGB_COLORS,
SRCCOPY);
ReleaseDC(pDC);
}
根据上述代码就可以打开任意单通道jpg文件并显示。接下来添加第二个响应函数的代码,该代码中需要调用pytorch模型进行预测并显示结果。将前面训练好的model.pt文件拷贝到本项目X64 Release文件夹下面,然后编辑代码如下:
void CMnistMFCDlg::OnBnClickedButtonDetect()
{
// TODO: 在此添加控件通知处理程序代码
torch::jit::script::Module module;
try {
module = torch::jit::load("model.pt"); //加载模型
}
catch (const c10::Error& e) {
MessageBox("无法加载model.pt模型");
return;
}
std::vector<int64_t> sizes = { 1, 1, m_img.rows, m_img.cols }; //依次为batchsize、通道数、图像高度、图像宽度
at::TensorOptions options(at::ScalarType::Byte);
at::Tensor tensor_image = torch::from_blob(m_img.data, at::IntList(sizes), options);//将opencv的图像数据转为Tensor张量数据
tensor_image = tensor_image.toType(at::kFloat);//转为浮点型张量数据
at::Tensor result = module.forward({ tensor_image }).toTensor();//推理
auto max_result = result.max(1, true);
auto max_index = std::get<1>(max_result).item<float>();
CString str;
str.Format("%.0f", max_index);
MessageBox(str);
}
最终效果如下:
最后本教程给出了详细代码,MFC部分就不给出了,因为很多人可能不需要这个。如果需要的再联系我吧。