ncnn源码分析1_一名小菜鸟的学习之路的博客-程序员宝宝_ncnn源码分析

技术标签: c++  ncnn学习笔记  机器学习  深度学习  caffe  神经网络  

        前段时间,分别尝试了使用腾讯开源的深度学习推理框架ncnn、陈天奇大神团队开源的tvm,及最新的阿里开源mnn,就好用程度来说,腾讯的ncnn当之无愧的第一名,这里大致写一下源码学习心得体会,方便后面进一步学习。

        ncnn接口函数        

        在使用ncnn来部署模型时,我们会预先定义一个Net对象,然后使用load_param和load_model两个接口载入模型结构参数和模型权重参数:

ncnn::Net net;
net.load_param("mobilenet_yolo.param");
net.load_model("mobilenet_yolo.bin");

这里,我们打开ncnn源码中src子文件夹下面的net.h和net.cpp文件,可以看到:

class Net
{
    
public:
    // empty init
    Net();
    // clear and destroy
    ~Net();
 
public:
    // option can be changed before loading
    // 在载入之前,可以通过opt更改网络的一些设置
    Option opt;
 
 
#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
    // 外部内存载入param
    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:
    // parse the structure of network
    // fuse int8 op dequantize and quantize by requantize
    // 网络重用
    int fuse_network();
 
    // 外部Extractor接口
    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
    // 通过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;
};

        代码中已经将Vulkan相关代码给剔除掉了,这里可以看到上面用到的load_param和load_model接口,我们传入参数为const char*类型的参数。

        然后,我们打开net.cpp文件:

// 从文件中载入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)

// 载入网络参数
int Net::load_param(FILE* fp)

        这里可以对照着我们的param列表来读:

7767517
24 25
Input            data                             0 1 data
Convolution      conv1                            1 1 data conv1 0=64 1=3 11=3 5=1 6=1728
PReLU            prelu1                           1 1 conv1 prelu1 0=64
Pooling          pool1                            1 1 prelu1 pool1 0=1 1=3 2=2 4=0 5=0
ConvolutionDepthWise conv2_dw                         1 1 pool1 conv2_dw 0=64 1=3 11=3 5=1 6=576 7=64
PReLU            prelu2_dw                        1 1 conv2_dw prelu2_dw 0=64
Convolution      conv2_sep                        1 1 prelu2_dw conv2_sep 0=128 1=1 11=1 5=1 6=8192
PReLU            prelu2_sep                       1 1 conv2_sep prelu2_sep 0=128
Pooling          pool2                            1 1 prelu2_sep pool2 0=1 1=3 2=2 4=0 5=0
ConvolutionDepthWise conv3_dw                         1 1 pool2 conv3_dw 0=128 1=3 11=3 5=1 6=1152 7=128
PReLU            prelu3_dw                        1 1 conv3_dw prelu3_dw 0=128
Convolution      conv3_sep                        1 1 prelu3_dw conv3_sep 0=256 1=1 11=1 5=1 6=32768
PReLU            prelu3_sep                       1 1 conv3_sep prelu3_sep 0=256
Pooling          pool3                            1 1 prelu3_sep pool3 0=1 1=2 2=2 4=0 5=0
ConvolutionDepthWise conv4_dw                         1 1 pool3 conv4_dw 0=256 1=2 11=2 5=1 6=1024 7=256
PReLU            prelu4_dw                        1 1 conv4_dw prelu4_dw 0=256
Convolution      conv4_sep                        1 1 prelu4_dw conv4_sep 0=512 1=1 11=1 5=1 6=131072
PReLU            prelu4_sep                       1 1 conv4_sep prelu4_sep 0=512
ConvolutionDepthWise conv5_dw                         1 1 prelu4_sep conv5_dw 0=512 1=3 11=3 5=1 6=4608 7=512
PReLU            prelu5_dw                        1 1 conv5_dw prelu5_dw 0=512
Convolution      conv5_sep                        1 1 prelu5_dw conv5_sep 0=512 1=1 11=1 5=1 6=262144
PReLU            prelu5_sep                       1 1 conv5_sep prelu5_sep 0=512
InnerProduct     conv6_3                          1 1 prelu5_sep conv6_3 0=212 1=1 2=108544
BatchNorm        bn6_3                            1 1 conv6_3 bn6_3 0=212

        进入load_param接口后:

        (1)就会读取magic数,通过magic数是否等于7767517就可以判断当前param文件是否为最新版本的param文件:

    int magic = 0;
    // 读取magic数
    int nbr = fscanf(fp, "%d", &magic);
    // 读取失败
    if (nbr != 1)
    {
    
        fprintf(stderr, "issue with param file\n");
        return -1;
    }
    // 最新的magic数
    if (magic != 7767517)
    {
    
        fprintf(stderr, "param is too old, please regenerate\n");
        return -1;
    }

        (2)解析出网络的layer层数及blob数:

    // 对layer进行解析
    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网络的layers和blobs
    layers.resize((size_t)layer_count);
    blobs.resize((size_t)blob_count);

        (3)遍历所有的layer,解析每个layer层的类型(layer_type)、名称(layer_name)、输入数(bottom_count)和输出数(top_count)

        int nscan = 0;
 
        // layer的类型和名字
        char layer_type[257];
        char layer_name[257];
        int bottom_count = 0;
        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;
        }

        (4)根据layer的类型,创建layer

        // 创建layer
        Layer* layer = create_layer(layer_type);
        // layer_type不是默认类型
        if (!layer)
        {
       
            // 从自定义layer读取
            layer = create_custom_layer(layer_type);
        }
        // 如果自定义layer中也不存在当前类型layer
        if (!layer)
        {
    
            fprintf(stderr, "layer %s not exists or registered\n", layer_type);
            clear();
            return -1;
        }

        (5)设置layer参数:layer的类型、名字、输入和输出:

         // layer的type和name
        layer->type = std::string(layer_type);
        layer->name = std::string(layer_name);

        在设置输入时,如果当前blob名不存在,就将当前blob名添加到net的blobs数组里面,查看一下Blob定义可以看到:

