《腾讯NCNN框架的模型转换x86/君正MIPS嵌入式编译及量化推理》详细教程

1.在Ubuntu上编译运行ncnn

1)编译ncnn x86 linux

// ubuntu安装依赖
sudo apt install build-essential git cmake libprotobuf-dev protobuf-compiler libomp-dev libvulkan-dev vulkan-tools libopencv-dev
// 下载ncnn以及三方库
git clone https://github.com/Tencent/ncnn.git
git submodule update --init
// 编译
cd ncnn
mkdir -p build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DNCNN_VULKAN=ON -DNCNN_BUILD_EXAMPLES=ON ..
make -j$(nproc)

在这里插入图片描述

// 安装到install文件夹
make install prefix=./install

检查一下install文件夹里是不是生成了bin,include和lib
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2)测试ncnn x86 linux

上面我们编译的时候打开了-DNCNN_BUILD_EXAMPLES=ON,所以这里就用编译好的例子试一下,由于squeezenet提供了权重文件,所以直接测这个。
在这里插入图片描述
把权重文件复制到build/examples里
在这里插入图片描述
我们可以看一下squeezent.cpp里,ncnn需要加载.bin和.param模型文件,所以放到同一文件夹。
在这里插入图片描述
然后运行

cd build/examples
./squeezenet ../../images/256-ncnn.png

在这里插入图片描述
运行成功!

2. 模型转换

这里用pytorch onnx来举例子,简单的模型可以用ncnn编译好的bin来直接转换。

1)onnx

在这里插入图片描述

./onnx2ncnn a.onnx a.param a.bin

这边的param就是模型的结构描述文件,bin是模型的具体权重
如果不行尝试使用onnx-simplifier先处理一下模型

onnxsim a.onnx a_sim.onnx

然后再转换a_sim.onnx,这里不多赘述,自行尝试。

2)pnnx

如果我们直接从onnx转换到ncnn的模型经常会出现不兼容不能完全转换的情况,所以我们这边直接使用pnnx来进行模型转换。

PyTorch Neural Network eXchange
pnnx github
PyTorch Neural Network eXchange(PNNX) is an open standard for PyTorch model interoperability. PNNX provides an open model format for PyTorch. It defines computation graph as well as high level operators strictly matches PyTorch.

我们以efficientnet为例
链接: https://github.com/lukemelas/EfficientNet-PyTorch

import torch
from torchsummary import summary
from efficientnet_pytorch import EfficientNet

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 初始化模型
model = EfficientNet.from_pretrained('efficientnet-b0', advprop=True, num_classes=10)
model.to(device)
# 加载训练好的权重
params_dict = torch.load(r"best.pth")
# 如果训练的时候是多卡使用DataParallel来训的需要用.module.state_dict()得到权重dict
model.load_state_dict(params_dict.module.state_dict())

summary(model, (3, 416, 416))
# efficientnet训练时候用了memory efficient swish激活,导出的时候换成普通swich提高兼容性
model.set_swish(memory_efficient=False)
# model_pt = torch.save(model_pt)

model.eval()

dummy_in = torch.randn(1, 3, 416, 416, requires_grad=True).to(device)
# 导出torchscript权重
mod = torch.jit.trace(model,dummy_in)
torch.jit.save(mod,"efb0_pnnx.pt")

安装pnnx然后使用pnnx转换我们刚才用TorchScript保存的pt文件

pip install pnnx
pnnx ./efb0_pnnx.pt inputshape=[1,3,416,416]

然后会生成一堆文件,我们需要的就是.param和.bin文件
在这里插入图片描述

3.在x86上加载推理模型

1)准备工作

编译好的ncnn(看第一步)
编译好的opencv(如果不想重新编译直接sudo apt install libopencv-dev)

2)编写C++推理代码

文件结构
demo
----CMakeList.txt
----1.jpg
----src
--------effcientnetb0.cpp
----build
----bin

#include <iostream>
#include "net.h"
#include <algorithm>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <stdio.h>


