ncnn模型加载的三种方式

https://blog.csdn.net/enchanted_zhouh/article/details/106063552

      本文主要讲解ncnn模型加载的三种方式,模型以上文(https://blog.csdn.net/Enchanted_ZhouH/article/details/105861646)的resnet18模型为示例,模型文件如下:

resnet18.param    //模型结构文件
resnet18.bin     //模型参数文件
  • 1
  • 2
       第一种方式:直接加载param和bin

       最简单的方式为直接加载param和bin文件,适合快速测试模型效果,加载模型代码如下:

ncnn::Net net;
net.load_param("resnet18.param");
net.load_model("resnet18.bin");
  • 1
  • 2
  • 3

       param为模型文件,打开如下:

7767517
78 86
Input            x                        0 1 x
Convolution      123                      1 1 x 123 0=64 1=7 11=7 2=1 12=1 3=2 13=2 
                                .
                                .
                                .
Flatten          190                      1 1 189 190
InnerProduct     y                        1 1 190 y 0=1000 1=1 2=512000
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

       模型的输入为x,输出为y。因此,定义输入和输出的代码如下:

ncnn::Mat in;
ncnn::Mat out;
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.set_num_threads(4);
ex.input("x", in);
ex.extract("y", out);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

       测试一张图片,test.jpg换成自己的图片即可,整体代码如下:

#include <opencv2/highgui/highgui.hpp>
#include <vector>
#include "net.h"

using namespace std;

int main()
{
	cv::Mat img = cv::imread("test.jpg");
	int w = img.cols;
	int h = img.rows;
	ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_BGR, w, h, 224, 224);
	
	ncnn::Net net;
	net.load_param("resnet18.param");
	net.load_model("resnet18.bin");
	ncnn::Extractor ex = net.create_extractor();
	ex.set_light_mode(true);
	ex.set_num_threads(4);

	ncnn::Mat out;
	ex.input("x", in);
	ex.extract("y", out);

	ncnn::Mat out_flattened = out.reshape(out.w * out.h * out.c);
	vector<float> score;
	score.resize(out_flattened.w);
	for (int i = 0; i < out_flattened.w; ++i) {
		score[i] = out_flattened[i];
	}
	vector<float>::iterator max_id = max_element(score.begin(), score.end());
	printf("predicted class: %d, predicted value: %f", max_id - score.begin(), score[max_id - score.begin()]);

	net.clear();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

       运行结果如下:

predicted class: 588, predicted value: 154.039322
  • 1
       第二种方式:加载二进制的 param.bin 和 bin

       第一种方式有个明显的问题,param模型文件是明文的,如果直接发布出去,任何使用者都可以窥探到模型结构,这很不利于加密工作的进行。

       因此,引出第二种模型加载方式,将param转换成二进制文件,ncnn编译好后,tools里有个ncnn2mem工具,使用此工具可以生成param.bin、id.h和mem.h三个文件,命令如下:

ncnn2mem resnet18.param resnet18.bin resnet18.id.h resnet18.mem.h
  • 1

       生成三个文件如下:

resnet18.param.bin    //二进制的模型结构文件
resnet18.id.h        //模型结构头文件
resnet18.mem.h       //模型参数头文件
  • 1
  • 2
  • 3

       param.bin不是明文的,没有可见字符串,适合模型的发布,加载模型代码如下:

ncnn::Net net;
net.load_param_bin("resnet18.param.bin");
net.load_model("resnet18.bin");
  • 1
  • 2
  • 3

       由于param.bin窥探不到模型结构,因此,需要导入id.h头文件来获取模型的输入和输出,resnet18.id.h文件如下:

namespace resnet18_param_id {
const int LAYER_x = 0;
const int BLOB_x = 0;
        .
        .
        .
const int LAYER_y = 77;
const int BLOB_y = 85;
} // namespace resnet18_param_id
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

       如上可见,模型的输入为resnet18_param_id::BLOB_x,输出为resnet18_param_id::BLOB_y,定义输入和输出的代码如下:

#include "resnet18.id.h"
ncnn::Mat in;
ncnn::Mat out;
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.set_num_threads(4);
ex.input(resnet18_param_id::BLOB_x, in);
ex.extract(resnet18_param_id::BLOB_y, out);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

       同理,整体预测代码如下:

#include <opencv2/highgui/highgui.hpp>
#include <vector>
#include "net.h"
#include "resnet18.id.h"

using namespace std;

int main()
{
	cv::Mat img = cv::imread("test.jpg");
	int w = img.cols;
	int h = img.rows;
	ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_BGR, w, h, 224, 224);
	
	ncnn::Net net;
	net.load_param_bin("resnet18.param.bin");
	net.load_model("resnet18.bin");
	ncnn::Extractor ex = net.create_extractor();
	ex.set_light_mode(true);
	ex.set_num_threads(4);

	ncnn::Mat out;
	ex.input(resnet18_param_id::BLOB_x, in);
	ex.extract(resnet18_param_id::BLOB_y, out);

	ncnn::Mat out_flattened = out.reshape(out.w * out.h * out.c);
	vector<float> score;
	score.resize(out_flattened.w);
	for (int i = 0; i < out_flattened.w; ++i) {
		score[i] = out_flattened[i];
	}
	vector<float>::iterator max_id = max_element(score.begin(), score.end());
	printf("predicted class: %d, predicted value: %f", max_id - score.begin(), score[max_id - score.begin()]);

	net.clear();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

       运行结果如下:

predicted class: 588, predicted value: 154.039322
  • 1
       第三种方式:从内存加载param 和 bin

       虽然第二种模型加载方式可以避免模型结构的泄露,但是模型和代码还是处于分离状态的,如果将其打包发布,那么模型文件需要独立出来进行打包。

       打个比方,如果写了一个算法的.so接口供前端调用,不仅需要发布.so文件,还需要发布model文件,这样着实不太方便。如若可以将model一起打包进.so接口,直接将包含了代码和模型的.so扔给前端人员调用,这样更为便利。

       第二种方式中,已经生成了id.h和mem.h两个头文件,此处,只需要这两个头文件即可,不需要再调用param和bin文件。

       从内存加载模型的代码如下:

#include "resnet18.mem.h"
ncnn::Net net;
net.load_param(resnet18_param_bin);
net.load_model(resnet18_bin);
  • 1
  • 2
  • 3
  • 4

       定义输入和输出的代码和第二种方式保持一致,如下:

#include "resnet18.id.h"
ncnn::Mat in;
ncnn::Mat out;
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.set_num_threads(4);
ex.input(resnet18_param_id::BLOB_x, in);
ex.extract(resnet18_param_id::BLOB_y, out);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

       整体预测代码如下:

#include <opencv2/highgui/highgui.hpp>
#include <vector>
#include "net.h"
#include "resnet18.id.h"
#include "resnet18.mem.h"

using namespace std;

int main()
{
	cv::Mat img = cv::imread("test.jpg");
	int w = img.cols;
	int h = img.rows;
	ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_BGR, w, h, 224, 224);
	
	ncnn::Net net;
	net.load_param(resnet18_param_bin);
	net.load_model(resnet18_bin);
	ncnn::Extractor ex = net.create_extractor();
	ex.set_light_mode(true);
	ex.set_num_threads(4);

	ncnn::Mat out;
	ex.input(resnet18_param_id::BLOB_x, in);
	ex.extract(resnet18_param_id::BLOB_y, out);

	ncnn::Mat out_flattened = out.reshape(out.w * out.h * out.c);
	vector<float> score;
	score.resize(out_flattened.w);
	for (int i = 0; i < out_flattened.w; ++i) {
		score[i] = out_flattened[i];
	}
	vector<float>::iterator max_id = max_element(score.begin(), score.end());
	printf("predicted class: %d, predicted value: %f", max_id - score.begin(), score[max_id - score.begin()]);

	net.clear();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

       运行结果如下:

predicted class: 588, predicted value: 154.039322
  • 1

       至此,三种ncnn模型加载方式全部介绍完毕,做个简单的总结如下:

       1. 直接加载ncnn模型可以快速测试模型效果,但是param是明文的,打开文件可以直接看到模型结构,不利于加密;

       2. 将param文件转为二进制,可以起到一定的加密作用;

       3. 将模型结构和参数直接读进内存,和代码整合在一起,利于打包发布,只需提供一个打包好的库即可(.so/.dll等),不用将模型文件单独拷贝到部署机器上,大大方便了算法的部署以及加密。

       参考资料:https://github.com/Tencent/ncnn/wiki/use-ncnn-with-alexnet.zh

原文地址:https://www.cnblogs.com/shuimuqingyang/p/13958561.html