// blob
// name && producer && consumers
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;
};

        Blob用于记录数据传输过程,producer记录当前blob从那一层产生的,consumer记录当前blob被哪些层进行调用:

        // layer的输入
        layer->bottoms.resize(bottom_count);
 
        // 解析layer的输入
        for (int j=0; j<bottom_count; j++)
        {
    
            char bottom_name[257];
            // 解析botoom的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;
        }

        设置输出的过程和这个类似,就不重复了,最后就是参数载入了,例如,前面的param文件后面的参数:

     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;

        具体参数字典的参数载入,及layer的网络参数载入需要对paramdict.h和paramdict.cpp及layer.h和layer.cpp文件进行解析,后面再进行补充。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wangchao1823385450/article/details/105957658

智能推荐

VR制作流程有哪些,新手如何制作VR视频?_康1998的博客-程序员宝宝_vr制作

VR视频是VR技术和全景拍摄技术的综合应用。VR视频能720度全方位记录场景实况,用户可以通过手机身临其境般进行线上体验,让用户能够更加全面的观赏拍摄场景。并且可以自主调整观看角度,带来更好的沉浸感和体验。相信许多朋友一定很好奇VR视频是怎么制作的,下面一起来看看吧。VR制作流程有哪些?VR制作流程分为前期拍摄和后期剪辑两部分。前期拍摄主要是通过全景摄像机多角度的实时拍摄出来的。后期剪辑主要是将拍摄好的素材进行剪辑和加工,最终变成我们所看到的成片。(详情见链接:VR视频是怎么拍摄的?拍摄VR全景视频要

PX4教程翻译(1)序+前言_大强强小强强的博客-程序员宝宝

序最近打算实现树莓派控制 Pixhawk 无人机,做了近一个星期,还是没有成功。由于没有系统的中文教程,零散地看了许多博客。“正如万山圈子里,一山放过一山拦。”各种Bugs层出不穷。于是决心静心学习官网教程。鉴于没有找到中文版,决心自己翻译此教程(毕竟也是过了六级的人啊),版本为 master。自己加深印象,也希望能够帮助更多志同道合的朋友。自己才疏学浅,翻译并不一定准确,有含糊之处欢迎指...

Java机器学习库ML之一Dataset和Instance_fjssharpsword的博客-程序员宝宝

Java机器学习库ML官网:http://java-ml.sourceforge.net/对于一个机器学习库来说,最基础就是数据处理能力,ml库给了dataset和instance两个类,dataset是矩阵,instance是行(可以理解是一个list,或一个double数组)。本文给出最基本的Dataset和Instance操作,可以完成对一个矩阵的遍历,定位到每一行里的每一个列,可惜

Java_JavaEE_SSH_Struts2 Ajax支持闲谈;_cyb_23的博客-程序员宝宝

Ajax,Acynchronous JavaScript And XML,即异步JavaScript 和 XML技术,也是Web2.0的核心技术之一;通过Ajax技术,浏览者与服务器之间采用异步通信机制,从而避免浏览者的等待,带给浏览者连续的体验。它让用户可以连续发送多次异步请求,而无须等待服务器响应。当服务器的响应成功返回浏览器时,浏览器使用DOM(Document Object Model)将

[Python从零到壹] 十二.机器学习之回归分析万字总结全网首发(线性回归、多项式回归、逻辑回归)_Eastmount的博客-程序员宝宝_逻辑回归和多项式回归

前一篇文章讲述了数据分析部分,主要普及网络数据分析的基本概念,讲述数据分析流程和相关技术,同时详细讲解Python提供的若干第三方数据分析库,包括Numpy、Pandas、Matplotlib、Sklearn等。本文介绍回归模型的原理知识,包括线性回归、多项式回归和逻辑回归,并详细介绍Python Sklearn机器学习库的LinearRegression和LogisticRegression算法及回归分析实例。进入基础文章,希望对您有所帮助。

qt多线程的同步问题_南波儿万的博客-程序员宝宝

今天在写qt程序的过程中遇到了一个问题。我的程序理论上是有两个线程,但实际上完全不是。除了主线程之外我的另一个线程主要的任务是监听一个网络端口,收到数据后以信号的方式发送给主线程,然后主线程在对应的槽函数中处理相应的数据。同时我的主线程还处理了许多按键的槽函数,在按键的槽函数中会弹出一个零时的对话框窗口。我的问题是当我在处理按键槽函数弹出的窗口时(注意此时按键槽函数还没有返回)出现场收到了网络监听线程发给主线程的信号并进入对应的槽函数进行处理,此时程序出现了卡死的现象。对于一个专业的软件工程师,这种情况是绝

随便推点

解释的很好的。堆,栈。程序的内存分布。 bss data rodata text heap stack。_linbounconstraint的博客-程序员宝宝

一个程序一般分为3段:text段,data段,bss段text段:就是放程序代码的,编译时确定,只读,data段:存放在编译阶段(而非运行时)就能确定的数据,可读可写就是通常所说的静态存储区,赋了初值的全局变量和静态变量存放在这个区域,常量也存放在这个区域bss段:定义而没有赋初值的全局变量和静态变量,放在这个区域 这个够不够清楚呢?堆栈就是栈的简称。堆和栈的区别一、预备知识—

自动化测试框架[Cypress持续集成之Jenkins]_Davieyang.D.Y的博客-程序员宝宝_cypress jenkins

Jenkins是一款开源的CI/CD软件,用于各种任务的自动化执行,包括构建、测试、部署等,其流水线(Pipeline)是用户定义的一个CD流水线模型,流水线的代码定义了整个的构建过程,包括构建、测试和交付应用程序;流水线包括声明式流水线和脚本化流水线两种,而Pipeline属于声明式流水线

csdn java并发编程指南_由浅入深,逐步了解 Java 并发编程中的 Synchronized!_某某钊的博客-程序员宝宝

作者 | sowhat1412 责编 | 张文头图 | CSDN 下载自视觉中国synchronized 作用synchronized 关键字是 Java 并发编程中线程同步的常用手段之一。1.1 作用:确保线程互斥的访问同步代,锁自动释放,多个线程操作同个代码块或函数必须排队获得锁,保证共享变量的修改能够及时可见,获得锁的线程操作完毕后会将所数据刷新到共享内存区;不解决重排序,但保证有序性。1...

人工智能产业发展深度报告:格局、潜力与展望_数据派THU的博客-程序员宝宝

来源:HKSAIR本文约10800字,建议阅读20+分钟本文带你了解人工智能产业发展。人工智能市场格局人工智能(Artificial Intelligence,AI)是利用机器学习和数据...

Spring的核心概念_妹妹来寻我的博客-程序员宝宝

一、初识SpringSpring简述以及用途学习一个框架,我们必然要先知道这个框架可以做什么。做过大型项目的朋友都知道,这些比较大的企业级应用结构复杂,涉及的资源众多,事务密集等等,传统Java EE解决企业级应用问题时的“重量级”架构体系,使它的开发效率、维护成本、应用性能等方面都令人失望。比如各个层级对象之间的耦合度总是达不到一个理想的状态,正是因为对象之间保持着一定程度的依赖,可能当我们...

推荐文章

热门文章

相关标签