美文网首页
ncnn源码阅读笔记(二)

ncnn源码阅读笔记(二)

作者: 半笔闪 | 来源:发表于2019-10-10 12:03 被阅读0次

    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类元素

    1. 层类型
    2. 层名称
    3. 输入数据结构数量(bottom blob)
    4. 输出数据结构数量(top blob)
    5. 网络输入层名(一个或多个)
    6. 网络输出层名(一个或多个)
    7. 特殊参数(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;
        }
    

    相关文章

      网友评论

          本文标题:ncnn源码阅读笔记(二)

          本文链接:https://www.haomeiwen.com/subject/oudgpctx.html