使用 NCNN 部署 PyTorch 模型

1,989 阅读6分钟

使用适用于移动设备的 NCNN 将预训练 PyTorch 模型部署到 C++ 应用程序的介绍性示例。

image.png

在手机上部署深度神经网络。(a) 作者图片,(b) 作者图片,(c) 图片来自“Attention is All You Need”Vaswani 等人。[1],(d) Shiwa ID 在Unsplash上的图片。

介绍

如今已经发展出很多深度学习模型,如CNNs、Transformers等,而Large Language Models (LLMs),如ChatGPT,极大地提升了模型的智能化水平。为了增加它们在现实生活中的使用,探索如何将模型部署到移动设备(如智能手机、AR 眼镜等)非常重要。因此,本文旨在提供一个介绍性示例,介绍如何将预训练的 PyTorch 模型部署到使用 NCNN 的 C++ 应用程序,可以进一步集成到移动设备的 Android 中。

我特别喜欢讨论以下内容。

  • 什么是 NCNN?
  • 使用 NCNN 将预训练的 PyTorch 模型部署到 C++ 应用程序中的管道。
  • 使用 NCNN 将预训练 PyTorch 模型部署到 C++ 应用程序的介绍性示例。

NCNN(神经网络计算库)

NCNN 是腾讯开发的高性能神经网络推理框架,专为移动和嵌入式设备设计[2]。NCNN 轻量级且高效,使其成为在计算资源有限的设备上部署深度学习模型的理想选择。

NCNN 的模型由两个文件组成:.param 和.bin。.param 文件包含模型结构和参数信息,包括层类型、层名称、层参数和输入/输出形状。这个文件通常是一个轻量级的文本文件,可以很容易地被 NCNN 框架解析和加载。

.bin 文件包含二进制格式的训练模型权重。这些权重是在训练过程中学习的,并用于在推理过程中进行预测。.bin 文件通常比 .param 文件大得多,因为它包含学习参数的实际数值。

使用 NCNN 将预训练 PyTorch 模型部署到 C++ 应用程序中的管道

image.png

使用 NCNN 将预训练的 PyTorch 模型部署到 C++ 应用程序中的管道 — 图片由作者提供。

对于部署,预训练的 PyTorch 模型 (.pth/.pt) 首先在 PyTorch 中转换为 ONNX 文件 (.onnx)。然后 .onnx 模型文件将被 NCNN 转换器转换为由两个文件组成的 NCNN 模型:一个 .param 文件和一个 .bin 文件。最后,NCNN 模型文件由 C++ 代码加载,以使用 NCNN 库执行推理。C++ 代码可以进一步集成到 Android 中以在移动设备上执行推理。在这篇文章中,我重点讨论了在 Linux (Ubuntu) 计算机上部署 NCNN 模型。我将在下一篇文章中讨论将 C++ 推理代码集成到 Android 中。

转换为 ONNX 文件

在我之前的帖子使用 ONNX 运行时将 PyTorch 模型部署到 C++ 应用程序中,我讨论了如何使用PyTorch 中的torch.onnx.export函数将 PyTorch 预训练模型转换为 ONNX 文件。 关于ONNX和转换的更多详细信息,请参考我之前的帖子。

torch.onnx.export(net,        # 正在运行的模型       
                  x,          # 模型输入
                  "model.onnx" ,   # 保存模型的位置
                  export_params= True , # 存储训练的权重
                  opset_version= 11 ,    # ONNX 版本
                  do_constant_folding= True , 
                  input_names= [ 'input' ], # 设置模型输入名称    
                  output_names=[ 'output' ], # 设置模型输出名称
)

使用 NCNN 将预训练 PyTorch 模型部署到 C++ 应用程序的端到端示例

我仍然使用CIFAR10 数据集[3]的 PyTorch 教程图像分类示例(在我之前的帖子中讨论过)来解释如何使用 NCNN 进行部署。我的解释从PyTorch 生成image_classifier.onnx开始。 下一步是将image_classifier.onnx转换为 NCNN 所需的模型格式。

要使用 NCNN,我首先在 Ubuntu 中安装和设置 NCNN。下面是安装 NCNN 的步骤。

git clone https://github.com/Tencent/ncnn.git 
cd ncnn 
mkdir -p build 
cd build 
cmake -DCMAKE_BUILD_TYPE=Release -DNCNN_VULKAN=OFF \ 
-DNCNN_SYSTEM_GSLLANG=ON -DNCNN_BUILD_EXAMPLES=ON \ 
-DNCNN_SHARED_LIB=ON ..制作
make
安装

