caffe源码 layer分析

时间:2022-01-04 04:11:37

               Layer 分析

1 layer 总体介绍

2 data 

3 neuron 

4 vision 

5 common

6 loss

添加自己layer

 

 

一、layer总体介绍

  1、 layer层作用

Caffe十分强调网络的层次性,数据输入,卷积,非线性变换(ReLU等),网络连接,损失函数计算等操作都由一个Layer实现。layer是网络的基本单元,由此派生出各种层类。创建一个caffe模型只需要定义一个prototxt文件即可。也可以通过修改layer或增加自己layer来实现自己的模型

层和层参数定义在src/caffe/proto/caffe.proto 文件中。编译之后产生.pb.cc 和一个pb.h的文件。在neuron_layers.hpp、data_layer.hpp、vision_layers.hpp、common_layers.hpp、loss_layers.hpp这5大类层头文件中都有调用,可以把prototxt中每层的配置参数读入到配置网络的每一层的程序中。

 2layer.hpp 抽象基类介绍

Layer.hpp是所有layer相关的头文件,是抽象出来的基类,其他5大类都是在它基础上继承得到。

layer中主要有三个参数: (1layer_param_ protobuf文件中存储的layer参数2blobs_ 存储layer的参数,在程序中用的;(3param_propagate_down_   bool类型用来表示是否计算各个blob参数的diff,即传播误差。

Layer中三个主要函数:SetUp()根据实际的参数设置进行实现,对各种类型的参数初始化;Forward()Backward()对应前向计算和反向更新,输入统一都是bottom,输出为top,其中Backward里面propagate_down参数,用来表示该Layer是否反向传播参数。

3layer  5大类总体概括

   Layer中程序框架可以通过图1简单了解

caffe源码 layer分析


从图中可以得到layer主要有5大类组成,下边对5大类先简单介绍一下:

1)数据层

 Data层为数据输入层,头文件定义src/caffe/include/caffe/data_layer.hpp中,作为网络的最底层,主要实现数据输入和格式的转换。

2神经元层

       Neuron层为元素级别运算层,头文件定义在src/caffe/include/caffe/neuron_layers.hpp中,其派生类主要是元素级别的运算(比如Dropout运算,激活函数ReLuSigmoid等)

3特征表达层 

Vision层为图像卷积相关层,头文件定义在src/caffe/include/caffe/vision_layers.hpp 中,convolusionpoolingLRN都在里面,按官方文档的说法,是可以输出图像的,这个要看具体实现代码了。里面有个im2col的实现

 (4)网络连接层

Common层为网络连接层,头文件定义在src/caffe/include/caffe/common_layer.hpp中。Caffe提供了单个层或多个层的连接,并在这个头文件中声明。包括了常用的全连接层InnerProductLayer

 (5)损失函数层

    Loss层为损失函数层,头文件定义在src/caffe/include/caffe/loss_layers.hpp。前面的data layercommon layer都是中间计算层,虽然会涉及到反向传播,但传播的源头来自于loss_layer,即网络的最终端。这一层因为要计算误差,所以输入都是2blob,输出1blob

 

    总之,data负责输入,vision负责卷积相关的计算,neuroncommon负责中间部分的数据计算,而loss是最后一部分,负责计算反向传播的误差。具体的实现都在src/caffe/layers里面,

 

二、data数据层

   Data层为数据输入层,头文件定义data_layer.hpp中,作为网络的最底层,主要实现数据输入和格式的转换。Data 作为原始数据的输入层,caffe代码中实现了多种数据输入,其中Databaseleveldblmdb数据库)、In-Memory(内存)、HDF5 Inputhdf5)、Images(原始图像)读入数据。具体使用如下:

 

1、 Database

 (1) 类型:Data

必须参数:source : 包含数据的目录名称

          batch_size :  一次处理的输入数量 

可选参数:rand_skip : 在开始的时候从输入中跳过这个数值,这在异步随机梯度下

                   (SGD)的时候非常有用                                                                    

          Backend(default LEVELDB) : 选择使用 LEVELDB 或者 LMDB.

