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 文件则记录神经网络运算所需要的数据信息(比如卷积层的权重、偏置信息等)

ncnn官方demo中的模型文件

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", &params[id].f);
    else
        nscan = sscanf(vstr, "%d", &params[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 协议 ,转载请注明出处!