本文主要講解ncnn模型加載的三種方式,模型以上文(https://blog.csdn.net/Enchanted_ZhouH/article/details/105861646)的resnet18模型爲示例,模型文件如下:
resnet18.param //模型結構文件
resnet18.bin //模型參數文件
第一種方式:直接加載param和bin
最簡單的方式爲直接加載param和bin文件,適合快速測試模型效果,加載模型代碼如下:
ncnn::Net net;
net.load_param("resnet18.param");
net.load_model("resnet18.bin");
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
模型的輸入爲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);
測試一張圖片,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;
}
運行結果如下:
predicted class: 588, predicted value: 154.039322
第二種方式:加載二進制的 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
生成三個文件如下:
resnet18.param.bin //二進制的模型結構文件
resnet18.id.h //模型結構頭文件
resnet18.mem.h //模型參數頭文件
param.bin不是明文的,沒有可見字符串,適合模型的發佈,加載模型代碼如下:
ncnn::Net net;
net.load_param_bin("resnet18.param.bin");
net.load_model("resnet18.bin");
由於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
如上可見,模型的輸入爲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);
同理,整體預測代碼如下:
#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;
}
運行結果如下:
predicted class: 588, predicted value: 154.039322
第三種方式:從內存加載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);
定義輸入和輸出的代碼和第二種方式保持一致,如下:
#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);
整體預測代碼如下:
#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;
}
運行結果如下:
predicted class: 588, predicted value: 154.039322
至此,三種ncnn模型加載方式全部介紹完畢,做個簡單的總結如下:
1. 直接加載ncnn模型可以快速測試模型效果,但是param是明文的,打開文件可以直接看到模型結構,不利於加密;
2. 將param文件轉爲二進制,可以起到一定的加密作用;
3. 將模型結構和參數直接讀進內存,和代碼整合在一起,利於打包發佈,只需提供一個打包好的庫即可(.so/.dll等),不用將模型文件單獨拷貝到部署機器上,大大方便了算法的部署以及加密。
參考資料:https://github.com/Tencent/ncnn/wiki/use-ncnn-with-alexnet.zh