剖析Caffe源码之Net---Net构造函数

 

目录

Net构造函数

读取Prototxt

ReadProtoFromTextFile

 UpgradeNetAsNeeded

设置网络状态

 Init函数

FilterNet

InsertSplits

调整参数Vector空间大小

创建每层Layer

保存输入输出Blob

 AutoTopBlobs

SetUp

设置权重和计算占用内存

设置训练参数

根据反向传播设置loss Blob

设置网络输出Blob

记录输出Blob id与name对应关系

记录所有layer id与name映射关系

设置Weights

设置debug级别

Init流程总结

 


Net类中的构造函数是Net网络中非常重要的函数,主要提供了对网络prototxt配置的读取,创建各个层,以及对各层输入输出的blob shape设置,过滤不必要的Layer,以及其中各个Layer功能,起到呈上起下的作用,因为构造函数流程比较长,尤其是涉及到Init函数,要想仔细说明,需要篇幅比较长,故单独列篇章进行分析.

Net构造函数

构造函数将分步骤进行剖析.首先来看Net显示构造构造函数源码如下:

该函数主要分为三部:读取prototxt文件中拓扑网络参数、设置网络状态、Init初始化函数

读取Prototxt

Net构造函数第一步就是首先要从拓扑网络结构中读取参数,调用ReadNetParamsFromTextFileOrDie(),其函数第一参数为Prototxt文件名(带路径),第二个参数为读取到的NetParameter参数,函数源码如下:

 首先从prototxt文件中读取参数,然后根据读取的参数决定是否升级(主要是由于layers参数被丢弃)

ReadProtoFromTextFile

该函数属于caffe IO模块之中,主要涉及到对硬盘读写文件操作,源码如下:

主要是调用protobuf中的接口,对文件进行读操作,相对比较简单,如果对protobuf不熟悉的,可以查看Protobuf介绍及简单使用(下)之文件读写,该部分的讲解内容。

 UpgradeNetAsNeeded

将配置从prototxt文件中读取之后,一般不需要特殊操作,但是由于caffe的历史原因,在之前旧版本中对各个Layer参数使用的是 V1LayerParameter layers结构,而后来进行了改进使用的是repeated LayerParameter layer结构,为了兼容旧版本的参数信息配置,需要对旧的版本中配置升级到layer中,以便后续处理,如果prototxt文件配置使用的是layer则就可以忽略这段代码。

NetNeedsUpgrade()函数升级处理较为复杂,但是遵循一定的逻辑,就是参数一层层升级。

 上述代码首先升级最底一层V1LayerParameter结构体中的V0LayerParameter参数升级,调用UpgradeV0Net()接口将每一层的V1LayerParameter参数下面的V0LayerParameter升级到Net层数据结构中,UpgradeV0Net()不在详细描述,有兴趣的读者可以亲自对着数据结构和代码进行阅读。

升级完成V0LayerParameter结构后,继续升级V1LayerParameter中data部分,data部分主要包括 DataParameter、ImageDataParameter、WindowDataParameter三个data的参数数据升级。

升级完data部分之后,继续升级,如上述代码主要是升级 V1LayerParameter数据中的bottom,top等一些与Layer相关的较为离散的参数,将其升级到新建的 LayerParameter layer中,自此V1LayerParameter大部参数以及升级到 LayerParameter layer中。

 除了升级V1LayerParameter之外还需要升级Net中的input参数,因为该参数也进行了废弃,主要是描述Blob各个维度的大小,已经使用input_shape进行了替代

 最后是对BatchNorm Layer进行单独升级

对整个升级过程进行总结: V0LayerParameter--->data部分主要包括 DataParameter、ImageDataParameter、WindowDataParameter)----->V1LayerParameter数据中的bottom,top等参数---->Net input升级---->BatchNorm Layer

设置网络状态

从Prototxt文件中读取网络配置参数,并进行相应升级之后,接下来将要设置NetParameter参数中 NetState 即网络状态

网络状态  NetState 共用三个参数分别为phase、level、stage。其中phase表明该网络是用于train还是test, level可以设置运行级别,用于将不在级别范围之内的layer进行过滤,stage表明网络需要运行到哪一阶段,不在阶段之内的layer进行过滤。

phase默认为test, level默认为0, stage默认为空,一般在应用程序中可以在创建网络时进行设置。

 Init函数

Init()函数是Net中比较重要的函数,整体流程也比较长,它的主要作用是根据上步骤解析prototxt文件得到NetParameter参数,计算出每层的输入输出,并将每层按照先后关系给串起来。由于Init流程较为复杂将分别进行讲解。

FilterNet

该流程是Net::init()流程处理的第一步,主要是根据网络中的state()参数,以及每层的LayerParameterlayer中include和exclude设置,过滤相应的层,将过滤后的层param_filtered 作为输出做下一步处理,下面进一步分析FilterNet()代码:

 首先获取到NetParameters中的网络状态参数state(后期过滤使用)

