ncnn参数数据的读取load_param

2023-12-20 18:29:23

注:文末附最小可运行的代码,不需要积分即可下载

ncnn数据加载,主要有两个函数,分别为Net::load_model(),Net::load_param(),这两个函数在net.cpp里面定义了,对于数据读取,最终是调用了datareader.cpp中实现的方法,因为在加载模型数据时,是先通过.param文件来获取layer,再通过.bin来获取layer的参数,所以下面我们先来解开Net::load_param(),看看里面到底有些啥。

关于Net::load_model()的解析,在另外一篇文章ncnn模型数据的读取load_model 进行细述

首先看param的加载,主要涉及以下2个cpp文件:
datareader.cpp
datareader.h
net.cpp
net.h

一、datareader.cpp、datareader.h

这里涉及几个类DataReader,DataReaderFromStdio,DataReaderFromStdioPrivate


1、定义DataReader,只定义虚函数,不实现
2、DataReaderFromStdio继承DataReader,并实现相应方法,定义了删除,读取,扫描,等操作
3、DataReaderFromStdio构造函数传入了DataReaderFromStdioPrivate,获取了数据流
4、DataReaderFromStdioPrivate对FILE* _fp进行封装,最终的数据来源于FILE* 的读取

1、首先定义了一个父类,只有虚函数,没做任何实现

class NCNN_EXPORT DataReader{
		// parse plain param text
    // return 1 if scan success
    virtual int scan(const char* format, void* p) const; // 用来读取parma

		// read binary param and model data
    // return bytes read
    virtual size_t read(void* buf, size_t size) const; // 用来读取bin

    // get model data reference
    // return bytes referenced
    virtual size_t reference(size_t size, const void** buf) const;
}

DataReaderFromMemoryDataReaderFromStdio均继承自DataReader,这里提供了两种读取方式,我们这里只对DataReaderFromStdio进行说明。
定义DataReader的好处就是在后面定义了int Net::load_param(const DataReader& dr),传入参数为父类DataReader,那么DataReaderFromMemoryDataReaderFromStdio均可直接传入,方便统一处理,只不过是数据来源不一样而已。

DataReaderFromStdio定义了对数据的读取,读取方式有两种,即scan(const char* format, void* p)read(void* buf, size_t size),scan是用来读取parma的,而read是用来读取bin的。


class NCNN_EXPORT DataReaderFromStdio : public DataReader // 继承了DataReader,并做相应实现
{
public:
    explicit DataReaderFromStdio(FILE* fp); // 定义构造函数,并禁止隐式类型转换
    virtual ~DataReaderFromStdio(); // 里面定义了对d的删除

    virtual int scan(const char* format, void* p) const;// 用来读取parma,通过私有类d来操作
    virtual size_t read(void* buf, size_t size) const;// 用来读取bin,通过私有类d来操作

private:
    DataReaderFromStdio(const DataReaderFromStdio&);
    DataReaderFromStdio& operator=(const DataReaderFromStdio&);

private:
    DataReaderFromStdioPrivate* const d; //对FILE* fp进行无操作封装
};

上面还定义了DataReaderFromStdioPrivate,来看看下这个是做了什么
可以看到,什么也没做,就是把FILE* fp封装了一下而已

class DataReaderFromStdioPrivate
{
public:
    DataReaderFromStdioPrivate(FILE* _fp) //构造函数,接受一个 FILE 指针 _fp,并将其赋值给 fp 成员变量。
        : fp(_fp) // fp(_fp) 的作用是将传入的 _fp 值赋给 fp 成员变量。这种方式是在对象构造时直接初始化成员变量,而不是在构造函数体中使用赋值语句
    {
    }
    FILE* fp; //公有成员变量 fp,它是一个指向 FILE 结构的指针,用于表示输入流。
};

至此可以看出来DataReaderFromStdioPrivate只是对FILE* fp进行无操作封装后传递给DataReaderFromStdio的私有变量DataReaderFromStdioPrivate* const d;然后DataReaderFromStdio里的scanread,就对d->fp,即FILE* fp进行操作

这里再啰嗦几句:

以yolov7为例
1、上面实际上就是通过FILE* 获取到yolov7.parma文件的句柄,然后自定义函数来实现一些方法,最终实现对数据流的操作,比如删除,读取,扫描,等操作。
2、主要流程为
1)将文件句柄封装到DataReaderFromStdioPrivate
2)把DataReaderFromStdioPrivate传递给DataReaderFromStdio
3)DataReaderFromStdio继承自DataReader,而DataReader只定义了虚函数,无方法实现
4)最终,DataReaderFromStdio里面定义了对文件流操作的方法
总之,就是将文件流封装为一个方便逐一读取数据的类,以供下面的load_param进行读取

二、net.cpp,net.h

net就是通过读取上面封装好的DataReaderFromStdio类里面的数据流,提取出网络层的超参,创建layer对象,并将超参赋值给对应的layer对象里面,最终layer会保存在NetPrivate* const d;const std::vector<Layer*>& layers() const;

这里封了多层,传入字符串,接着使用fopen打开,然后传入DataReaderFromStdio
最终调用了Net::load_param(const DataReader& dr),也是在这里将参数通过
SCAN_VALUE解开的。

