ncnn提供了多种模型转换工具,可以快速的将caffe、onnx等模型一键转换为ncnn的格式,在源代码编译后这些工具存放在you_dir/ncnn/build/tools目录下。本次使用onnx2ncnn工具,把在pytorch导出的resnet18的onnx文件,转换为ncnn格式并完成推理。
.
├── bin
│ ├── onnx2ncnn
├── CMakeLists.txt
├── image
│ ├── dog.jpg
│ └── bus.jpg
├── model_param
├── python
│ ├── export_res18.py
└── src
├── resnet18.cpp
src目录下存放我们的源代码,python目录下存放python的脚本,model_param存放模型文件,image目录下存放推理用的图片,bin目录下存放可执行文件和项目生成的可执行文件。其中onnx2ncnn,是在you_dir/ncnn/build/tools/onnx/onnx2ncnn拷贝过来的。
import torch
import torchvision.models as models
import torch.onnx as onnx
# 加载预训练的ResNet-18模型
resnet = models.resnet18(pretrained=True)
# 将模型设置为评估模式
resnet.eval()
# 创建一个示例输入张量
dummy_input = torch.randn(1, 3, 224, 224)
# 使用torch.onnx.export函数导出模型为ONNX格式
onnx_file_path = "../model_param/resnet18.onnx"
onnx.export(resnet, dummy_input, onnx_file_path)
print("ResNet-18模型已成功导出为ONNX格式:", onnx_file_path) 上面是export_res18.py的代码,代码也很简单,就是给一个示例输入,然后跑一遍模型,相对应的图结构就会保存下来。运行代码后会在model_param/下生成一个resnet18.onnx的模型文件。
ncnn官方提供了模型转换工具,来将导出的onnx模型转换为ncnn支持的格式,所有模型转换的源代码都在ncnn/tools目录下,在编译后也同样会在build/tools/下生成对应的可执行程序。我们将you_dir/ncnn/build/tools/onnx/onnx2ncnn复制到我们的bin目录中来。
我们在项目根目录下执行:
bin/onnx2ncnn model_param/resnet18.onnx model_param/resnet18.param model_param/resnet18.bin 即使用onnx2ncnn工具,将resnet18.onnx转换为resnet18.param和resnet18.bin,其中resnet18.param为模型的参数信息(记录的是计算图的结构),resnet18.bin里存放的是模型的所有具体的参数。
我们可以看一看resnet18.param的参数
7767517
58 66
Input input.1 0 1 input.1
Convolution Conv_0 1 1 input.1 input.4 0=64 1=7 11=7 2=1 12=1 3=2 13=2 4=3 14=3 15=3 16=3 5=1 6=9408
ReLU Relu_1 1 1 input.4 onnx::MaxPool_125
Pooling MaxPool_2 1 1 onnx::MaxPool_125 input.8 0=0 1=3 11=3 2=2 12=2 3=1 13=1 14=1 15=1 5=1
Split splitncnn_0 1 2 input.8 input.8_splitncnn_0 input.8_splitncnn_1
Convolution Conv_3 1 1 input.8_splitncnn_1 input.16 0=64 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=36864
ReLU Relu_4 1 1 input.16 onnx::Conv_129
Convolution Conv_5 1 1 onnx::Conv_129 onnx::Add_198 0=64 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=36864
BinaryOp Add_6 2 1 onnx::Add_198 input.8_splitncnn_0 onnx::Relu_132 0=0
ReLU Relu_7 1 1 onnx::Relu_132 input.24
Split splitncnn_1 1 2 input.24 input.24_splitncnn_0 input.24_splitncnn_1
Convolution Conv_8 1 1 input.24_splitncnn_1 input.32 0=64 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=36864
ReLU Relu_9 1 1 input.32 onnx::Conv_136
Convolution Conv_10 1 1 onnx::Conv_136 onnx::Add_204 0=64 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=36864
BinaryOp Add_11 2 1 onnx::Add_204 input.24_splitncnn_0 onnx::Relu_139 0=0
ReLU Relu_12 1 1 onnx::Relu_139 input.40
Split splitncnn_2 1 2 input.40 input.40_splitncnn_0 input.40_splitncnn_1
Convolution Conv_13 1 1 input.40_splitncnn_1 input.48 0=128 1=3 11=3 2=1 12=1 3=2 13=2 4=1 14=1 15=1 16=1 5=1 6=73728
ReLU Relu_14 1 1 input.48 onnx::Conv_143
Convolution Conv_15 1 1 onnx::Conv_143 onnx::Add_210 0=128 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=147456
Convolution Conv_16 1 1 input.40_splitncnn_0 onnx::Add_213 0=128 1=1 11=1 2=1 12=1 3=2 13=2 4=0 14=0 15=0 16=0 5=1 6=8192
BinaryOp Add_17 2 1 onnx::Add_210 onnx::Add_213 onnx::Relu_148 0=0
ReLU Relu_18 1 1 onnx::Relu_148 input.60
Split splitncnn_3 1 2 input.60 input.60_splitncnn_0 input.60_splitncnn_1
Convolution Conv_19 1 1 input.60_splitncnn_1 input.68 0=128 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=147456
ReLU Relu_20 1 1 input.68 onnx::Conv_152
Convolution Conv_21 1 1 onnx::Conv_152 onnx::Add_219 0=128 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=147456
BinaryOp Add_22 2 1 onnx::Add_219 input.60_splitncnn_0 onnx::Relu_155 0=0
ReLU Relu_23 1 1 onnx::Relu_155 input.76
Split splitncnn_4 1 2 input.76 input.76_splitncnn_0 input.76_splitncnn_1
Convolution Conv_24 1 1 input.76_splitncnn_1 input.84 0=256 1=3 11=3 2=1 12=1 3=2 13=2 4=1 14=1 15=1 16=1 5=1 6=294912
ReLU Relu_25 1 1 input.84 onnx::Conv_159
Convolution Conv_26 1 1 onnx::Conv_159 onnx::Add_225 0=256 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=589824
Convolution Conv_27 1 1 input.76_splitncnn_0 onnx::Add_228 0=256 1=1 11=1 2=1 12=1 3=2 13=2 4=0 14=0 15=0 16=0 5=1 6=32768
BinaryOp Add_28 2 1 onnx::Add_225 onnx::Add_228 onnx::Relu_164 0=0
ReLU Relu_29 1 1 onnx::Relu_164 input.96
Split splitncnn_5 1 2 input.96 input.96_splitncnn_0 input.96_splitncnn_1
Convolution Conv_30 1 1 input.96_splitncnn_1 input.104 0=256 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=589824
ReLU Relu_31 1 1 input.104 onnx::Conv_168
Convolution Conv_32 1 1 onnx::Conv_168 onnx::Add_234 0=256 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=589824
BinaryOp Add_33 2 1 onnx::Add_234 input.96_splitncnn_0 onnx::Relu_171 0=0
ReLU Relu_34 1 1 onnx::Relu_171 input.112
Split splitncnn_6 1 2 input.112 input.112_splitncnn_0 input.112_splitncnn_1
Convolution Conv_35 1 1 input.112_splitncnn_1 input.120 0=512 1=3 11=3 2=1 12=1 3=2 13=2 4=1 14=1 15=1 16=1 5=1 6=1179648
ReLU Relu_36 1 1 input.120 onnx::Conv_175
Convolution Conv_37 1 1 onnx::Conv_175 onnx::Add_240 0=512 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=2359296
Convolution Conv_38 1 1 input.112_splitncnn_0 onnx::Add_243 0=512 1=1 11=1 2=1 12=1 3=2 13=2 4=0 14=0 15=0 16=0 5=1 6=131072
BinaryOp Add_39 2 1 onnx::Add_240 onnx::Add_243 onnx::Relu_180 0=0
ReLU Relu_40 1 1 onnx::Relu_180 input.132
Split splitncnn_7 1 2 input.132 input.132_splitncnn_0 input.132_splitncnn_1
Convolution Conv_41 1 1 input.132_splitncnn_1 input.140 0=512 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=2359296
ReLU Relu_42 1 1 input.140 onnx::Conv_184
Convolution Conv_43 1 1 onnx::Conv_184 onnx::Add_249 0=512 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=2359296
BinaryOp Add_44 2 1 onnx::Add_249 input.132_splitncnn_0 onnx::Relu_187 0=0
ReLU Relu_45 1 1 onnx::Relu_187 input.148
Pooling GlobalAveragePool_46 1 1 input.148 onnx::Flatten_189 0=1 4=1
Flatten Flatten_47 1 1 onnx::Flatten_189 onnx::Gemm_190
InnerProduct Gemm_48 1 1 onnx::Gemm_190 191 0=1000 1=1 2=512000
简单的和大家分析一下怎么看这个参数。首先7767517是一个magic数,表明这是ncnn的格式。58 66分别是layer和blob的个数。可能很多初学者分不清layer和blob的区别,我在这里为大家简单介绍一下
我们使用netron打开resnet18.param可以看到resnet18的结构,其中像Convolution,ReLU,Pooling,Split,BinaryOp都是一个算子也就是layer。blob可以看作是中间数据的存储,以Split算子为例,它有1个输入2个输出,则一共有3个blob,像Convolution和Relu等算子它输入输出都是1个blob。所以一般情况下blob数看到回比layer数多的多。
回到参数当中来:
Convolution Conv_0 1 1 input.1 input.4 0=64 1=7 11=7 2=1 12=1 3=2 13=2 4=3 14=3 15=3 16=3 5=1 6=9408
Convolution:layer类型
Conv_0:layer的名字
input.1:输入blob的名字
input.4:输出blob的名字
0=64 1=7 11=7 2=1 12=1 3=2 13=2 4=3 14=3 15=3 16=3 5=1 6=9408:layer的参数信息
其实对ncnn的使用者来说,我们主要需要关注的是整个模型的输入输出。对于当前这个网络来说,整个网络的输入blob名字是input.1,输出blob是191,这些信息我们在写推理代码的时候回用到。
#include "net.h"
#include <algorithm>
#if defined(USE_NCNN_SIMPLEOCV)
#include "simpleocv.h"
#else
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#endif
#include <stdio.h>
#include <vector>
static int detect_resnet18(const cv::Mat& bgr, std::vector<float>& cls_scores)
{
ncnn::Net resnet18;
resnet18.opt.use_vulkan_compute = true;
//分别加载模型的参数和数据
if (resnet18.load_param("model_param/resnet18.param"))
exit(-1);
if (resnet18.load_model("model_param/resnet18.bin"))
exit(-1);
//opencv读取图片是BGR格式,我们需要转换为RGB格式
ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, bgr.cols, bgr.rows, 224, 224);
//图像归一标准化,以R通道为例(x/225-0.485)/0.229,化简后可以得到下面的式子
//需要注意的式substract_mean_normalize里的标准差其实是标准差的倒数,这样在算的时候就可以将除法转换为乘法计算
//所以norm_vals里用的是1除
const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
in.substract_mean_normalize(mean_vals, norm_vals);
ncnn::Extractor ex = resnet18.create_extractor();
//把图像数据放入input.1这个blob里
ex.input("input.1", in);
ncnn::Mat out;
//提取出推理结果,推理结果存放在191这个blob里
ex.extract("191", out);
cls_scores.resize(out.w);
for (int j = 0; j < out.w; j++)
{
cls_scores[j] = out[j];
}
return 0;
}
static int print_topk(const std::vector<float>& cls_scores, int topk)
{
// partial sort topk with index
int size = cls_scores.size();
std::vector<std::pair<float, int> > vec;
vec.resize(size);
for (int i = 0; i < size; i++)
{
vec[i] = std::make_pair(cls_scores[i], i);
}
std::partial_sort(vec.begin(), vec.begin() + topk, vec.end(),
std::greater<std::pair<float, int> >());
// print topk and score
for (int i = 0; i < topk; i++)
{
float score = vec[i].first;
int index = vec[i].second;
fprintf(stderr, "%d = %f\n", index, score);
}
return 0;
}
int main(int argc, char** argv)
{
if (argc != 2)
{
fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
return -1;
}
const char* imagepath = argv[1];
//使用opencv读取图片
cv::Mat m = cv::imread(imagepath, 1);
if (m.empty())
{
fprintf(stderr, "cv::imread %s failed\n", imagepath);
return -1;
}
std::vector<float> cls_scores;
detect_resnet18(m, cls_scores);
//打印得分前三的类别
print_topk(cls_scores, 3);
return 0;
}
推理代码主要参照ncnn/examples/squeezenet.cpp编写,这里把代码需要注意的地方给大家解释一下
ncnn::Net resnet18;
resnet18.opt.use_vulkan_compute = true;
//分别加载模型的参数和数据
if (resnet18.load_param("model_param/resnet18.param"))
exit(-1);
if (resnet18.load_model(""))
exit(-1);
//opencv读取图片是BGR格式,我们需要转换为RGB格式
ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, bgr.cols, bgr.rows, 224, 224); 创建一个Net,再加载模型的参数和数据,model_param/resnet18.param和model_param/resnet18.bin就是我们使用onnx2ncnn工具将resnet18.onnx转换出来的。Opencv读取图片默认是BGR格式,我们需要转换成RGB格式。
const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
in.substract_mean_normalize(mean_vals, norm_vals); imagenet图片三通道的均值和标准差分别是mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]。以R通道为例,原始图片的像素值是从0到255,所以像素值归一化即像x/255,减去均值再除以标准差就是(x/255-0.485)/0.299,把255乘下去也就是(x-0.485×255)/255×0.299。如果把归一化和标准化一起处理的话,等价均值就是0.485×255,等价标准差就是255×0.299。但由于substract_mean_normalize里的标准差实际是标准差的倒数,这样可以把除法转换为乘法来计算加快效率,所以这里norm_vals用的是标准差的倒数。
ncnn::Extractor ex = resnet18.create_extractor();
//把图像数据放入input.1这个blob里
ex.input("input.1", in);
ncnn::Mat out;
//提取出推理结果,推理结果存放在191这个blob里
ex.extract("191", out); 创建提取类,把输入放在input.1这个blob里面,再提取191这个blob的值放在out里。我们前面提到过整个网络的输入输出就是input.1和191。最后我们就可以使用print_topk打印出分数前三高的类别。
project(NCNN_DEMO)
cmake_minimum_required(VERSION 2.8.12)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} "/home/hupeng/code/github/ncnn/build/install")
find_package(ncnn REQUIRED)
find_package(OpenCV REQUIRED)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
add_executable(resnet18 src/resnet18.cpp)
target_link_libraries(resnet18 ncnn ${OpenCV_LIBS}) set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} "/home/hupeng/code/github/ncnn/build/install")是设置了ncnn库的搜索路径,因为我在编译安装ncnn的时候不是安装在系统目录下,所有这里要知名一下,set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)设置了一下可执行文件的输出路径是在bin目录下,
target_link_libraries(resnet18 ncnn ${OpenCV_LIBS})链接到ncnn和opencv库。