接下来将NetParameter param中的内容copy到 param_filtered,这样就可以避免对其中的网络参数进行单一赋值(那么多参数单一赋值,想想都可怕),内存copy完成之后,将layer内容清空,因为接下来要对layer重新过滤,过滤掉不需要的内容。

下一步变量网络中的每一层,对每层按照要求进行一一过滤,首先获取到每层参数param.layer(i),接着获取到每层的名称layer_name = layer_param.name().

对layer中的include和exclude进行检查,两个参数不能在同一个layer中同时设置,因为同时设置include和exclude之后,会无法判断该层是按照include规则(需要包含该层规则),还是需要按照exclude(过滤改层规则)来进行判断。

layer_included变量是过滤标志位,它既是一个中间变量,也当做了最终过滤标准即最终该层是否应该包含在网络中,如果为True则包含在网络中,如果为Flase则不包含在网络中。

在判断是否包含网络中规则分两步情况,一种是exclude参数为标准,一致是include参数为标准

当该层中有exclude参数,则按照该参数进行过滤,如果StateMeetsRule()返回为True,则需要过滤该layer,layer_include设为False, 如果StateMeetsRule()返回False,则不需要过滤该layer, layer_include为True

当该层中有include参数,则如果StateMeetsRule()返回为True,意思与之前相反,表明是网络中需要包含该层,layer_include被设置为True, 如果StateMeetsRule返回为false,则标明该网络中不需要包含该层,layer_include被设置为False.

在第6步中,根据最终规律规则结果,如果layer_include为True,则网络需要包含该层,则将该层参数添加并拷贝到 param_filtered中的layer中,FilterNet()流程图如下:

以上步骤是整个FilterNet()中的处理过程,逻辑处理相对比较清晰,那么继续下一个问题StateMeetsRule(),过滤的具体原则:

首先第一个原则按照网络中的NetStateRule的 phase是否由设置,如果有设置则查看该layer的phase设置是否与网络中的NetState的phase参数相等,如果不相等则直接返回true,如果相等则继续下一步。phase的设置只有两个Train和Test,经常用来设置是用于训练还是测试,有些是否某些层只在训练中或测试中才使用,就会用到该层进行过滤设置。

 按照phase过滤完之后,继续下一步按照level过滤,如果Layer中的NetStateRule 参数设置有min_level和max_level即该层允许能够工作在level范围,如果Netstat中的level在(min_level, max_level)范围之内则说明该Layer可以在该Net中工作,如果不是该范围,则该Layer被过滤直接返回false.

第三个过滤规则是按照layer中的stage和not_stage过滤,stage标明该层允许工作在什么阶段, not_stage表还层不能工作在什么阶段。如果layer中的stage与Netstate中的stage相等返回true,如果不相等则返回False.而not_stage判断逻辑恰好与stage逻辑相反 。

StateMeetsRule()函数的过滤规则先后顺序如下:phase--->level--->stage.StateMeetsRule()流程图如下:

 

InsertSplits

该函数以上步骤中过滤后的Layer为参数,对Layer组织形式再次做出改变,主要是将网络中多个Layer共享相同的输入bottom场景进行处理,在共享相同的bottom层之前插入一个新层,主要是为了应对反向网络传输中的梯度累加,有相同的bottom对反向网络中的梯度累加场景不是很好处理,通过引入一个新的分裂层,来排除多个Layer共享相同的Layer之间相互影响。

下面用一个简单的例子说明后面算法:

例如:LeNet网络中的数据层的top label blob对应两个输入层,分别是accuracy层和loss层,那么需要在数据层在插入一层。如下图:

在数据层之间插入了一个新的层, label_mnist1_split层,为该层创建两个top blob分别为:Label_mnist_1_split_0和:Label_mnist_1_split_1.

上述例子是一个简单的网络构造,将其写成prototxt文:

name: "LeNet"
layer {
  name: "mnist"
  type: "Data"
  top: "label"
  transform_param {
    scale: 0.00390625
  }
  data_param {
    source: "examples/mnist/mnist_train_lmdb"
    batch_size: 64
    backend: LMDB
  }
}
layer {
  name: "AccuracyName"
  type: "accuracy"
  bottom: "label"
  top: "AccuracyResult"  
}
layer {
  name: "LossName"
  type: "loss"
  bottom: "label"
  top: "LossResult"  
}

写成protxt一个简单的骨架(先不讨论里面每层的必要参数设置,不是本节讨论的重点,只是为了更好说明其split内部算法)

InsertSplits()函数里面处理较为复杂

为了能够更好说明其内部处理,以上述例子分解其中的过程。

