ncnn源码阅读笔记(二)
Net
使用ncnn部署模型时,我们要先定义一个Net对象,然后使用load_param和load_model载入模型结构参数和模型权重参数
ncnn::Net xxxnet; //声明一个Net对象
xxxnet.load_param(xxx.param); //载入模型结构参数文件
xxxnet.load_model(xxx.bin); //载入模型权重参数文件
在src/目录下有一个net.h和net.cpp,这就是Net类的定义。其中包含了vulkan的代码,上一篇介绍过vulkan主要时用来做计算加速的,这里先暂时剔除valkan的相关代码,来看原始的代码。在#if NCNN_VULKAN和#endif // NCNN_VULKAN之间的代码都是vulkan相关的代码。剔除vulkan相关代码和头文件后,来看net.h的源码,有两个class,Net和Extractor,先来看Net:
namespace ncnn {
class Extractor;
class Net
{
public:
// empty init
//空构造函数
Net();
// clear and destroy
//析构函数
~Net();
public:
// option can be changed before loading
//Option对象是用于load参数之前传入基本设置,比如线程数等
Option opt;
/*这一块用于用于注册用户自定义的layer,可以先略过不看,主要是两个注册自定义layer的函数,一个是传入命名自定义,一个是传入索引自定义
#if NCNN_STRING
// register custom layer by layer type name
// return 0 if success
int register_custom_layer(const char* type, layer_creator_func creator);
#endif // NCNN_STRING
// register custom layer by layer type
// return 0 if success
int register_custom_layer(int index, layer_creator_func creator);
*/
//接下来是多态的两个函数load_param和load_model
#if NCNN_STDIO
#if NCNN_STRING
// load network structure from plain param file
// return 0 if success
//通过文本文件加载网络结构
int load_param(FILE* fp);
//通过路径加载网络结构
int load_param(const char* protopath);
//通过内存加载网络结构
int load_param_mem(const char* mem);
#endif // NCNN_STRING
// load network structure from binary param file
// return 0 if success
//通过二进制文件加载网络结构
int load_param_bin(FILE* fp);
int load_param_bin(const char* protopath);
// load network weight data from model file
// return 0 if success
//通过model文件加载网络权重
int load_model(FILE* fp);
int load_model(const char* modelpath);
#endif // NCNN_STDIO
// load network structure from external memory
// memory pointer must be 32-bit aligned
// return bytes consumed
//通过外置内存加载网络结构
int load_param(const unsigned char* mem);
// reference network weight data from external memory
// weight data is not copied but referenced
// so external memory should be retained when used
// memory pointer must be 32-bit aligned
// return bytes consumed
//通过外置内存加载网络权重
int load_model(const unsigned char* mem);
// unload network structure and weight data
//清除网络结构和网络权重
void clear();
// construct an Extractor from network
//在Net内构建一个Extractor对象
Extractor create_extractor() const;
protected:
// parse the structure of network
// fuse int8 op dequantize and quantize by requantize
//重置网络,用于重用网络
int fuse_network();
//友元类,主要作用让Extractor对象可以访问Net对象中的私有和保护的属性和函数
friend class Extractor;
#if NCNN_STRING
//通过name查找对应的blob索引
int find_blob_index_by_name(const char* name) const;
//通过name查找对应的layer索引
int find_layer_index_by_name(const char* name) const;
//通过layer类型查找索引
int custom_layer_to_index(const char* type);
//根据类型创建layer
Layer* create_custom_layer(const char* type);
#endif // NCNN_STRING
//根据索引创建layer
Layer* create_custom_layer(int index);
//前向推理层
int forward_layer(int layer_index, std::vector<Mat>& blob_mats, Option& opt) const;
protected:
//用于存储网络的blob的vector
std::vector<Blob> blobs;
//用于存储网络的layer的vector
std::vector<Layer*> layers;
用于存储注册的layer
std::vector<layer_registry_entry> custom_layer_registry;
};
load_param
看完了net.h,根据在上一篇最后的说的阅读顺序,我们来看看其中的函数实现,首先来看load_param函数,在net.cpp中,主要来看最常用的通过路径加载网络结构的load_param
int Net::load_param(const char* protopath)
{
//打开网络结构文件
FILE* fp = fopen(protopath, "rb");
if (!fp)//如果打开失败
{
fprintf(stderr, "fopen %s failed\n", protopath);
return -1;
}
//最终还是调用int Net::load_param(FILE* fp)
int ret = load_param(fp);
//关闭文件
fclose(fp);
return ret;
}
可以看到最终调用的还是int Net::load_param(FILE* fp),所以就来看这个函数,这个函数代码行数有点多,我们分段来阅读:
1)fp参数
首先看看传进来的参数是什么,传进来的参数就是我们的xxx.param文件里的内容,如下(以=LeNet的网络结构为例):
7767517
9 9
Input data 0 1 data 0=28 1=28 2=1
Convolution conv1 1 1 data conv1 0=20 1=5 2=1 3=1 4=0 5=1 6=500
Pooling pool1 1 1 conv1 pool1 0=0 1=2 2=2 3=0 4=0
Convolution conv2 1 1 pool1 conv2 0=50 1=5 2=1 3=1 4=0 5=1 6=25000
Pooling pool2 1 1 conv2 pool2 0=0 1=2 2=2 3=0 4=0
InnerProduct ip1 1 1 pool2 ip1 0=500 1=1 2=400000
ReLU relu1 1 1 ip1 ip1_relu1
InnerProduct ip2 1 1 ip1_relu1 ip2 0=10 1=1 2=5000
Softmax prob 1 1 ip2 prob 0=0
2)xxx.param第一行,magic数
先来看xxx.param文件的第一行,跟Java的magic数类似,ncnn也有一个magic数7767517,这个magic数的作用是确定读进来的xxx.param文件是最新版本的
int magic = 0;
//读取第一行的magic数
int nbr = fscanf(fp, "%d", &magic);
if (nbr != 1)
{
fprintf(stderr, "issue with param file\n");
return -1;
}
if (magic != 7767517)
{
fprintf(stderr, "param is too old, please regenerate\n");
return -1;
}
3)xxx.param第二行,网络的layer层数及blob数
// parse
int layer_count = 0;
int blob_count = 0;
//读取第二行的9 9,layer层数和blob数
nbr = fscanf(fp, "%d %d", &layer_count, &blob_count);
//读取失败
if (nbr != 2 || layer_count <= 0 || blob_count <= 0)
{
fprintf(stderr, "issue with param file\n");
return -1;
}
//读取成功则把在Net对象中用来存储layer和blob的两个vector resize出来
layers.resize((size_t)layer_count);
blobs.resize((size_t)blob_count);
4)xxx.param第三行到最后一行,解析网络结构的每一层
从第三行开始是网络结构的层,每一行都有7类元素
- 层类型
- 层名称
- 输入数据结构数量(bottom blob)
- 输出数据结构数量(top blob)
- 网络输入层名(一个或多个)
- 网络输出层名(一个或多个)
- 特殊参数(0个或多个): 一种是k=v的类型;另一种是k=len,v1,v2,v3….(数组类型)。该层在ncnn中是存放到paramDict结构中,不同类型层,各种参数意义不一样。
以下面这层卷积层为例:
Convolution conv1 1 1 data conv1 0=20 1=5 2=1 3=1 4=0 5=1 6=500
//层类型:Convolution 层名称:conv1 bottom blob:1 top blob:1 网络输入层名:data 网络输出层名:conv1 特殊参数:0=20 1=5 2=1 3=1 4=0 5=1 6=500
这里给出不同层类型的对应特殊参数的对照表https://github.com/Tencent/ncnn/wiki/operation-param-weight-table
理论解析完层,再来看代码:
//特殊参数存放的数据结构
ParamDict pd;
//初始化blob的索引
int blob_index = 0;
//遍历每一层
for (int i=0; i<layer_count; i++)
{
int nscan = 0;
//用来存储layer的类型
char layer_type[257];
//用来存储layer的名称
char layer_name[257];
//用来存储输入数据结构数量(bottom blob)
int bottom_count = 0;
//用来存储输出数据结构数量(top blob)
int top_count = 0;
//读入网络结构的层的type,name,输入bottom数和输出top数目
nscan = fscanf(fp, "%256s %256s %d %d", layer_type, layer_name, &bottom_count, &top_count);
每层读前面四个数是否成功
if (nscan != 4)
{
continue;
}
//创建layer
Layer* layer = create_layer(layer_type);
//如果layer不是默认类型,创建自定义layer
if (!layer)
{
layer = create_custom_layer(layer_type);
}
//如果自定义layer没有注册过
if (!layer)
{
fprintf(stderr, "layer %s not exists or registered\n", layer_type);
clear();
return -1;
}
//把读入的layer的类型和名称赋值给创建的layer对象
layer->type = std::string(layer_type);
layer->name = std::string(layer_name);
//根据读入的bottom blob的数量resize layer的输入数据结构
layer->bottoms.resize(bottom_count);
//解析layer的输入
for (int j=0; j<bottom_count; j++)
{
//用来存储bottom的名字
char bottom_name[257];
//读入botoom的名字
nscan = fscanf(fp, "%256s", bottom_name);
if (nscan != 1)
{
continue;
}
//Net对象的函数,通过bottom的名字查找对应的blob 的索引
int bottom_blob_index = find_blob_index_by_name(bottom_name);
//如果没有找到,则向blobs的vector中插入一个名为bottom_name的blob
if (bottom_blob_index == -1)
{
//设置第“index索引”个blob的参数
Blob& blob = blobs[blob_index];
//blob的索引
bottom_blob_index = blob_index;
//blob的名字
blob.name = std::string(bottom_name);
// fprintf(stderr, "new blob %s\n", bottom_name);
//更新blob索引
blob_index++;
}
//设置当前blob的参数
Blob& blob = blobs[bottom_blob_index];
//第i层以当前blob作为层的输入
blob.consumers.push_back(i);
//第i层的输入数据结构的第j个输入
layer->bottoms[j] = bottom_blob_index;
}
//输出数据结构的初始化基本和输入数据结构的初始化相同
//解析layer的输入
layer->tops.resize(top_count);
for (int j=0; j<top_count; j++)
{
Blob& blob = blobs[blob_index];
//用来存储top的名字
char blob_name[257];
//读入top的名字
nscan = fscanf(fp, "%256s", blob_name);
if (nscan != 1)
{
continue;
}
//设置输出blob对应的名字
blob.name = std::string(blob_name);
//设置这个blob的生产者,即输出这个blob的层索引
blob.producer = i;
//设置第i层输出数据结构的第j个输入
layer->tops[j] = blob_index;
//更新blob索引
blob_index++;
}
// layer specific params
//用ParamDict 对象接收xxx.param第三行以后的每一行后面的特殊参数
// 一种是k=v的类型;另一种是k=len,v1,v2,v3….(数组类型)。该层在ncnn中是存放到paramDict结构中,不同类型层,各种参数意义不一样。
int pdlr = pd.load_param(fp);
if (pdlr != 0)
{
fprintf(stderr, "ParamDict load_param failed\n");
continue;
}
//传递给对应layer对象
int lr = layer->load_param(pd);
if (lr != 0)
{
fprintf(stderr, "layer load_param failed\n");
continue;
}
//把解析初始化好的layer对象放入Net对象的layer的vector中
layers[i] = layer;
}