ncnn源码分析_1
ncnn 源码分析 — 参数与模型载入
1. 实例代码
使用ncnn进行前向计算的步骤很简单,就如下几行代码即可完成。
// 代码来自 ncnn/examples/shufflenetv2.cpp
/* Step 1.1 : 加载.parma 文件 和 .bin 文件 */
ncnn::Net shufflenetv2;
shufflenetv2.load_param("shufflenet_v2_x0.5.param");
shufflenetv2.load_model("shufflenet_v2_x0.5.bin");
/* Step 1.2 : 构建并配置 提取器 */
ncnn::Extractor ex = shufflenetv2.create_extractor();
/* Step 1.3 : 设置输入(将图片转换成ncnn::Mat结构作为输入) */
ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR, bgr.cols, bgr.rows, 224, 224);
const float norm_vals[3] = {1/255.f, 1/255.f, 1/255.f};
in.substract_mean_normalize(0, norm_vals);
ex.input("data", in);
/* Step 1.4 : 提取输出 */
ncnn::Mat out;
ex.extract("fc", out);
2. 代码分析
我姑且将其分为:加载模型、构建并配置提取器、设置输入、输出处理、模型封装五个部分来加以分析
模型载入:
相关代码:
net.h/cpp blob.h/cpp layer.h/.cpp
paramdict.cpp/h
modelbin.h/.cpp
相关文件:
xx.bin xx.param
3. 加载模型
ncnn 在使用 .param 和 .bin 两个文件来描述一个神经网络模型。 模型加载的根本目的是将 .param 和 .bin 文件的信息加载到目标神经网络(一个ncnn::Net结构)中
其中:
.param:描述神经网络的结构,包括层名称,层输入输出信息,层参数信息(如卷积层的kernal大小等)等。
.bin 文件则记录神经网络运算所需要的数据信息(比如卷积层的权重、偏置信息等)
3.1 param 文件
一个.param文件由以下几部分组成:
1)MagicNum
固定位7767517,可以通过这个数来判定版本 -> 为什么这个数字,不知道问倪神去吧
2)layer、blob个数
上图示例的文件两个数字分别为:75、83
layer:我们知道神经网络是一层一层向前推进计算的,每一层我们用一个layer表示;
blob:每一个layer都可能会有输入、输出,在ncnn中,它们统一用一个多维(3维)向量表示,我们称每一个输入、输出的原子为一个blob,并为它起名
2.1.1.2 layer的描述
layer 在 .param 中是一个相对复杂的元素(从第3行起的每一行描述一个layer),所以我们把它单独抽出来进行说明。
1)层类型 比如Input、Convolution、ReLU
2)层名 模型训练者为该层起得名字(毕竟相同类型的层可能多次使用,我们要区分它们)
3)层输入输出 包含:层输入blob数量,层输出blob数量,层输入、输出blob的名称
4)层配置参数
比如 卷积层(Convolution Layer)的 卷积核大小、步长信息 等
在 具体层里面都有一个函数: load_param, 从里面可以查询到相关信息。
data层: 0=长 1=宽 3=通道
Convolution层 0=输出单元 1=卷积核大小 2=核膨胀[见膨胀卷积] 3=stride
4=padding 5=是否存在偏置 6=权重数量
pooling 层 0=池化类型 1=卷积核大小 2=步长stride 3=padding 4=全局池化 5=padding类型
ReLU 层 0=0.000000 无参数
softmax 层 0=0 无参数
Concat Split Dropout 无参数
ConvolutionDepthWise 7=group 数目
3.2 读取
下面我们具体从代码的角度来看看如何读取这个文件的:(文件为 net.cpp/h 和 paramdict.cpp/h) 为了方便, 我们将 Vulkan 相关代码剔除掉。
net.h 主要是 Net 类的接口, 其中最重要的功能是实现 load_param(载入模型参数) 和 load_model(载入模型的数据) 功能
// net.h
class Net
{
public:
// empty init
Net();
// clear and destroy
~Net();
#if NCNN_STRING
// register custom layer by layer type name
// return 0 if success
// 注册自定义类型层, 通过string类型名
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类型的 layer 索引
int register_custom_layer(int index, layer_creator_func creator);
#if NCNN_STDIO
#if NCNN_STRING
// load network structure from plain param file
// return 0 if success
// 从文件指针中载入参数
int load_param(FILE* fp);
// 从 param 文件中载入参数
int load_param(const char* protopath);
// 从 mem 中载入参数
int load_param_mem(const char* mem);
#endif // NCNN_STRING
// load network structure from binary param file
// return 0 if success
// 从二进制文件指针中载入 param 参数
int load_param_bin(FILE* fp);
// 从二进制文件中载入参数
int load_param_bin(const char* protopath);
// load network weight data from model file
// return 0 if success
// 从 file 指针中传入模型
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
// 从网络构建一个执行器
Extractor create_extractor() const;
protected:
friend class Extractor; // 外部 Extractor 接口
#if NCNN_STRIN
// 通过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
// 通过 index 穿件 layer
Layer* create_custom_layer(int index);
// 前向推理层
int forward_layer(int layer_index, std::vector<Mat>& blob_mats, Option& opt) const;
protected:
// blobs & layers
std::vector<Blob> blobs;
std::vector<Layer*> layers;
// layers
std::vector<layer_registry_entry> custom_layer_registry;
};
在此我们可以先来看一下 blob类 ! 着重看一下,对应的生产者、消费者模型
// Blob 用于记录数据传输过程, producer 记录当前blob从那一层产生的,
// consumer 记录当前blob被哪些层调用:
class Blob
{
public:
// empty
Blob();
public:
#if NCNN_STRING
// blob name
std::string name;
#endif // NCNN_STRING
// layer index which produce this blob as output
// 生产者
int producer;
// layer index which need this blob as input
// 消费者
std::vector<int> consumers;
};
然后我们打开 net.cpp 文件,来看一下 load_param 的具体实现:
// 从文件中载入 net 参数
int Net::load_param(const char* protopath)
{
FILE* fp = fopen(protopath, "rb");
if (!fp)
{
fprintf(stderr, "fopen %s failed\n", protopath);
return -1;
}
// 从文件指针中载入 param
int ret = load_param(fp);
fclose(fp);
return ret;
}
参数载入接口中, 调用了另外一个参数载入接口: load_param(FILE * fp)
(1) 读取 magic number, 通过判断 magic number 是否等于 7767517, 就可以判断当前param文件是否是最新的 param 文件
int magic = 0;
// 读取 magic number
int nbr = fscanf(fp, "%d", &magic);
// 读取失败
if (nbr != 1)
{
fprintf(stderr, "issue with param file\n");
return -1;
}
// 判断是否是最新的 magic number
if (magic != 7767517)
{
fprintf(stderr, "param is too old, please regenerate\n");
return -1;
}
(2) 解析出网络的 layer 层数和 blob 数目
// 对 layer 和 blob 进行解析
int layer_count = 0;
int blob_count = 0;
// 层数 && blob 数目
nbr = fscanf(fp, "%d %d", &layer_count, &blob_count);
// 层数和 blob数读取失败
if (nbr != 2 || layer_count <= 0 || blob_count <= 0)
{
fprintf(stderr, "issue with param file\n");
return -1;
}
// resize 网络的层数和blob数目
layers.resize((size_t)layer_count);
blobs.resize((size_t)blob_count);
(3) 遍历所有的 layer, 解析每层 layer 层的类型(layer type)、名称(layer name)、输入数目(bottom_count) 和 输出数目(top_count)
for (int i=0; i<layer_count; i++)
{
int nscan = 0;
// layer 的类型和名称
char layer_type[257];
char layer_name[257];
int bottom_count = 0;
int top_count = 0;
// 读取层类型、名称。输入bottom数目和输出top数目
nscan = fscanf(fp, "%256s %256s %d %d", layer_type, layer_name, &bottom_count, &top_count);
if (nscan != 4) // 解析失败
{
continue;
}
(4) 根据layer的类型, 创建 layer
// 创建 layer
Layer* layer = create_layer(layer_type);
// layer_type 不是默认类型
if (!layer)
{
// 从自定义 layer 读取
layer = create_custom_layer(layer_type);
}
if (!layer) // 如果自定义 layer 中不存在当前类型的 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);
(5) 在设置输入时,如果当前blob名不存在,就将当前blob名添加到net的blobs数组里面
layer->bottoms.resize(bottom_count); // layer的输入
// 解析 layer 的输入
for (int j=0; j<bottom_count; j++)
{
char bottom_name[257];
// 解析 bottom的name
nscan = fscanf(fp, "%256s", bottom_name);
if (nscan != 1)
{
continue;
}
// 按照 bottom 的name 查找对应 blob 的index
int bottom_blob_index = find_blob_index_by_name(bottom_name);
// 如果没有找到 bottom_name 对应的 blob
// 将向 blobs 数组中插入一个名为 bottom_name 的 blob
if (bottom_blob_index == -1)
{
// 设置第blob_index个blob 的参数
Blob& blob = blobs[blob_index];
// blob的索引
bottom_blob_index = blob_index;
// 设置blob的name
blob.name = std::string(bottom_name);
// 更新全局的 blob 索引
blob_index++;
}
// 设置当前的blob的参数
Blob& blob = blobs[bottom_blob_index];
// 使用当前的blob记录传输关系, 第i层以当前blob为输入
blob.consumers.push_back(i);
// 第i层layer的第j个输入
layer->bottoms[j] = bottom_blob_index;
}
(6) 设置输出的过程和这个类似,在此不再赘述,最后就是参数载入了:
例如: conv1的参数: 0=64 1=3 11=3 5=1 6=1728
//解析 blob后面跟随的特定参数字典 pd
int pdlr = pd.load_param(fp);
if (pdlr != 0)
{
fprintf(stderr, "ParamDict load_param failed\n");
continue;
}
// layer 载入 param
int lr = layer->load_param(pd);
if (lr != 0)
{
fprintf(stderr, "layer load_param failed\n");
continue;
}
layers[i] = layer;
这其中有两个重要的函数:
pd.load_param(fp) 和 layer_param(pd)。前者负责解析 .param 文件中特定的参数, 后者则是用解析的参数来构建对应的layer
(7) 用参数字典来解析layer相关参数! 看一下这个自己构造layer, 以及解析的过程
在使用load_param接口载入参数时,需要用参数字典ParamDict来解析.param文件中的特定参数,那么参数字典具体如何进行解析的?我们首先看一下paramdict.h文件中定义的数据成员变量:
// parameters
struct
{
// 是否已经被载入:1表示已载入
int loaded;
// 单个值可能为整形也有可能为浮点型
union { int i; float f; };
// 还有可能是数组
Mat v;
} params[NCNN_MAX_PARAM_COUNT];
这里,NCNN_MAX_PARAM_COUNT大小为20,params是一个大小为32的结构体数组,即一行中特定参数数量不能超过20,当然,一般情况下也不会超过20。
// at most 20 parameters
#define NCNN_MAX_PARAM_COUNT 20
然后,我们看一下paramdict.cpp源码,可以看到,这里会解析出当前行的index(id),也即是等号左边部分:
// 解析之后的 key=value 对
int id = 0;
while (fscanf(fp, "%d=", &id) == 1)
{
...
}
在这里可以结合 https://github.com/Tencent/ncnn/wiki/param-and-model-file-structure 阅读源码:
index-value 的规则为:
- index为0~19: 对应整形或浮点型数据
index小于 -23000: 对应整形或浮点型数组, 等号右边第一个参数就是数组长度,后面顺序就是数组内容,[array size],int,int,…,int或[array size],float,float,…,float,例如:
0=1 1=2.5 -23303=2,2.0,3.0
index为 -23303,表明当前参数为数组,等号右边第一个参数为2,表明数组长度为2,后面2.0,3.0就是数组的内容
bool is_array = id <= -23300; // index <= -23300:数组
if (is_array) // 如果是数组
{ // 计算id
id = -id - 23300;
}
if (is_array) // 如果当前参数是数组类型
{
int len = 0; // 数组长度
int nscan = fscanf(fp, "%d", &len);
if (nscan != 1) // 等于1才表示读取成功
{
fprintf(stderr, "ParamDict read array length failed\n");
return -1;
}
params[id].v.create(len); // 创建数组:就是一个Mat
for (int j = 0; j < len; j++)
{
char vstr[16]; // 从二值文件中读取string
nscan = fscanf(fp, ",%15[^,\n ]", vstr);
if (nscan != 1) // 如果读取失败
{
fprintf(stderr, "ParamDict read array element failed\n");
return -1;
}
// 是否为浮点型:看解析的字符串中是否存在'.'或'e'
// 小数点计数法和科学计数法
bool is_float = vstr_is_float(vstr);
// 如果是浮点数
if (is_float) // vstr赋值给params[id].v[j]
{
float* ptr = params[id].v;
nscan = sscanf(vstr, "%f", &ptr[j]);
}
else // vstr赋值给params[id].v[j]
{
int* ptr = params[id].v;
nscan = sscanf(vstr, "%d", &ptr[j]);
}
if (nscan != 1) // 赋值失败
{
fprintf(stderr, "ParamDict parse array element failed\n");
return -1;
}
}
}
这里有个vstr_is_float函数,原理很简单,就是判断数字对应字符串中是否存在小数点’.’或字母’e’,对应小数的两种写法,一种正常的小数点表示法,一种是科学计数法。
static bool vstr_is_float(const char vstr[16])
{
// look ahead for determine isfloat
for (int j=0; j<16; j++)
{
if (vstr[j] == '\0')
break;
if (vstr[j] == '.' || tolower(vstr[j]) == 'e')
return true;
}
return false;
}
如果不是数组,直接读取即可:
else // 不是数组
{
char vstr[16];
int nscan = fscanf(fp, "%15s", vstr); // 直接将字符串赋值给vstr
if (nscan != 1) // 赋值失败
{
fprintf(stderr, "ParamDict read value failed\n");
return -1;
}
bool is_float = vstr_is_float(vstr); // 判断是否为浮点数
if (is_float) // 将字符串中的值赋给参数字典
nscan = sscanf(vstr, "%f", ¶ms[id].f);
else
nscan = sscanf(vstr, "%d", ¶ms[id].i);
if (nscan != 1) // 赋值失败
{
fprintf(stderr, "ParamDict parse value failed\n");
return -1;
}
}
params[id].loaded = 1; // 载入成功
(8) 用参数字典来解析layer相关参数
如前所示, layer会根据参数字典来构造 layer
// layer载入param
int lr = layer->load_param(pd);
转到 layer层的 load_param 接口可以看到:
// 载入参数:参数列表
int Layer::load_param(const ParamDict& /*pd*/)
{
return 0;
}
这里并没有实现,在layer.h头文件中有:
// load layer specific parameter from parsed dict
// return 0 if success
virtual int load_param(const ParamDict& pd);
! load_param实际上是一个虚函数,熟悉C++的同学应该知道,调用虚函数时,实际调用的是继承类的版本,那么到底如何调用的?,我们可以往回看,有这样一段代码:
Layer* layer = create_layer(layer_type) // 创建layer
if (!layer) // layer_type不是默认类型
{
layer = create_custom_layer(layer_type); // 从自定义layer读取
}
if (!layer) // 如果自定义layer中也不存在当前类型layer
{
fprintf(stderr, "layer %s not exists or registered\n", layer_type);
clear();
return -1;
}
回到layer.cpp文件中,可以看到,代码中先找到当前层layer类型对应层注册器中类型的索引index。
layer_type -> index -> create_layer
// 将string对应layer类型转换成对应index
int layer_to_index(const char* type)
{
for (int i=0; i<layer_registry_entry_count; i++)
{
if (strcmp(type, layer_registry[i].name) == 0)
return i;
}
return -1;
}
// 根据index创建layer:
Layer* create_layer(int index)
{
if (index < 0 || index >= layer_registry_entry_count) // index不能超过索引范围
return 0;
layer_creator_func layer_creator = layer_registry[index].creator; // 创建layer构造器
if (!layer_creator) // layer构造器创建失败
return 0;
Layer* layer = layer_creator(); // 构造layer
layer->typeindex = index; // 设置layer的类型index
return layer;
}
// 根据字符串layer类型创建layer -> 调用上面两个函数, 创建layer
Layer* create_layer(const char* type)
{
int index = layer_to_index(type);
if (index == -1)
return 0;
return create_layer(index);
}
line 18 有个layer_registry,其定义为:
static const layer_registry_entry layer_registry[] =
{
#include "layer_registry.h"
};
而这个”layer_registry.h”文件是在build项目的时候自动产生的,部分内容如下:
// Layer Registry header
//
// This file is auto-generated by cmake, don't edit it.
#if NCNN_STRING
{"AbsVal",AbsVal_final_layer_creator},
#else
{AbsVal_final_layer_creator},
#endif
#if NCNN_STRING
{"ArgMax",0},
#else
{0},
#endif
#if NCNN_STRING
{"BatchNorm",BatchNorm_final_layer_creator},
#else
{BatchNorm_final_layer_creator},
#endif
#if NCNN_STRING
{"Bias",Bias_final_layer_creator},
#else
{Bias_final_layer_creator},
#endif
而 layer_registry_entry 的结构为:
// layer factory function
typedef Layer* (*layer_creator_func)();
struct layer_registry_entry
{
#if NCNN_STRING
// layer type name
const char* name;
#endif // NCNN_STRING
// layer factory entry
layer_creator_func creator;
};
我们代入一组参数进去就是:
name = "AbsVal";
layer_creator_func = AbsVal_final_layer_creator;
这里layer_creator_func定义为:
typedef Layer* (*layer_creator_func)();
那么,layer_creator_func AbsVal_final_layer_creator转换过去就是:
Layer* AbsVal_final_layer_creator()
在layer.h文件最下面还有一个定义:
// ## 字符串连接
#define DEFINE_LAYER_CREATOR(name) \
::ncnn::Layer* name##_layer_creator() { return new name; }
我们在absval.cpp文件中可以看到 DEFINE_LAYER_CREATOR(AbsVal),相当于就是声明了一个函数:
// #define DEFINE_LAYER_CREATOR(name) \
// ::ncnn::Layer* name##_layer_creator() { return new name; }
// 由上面这段代码可知,DEFINE_LAYER_CREATOR(AbsVal)等价于:
::ncnn::Layer* AbsVal_layer_creator() { return new AbsVal; }
上面那句话相当于就是new了一个AbsVal层,但是这里还是对应不起来,上面的是 AbsVal_final_layer_creator(),这里声明的是AbsVal_layer_creator(),这里就涉及到ncnn还有一层继承,使用cmake编译ncnn项目后,除了生成了layer_registry.h文件之外,还生成了一个layer_declaration.h文件,打开这个文件,一切就清楚了:
// Layer Declaration header
//
// This file is auto-generated by cmake, don't edit it.
#include "layer/absval.h"
namespace ncnn {
class AbsVal_final : virtual public AbsVal
{
public:
virtual int create_pipeline(const Option& opt) {
{ int ret = AbsVal::create_pipeline(opt); if (ret) return ret; }
return 0;
}
virtual int destroy_pipeline(const Option& opt) {
{ int ret = AbsVal::destroy_pipeline(opt); if (ret) return ret; }
return 0;
}
};
DEFINE_LAYER_CREATOR(AbsVal_final)
} // namespace ncnn
#include "layer/batchnorm.h"
namespace ncnn {
class BatchNorm_final : virtual public BatchNorm
{
public:
virtual int create_pipeline(const Option& opt) {
{ int ret = BatchNorm::create_pipeline(opt); if (ret) return ret; }
return 0;
}
virtual int destroy_pipeline(const Option& opt) {
{ int ret = BatchNorm::destroy_pipeline(opt); if (ret) return ret; }
return 0;
}
};
DEFINE_LAYER_CREATOR(BatchNorm_final)
} // namespace ncnn
AbsVal_final层继承了AbsVal层,如果当前操作系统不是linux系统,就会将create_pipeline()和destroy_pipeline()抽象出来,具体调用时,就调用对应优化了的代码。那么layer载入ParamDict具体实现就对应于各个layer的载入流程了。
3.3 bin文件
前面已经大致总结了ncnn的param文件载入,根据param文件创建网络结构,然后通过bin文件载入每一层对应的网络参数。这里就总结一下,如何载入每一层的参数:
我们常用的网络参数载入的接口为:
// 从二进制文件中载入模型
int load_model(const char* modelpath);
找到对应net.cpp文件实现部分有:
// 从二进制文件中载入模型
int Net::load_model(const char* modelpath)
{
FILE* fp = fopen(modelpath, "rb");
if (!fp)
{
fprintf(stderr, "fopen %s failed\n", modelpath);
return -1;
}
int ret = load_model(fp);
fclose(fp);
return ret;
}
和载入模型参数一样,ncnn模型载入这里调用了另外一个接口,从文件指针载入权重参数:
// 从文件指针载入模型
int Net::load_model(FILE* fp)
{
if (layers.empty()) // 判断当前layer是否为空
{
fprintf(stderr, "network graph not ready\n");
return -1;
}
int ret = 0; // load file
ModelBinFromStdio mb(fp); // 从二进制文件读取
for (size_t i=0; i<layers.size(); i++) // 遍历所有的层
{
Layer* layer = layers[i]; // 读取第i层
//Here we found inconsistent content in the parameter file.
if (!layer){ // 如果第i层不存在
fprintf(stderr, "load_model error at layer %d, parameter file has inconsistent content.\n", (int)i);
ret = -1;
break;
}
// 载入模型参数
int lret = layer->load_model(mb);
if (lret != 0)
{
fprintf(stderr, "layer load_model %d failed\n", (int)i);
ret = -1;
break;
}
int cret = layer->create_pipeline(opt); // 从opt处创建网络的pipline
if (cret != 0) // 如果创建第i层的pipline失败
{
fprintf(stderr, "layer create_pipeline %d failed\n", (int)i);
ret = -1;
break;
}
}
// 网络复用
fuse_network();
return ret;
}
按照代码注释,应该还是比较好懂得,这里需要解析两个部分,第一个部分为ModelBinFromStdio,对应于二进制模型文件解析,另外一部分为 layer->load_model(mb),对应于具体某个层的参数载入:
(1)二进制模型文件解析
这里对应于modelbin.h和modelbin.cpp文件,首先看一下modelbin.h文件:
class ModelBin
{
public:
virtual ~ModelBin();
// element type
// 0 = auto
// 1 = float32
// 2 = float16
// 3 = int8
// load vec
virtual Mat load(int w, int type) const = 0;
// load image
virtual Mat load(int w, int h, int type) const;
// load dim
virtual Mat load(int w, int h, int c, int type) const;
};
#if NCNN_STDIO
// 载入模型参数到一个Mat中
class ModelBinFromStdio : public ModelBin
{
public:
// construct from file
ModelBinFromStdio(FILE* binfp);
virtual Mat load(int w, int type) const;
protected:
FILE* binfp;
};
#endif // NCNN_STDIO
// 载入模型参数到一个Mat中
class ModelBinFromMemory : public ModelBin
{
public:
// construct from external memory
ModelBinFromMemory(const unsigned char*& mem);
virtual Mat load(int w, int type) const;
protected:
const unsigned char*& mem;
};
class ModelBinFromMatArray : public ModelBin
{
public:
// construct from weight blob array
ModelBinFromMatArray(const Mat* weights);
virtual Mat load(int w, int type) const;
protected:
mutable const Mat* weights;
};
找到对应实现部分,就是modelbin.cpp,可以看到,ModelBinFromStdio mb(fp);就是将文件指针传给binfp对象
ModelBinFromStdio::ModelBinFromStdio(FILE* _binfp) : binfp(_binfp)
{
}
下面再看一下layer载入参数,layer具体操作对应于具体类型的层操作,例如batchnorm,可以看到:
// 载入模型
int BatchNorm::load_model(const ModelBin& mb)
{
// slope数据
slope_data = mb.load(channels, 1);
// 载入失败:返还-100
if (slope_data.empty())
return -100;
// mean数据
mean_data = mb.load(channels, 1);
// 载入数据失败,返还-100
if (mean_data.empty())
return -100;
// variance数据
var_data = mb.load(channels, 1);
// 载入数据失败,返还-100
if (var_data.empty())
return -100;
// bias数据
bias_data = mb.load(channels, 1);
// 载入数据失败,返还-100
if (bias_data.empty())
return -100;
// 创建矩阵
a_data.create(channels);
if (a_data.empty())
return -100;
// 创建矩阵
b_data.create(channels);
if (b_data.empty())
return -100;
for (int i=0; i<channels; i++)
{
// sqrt variance
float sqrt_var = sqrt(var_data[i] + eps);
a_data[i] = bias_data[i] - slope_data[i] * mean_data[i] / sqrt_var;
b_data[i] = slope_data[i] / sqrt_var;
}
return 0;
}
实际上调用的是ModelBinFromStdio 的load接口:
Mat ModelBinFromStdio::load(int w, int type) const
后面type对应有四种类型:auto,float32,float16和int8
// 0 = auto
// 1 = float32
// 2 = float16
// 3 = int8
然后,根据这四种类型进行模型参数载入,感觉没什么好说的,主要是里面有个alignSize函数需要做个笔记:
static inline size_t alignSize(size_t sz, int n)
{
return (sz + n-1) & -n;
}
alignSize就是申请sz大小的内存,实际申请内存是 y =(sz+n-1)&-n 大小的内存,y >= sz,且y是n的整数倍,然后对(sz+n-1)& -n的解释是:
假设n为16,-n就是0xfffffff0,(sz+n-1),加这个n-1一是为了保证sz刚好是16的倍数不会多算,二十为了防止不是16的倍数会少算,如,sz=3, 就是从二进制角度舍弃19小于16部分。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!