该函数里面变量较多,但其本质上第一步就是遍历每层,并得到每层的bottom和top,并记录每个输出输入在每层的位置,为了记录其位置,里面才有用pair<int,int> 格式,其中第一个为bottom或top所处于的层的位置,第二个参数为bottom或top分别为该层的输入或者输出参数中第几个位置,这样就能很方便的就能找出bottom和top的位置,并在所有的top中记录其被作为下一层输出参数的个数top_idx_to_bottom_count,如果大于1,则说明该输出为多个层的共享输入。

 

第二步,整理完成所有层的top和bottom信息之后,根据top_idx_to_bottom_count值大小,决定是否需要插入split层,如果大于1,则说明需要插入split层,则进行插层动作,

所插入层的命名规则为,如下代码:

命名规则为:blob_name + "_" + layer_name + “_”+ blob_idx + "_split_" + split_idx 

按照上述例子讲解其主要过程:

首先按照第一步遍历每层的结果:

在第一层Minist由于没有输入,只有一个输出,所以经过处理之后,为:

处理AccuracyName之后的结果:

遍历完 LossResult层结果:

上述每层的输入输出信息统计完之后,就可以知道其输出Label的计数为2个,作为其他两个层的输入共享。

由此进入第二步根据上述的信息重新组成网络:

上述代码首先就是遍历每层,将其重新组成新的网络,首先添加该层到param_split中 ,将其内存完全copy到其中,由于在第一层Minist中,没有输入只有输出top Blob名为Label,所以第一层首先进入到第一步,遍历其输出top,根据上一步得到信息,其输出top为共享两个层的输入,split_count为2,则需要在Mnist层后插入新的split层,插入函数为ConfigureSplitLayer:

分别设置其插入的split层的输入,输出,以及其Layer name以及type。插入完成之后,进入到下一个层第二步,如果之前有新插入的层,那边该层的输入 name也需要重新设置。

InsertSplits()整个处理流程图如下:

 

调整参数Vector空间大小

经过InserSplits处理过后,得到最终网络拓扑组织结构param,后面的caffe处理过程全部按照处理过后的网络拓扑结构信息进行相应处理,首先调整需要保存网络层中的信息的vector大小:

上述变量后缀带‘-’为保存在该类中的变量,为接下来保存网络中的网络信息申请足够的信息: 

变量名作用
bottom_vecs_保存每一层的输入bottom blob,组织形式类似与C语言中的链接嵌套链表,第一层保证每层layer id,第二层为每层的bottom blob
top_vecs_保存每一层的输出top blob,组织形式类似与C语言中的链接嵌套链表,第一层保证每层layer id,第二层为每层的top blob
bottom_id_vecs_保存每一层的数入bottom id,组织形式类似与C语言中的链接嵌套链表,第一层保证每层layer id,第二层为每层的bottom blob该层的id
top_id_vecs_保存每一层的输出top id,组织形式类似与C语言中的链接嵌套链表,第一层保证每层layer id,第二层为该的 blob在blobs_中的id
param_id_vecs_保存每一层的参数id
bottom_need_backward_保存每一层的bottom是否需要所反向传播

该步骤仅仅是为以后的保存信息申请足够的空间,都是以二维vector的形式进行组织。

创建每层Layer

各个参数空间申请完成之后,接下来就要根据重新组织的网络拓扑结构,创建每层的Layer,其代码如下:

遍历每一层,获取到每层的layer_param参数之后,接下来调用LayerRegistry类的CreaterLayer() 接口,创建每一层的类即进行类的实例化,该步骤会调用到所对应layer class的构造函数。对LayerRegistry类的解释可以进一步查看《剖析Caffe源码之Layer_factory》进行详细学习其中的技术细节,里面大量用到了C++的factort模式。

创建完每Layer的实例化之后,将每层的信息按照layer id(即先后关系)保存在layers_变量中, 而layer_names_保存的是每层的name,则有了解到Net中的两个比较重要的变量:

变量名作用
layers_保存每层layer的实例化
layer_names_保存每层的名称

保存输入输出Blob

每层的Layer实例化创建完成之后,接下来就要梳理其每层的输入输出blob,并申请Blob空间,代码如下:

上述两个步骤分为两个输入bottom和输出top两个部分, 由于整个caffe网络拓扑结构第一层都是输入层,而输入层一般只有输出而没有输入,所以caffe代码里涉及到输入输出处理最好是先看输出,再看输入。整个拓扑网络结构也的输入也都是由上层的输出得来的,理解这一点对梳理代码流程比较重要。

看输出层就得由AppendTop()函数分析起来,该函数代码如下:

 该函数主要由三个分支,首先按照第一层输入层的逻辑分支来看:

