让模型从Pytorch到NCNN——Pytorch模型向NCNN框架部署的小白向详细流程(PNNX模型转换、Linux模型使用)

2024-08-09 21:36:03 浏览数 (2)

参考文章和项目地址: [1] Tencent/ncnn: ncnn is a high-performance neural network inference framework optimized for the mobile platform (github.com) [2] pnnx/pnnx: PyTorch Neural Network eXchange (github.com) [3] 使用pnnx把pytorch模型转ncnn模型-CSDN博客 [4] https://gitee.com/luo_zhi_cheng/awesome-ncnn/blob/master/FAQ.md

在这里特别感谢:ncnn开发者团队、nihui姐!万分感激orz

零、NCNN 使用动机与简介

使用背景

实际上在写这篇博客的时候我还没有试着部署到树莓派等嵌入式设备上,并且后续才发现我转换的模型有些许问题(悲)不过这是我模型本身代码的问题,和转换与部署过程无关,我可能之后会继续续写这篇博客。

一开始,稚嫩的我只是想能在实际应用场合中使用一些深度模型(结果没想到后面坑这么大),这就需要涉及到,如何将实验室里基于pytorch的一个开发模型,部署到算力和系统架构都不同的嵌入式设备中。随后我了解到了,部署到嵌入式设备上需要借助一些深度学习模型部署框架,这其中最著名的可能就是 ONNX (Open Neural Network Exchange) 了,我在阅读论文中也时常看到这个词汇,当时还不知道是啥。其他的还有腾讯家的 NCNN、阿里家的 MNN (Mobile Neural Network) 等。在与指导老师沟通后,我最终决定尝试腾讯的部署框架,也就是今天的主角:

NCNN