int main(int argc, char** argv)
{
    const char* img_path = argv[1];
	// opencv读取图片
    cv::Mat x = cv::imread(img_path, 1);
    cv::Mat m;
    // 训练也使用opencv指定插值方法进行resize,这边推理使用同样的方法
    cv::resize(x,m,(416,416),0,0,cv::INTER_LINEAR);
    // 千万不要用opencv做图像归一化,因为ncnn只能读入整型pixcel值
    // m = m / 255.0 ;
    if (m.empty())
    {
        fprintf(stderr, "cv::imread %s failed\n", img_path);
        return -1;
    }
	// 创建ncnn网络
    ncnn::Net efficientb0; 
    efficientb0.opt.use_vulkan_compute = true; 

	// 加载权重
    if (efficientb0.load_param("model_param/efficientb0/efb0_pnnx.ncnn.param"))
        exit(-1);
    if (efficientb0.load_model("model_param/efficientb0/efb0_pnnx.ncnn.bin"))
        exit(-1);
	
	// 把opencv mat的data矩阵加载到ncnn mat中准备作为推理的输入,建议用opencvresize好了输入
    ncnn::Mat in = ncnn::Mat::from_pixels(m.data, ncnn::Mat::PIXEL_BGR2RGB, m.cols, m.rows);
    //如果预处理只是简单的除以255,那么就直接按照下面的输入,后面详细说这个函数
    const float mean_vals[3] = {0, 0, 0};
    const float norm_vals[3] = {1/255.f, 1/255.f, 1/255.f};
    in.subtract_mean_normalize(mean_vals,norm_vals);
    ncnn::Extractor ex = efficientb0.create_extractor();
	//在.param文件中找到输入的节点名称in0
    ex.input("in0", in);  

    ncnn::Mat out;
    //在.param文件中找到输出的节点名称out0,推理结束
    ex.extract("out0", out); 

	//输出推理结果
    for (int i = 0; i < out.w; i++)
    {
        std::cout << i <<out[i] << std::endl;
    }
    return 0;
}

如何查看输入输出的名称如下图
在这里插入图片描述
在这里插入图片描述

3)编写Cmakelist编译

project(NCNN_DEMO)
cmake_minimum_required(VERSION 2.8.12)
set(CMAKE_BUILD_TYPE Debug)

set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} "改成第一步编译好的ncnn路径xxx/ncnn/build/install/")

find_package(OpenCV REQUIRED)
find_package(ncnn)
if(ncnn_FOUND)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)

    add_executable(efficientnetb0 src/efficientb0.cpp)
    target_link_libraries(efficientnetb0 ncnn ${OpenCV_LIBS})

else()
    message(WARNING "ncnn not found, please check CMAKE_PREFIX_PATH")
endif()   
# 创建build文件夹
cd build
cmake ..
make
//使用编译好的程序进行推理
./bin/effcientnetb0 ./1.jpg

输出结果
在这里插入图片描述可以看到linear层的推理结果输出了,选最大的一个index就是分类结果,至此,x86上全部的推理工作就做好了。

4.交叉编译MIPS

Arm交叉编译的文章太多,我用MIPS来讲一下,开发板用的是君正X2000做例子

1)编译OpenCV

参考这篇link

2)编译MIPS版本NCNN

这里我看一些网上的教程会先去编译protobuf然后再编译ncnn,如果不需要编译SIMPLEOCV等不需要编译protobuf,我直接按照官网龙芯的编译方法编译也没有问题,MSA可能会有bug,https://github.com/Tencent/ncnn/issues/5204,所以这边先关闭。
龙芯的编译方法参考 https://github.com/Tencent/ncnn/wiki/how-to-build#build-for-loongson-2k1000
唯一不同的是用-DCMAKE_TOOLCHAIN_FILE命令把厂商提供的编译环境导入进去,如果没有这个就用cmake-gui慢慢配吧,主要就是gcc和g++的路径还有一些flag的设置,参考
https://zhuanlan.zhihu.com/p/158734315
https://github.com/brightening-eyes/ncnn/commit/c5bb0e52ed9b33c3d23fbe419764ef1c7b274215

mkdir -p build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchains/mips32r2-linux-gnu.toolchain.cmake -DNCNN_DISABLE_RTTI=ON -DNCNN_DISABLE_EXCEPTION=ON -DNCNN_RUNTIME_CPU=OFF -DNCNN_MSA=OFF -DNCNN_MMI=ON -DNCNN_SIMPLEOCV=OFF ..
cmake --build . -j 8
cmake --build . --target install

最后得到静态库libncnn.a
在这里插入图片描述

5.量化推理MIPS

1)量化

