三.ubuntu22.04 使用C++部署PyTorch模型

前面一/二步骤配置好conda torch环境后,这里举例运行怎么将PyTorch模型应用到C++中,参照https://zhuanlan.zhihu.com/p/513571175 使用

这里使用libtorch, (部署到QNX平台还有使用SNPE,模型->ONNX->SNPE)
使用PyTorch的C++版本LibTorch来重写推理代码,并用JIT跟踪来得到LibTorch可以直接读取的模型参数。

1.安装libtorch

进入官网 https://pytorch.org/get-started/locally/ 下载libtorch, 这里简便,使用的是CPU版本
Screenshot from 2025-09-10 15-24-25
下载完成后,随便丢到一个地方去解压,完成。建议linux小白将libtorch放在/usr/local/lib下,并保证libtorch文件夹下存在include这个文件夹。

测试是否安装成功:

1.创建CMakeList.txt

cmake_minimum_required(VERSION 3.5)
project(LibTorchDemo)

# package
find_package(OpenCV REQUIRED)
find_package(Torch REQUIRED PATHS "/usr/local/lib/libtorch")

add_executable(digit digit.cpp)
# libtorch
target_link_libraries(digit ${TORCH_LIBRARIES})
target_link_libraries(digit ${OpenCV_LIBS})

2.创建digit.cpp

#include "iostream"
#include "opencv2/opencv.hpp"
#include "torch/script.h"

int main(int argc, char const *argv[])
{
    std::cout << "hello world!" << std::endl;
    return 0;
}

3.运行编译

$mkdir build
$cd build
$cmake .. && make -j8
$./digit

如果没有任何报错,说明没问题

2.第一步:先用PyTorch训练一个网络

既然我们需要将PyTorch模型使用C++部署,那么首先肯定需要一个Torch的模型。我们先使用PyTorch简单训练一个手写数字识别

创建digit.py

# 数据集:sklearn 的 8×8 手写数字
from sklearn.datasets import load_digits
import torch
from torch import nn
import torch.utils.data as Data
import numpy as np
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
import os

class Digit(nn.Module):
    def __init__(self):
        super().__init__()
        # 网络结构:微型四段卷积
        self.conv = nn.Sequential(
            nn.Conv2d(1, 16, 3, 1, 1), # 8×8 → 8×8
            # 激活函数用 Tanh 有点老,可以换 ReLU/GELU
            nn.Tanh(),
            nn.Conv2d(16, 32, 3, 2, 1), # 8×8 → 4×4
            nn.Tanh(),
            nn.Conv2d(32, 16, 3, 2, 1), # 4×4 → 2×2
            nn.Tanh(),
            nn.Conv2d(16, 8, 3, 1, 1) # 2×2 → 2×2
        )

        self.output = nn.Linear(32, 10) # 2×2×8 = 32

    def forward(self, x):
        out = self.conv(x)
        out = self.output(out.flatten(1))
        return out

RATIO = 0.8
BATCH_SIZE = 128
EPOCH = 10

if __name__ == "__main__":
    # 一共 1797 张灰度图,已经拉平成 64 维向量,像素值 0–16。
    # 后面 reshape 成 (1,8,8) 才能喂进 Conv2d。
    X, y = load_digits(return_X_y=True)
    # 归一化到 0–1, 像素天然最大 16,所以除以 16 就到 [0,1]。
    X = X / 16.
    sample_num = len(y)
    X = [x.reshape(1, 8, 8).tolist() for x in X]

    indice = np.arange(sample_num)
    np.random.shuffle(indice)

    # List 化 → FloatTensor
    X = torch.FloatTensor(X)
    y = torch.LongTensor(y)
    offline = int(sample_num * RATIO)

    train = Data.TensorDataset(X[indice[:offline]], y[indice[:offline]])
    test  = Data.TensorDataset(X[indice[offline:]], y[indice[offline:]])

    train_loader = Data.DataLoader(train, BATCH_SIZE, True)
    test_loader  = Data.DataLoader(test,  BATCH_SIZE, False)
    
    model = Digit()
    optimizer = torch.optim.RMSprop(model.parameters(), lr=1e-3)
    criterion = nn.CrossEntropyLoss(reduction="mean")

    test_losses = []
    test_accs = []

    for epoch in range(EPOCH):
        model.train()
        for bx, by in train_loader:
            out = model(bx)
            loss = criterion(out, by)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        model.eval()
        correct = 0
        total = 0
        test_loss = []
        test_acc = []
        for bx, by in test_loader:
            with torch.no_grad():
                out = model(bx)
                pre_lab = out.argmax(1)
                loss = criterion(out, by)

            test_loss.append(loss.item())
            test_acc.append(accuracy_score(pre_lab, by))

        test_losses.append(np.mean(test_loss))
        test_accs.append(np.mean(test_acc))
    
    plt.figure(dpi=120)
    plt.plot(test_losses, 'o-', label="loss")
    plt.plot(test_accs, 'o-', label="accuracy")
    plt.legend()
    plt.grid()
    plt.show()

    if not os.path.exists("model"):
        os.makedirs("model")
    torch.save(model.state_dict(), "model/digit.pth")

