使用适用于移动设备的 NCNN 将预训练 PyTorch 模型部署到 C++ 应用程序的介绍性示例。
在手机上部署深度神经网络。(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++ 应用程序中的管道
使用 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/ 下生成两个有用的工具onnx2ncnn和ncnnoptimize,其中ncnn_root是 NCNN 的根目录。************
对于此示例,我将创建以下文件树并将所有模型文件放入models子目录 。
我将onnx2ncnn和ncnnoptimize复制到这个例子的根目录。在根目录下,我通过执行以下命令将 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)。
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
我们看到马图像被正确分类为马。
上面讨论的部署的完整实现可以从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