说明:LEVELDB 或者 LMDB都是键/值对(Key/Value Pair)嵌入式数据库管理系统编程

      库。lmdb的内存消耗是leveldb1.1倍,但lmdb的速度比leveldb10%15%

      更重要的是lmdb允许多种训练模型同时读取同一组数据集。因此lmdb取代了

      leveldb成为Caffe默认的数据集生成格式

(2) 程序说明:

      该部分在src/caffe/layers/data_layer.cpp 中实现。

      程序中主要两个函数 : DataLayerSetUp()初始化参数和启动一个线程预先从leveldb 

      中拉取一批数据,InternalThreadEntry():正向传播时,先把预先拉取好数据拷贝到

      指定的cpu或者gpu的内存。然后启动新线程再预先拉取数据,这些数据留到下一次

      正向传播使用。

(3) 该层配置示例:

layer {

  name: "mnist"

  type: "Data"

  top: "data"

  top: "label"

  include {

    phase: TRAIN

  }

  transform_param {

    scale: 0.00390625

  }

  data_param {

    source: "examples/mnist/mnist_train_lmdb"

    batch_size: 64

    backend: LMDB

  }

}

2、   In-Memory

类型:MemoryData

必须参数:无

可选参数:batch_size : batch大小                                                                    

          channels : 通道数

              height:  高度

              width:  宽度

说明内存数据层直接从内存中读取数据而不用复制它。为了使用它,必须调用MemoryDataLayer::Reset()或者Net.set_input_arrays()来指定一个连续的数据源(如 4D row major array),它可以一次读取一个batch大小。

程序说明:

      该部分在src/caffe/layers/menory_data_layer.cpp 中实现。

3、 HDF5 Input

类型:HDF5Data

必须参数:source : 读取文件名字

          batch_size :  一次处理的输入数量 

可选参数:shuffle = 3 [default = false] : 是否随机打乱                                                               

说明:LEVELDB 或者 LMDB都是键/值对(Key/Value Pair)嵌入式数据库管理系统编程

      库。lmdb的内存消耗是leveldb1.1倍,但lmdb的速度比leveldb10%15%

      更重要的是lmdb允许多种训练模型同时读取同一组数据集。因此lmdb取代了

      leveldb成为Caffe默认的数据集生成格式

程序说明:

      该部分在src/caffe/layers/data_layer.cpp 中实现。

      程序中主要两个函数 : DataLayerSetUp()初始化参数和启动一个线程预先从leveldb中拉取一批数据,InternalThreadEntry():正向传播时,先把预先拉取好数据拷贝到指定的cpu或者gpu的内存。然后启动新线程再预先拉取数据,这些数据留到下一次正向传播使用。

 

二、neuron神经元层

   neuron层为数据输入层,头文件定义/src/caffe/include/caffe/relu_layer.hpp中,主要实现对元素级别的操作。该层输入数据大小和输出数据大小相同。Dropout运算,激活函数ReLu、Sigmoid、absval、 bnll、power、relu、sigmoid、tanh11都在该层实现。具体使用如下:

1ReLU / Rectified-Linear and Leaky-ReLU

 (1) 类型:ReLU

可选参数:negative_slope [default 0]: 实现y = max(x,0) + negative_slope*min(x,0)

 

说明:

(2) 程序说明:

(3) 该层配置示例:

layer {  name: "relu1"  type: "ReLU"  bottom: "conv1"  top: "conv1"}

(4)程序主要实现部分

 Forward: top_data[i] = std::max(bottom_data[i], Dtype(0)) + negative_slope * std::min(bottom_data[i], Dtype(0));

 Backward:  bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0) + negative_slope * (bottom_data[i] <= 0));

 

 

三、common络连接层

   common层为复杂数据运算层,头文件定义src/caffe/include/caffe/common_layer.hpp中,NeruonLayer仅仅负责简单的一对一计算,而剩下的那些复杂的计算则通通放在了common_layers.hpp中。像ArgMaxLayerConcatLayerFlattenLayerSoftmaxLayerSplitLayerSliceLayer等各种对blob增减修改的操作。neuroncommon负责中间部分的数据计算。

具体使用如下:

 