运行

python digit.py

测试集精度和损失曲线如下:
Screenshot from 2025-09-10 15-43-34
运行结束后可以得到模型文件,就在model文件夹: digit.pth

3.第二步:使用tracing将模型文件转化成TorchScript

PyTorch导出的模型文件是不能直接被libtorch读取的,因为PyTorch默认导出的后端的序列化是joblib。
PyTorch通过JIT搭建了Python和C++的桥梁,我们可以将模型转成TorchScript Module,将Python运行时的部分运行时包裹进去。
转换方法非常简单:

import torch
from digit import Digit

model = Digit()
model.load_state_dict(torch.load("model/digit.pth", map_location="cpu"))

sample = torch.randn(1, 1, 8, 8)

trace_model = torch.jit.trace(model, sample)
trace_model.save("model/digit.jit")

原理大概就是模拟一个输入sample,让模型走一遍前向推理,将state_dict中的每个参数跟踪一边。因此sample只需要是一个符合输入规范的张量即可。

运行上述代码,可以在model文件夹中得到digit.jit。这个jit文件可以直接被python使用,下面我就来使用JIT跟踪的模型来进行Python端的预测,一来验证我们的JIT跟踪得到的TorchScript模型是否可以正常工作,二来比较JIT会对模型推理的加速起到多大的效果。

例子图片 保存到image/sample.png
image

运行下述测试代码,由于Python本身的特性和JIT的即时编译的特性,模型在同一进程生命周期内运行时前几次会比较慢,所以在测试前,需要空跑几次:

import time
import torch
import cv2 as cv
from digit import Digit
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

def run_model(model, image):
    s = time.time()
    out = model(image)
    pre_lab = torch.argmax(out, dim=1)
    cost_time = round(time.time() - s, 5)
    return cost_time

image = cv.imread("image/sample.png")
image = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
image = 1 - image / 255.
image = cv.resize(image, (8, 8))


image = torch.FloatTensor(image).unsqueeze(0).unsqueeze(0).contiguous()
origin_model = Digit()
origin_model.load_state_dict(torch.load("model/digit.pth"))
jit_model = torch.jit.load("model/digit.jit")

# init jit
for _ in range(3):
    run_model(origin_model, image)
    run_model(jit_model, image)

test_times = 10

# begin testing
results = pd.DataFrame({
    "type" : ["orgin"] * test_times + ["jit"] * test_times,
    "cost_time" : [run_model(origin_model, image) for _ in range(test_times)] + [run_model(jit_model, image) for _ in range(test_times)]
})

plt.figure(dpi=120)
sns.boxplot(
    x=results["type"],
    y=results["cost_time"]
)
plt.show()

性能箱图如下:
image
可以看到,JIT的推理速度比原本的有些许的提升。我们的TorchScript模型也可以正常工作。

4.第三步:使用libtorch重写推理程序

由于TorchScript可以被C++直接调用,所以我们只需要使用libtorch重写推理代码,并将模型读入就完成了。

libtorch的语法和PyTorch基本一致,学起来很快,于此锦恢就不再赘述了。相应的,在C++中,我们用cv::Mat来取代Python中的numpy.ndarray对象,如何将cv::Mat转成libtorch可以读入的数据结构也会在demo中涉及。

下面的例子会完成一个C++命令行程序,它的第一个参数为模型,第二个参数为需要读入的手写数字图像的路径,预测结果会打印到控制台上。期待已久的C++代码如下:

