Layer 分析
1 layer 总体介绍
2 data 层
3 neuron 层
4 vision 层
5 common层
6 loss层
7 添加自己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中每层的配置参数读入到配置网络的每一层的程序中。
2、layer.hpp 抽象基类介绍
Layer.hpp是所有layer相关的头文件,是抽象出来的基类,其他5大类都是在它基础上继承得到。
layer中主要有三个参数: (1)layer_param_ 是protobuf文件中存储的layer参数; (2)blobs_ 存储layer的参数,在程序中用的;(3)param_propagate_down_ bool类型用来表示是否计算各个blob参数的diff,即传播误差。
Layer中三个主要函数:SetUp()根据实际的参数设置进行实现,对各种类型的参数初始化;Forward()和Backward()对应前向计算和反向更新,输入统一都是bottom,输出为top,其中Backward里面propagate_down参数,用来表示该Layer是否反向传播参数。
3、layer 5大类总体概括
Layer中程序框架可以通过图1简单了解
从图中可以得到layer主要有5大类组成,下边对5大类先简单介绍一下:
(1)数据层
Data层为数据输入层,头文件定义src/caffe/include/caffe/data_layer.hpp中,作为网络的最底层,主要实现数据输入和格式的转换。
(2)神经元层
Neuron层为元素级别运算层,头文件定义在src/caffe/include/caffe/neuron_layers.hpp中,其派生类主要是元素级别的运算(比如Dropout运算,激活函数ReLu,Sigmoid等)
(3)特征表达层
Vision层为图像卷积相关层,头文件定义在src/caffe/include/caffe/vision_layers.hpp 中,像convolusion、pooling、LRN都在里面,按官方文档的说法,是可以输出图像的,这个要看具体实现代码了。里面有个im2col的实现。
(4)网络连接层
Common层为网络连接层,头文件定义在src/caffe/include/caffe/common_layer.hpp中。Caffe提供了单个层或多个层的连接,并在这个头文件中声明。包括了常用的全连接层InnerProductLayer类,
(5)损失函数层
Loss层为损失函数层,头文件定义在src/caffe/include/caffe/loss_layers.hpp中。前面的data layer和common layer都是中间计算层,虽然会涉及到反向传播,但传播的源头来自于loss_layer,即网络的最终端。这一层因为要计算误差,所以输入都是2个blob,输出1个blob。
总之,data负责输入,vision负责卷积相关的计算,neuron和common负责中间部分的数据计算,而loss是最后一部分,负责计算反向传播的误差。具体的实现都在src/caffe/layers里面,
二、data数据层
Data层为数据输入层,头文件定义data_layer.hpp中,作为网络的最底层,主要实现数据输入和格式的转换。Data 层作为原始数据的输入层,caffe代码中实现了多种数据输入,其中Database(leveldb、lmdb数据库)、In-Memory(内存)、HDF5 Input(hdf5)、Images(原始图像)读入数据。具体使用如下:
1、 Database
(1) 类型:Data
必须参数:source : 包含数据的目录名称
batch_size : 一次处理的输入数量
可选参数:rand_skip : 在开始的时候从输入中跳过这个数值,这在异步随机梯度下
(SGD)的时候非常有用
Backend(default LEVELDB) : 选择使用 LEVELDB 或者 LMDB.
说明:LEVELDB 或者 LMDB都是键/值对(Key/Value Pair)嵌入式数据库管理系统编程
库。lmdb的内存消耗是leveldb的1.1倍,但lmdb的速度比leveldb快10%至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的内存消耗是leveldb的1.1倍,但lmdb的速度比leveldb快10%至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、tanh等11个都在该层实现。具体使用如下:
1、ReLU / 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中。像ArgMaxLayer、ConcatLayer、FlattenLayer、SoftmaxLayer、SplitLayer和SliceLayer等各种对blob增减修改的操作。neuron和common负责中间部分的数据计算。
具体使用如下:
1、Inner 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)功能: 实现y←wx + 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库,但是都没好好看过源码。自己也写过几千行的程序,但抽象的不好,代码框架不够清晰。
未完待续,抽时间好好整理