1Inner Product

 (1) 类型:InnerProduct

必须参数:num_output (c_o): 输出神经元的个数

: 包含数据的目录名称

          batch_size :  一次处理的输入数量 

可选参数:weight_filler [default type: 'constant' value: 0]: 这个强烈建议,他的初始化会 

                                                 影响到结果                                                                   

          bias_filler [default type: 'constant' value: 0]

              bias_term [default true]: 是否配置bias

说明:

2)输入:n * c_i * h_i * w_i

     输出:n * c_o * 1 * 1

3)功能: 实现ywx + b;

(3) 程序说明:

      该部分在src/caffe/layers/inner_product_layer.cpp中实现。

      程序中主要两个函数 : DataLayerSetUp()初始化参数和启动一个线程预先从leveldb 

      中拉取一批数据,InternalThreadEntry():正向传播时,先把预先拉取好数据拷贝到

      指定的cpu或者gpu的内存。然后启动新线程再预先拉取数据,这些数据留到下一次

      正向传播使用。

(3) 该层配置示例:

layer {

  name: "mnist"

  type: "Data"

  top: "data"

  top: "label"

  include {

    phase: TRAIN

  }

  transform_param {

    scale: 0.00390625

  }

  data_param {

    source: "examples/mnist/mnist_train_lmdb"

    batch_size: 64

    backend: LMDB

  }

}



四、阅读代码心得

(1)layer层总共分5大类:neuron,data,common,vision,loss。首先看每一类的头文件,通过头文件可以看出它派生出哪些类,头文件中对每个类都有注释。也可以通过官网http://caffe.berkeleyvision.org/doxygen/annotated.html 类列表上了解每个派生类的功能。
  (2)读完头文件,看一下layer.hpp,毕竟这是所有类的基类,了解他有哪些属性和方法。然后根据每大类头文件具体实现的类,首先找到proto文件,查看该类都有哪些参数。接着边看代码边推导该类实现过程,
  (3)开始看具体类代码时,发现都是blob流,所以先读blob.hpp和blob.cpp,看看他都定义了哪些属性和方法,不熟悉这个影响后面阅读速度。
  (4)读到下边就发现大部分类中都有矩阵运算,赶快先读math_funtion.cpp和math_funtion.cpp,这里面有大量的cblas矩阵运算。先把这里面矩阵运算函数实现功能了解一下,看到每层矩阵运算记不清的就来这里查,caffe变量定义很好,符合见名知意。比如:ge 相乘,m矩阵,v 向量caffe_cpu_gemv() 函数代表 y=alphaAx+beta*y。
  (5)对于data层,会用到data_transformer.cpp,internal_thread.cpp 来进行数据变换和启动线程,所以先了解这两个代码。
  (6)对于卷积核等权重初始化方式,需要提前看filler.hpp 这个程序实现了多种参数初始化的方法。
  (7)对于卷积相关操作还需了解im2col.cpp. 


     (1) data类是网络的最底层,用于数据入口。
     (2) 代码总体思路:先启动一个线程,用于预取数据在CPU中,然后启动线程把需要 crop,scale,mirror等操作放到GPU中。 这个,可以通过base_data_layer.cpp可以看到。如何启动线程,如果送到GPU的, image_layer和data_layer都是引用这个得到。 这也是为什么这两个层没有forward() 函数,因为他们都放到他们BaseDataLayer这个层的基类里面的。
     
(3) 基于这个思路,data层先抽象出来BaseDataLayer和BasePrefetchingDataLayer两个基类,把不同的输入方式共有的操作,封装在这两个类中。imagedata层 通过opencv2,读取数据,datalayer因为数据放在DB数据库中,所以利用Datum等操作读入数据。 不同的操作。
      (4) DataLayer和ImageDataLayer两种输入方式都继承了BaseDataLayer和BasePrefetchingDataLayer这两个基类,实现他们共有操作。然后InternalThreadEntry()函数利用多态性方法,实现一个从DB数据库中读取,一个从image files读取。
       (5) imagedata 输入中 在layersetup()中就已经完成了shuffle(),但是在开启InternalThreadEntry()中,有shuffle() 一次。 这个有点怪。  while (infile >> filename >> label) {
    lines_.push_back(std::make_pair(filename, label));
  } 通过增加容器,可以在这里改,得到输入个数的不同。
      (6)data层中的 数据,先去一个线程, 在layersetup() 的时候就把数据放到线程中prefetch_data_。 在forward()的时候,直接在 prefetch_data_中取数据就好。