通常如果嵌入式内存足够可以直接跑上述转换的param和bin,但嵌入式往往希望更小的memory footprint以及更快的速度,精度上可以些许让步,所以int8量化是通常会选择的加速方式。

这里我们采用后量化,先用ncnnoptimize优化模型

ncnn_compile/ncnn/build/install/bin/ncnnoptimize efb0_pnnx.ncnn.param efb0_pnnx.ncnn.bin efb0_pnnx_opt.ncnn.param efb0_pnnx_opt.ncnn.bin 0

然后创建images文件夹,把校准图像放进去,生成图像的路径列表

find images/ -type f > imagelist.txt

在这里插入图片描述
然后得到校准table文件,注意如果你做输入的normalize用不到均值和方差只需要除以255的话这里的值可以填成0 0 0和1/255 1/255 1/255,因为计算方法是 x = ( x / 255 − m e a n ) n o r m x=\frac{(x/255-mean)}{norm} x=normx/255mean ,等价与 x = ( x − m e a n ∗ 255 ) n o r m ∗ 255 x=\frac{(x-mean*255)}{norm*255} x=norm255xmean255,这里的mean就是归一化后mean*255,而这里的norm实际上是 1 n o r m ∗ 255 \frac{1}{norm*255} norm2551ncnn为了计算速度改成了乘法,所以如果训练的时候是直接除以255的那就设置成0和1/255,就等同于 ( x − 0 ) ∗ 1 255 = x 255 (x-0)*\frac{1}{255}=\frac{x}{255} x02551=255x,如果像imagenet这种使用了统计的mean和norm就设置成
( x − m e a n ) ∗ 1 n o r m ∗ 255 (x-mean)*\frac{1}{norm*255} xmeannorm2551

具体就是如果imagenet的mean和value是如下
img_mean = [0.485, 0.456, 0.406], img_std = [0.229, 0.224, 0.225]
那在ncnn这边就设置成
img_mean = {0.485*255.f, 0.456*255.f, 0.406*255.f}, img_std = {1/0.229/255.f, 1/0.224/255.f, 1/0.225/255.f}
其实也就等同于
img_mean = {0.485, 0.456, 0.406}, img_std = {1/0.229, 1/0.224, 1/0.225}

ncnn_compile/ncnn/build/install/bin/ncnn2table efb0_pnnx_opt.ncnn.param efb0_pnnx_opt.ncnn.bin imagelist.txt efb0_pnnx_opt.ncnn.table mean=[0,0,0] norm=[1,1,1] shape=[3,416,416] pixel=BGR thread=8 method=kl

在这里插入图片描述
然后使用table文件进行int8量化,得到新的int8的param和bin

cnn_compile/ncnn/build/install/bin/ncnn2int8 efb0_pnnx_opt.ncnn.param efb0_pnnx_opt.ncnn.bin efb0_pnnx_opt_int8.ncnn.param efb0_pnnx_opt_int8.ncnn.bin efb0_pnnx_opt.ncnn.table 

在这里插入图片描述

2)编译efficientnetb0用例

编写CMakeList.txt,注意set tools就用厂商提供的交叉编译工具,然后把刚刚交叉编译的opencv和ncnn加进去。
在这里插入图片描述
修改一下代码中param和bin的文件名称,然后编译

mkdir build
cd build
make

得到efficientnetb0_2运行程序

3)上板推理

把efficientnetb0_2,刚刚转换好的int8的param和bin以及opencv编译出来的libs上传到板子上
在板子上运行

export LD_LIBRARY_PATH=xxx/opencvlib
./efficientnetb0_2 1.jpg

运行成功!结果
在这里插入图片描述
用X86版本pytorch模型跑同一张图片
在这里插入图片描述
用x86版本TorchScrip模型跑同一张图片
在这里插入图片描述
结果基本一致!成功。

6.几个注意点

1)千万不要用opencv做图像归一化,因为ncnn的from_pixcel方法只能读入整型pixcel值

2)训练使用opencv指定插值方法进行resize,C++推理使用同样的方法,确保预处理方法一致,避免使用from_pixcel_resize方法,不同的resize算法可能导致算法性能下降

3)python也能调用ncnn的param和bin,先使用python测试pytorch模型和ncnn输出一致再调试C++代码。PNNX处理TorchScript的pt文件之后会生成python版本ncnn的示例代码,可以参考一下。如下图倒数第二个文件就是。

在这里插入图片描述

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值