#include "iostream"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/types_c.h"
#include "torch/script.h"
#include "fstream"
using namespace cv;
using namespace std;
void checkPath(const char* path) {
    std::ifstream in;
    in.open(path);
    bool flag = (bool)in;
    in.close();
    if (flag) return;
    else {
        std::cout << "file " << path << " doesn't exist!" << std::endl;
        exit(-1);
    }
}

int main(int argc, char const *argv[])
{
    if (argc != 3) {
        std::cout << "usage : digit <model path> <image path>" << std::endl;
        return -1;
    }

    checkPath(argv[1]);
    checkPath(argv[2]);
    cv::Mat img = cv::imread(argv[2]), gimg, fimg, rimg;
    cv::cvtColor(img, gimg, CV_BGR2GRAY);

    // 这个线性变换公式:
    // dst = src * alpha + beta
    // 这里 alpha = -1/255, beta = 1 → 把 0~255 映射到 1 → 0(反向!)
    // 通常 CNN 希望 0~1 或 -1~1;这里反向缩放会让白字变 0,黑底变 1,得确认训练时是否同样做的反向。
        // 归一化方向确认: 若训练时做的是 /255. 且没有反向,请把convertTo(fimg, CV_32F, 1.0/255, 0); 否则推理结果会始终错乱。
    gimg.convertTo(fimg, CV_32F, - 1. / 255., 1.);
    // 后面 resize 到 8×8,说明网络输入就是 8×8 单通道,典型的“微型 MNIST”玩具模型。
    cv::resize(fimg, rimg, {8, 8});

    // cv::Mat → at::Tensor
    // from_blob 不会复制内存,只是“包裹”现有指针。
    // 形状 NCHW = (batch=1, channel=1, H=8, W=8)。
    // 生命周期风险:rimg 离开作用域后内存会释放,而 tensor 仍指向它。
    // 这里刚好紧跟着就用,所以安全;但若把 tensor 返回给外部函数就要 tensor.clone(), 即img_tensor = torch::from_blob(...).clone();   // 如果 tensor 要传出去
    // convert Mat to tensor
    at::Tensor img_tensor = torch::from_blob(
        rimg.data,
        {1, 1, 8, 8},
        torch::kFloat32
    );

    // 如果模型是在 GPU 上 trace 的,默认会尝试加载到 CUDA;在 QNX 这种通常没 NVIDIA 驱动的系统里会抛异常。
    // 解决:在 C++ 端
    // model.to(at::kCPU);
    // 或在 Python trace 时 .to('cpu')。
    // load model
    torch::jit::Module model = torch::jit::load(argv[1]);

    // 关闭梯度计算: 等价 Python 的 with torch.no_grad():,省内存、提速。
    // torch.no_grad()
    torch::NoGradGuard no_grad;
    
    // 前向 + 取标签
    // model({...}) 返回 IValue → 转 Tensor。
    // argmax(out, 1) 在第 1 维(即类别维)找最大索引;MNIST 输出 10 类。
    // .item().toInt() 把 0-D tensor 转 int;若 batch>1 需循环。
    // forward
    torch::Tensor out = model({img_tensor}).toTensor();
    int pre_lab = torch::argmax(out, 1).item().toInt();

    std::cout << "predict number is " << pre_lab << std::endl;
    return 0;
}

请一定加入torch::NoGradGuard no_grad; 这句话,否则内存会炸。

然后我们编译一把:

$cd build && cmake .. && make -j8

测试,将之前生成的digit.jit和使用的sample.png放在编译好的C++可执行程序同级目录下,然后执行

$./digit ./digit.jit ./sample.png

得到
Screenshot from 2025-09-10 15-52-57

参考
优雅地使用C++部署你的PyTorch推理模型(一)LibTorch的安装与基本使用
https://zhuanlan.zhihu.com/p/513571175

使用cuda的待尝试
CUDA12.2+Libtorch+VS2022搭建神经网络运行环境及测试(Win10)
https://zhuanlan.zhihu.com/p/656804199

使用SNPE的待尝试
使用SNPE运行PyTorch模型
https://blog.51cto.com/u_16213325/13337144

posted on 2025-09-10 15:53  JJ_S  阅读(122)  评论(0)    收藏  举报