----------------------------------------------------------------
卷积操作
  (1)事实上的卷积运算的两个步骤。首先,二维图像是
/ /转换为一维向量。二、做卷积,其实是卷积
  (2) 把卷积相关的操作都封装在base_conv_layer中,这个层封装的特别好。使得其他的层代码都很简单了。从卷积conv_layer和解卷积层dconv_layer对比可以看出,通过他们封装后,只需要改变调用两个函数的顺序就行。
    (3)base_conv_layer 是conv_layer和dconv_layer这两个层的基类。把所有的运算都封装在base_conv_layer中。conv_layer和dconv_layer这两个layer很简单,就forward_cpu_gemm和backward_cpu_gemm这两个函数顺序对换即可。代码在这里封装的很好。
   (4) 所有看卷积层和解卷积层,主要看base_conv_layer 基层就行了。这个层封装了卷积运算。 卷积底层运算都是通过BLAS的函数实现, 这里增加一个base_conv_layer 层的目的,就是为了进一步封装BLAS, 在卷积和解卷积的什么调换两个函数的顺序就好了。
-----------------------------------------------------
神经元层,总共12个,这部分比较简单,输入输出大小一样,所以Reshape(),直接在基类实现而且ReshapeLike()函数即top和bottom一样。就能搞定,,各神经元类不用具体实现了。所以知道神经元公式,代码很容易看懂。
总之,caffe这个框架写的特别好,把c++封装继承多态的三大特性体现的淋漓尽致。框架结构明显,思路清晰。 除了增加新方法实现外,现在caffe代码与一年前相比,框架结构更好了。


 咱们主要用image_layer和datalayer,这两个层大部分操作相同,只是一个用opencv2读数据和一个用db读数据。把他们共同操作抽象出来为  BaseDataLayer和BasePrefetchingDataLayer,所以forward()函数BaseDataLayer实现,BaseDataLayer实现transform等操作放到GPU中。

      
卷积层,
   因为卷积和解卷积大部分操作相同,只是过程相反,所以,把所有关键的运算都封装在base_conv_layer中。conv_layer和dconv_layer这两个layer很简单,就forward_cpu_gemm和backward_cpu_gemm这两个函数顺序对换,代码在这里封装的很好。base_conv_layer基类中,封装了大量的BLAS运算,通过这层封装,接口用起来特别方便。在base_conv_layer中可以看出,卷积运算分为两步:首先二维图像转换为一维向量,然后做卷积。
 
neruon层,
总共12个,这部分比较简单,输入输出大小一样,所以Reshape()直接封装在neruon_layer基类中实现,并且用ReshapeLike()函数(top和
bottom一样)就能搞定,所以知道神经元运算公式,代码很容易看懂。
 
common层, 
   网络连接方式基本上操作都不同。没有什么可以抽象的,所以没有抽象类,都是从layer继承。inner_product_layer.cpp 就是实现y=wx+b .看着这个公式,代码很容易看懂。
 
loss层,
    这部分把loss公式搞明白了,代码很容易理解。编程上没有太多技巧。
 
 
    总之,新版caffe框架结构层次更加明显,代码比较清晰。原则是把相同的操作,尽量抽象用基类,如layer---> data_layer----->base_conv_layer----->conv_layer. 相同的函数也抽象出来,放到基类中,同时尽量使用新版本的库,比如opencv2和python3等。
 
    最后,特别感谢您给我时间看代码。 这个代码框架特别好,从代码中学到了好多编程技巧,也提高了对编程的理解。以前自己也用过opencv,MFC,QT库,但是都没好好看过源码。自己也写过几千行的程序,但抽象的不好,代码框架不够清晰。
 

未完待续,抽时间好好整理