NCNN 安装完成后,会在ncnn_root/build/tools/onnx/ncnn_root/build/tools/ 下生成两个有用的工具onnx2ncnnncnnoptimize,其中ncnn_root是 NCNN 的根目录。************

对于此示例,我将创建以下文件树并将所有模型文件放入models子目录

image.png

我将onnx2ncnnncnnoptimize复制到这个例子的根目录。在根目录下,我通过执行以下命令将 ONNX 模型转换为 NCNN 模型。

python3 -m onnxsim models/image_classifier.onnx \ 
models/image_classifier_sim.onnx 
./onnx2ncnn models/image_classifier_sim.onnx \ 
models/image_classifier.param models/image_classifier.bin 
./nnnoptimize models/image_classifier.param models/image_classifier.bin \
模型/ image_classifier_opt.param 模型/image_classifier_opt.bin 65536

上述命令中,ONNX模型首先经过onnxsim优化,生成执行时间更快、内存占用更低的优化ONNX模型。然后通过onnx2ncnn将优化后的 ONNX 模型转换为 NCNN 模型。最后,NCNN 模型通过ncnnoptimize进一步优化,以减小尺寸和复杂性,同时保持在移动和嵌入式设备上部署的准确性。数字 65536 是优化模型可以处理的最大输入图像大小。

使用优化的 NCNN 模型,我编写了用于执行图像分类的 C++ 推理代码。我首先制作用于编译项目的 CMakeLists.txt 文件。要使用 NCNN,我指定了 NCNN 的包含目录和链接目录。此外,由于 OpenCV 用于加载输入图像和可视化结果,我也在CMakeLists.txt中设置了 OpenCV (假设已安装 OpenCV)。

NCNN_cmake

CMakeLists.txt — 图片由作者提供。

现在,是时候制作image_classifier.cpp了。

#include <iostream> #include  <fstream>
 #include  <stdio.h> #include <algorithm> #include <vector> #include <string> #include < 
opencv2  / core / core.hpp> #include 
<  opencv2 /highgui / highgui.hpp> # include <opencv2/imgproc/imgproc.hpp> # include "net.h" // 必须包含此文件才能使用 NCNN net。int main ( int argc, char ** argv) { // 加载图像
 
 
    
 
 
 
    
     
    
  
    
    std::string imagepath ( "../images/horse.png" ) ; 
    cv::Mat img = cv:: imread (imagepath, cv::IMREAD_COLOR); 
    if (img.empty ()) {         std::cerr << "无法读取图像文件" << imagepath << std::endl ; 返回-1 ;     } // 指定图像分类的所有类的名称    std::vector<std::string> classes = { "plane" , "car" ,   "bird" , "cat" , "deer" ,   "dog" ,  

         

    
    

                                        , 
                                        "船" ,   "卡车" }; 
    
    // 加载 NCNN 模型
    ncnn::Net net; 
    int ret = 净值。load_param ( "../models/image_classifier_opt.param" ); 
    if (ret) std::cerr << "加载模型参数失败" << std::endl; 
    ret =净。load_model ( "../models/image_classifier_opt.bin" ); 
    if (ret) std::cerr << "加载模型权重失败" << std::endl; 
    
    // 将图像数据转换为ncnn格式
    // bgr中的opencv图像,模型需要bgr
     ncnn::Mat input = ncnn::Mat::
        ncnn::Mat::PIXEL_BGR, img.cols, img.rows); 
    
    // 图像数据预处理
    const  float mean_vals[ 3 ] = { 0.5f * 255.f , 0.5f * 255.f , 0.5f * 255.f }; 
    const  float norm_vals[ 3 ] = { 1 / 0.5f / 255.f , 1 / 0.5f / 255.f , 1 / 0.5f / 255.f }; 
    输入。substract_mean_normalize (mean_vals, norm_vals);
    
    // 推理
    ncnn::Extractor extractor = net. 创建提取器();
    提取器。输入(“输入”,输入);
    ncnn::垫输出;
    提取器。提取物(“输出”,输出);
    
    // 展平
    ncnn::Mat out_flatterned = output. 重塑(output.w * output.h * output.c);
    std::vector<float>分数;
    分数。调整大小(out_flatterned.w);
    for ( int j= 0 ; j<out_flatterned.w; j++) { 
        scores[j] = out_flatterned[j]; 
    }

    // 基于分数的预测
    std::string pred_class = 
        classes[std:: max_element (scores.begin ( ), 
                                 scores.end ()) - scores . 开始()];

    std::cout << “预测类是” << pred_class << “。” << 标准::结束; 

    // 保存并可视化结果
    cv:: imwrite ( "../images/out_horse.png" , img); 
    cv:: namedWindow ( "Input_image" , cv::WINDOW_NORMAL); 
    简历::imshow(“输入图像”
    cv:: waitKey ( 0 ); 
    std::cout << "完成" << std::endl; 
    返回 0;
}