Q: ncnn名字的来历 A: cnn就是卷积神经网络的缩写,开头的n算是一语n关。比如new/next(全新的实现),naive(ncnn是naive实现),neon(ncnn最初为手机优化),up主名字(←_←) 引自 Tencent/ncnn Wiki (github.com)(https://github.com/Tencent/ncnn/wiki#faq)

为何要用NCNN

由于我是小白,基本上也就听风就是雨,从我咨询的信息来看,ONNX 框架过老,很多新的算子都没有得到支持,以及据说模型在向 ONNX 转换时转换的效果以及推理的效果也不太行,用老师说的话就是:“天下苦 ONNX 久矣!”,此外,我还了解到了 MNN,这个框架我其实是想作为我的候选框架来使用的,其实也进行了尝试,可惜模型在转换时失败了,另外,也有人说 git 上有关 MNN 代码讨论的少一点,NCNN 相关模型开源的还是多一些,可能有更好的参考。

此外,在我使用 NCNN 的过程中,我发现基本上 NCNN 框架基本大多数都是针对图像处理领域,而我的项目其实和语音有强关系,这也为后续的困难重重埋下了伏笔。

NCNN简介

为了偷懒,这里我就直接引用 ncnn github 主页中给出的介绍了:

ncnn 是一个为手机端极致优化的高性能神经网络前向计算框架。 ncnn 从设计之初深刻考虑手机端的部署和使用。 无第三方依赖,跨平台,手机端 cpu 的速度快于目前所有已知的开源框架。 基于 ncnn,开发者能够将深度学习算法轻松移植到手机端高效执行, 开发出人工智能 APP,将 AI 带到你的指尖。 ncnn 目前已在腾讯多款应用中使用,如:QQ,Qzone,微信,天天 P 图等。

一、PNNX 模型转换(Windows)

模型转换方式

在使用 NCNN 框架前,我们首先得需要将我们基于 Pytorch 开发的模型代码转换到 NCNN 框架上去,之后才能去使用。之前在我去ncnn的wiki里寻找转换方式时,那时的wiki里还只有以下的模式:

  • Pytorch → ONNX → NCNN

onnx 本身就有对 pytorch 转换的支持,而 NCNN 也有 onnx2ncnn 的转换工具,但这样很明显非常麻烦,可能会出现某些算子不支持等问题,并且可以预料到 onnx 那么老的框架作为过渡,最后部署的推理效果应该不会太好。后续我又了解到了另一个工具,如今 wiki 也将其正式录入,即 PNNX(PyTorch Neural Network eXchange)

  • Pytorch (via PNNX)→ NCNN

PyTorch 神经网络交换 (PNNX) 是 PyTorch 模型互操作性的开放标准。PNNX 为 PyTorch 提供了一种开放的模型格式。它定义了计算图以及严格匹配 PyTorch 的高级运算符。 引自 https://github.com/pnnx/pnnx

pnnx 同样是 ncnn 开发团队制作的,少了中间商,自然转换效果要好上不少,转换过程也会方便很多。

PNNX 转换过程

推荐直接去 wiki:use ncnn with pytorch or onnx · Tencent/ncnn Wiki (github.com) 或者pnnx的github主页:pnnx/pnnx: PyTorch Neural Network eXchange (github.com) 去了解。 以下过程为使用命令行进行模型转换,可能直接使用python转换更加方便,请参考wiki的具体操作,除此之外,也可以直接访问https://convertmodel.com/进行转换,这个网站提供了一站式的各种常见CNN推理库的模型转换工具的本地在线版,直接使用即可。不过在作者撰写博客的时候这个网站暂时无法访问。

pytorch 模型转 torchscript:

首先需要将整个模型导出为 torchscript 模型。虽然导出的模型文件也是以.pt结尾,但是它并不是我们在github上下载的别人提供的预训练模型!而是需要我们使用torch.jit.trace或者torch.jit.script导出的模型文件。话不多说,直接给我使用的样例代码:

代码语言:javascript复制
import torch
import utils
from temp_model import MsVits
from text_processor import tokens2ids, pypinyin_g2p_phone
import commons
 ​
 ​
# 加载模型
device = torch.device("cpu")
hps = utils.get_hparams_from_file("./configs/biaobei_ms_istft_vits.json")
net_g = MsVits(checkpoint_path="./logs/G_2000.pth", hps=hps, device=device).to(device)
 ​
def get_text(text, hps):
    # 实现文本(str)转音素序列(tensor)
 ​
 ​
_ = net_g.eval()
​
# 随便从群聊中摘取的长句子,用于制作样例输入
text = "常年嗑爱素的人大都目光清澈,极度自信,且智商逐年升高,最后完全变成天才。嗑爱素会重塑身体结构,创造各种医学奇迹。人一旦开始嗑爱素就说明这个人的智慧品行样貌通通都是上上等,这辈子肯定能光明正大的做成任何事。嗑爱素的人具有强烈的社会认同和社会责任感,对治安稳定起到推进作用,对正常人有延年益寿的效果。"
 ​
# 制作样例输入
text = get_text(text, hps)
 ​
# 使用trace追踪模型
ts_model = torch.jit.trace(net_g, text)
 ​
# 保存模型文件
ts_model.save('biaobei_msvits.pt')
 ​

其实核心代码一共只有最后的两句,只需要制作样例输入即可,图像处理一类的模型可能更简单,直接使用torch.ones(1, 3, 224, 224)这样类似的当作输入就可以了。以下是我在转换时遇到的坑,请注意:

  • torch.jit.trace默认会使用模型的 forward() 函数进行追踪。如果你和我一样,使用的模型中,推理时使用的是 inference() 或者其他函数,则可以有多种方法解决:
    1. 偷偷将forward函数注释掉,然后再把你要用的函数改名成forward。
    2. 封装模型。自己新建一个模型去封装原来的模型,在新模型的 forward 函数中调用要使用的模型的函数。
  • torch.jit.trace要求追踪模型的输入和输出都必须是 tensor。
  • 在追踪模型时,必须要剔除模型推理中的随机操作。我的模型里可以添加合成时对语调、持续时长等的干扰,在追踪模型时必须要将这些设置为0,可能会出现的相关报错如下:
可能出现的报错可能出现的报错
  • 针对我的项目中,出现了torch.nn.utils.weight_norm在转换时出现 warning,并在之后使用pnnx时直接导致了pnnx转换失败,在改为提示中给出的新的使用方法 torch.nn.utils.parametrizations.weight_norm 并重新训练后成功完成转换。注意,新的 weight_norm 和旧版本计算方式并不一样,需要重新训练。在转换时相关的warning如下:
转换时的warning转换时的warning

若代码执行成功,会在当前目录中生成一个你指定名称的 torchscript 模型,在我的代码中就是biaobei_msvits.pt文件。当然,你也可以写一小段代码来验证一下你导出的模型是否可用:

代码语言:javascript复制
import torch
import utils
from text_processor import tokens2ids, pypinyin_g2p_phone
import soundfile
import commons
 ​
# 加载 torchscript 模型
device = torch.device("cpu")
model = torch.jit.load('biaobei_msvits.pt')
model.eval()  # 将模型设置为评估模式
 ​
def get_text(text):
    # 实现文本(str)转音素序列(tensor)
 ​
 ​
text = "今天tian"
text = get_text(text)
 ​
audio = model(text).data.cpu().float()
 ​
# 保存模型输出结果
soundfile.write('./wave_g/1.wav', audio.numpy(), 16000)

torchscript 模型通过 pnnx 转 ncnn

这里我们选择使用命令行进行转换,当然也可以 pip install pnnx 进行转换。直接使用团队提供的预编译好的 pnnx ,去Releases · pnnx/pnnx (github.com)中下载对应系统的移植包,并进行解压,详细用法请参考pnnx github主页:下面以 windows 为例,解压后文件如下:

打开所在目录的命令行,将我们刚刚导出的 torchscript 模型文件放入文件夹中,执行转换命令:

代码语言:javascript复制
# 用法
pnnx.exe [model.pt] [(key=value)...]
 ​
# 示例,使用动态输入,输入tensor形状从[1,1]到[1,817],数据类型为 int64
pnnx.exe model.pt inputshape=[1,1]i64 inputshape2=[1,817]i64

如果执行成功的话,你将会看到在文件目录中看到至少7个文件:

也许可能有多余的文件,这些都是调试用的,可以不管:

出现刚刚提到的7个文件后,就代表着你的模型转换成功了!遇到模型转换失败的情况也不用怕,可以去 ncnn 的主页加入技术讨论群,里面都有大佬进行解答。

二、模型部署与编译(Linux)

由于作者是个C 小白,下面的内容可能会显得非常稚嫩,而且还不知道说的对不对(),因此C 佬们可以不用看这一段,直接进行 NCNN 库的编译和部署即可。

获得了之前转换的七个文件后,我们直接将文件传入到工作站或嵌入式设备中着手开始部署。我使用的工作站是 Ubuntu 24.04 LTS。我们首先准备好 C 的工作环境,这里我就不再详细赘述。下面我们将开始链接 NCNN 的库。

为了省事,我们可以不用自己去编译,直接使用开发者提供的预编译库https://github.com/Tencent/ncnn/releases,我们是 ubuntu,下载ncnn-20240410-ubuntu-2204.zip,并使用 unzip 进行解压,并记住解压的路径。解压后为下方内容:

这里插一嘴,如果闲着没事干,可以在bash运行一下下面这条命令,这条命令是安装编译 ncnn 库所需要的所有依赖,适用于Debian 10 , Ubuntu 20.04 , 或 Raspberry Pi OS。如果之后想要自己编译 ncnn,请访问https://github.com/Tencent/ncnn/wiki/how-to-build

代码语言:javascript复制
sudo apt install build-essential git cmake libprotobuf-dev protobuf-compiler libomp-dev libvulkan-dev vulkan-tools libopencv-dev

我们预计使用 CMake 构建工程。新建项目文件夹,并将我们之前转出的七个文件传至项目文件夹中。我们先简单写一点代码来尝试加载 ncnn 模型,详细的 NCNN 使用方法请见 ncnn wiki:https://github.com/Tencent/ncnn/wiki。在项目文件夹中新建 test.cpp,内容如下:

代码语言:javascript复制
#include <iostream>
#include <chrono>
#include "net.h"
using namespace std;
 ​
int main() // 多行测试
{    
    const char *binfile = "./model.ncnn.bin";
    const char *paramfile = "./model.ncnn.param";
 ​
    static ncnn::Net* testNet = NULL;
 ​
    testNet = new ncnn::Net();
    testNet->load_param(paramfile);
    testNet->load_model(binfile);
}

如果你和我一样使用的是 VSCode,可以配置一下代码补全和错误检查的功能,编辑.vscode中的c_cpp_properties.json,向includePath中添加ncnn库的头文件目录,就是我们刚刚解压出的文件夹中的 include/ncnn:

随后再新建 CMakeLists.txt 进行项目cmake配置,内容如下:

代码语言:javascript复制
cmake_minimum_required(VERSION 3.10)
 ​
# 设置项目的名称,一般就设置为项目文件夹名字就行
project(ncnn_test)
 ​
# 指定源代码文件的位置
set(SOURCE_FILES
    ${CMAKE_CURRENT_SOURCE_DIR}/test.cpp
)
 ​
# 指定 ncnn 的 查找路径
set(ncnn_DIR "<ncnn_install_dir>/lib/cmake/ncnn" CACHE PATH "Directory that contains ncnnConfig.cmake")
 ​
# 确保能够正确定位 ncnn,可以自动设定正确的链接库及其顺序,而无需手动进行静态库的链接
find_package(ncnn REQUIRED)
 ​
# 创建可执行文件
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
 ​
# 链接 ncnn 库
target_link_libraries(${PROJECT_NAME} PRIVATE ncnn)

在 CMake 中,我们将 ncnn_DIR 指向解压目录下包含 ncnnConfig.cmake 的目录,一般就是在lib/cmake/ncnn中。如果你是自己编译的库,则需要先进行 make install,ncnn_install_dir 此时对应 build/install 目录。

之后就是使用 cmake 构建项目,在bash中,当前项目目录下运行下面命令:

代码语言:javascript复制
mkdir build
cd build
cmake ..
make

另:千万别头铁自己去手动进行所有依赖的链接哦!(别问作者为什么知道)诸如:

  • undefined reference to 'omp_get_thread_num'
  • undefined reference to `glslang::FinalizeProcess()'

都是手动进行依赖库链接的报错,请参考上方使用 cmake 进行项目构建。如果一切顺利,就应该能够在 build 目录下找到一个可执行文件,名称为 ncnn_test,本次的教程也就到这里截止辣~

三、附录

常见问题解答参考地址

  • FAQ.md · lzc/awesome-ncnn - Gitee.com
  • faq · Tencent/ncnn Wiki (github.com),wiki 中还有很多其他的 faq:

0 人点赞