NCNN的编译运行交叉编译
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=norm(x/255−mean) ,等价与
x
=
(
x
−
m
e
a
n
∗
255
)
n
o
r
m
∗
255
x=\frac{(x-mean*255)}{norm*255}
x=norm∗255(x−mean∗255),这里的mean就是归一化后mean*255,而这里的norm实际上是
1
n
o
r
m
∗
255
\frac{1}{norm*255}
norm∗2551ncnn为了计算速度改成了乘法,所以如果训练的时候是直接除以255的那就设置成0和1/255,就等同于
(
x
−
0
)
∗
1
255
=
x
255
(x-0)*\frac{1}{255}=\frac{x}{255}
(x−0)∗2551=255x,如果像imagenet这种使用了统计的mean和norm就设置成
(
x
−
m
e
a
n
)
∗
1
n
o
r
m
∗
255
(x-mean)*\frac{1}{norm*255}
(x−mean)∗norm∗2551,
具体就是如果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模型跑同一张图片
结果基本一致!成功。