假如load_param的传入参数为"yolov7.param",即net.load_param("yolov7.param");那么调用下面的函数

int Net::load_param(const char* protopath)
{
    FILE* fp = fopen(protopath, "rb");
    if (!fp)
    {
        NCNN_LOGE("fopen %s failed", protopath);
        return -1;
    }

    int ret = load_param(fp);
    fclose(fp);
    return ret;
}

获取到FILE* fp后,调用下面的函数,将FILE* fp封装到DataReaderFromStdio

int Net::load_param(FILE* fp)
{
    DataReaderFromStdio dr(fp);
    return load_param(dr);
}

获取到DataReaderFromStdio后,最终调用下面的int Net::load_param(const DataReader& dr)函数,这个函数就是将yolov7.param里面的内容一一解出来。
这里给出最简单的版本,已经去掉layer,本来解出来的参数是要赋值给对应layer的
因为这里读取的其实是layer的超参,为了能够更加清楚网络是怎么读取的,这里就简化写

关键点就是通过SCAN_VALUE这个函数来逐一读取参数


int Net::load_param(const DataReader& dr)
{
// 定义SCAN_VALUE的宏定义函数
#define SCAN_VALUE(fmt, v)                \
    if (dr.scan(fmt, &v) != 1)            \
    {                                     \
        printf("parse " #v " failed"); \
        return -1;                        \
    }
    
    int magic = 0;
    SCAN_VALUE("%d", magic) // 读取参数文件第一行

    if (magic != 7767517)
    {
        printf("param is too old, please regenerate");
        return -1;
    }

    // parse
    int layer_count = 0;
    int blob_count = 0;

    SCAN_VALUE("%d", layer_count) // 读取参数文件第二行,第一列
    SCAN_VALUE("%d", blob_count) // 读取参数文件第二行,第二列
    
    
    printf("layer_count: %d\n",layer_count);
    printf("blob_count: %d\n",blob_count);

    if (layer_count <= 0 || blob_count <= 0)
    {
        printf("invalid layer_count or blob_count");
        return -1;
    }



    int blob_index = 0;
    for (int i = 0; i < layer_count; i++)// 逐行读取参数文件
    {
        char layer_type[256];
        char layer_name[256];
        int bottom_count = 0;
        int top_count = 0;
        SCAN_VALUE("%255s", layer_type) // 读取网络类型,比如输入层为Input
        SCAN_VALUE("%255s", layer_name)// 读取网络名字,比如输入层的名字为images
        SCAN_VALUE("%d", bottom_count) // 读取网络的输入blob 比如输入层的输入为0层
        SCAN_VALUE("%d", top_count) //  读取网络的输出blob 比如输入层的输出为1层

        printf("%s\n",layer_type);
        printf("%s\n",layer_name);
        printf("%d\n",bottom_count);
        printf("%d\n",top_count);
        // Input            images                   0 1 images
        
        for (int j = 0; j < bottom_count; j++) // 遍历输入blob的名称,对于Input层为空
        {
            char bottom_name[256];
            SCAN_VALUE("%255s", bottom_name)

            // printf("%s ",bottom_name);

        } 
        
        
        for (int j = 0; j < top_count; j++) // 遍输出blob的名称,对于Input层为images
        {

            char blob_name[256];
            SCAN_VALUE("%255s", blob_name)

            // printf("%s ",blob_name);
            
        }

        int pdlr = pd.load_param(dr); // 读取网络的超参,比如0=32 1=3 11=3 2=1后面的32 3 3 1

		d->layers[i] = layer; //将创建的网络层保存到NetPrivate* const d;


    }
    
    
#undef SCAN_VALUE
    return 0;
}

上面的代码最后面通过int pdlr = pd.load_param(dr); 来获取网络的超参,简写版如下:

int ParamDic_load_param(const DataReader& dr)
{

    int id = 0;
    printf("vstr: ");
    while (dr.scan("%d=", &id) == 1) // 逐一读取,例如对0=32 1=3 11=3 2=1进行遍历,遍历四次
    {
        bool is_array = id <= -23300;

        if (is_array)
        {
            id = -id - 23300;
        }

        if (id >= NCNN_MAX_PARAM_COUNT)
        {
            printf("id < NCNN_MAX_PARAM_COUNT failed (id=%d, NCNN_MAX_PARAM_COUNT=%d)", id, NCNN_MAX_PARAM_COUNT);
            return -1;
        }

        
        char vstr[16];
        int nscan = dr.scan("%15s", vstr); // 进行读取,就是将0=32的32取出来

        printf("%s  ",vstr);
        if (nscan != 1)
        {
            printf("ParamDict read value failed");
            return -1;
        }

    }

    printf("\n");

    return 0;
}

注:简化版均去掉了layer的参数填入部分,这部分会关系到ncnn::Mat以及layer目录文件下的所有.cpp文件等,为了代码简单所以都去掉

以下是可直接运行的版本:

三、代码及其运行方法与结果

ncnn_load_param.zip

运行方法

cd build
cmake ..
make
./demo1

可以看到输出结果
在这里插入图片描述

文章来源:https://blog.csdn.net/qq_27149279/article/details/135101533
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。