image_classifier.cpp中,输入图像首先由 OpenCV 加载。然后加载 NCNN 模型文件 .param 和 .bin。请注意,如果 net.load_param 和 net.load_model 运行成功,则返回 0 值。否则,返回非零值以显示加载失败。

ncnn::网络网络;
int ret = 净值。load_param ( "../models/image_classifier_opt.param" ); 
if (ret) std::cerr << "加载模型参数失败" << std::endl; 
ret =净。load_model ( "../models/image_classifier_opt.bin" ); 
if (ret) std::cerr << "加载模型权重失败" << std::endl;

接下来,需要将 OpenCV 图像转换为 NCNN 要求的格式。由于 OpenCV 图像是 BGR 格式并且模型需要 BGR 图像作为输入,因此我使用ncnn::Mat::PIXEL_BGR

ncnn::Mat input = ncnn::Mat:: from_pixels (img.data, ncnn::Mat::PIXEL_BGR, 
                                         img.cols, img.rows);

为了正确地执行推理,需要以与训练阶段使用的方式相同的方式对图像进行归一化。然而,subtract_mean_normalize取 [0, 255] 中的图像像素,并通过 X * (1 / std) — mean * (1/std) 对图像像素进行归一化,其中X代表 BGR 通道的像素强度,均值为 255 ∗ [ BGR通道为0.5,0.5,0.5], BGR通道的std为255*[0.5,0.5,0.5],[0.5,0.5,0.5]和[0.5,0.5,0.5]为图像的均值和标准差CIFAR10 数据集 [0, 1] 中的像素。因此,输入图像在 C++ 中按如下方式归一化。

const  float mean_vals[ 3 ] = { 0.5f * 255.f , 0.5f * 255.f , 0.5f * 255.f }; 
const  float norm_vals[ 3 ] = { 1 / 0.5f / 255.f , 1 / 0.5f / 255.f , 1 / 0.5f / 255.f }; 
输入。substract_mean_normalize (mean_vals, norm_vals);

推理由ncnn_Extractor执行。需要提供输入节点和输出节点的名称,以便执行推理和提取结果。由于我在将 PyTorch 模型转换为 ONNX 模型时将输入节点的名称指定为“input”并将输出节点的名称指定为“output”,因此我将这些名称提供给ncnn_Extractor

ncnn::Extractor 提取器 = net. 创建提取器();
提取器。输入(“输入”,输入);
ncnn::垫输出;
提取器。提取物(“输出”,输出);

然后将推理结果展平以获得 CIFAR10 数据集类别的预测分数。预测的类别是得分最高的类别。

// 展平
ncnn::Mat out_flatterned = output. 重塑(output.w * output.h * output.c);
std::vector<float>分数;
分数。调整大小(out_flatterned.w);
for ( int j= 0 ; j<out_flatterned.w; j++) { 
    scores[j] = out_flatterned[j]; 
} 

// 基于分数的预测
std::string pred_class = 
    classes[std:: max_element (scores.begin ( ), scores.end ( )) - scores. 开始()];

此时,我们已准备好编译并运行代码以查看图像分类结果。

mkdir 构建 && cd 构建
cmake .. 
make 
./image_classification

image.png

我们看到马图像被正确分类为马。

上面讨论的部署的完整实现可以从Github获得,它从训练开始到使用 NCNN 部署到 C++ 图像分类中。

结论

在这篇文章中,我给出了一个介绍性示例,介绍如何使用 NCNN 将预训练的 PyTorch 模型部署到 C++ 应用程序中。特别是,我讨论了 (a) 什么是 NCC?,(b) 使用 NCNN 将预训练 PyTorch 模型部署到 C++ 应用程序中的管道,以及 (c) 显示 PyTorch 预训练模型部署到 C++ 应用程序中的具体图像分类示例使用 NCNN。我们在 Ubuntu 计算机中使用 NCNN 看到了正确的图像分类结果,表明部署成功。

在我的下一篇文章中,我将讨论如何将 C++ 图像分类代码集成到 Android 中以进行移动设备部署。感谢您阅读。

参考

[1] Vaswani 等人,Attention is All You Need,2017 年 12 月。

[2] 腾讯,神经网络计算机视觉, https: //github.com/tencent/ncnn,2019

[3] PyTorch,关于图像分类示例的 PyTorch 教程,https ://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html,2023