输入层的逻辑分支进入到1,首先申请一个新的Blob,并获取到新申请的Blob类指针(记住只是申请了新的Blob,并没有设置Blob的shape),将申请的Blob保存在blobs_中,并将其名字保存在blob_names_, blob_need_backward_记录的是该参数是否,需要做反向传播,初始化为false,这三个参数的大小都是一一对应。处理完之后将该blob加入到blob_vecs_中,因为该blob毕竟为每层的输出,top_id_vecs_记录的是该Blob作为top的id。最后一个参数blob_name_to_idx理解起来比较绕口,记录了新申请的blob的name以及在blobs_所对应的id,每新申请一个blob就记录一个到blob_name_to_idx,主要是为了防止blob有重复申请的问题。

上述逻辑过程是输入层的top逻辑,如果是一个中间层的逻辑,就首先会在blob_name_to_idx中查找该top blob,以确认是否有申请过内存 blob_name_to_idx->find(blob_name) != blob_name_to_idx->end(),如果有则说明申请过blob,就直接返回不在申请,该过程为代码中的第3步。

在代码中的第2步主要处理的,处理该输出又是该层输入场景,则说明该输出Blob在前一层已经申请过,则从blob_name_to_idx查找到该Blob,并记录到top_vecs_和top_id_vecs_:

 从该函数中又了解到几个新的变量:

变量名作用
blobs_记录所有申请的Blob都
blob_names_记录所有申请的Blob name,和blob_一一对应
blob_need_backward_记录所有的Blob是否需要反向传播

 该函数主要流程图如下:

AppendBottom()函数处理相对比较简单,因为caffe处理Bob的其中一个原则就是“Blob的申请只在输出top,不会在输入bottom”,因为整个神经网络的数据流方式都是遵循其本层的输入都是来自于上层或者上上层的输出。

代码如下:

该函数的主要处理部分有两个部分,第一步 首先从available_blobs查找是否已经申请过Blob,如果没有则认为该网络为非法的。接下来就是保持Blob到bottom_vecs_和bottom_id_vecs_中,最后将该Blob从available_blobs擦除。因为整个网络已经经过InserSplits处理,不可能存在有一个输出对应多个层的输入场景,如果有则认为非法。

 AutoTopBlobs

接下来处理AutoTopBlobs场景,如果该层的AutoTopBlobs为true,则处理一下情况

SetUp

每层的输入输出Blob都梳理完成之后,并且每层Layer都创建完好之后,建立循环调用每层的setup函数:

通过《剖析Caffe源码之Layer_factory》可知,setup函数为每个扩展Layer类的具体实现,其在setup函数中会调用reshape,会对其输入输出shape,根据需要重新设置,在设置的过程可以根据“Blob的申请只在输出top,不会在输入bottom”,因为整个神经网络的数据流方式都是遵循其本层的输入都是来自于上层或者上上层的输出“原则,只设置输出的Blob的shape(只是建议,每层具体算法未知),调用setup函数之后就知道了每层Blob的shape。

设置权重和计算占用内存

处理完setup()函数之后,接下来重新计算每层的权重以及占用内存大小,代码如下:

首先根据输出的top数量设置每层每个输出对应的权重,以及根据每层的top Blob计算该层占用的内存大小。最后计算出该整个神经网络占用内存大小情况,如果超过整个系统的内存则直接返回错误。

设置训练参数

每层的setup()函数设置完成之后,并且计算出所占用的内存之后,接下来就要出来每层的训练参数ParamSpec param和BlobProto blobs:

代码如下:

主要分为两个部分的处理

  • 首先处理该层的学习速率,并与need_backward求或,则设置该层的propagate_down ,是否需要做反向传播。
  • 然后调用AppendParam()函数,将该层的ParamSpec param 参数保存到params_,param_id_vecs_等参数中。

根据反向传播设置loss Blob

接下来按照其网络的反向传播过程,查找到哪些blob需要设置loss,该过程代码流程比较长:

最终所有需要反向传播的blob将会记录到 bottom_need_backward_,所有需要loss的Blob都记录到blobs_under_loss中,所有需要做反向传播的layer,都记录到blobs_under_loss中。上述代码流程比较长,没有贴全,稍后再补充该部分流程图。

设置网络输出Blob

接下来在available_blobs记录了所有的输出bob,由AppendBottom()函数可知,如果其top blob有作为对应的其他层的输入,则将会将其从available_blobs移除掉,如果此时还有剩余的没有被移除的,则表明没有作为其他层的输入,则该剩余的blob则被认为是整个网络的输出,这正是caffe设计的精妙之处一

记录输出Blob id与name对应关系

将所有的输出blob的index即id与name建立一一对应关系,方便后面进行管理:

记录所有layer id与name映射关系

设置所有的layer id与name建立一一映射关系:

设置Weights

设置weights:

设置debug级别

 根据设置的网络参数,设置debug_info级别

 是否为debug模型,默认为false,关闭debug模型

Init流程总结

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Huo的藏经阁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值