http://blog.csdn.net/u014568921/article/details/53995455
首先,要知道caffe里的卷积核都是三维的
在caffe中卷积核是三维的还是二维的?
下面分割线之间的内容来自http://blog.csdn.NET/u014114990/article/details/51125776
/******************************************************************************************/
下面讲一下,caffe中的实现。
Caffe中的卷积计算是将卷积核矩阵和输入图像矩阵变换为两个大的矩阵A与B,然后A与B进行矩阵相乘得到结果C(利用GPU进行矩阵相乘的高效性),三个矩阵的说明如下:
(1)在矩阵A中
M为卷积核个数,K=k*k,等于卷积核大小,即第一个矩阵每行为一个卷积核向量(是将二维的卷积核转化为一维),总共有M行,表示有M个卷积核。
(2)在矩阵B中
N=((image_h + 2*pad_h – kernel_h)/stride_h+ 1)*((image_w +2*pad_w – kernel_w)/stride_w + 1)
image_h:输入图像的高度
image_w:输入图像的宽度
pad_h:在输入图像的高度方向两边各增加pad_h个单位长度(因为有两边,所以乘以2)
pad_w:在输入图像的宽度方向两边各增加pad_w个单位长度(因为有两边,所以乘以2)
kernel_h:卷积核的高度
kernel_w:卷积核的宽度
stride_h:高度方向的滑动步长;
stride_w:宽度方向的滑动步长。
因此,N为输出图像大小的长宽乘积,也是卷积核在输入图像上滑动可截取的最大特征数。
K=k*k,表示利用卷积核大小的框在输入图像上滑动所截取的数据大小,与卷积核大小一样大。
(3)在矩阵C中
矩阵C为矩阵A和矩阵B相乘的结果,得到一个M*N的矩阵,其中每行表示一个输出图像即feature map,共有M个输出图像(输出图像数目等于卷积核数目)
(在Caffe中是使用src/caffe/util/im2col.cu中的im2col和col2im来完成矩阵的变形和还原操作)
举个例子(方便理解):
假设有两个卷积核为与,因此M=2,kernel_h=2,kernel_w=2,K= kernel_h * kernel_w=4
输入图像矩阵为,因此image_h=3,image_w=3,令边界扩展为0即pad_h=0,pad_w=0,滑动步长为1,即stride_h=1,stride_w=1
故N=[(3+2*0-2)/1+1]*[ (3+2*0-2)/1+1]=2*2=4
A矩阵(M*K)为(一行为一个卷积核),B矩阵(K*N)为(B矩阵的每一列为一个卷积核要卷积的大小)
A 矩阵的由来:::
B矩阵的由来:(caffe 有 imtocol.cpp代码,专门用于实现)
C=A*B=*=
C中的与分别为两个输出特征图像即feature map。验证了 有几个卷积核就有几个feature map
在Caffe源码中,src/caffe/util/math_functions.cu(如果使用CPU则是src/util/math_functions.cpp)中的caffe_gpu_gemm()函数,其中有两个矩阵A(M*K)
与矩阵 B(K*N),大家可以通过输出M、K、N的值即相应的矩阵内容来验证上述的原理,代码中的C矩阵与上述的C矩阵不一样,代码中的C矩阵存储的是偏置bias,
是A 与B相乘后得到M*N大小的矩阵,然后再跟这个存储偏置的矩阵C相加完成卷积过程。如果是跑Mnist训练网络的话,可以看到第一个卷积层卷积过程中,
M=20,K=25,N=24*24=576。
(caffe中涉及卷积具体过程的文件主要有:src/caffe/layers/conv_layer.cu、src/caffe/layers/base_conv_layer.cpp、 src/caffe/util/math_functions.cu、src/caffe/util/im2col.cu)
另外大家也可以参考知乎上贾扬清大神的回答,帮助理解http://www.zhihu.com/question/28385679
(对于他给出的ppt上的C表示图像通道个数,如果是RGB图像则通道数为3,对应于caffe代码中的变量为src/caffe/layers/base_conv_layer.cpp中
函数forward_gpu_gemm中的group_)
/********************************************************************************************************/
梳理caffe代码im2col(十七)
caffe中卷积计算详解
Caffe源码解析5:Conv_Layer
Caffe 代码阅读-卷积
Caffe Convolutional Layer 记录
Caffe源码学习系列二----卷积层
caffe卷积层代码阅读笔记
卷积运算转换为矩阵乘法
github上关于卷积操作的可视化介绍
A guide to convolution arithmetic for deep learning
在 Caffe 中如何计算卷积?
Caffe源码(四):base_conv_layer 分析
梳理caffe代码base_conv_layer(十八)
其中caffe_cpu_gemm是对cblas_dgemm函数的封装
Caffe Convolutional Layer 记录
先看base_conv_layer
成员数据如下
/// @brief The spatial dimensions of a filter kernel.
Blob<int> kernel_shape_;
/// @brief The spatial dimensions of the stride.
Blob<int> stride_;
/// @brief The spatial dimensions of the padding.
Blob<int> pad_;
/// @brief The spatial dimensions of the dilation.
Blob<int> dilation_;
/// @brief The spatial dimensions of the convolution input.
Blob<int> conv_input_shape_;
/// @brief The spatial dimensions of the col_buffer.
vector<int> col_buffer_shape_;
/// @brief The spatial dimensions of the output.
vector<int> output_shape_;
const vector<int>* bottom_shape_;
int num_spatial_axes_;
int bottom_dim_;
int top_dim_;
int channel_axis_;
int num_;
int channels_;
int group_;
int out_spatial_dim_;
int weight_offset_;
int num_output_;
bool bias_term_;
bool is_1x1_;
bool force_nd_im2col_;
对于卷积层中的卷积操作,还有一个group的概念要说明一下,groups是代表filter 组的个数。引入gruop主要是为了选择性的连接卷基层的输入端和输出端的channels,否则参数会太多。每一个group 和1/ group的input 通道和 1/group 的output通道进行卷积操作。比如有4个input, 8个output,那么1-4属于第一组,5-8属于第二个gruop。
主要函数
1. LayerSetUp 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
2.Reshape 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
3.forward_cpu_gemm 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
4.forward_cpu_bias 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
4.backward_cpu_gemm函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
5.weight_cpu_gemm 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
6.backward_cpu_bias 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
其中用到的一些矩阵运算函数在math_functions.cpp里实现
- 目录
- 主要函数
- caffe_cpu_gemm 函数
- caffe_cpu_gemv 函数
- caffe_axpy 函数
- caffe_set 函数
- caffe_add_scalar 函数
- caffe_copy 函数
- caffe_scal 函数
- caffeine_cup_axpby 函数
- caffe_add caffe_sub caffe_mul caffe_div 函数
- caffe_powx caffe_sqr caffe_exp caffe_abs 函数
- int caffe_rng_rand 函数
- caffe_nextafer 函数
- caffe_cpu_strided_dot 函数
- caffe_cpu_hamming_distance 函数
- caffe_cpu_asum 函数
- caffe_cpu_scale 函数
- 主要函数
主要函数
math_function 定义了caffe 中用到的一些矩阵操作和数值计算的一些函数,这里以float类型为例做简单的分析
1. caffe_cpu_gemm 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
功能: C=alpha*A*B+beta*C
A,B,C 是输入矩阵(一维数组格式)
CblasRowMajor :数据是行主序的(二维数据也是用一维数组储存的)
TransA, TransB:是否要对A和B做转置操作(CblasTrans CblasNoTrans)
M: A、C 的行数
N: B、C 的列数
K: A 的列数, B 的行数
lda : A的列数(不做转置)行数(做转置)
ldb: B的列数(不做转置)行数(做转置)
2. caffe_cpu_gemv 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
功能: y=alpha*A*x+beta*y
其中X和Y是向量,A 是矩阵
M:A 的行数
N:A 的列数
cblas_sgemv 中的 参数1 表示对X和Y的每个元素都进行操作
3.caffe_axpy 函数:
- 1
- 2
- 3
- 1
- 2
- 3
功能: Y=alpha*X+Y
N:为X和Y中element的个数
4.caffe_set 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
功能:用常数 alpha 对 Y 进行初始化
函数 void *memset(void *buffer, char c, unsigned count) 一般为新申请的内存做初始化,功能是将buffer所指向内存中的每个字节的内容全部设置为c指定的ASCII值, count为块的大小
5.caffe_add_scalar 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
功能: 给 Y 的每个 element 加上常数 alpha
6.caffe_copy 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
函数 void *memcpy(void *dest, void *src, unsigned int count) 把src所指向的内存区域 copy到dest所指向的内存区域, count为块的大小
7.caffe_scal 函数:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
功能:X = alpha*X
N: X中element的个数
8.caffeine_cup_axpby 函数:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
功能:Y= alpha*X+beta*Y
9.caffe_add、 caffe_sub、 caffe_mul、 caffe_div 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
功能:这四个函数分别实现element-wise的加减乘除(y[i] = a[i] + - * \ b[i])
10.caffe_powx、 caffe_sqr、 caffe_exp、 caffe_abs 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
功能 : 同样是element-wise操作,分别是y[i] = a[i] ^ b, y[i] = a[i]^2,y[i] = exp(a[i] ),y[i] = |a[i] |
11.int caffe_rng_rand 函数:
- 1
- 2
- 3
- 1
- 2
- 3
功能:返回一个随机数
12.caffe_nextafer 函数:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
功能 : 返回 b 最大方向上可以表示的最接近的数值。
13.caffe_cpu_strided_dot 函数:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
功能: 返回 vector X 和 vector Y 的内积。
incx, incy : 步长,即每隔incx 或 incy 个element 进行操作。
14.caffe_cpu_hamming_distance 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
功能:返回 x 和 y 之间的海明距离。(两个等长字符串之间的海明距离是两个字符串对应位置的不同字符的个数。)
15. caffe_cpu_asum 函数:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
功能:计算 vector x 的所有element的绝对值之和。
16.caffe_cpu_scale 函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
功能:y = alpha*x
关于deconv
这个概念很混乱,没有统一的定义,在不同的地方出现,意义却不一样。
上采样的卷积层有很多名字:全卷积(full convolution),网络内上采样( in-network upsampling),微步幅卷积(fractionally-strided convolution),反向卷积(backwards convolution),去卷积(deconvolution),上卷积(upconvolution),以及转置卷积(transposed convolution)。用「去卷积」这个术语是非常不推荐的,因为这是一个过载的术语:在数学运算或计算机视觉中的其他应用有着完全不同的含义。
神经网络中,怎样计算caffe中反卷积层(deconv)的感受野(receptive field)
What are deconvolutional layers?
deconv_layer.cpp
http://imbinwang.github.io/blog/inside-caffe-code-layer
Layer(层)是Caffe中最庞大最繁杂的模块,它是网络的基本计算单元。由于Caffe强调模块化设计,因此只允许每个layer完成一类特定的计算,例如convolution操作、pooling、非线性变换、内积运算,以及数据加载、归一化和损失计算等。
模块说明
每个layer的输入数据来自一些’bottom’ blobs, 输出一些’top’ blobs。Caffe中每种类型layer的参数说明定义在caffe.proto
文件中,具体的layer参数值则定义在具体应用的protocals buffer网络结构说明文件中。例如,卷积层(ConvolutionLayer)的参数说明在caffe.proto
中是如下定义的,
// in caffe.proto
// Message that stores parameters used by ConvolutionLayer
message ConvolutionParameter {optional uint32 num_output = 1; // The number of outputs for the layer
optional bool bias_term = 2 [default = true]; // whether to have bias terms
// Pad, kernel size, and stride are all given as a single value for equal
// dimensions in height and width or as Y, X pairs.
optional uint32 pad = 3 [default = 0]; // The padding size (equal in Y, X)
optional uint32 pad_h = 9 [default = 0]; // The padding height
optional uint32 pad_w = 10 [default = 0]; // The padding width
optional uint32 kernel_size = 4; // The kernel size (square)
optional uint32 kernel_h = 11; // The kernel height
optional uint32 kernel_w = 12; // The kernel width
optional uint32 group = 5 [default = 1]; // The group size for group conv
optional uint32 stride = 6 [default = 1]; // The stride (equal in Y, X)
optional uint32 stride_h = 13; // The stride height
optional uint32 stride_w = 14; // The stride width
optional FillerParameter weight_filler = 7; // The filler for the weight
optional FillerParameter bias_filler = 8; // The filler for the bias
enum Engine {DEFAULT = 0;CAFFE = 1;CUDNN = 2;}optional Engine engine = 15 [default = DEFAULT];
}
其中的参数说明包括卷积核的个数、大小和步长等。在examples\mnist\lenet_train_test.prototxt
网络结构说明文件中,具体一个卷积层(ConvolutionLayer)是这样定义的,
# in examples\mnist\lenet_train_test.prototxt
layer {name: "conv1" // 层的名字
type: "Convolution" // 层的类型,说明具体执行哪一种计算
bottom: "data" // 层的输入数据Blob的名字
top: "conv1" // 层的输出数据Blob的名字
param { // 层的权值和偏置相关参数
lr_mult: 1}param {lr_mult: 2}convolution_param { // 卷积层卷积运算相关的参数
num_output: 20kernel_size: 5stride: 1weight_filler {type: "xavier"}bias_filler {type: "constant"}}
}
层的输入输出结构,图示是这样的,
每种类型的layer需要定义三种关键操作LayerSetUp, Forward, Backward:
- LayerSetUp: 网络构建时初始化层和层的连接
- Forward: 网络数据前向传递,给定bottom输入数据,计算输出到top
- Backward: 网络误差反向传递,给定top的梯度,计算bottom的梯度并存储到bottom blob
实现细节
Caffe中与Layer相关的头文件有7个,
layer.hpp
: 父类Layer,定义所有layer的基本接口。data_layers.hpp
: 继承自父类Layer,定义与输入数据操作相关的子Layer,例如DataLayer,HDF5DataLayer和ImageDataLayer等。vision_layers.hpp
: 继承自父类Layer,定义与特征表达相关的子Layer,例如ConvolutionLayer,PoolingLayer和LRNLayer等。neuron_layers.hpp
: 继承自父类Layer,定义与非线性变换相关的子Layer,例如ReLULayer,TanHLayer和SigmoidLayer等。loss_layers.hpp
: 继承自父类Layer,定义与输出误差计算相关的子Layer,例如EuclideanLossLayer,SoftmaxWithLossLayer和HingeLossLayer等。common_layers.hpp
: 继承自父类Layer,定义与中间结果数据变形、逐元素操作相关的子Layer,例如ConcatLayer,InnerProductLayer和SoftmaxLayer等。layer_factory.hpp
: Layer工厂模式类,负责维护现有可用layer和相应layer构造方法的映射表。
每个Layer根据自身需求的不同,会定义CPU或GPU版本的实现,例如ConvolutionLayer的CPU和GPU实现就定义在了两个文件中conv_layer.cpp, conv_layer.cu
。
父类Layer
layer.hpp
中定义了Layer的基本接口,成员变量,
protected:/** The protobuf that stores the layer parameters */// 层说明参数,从protocal buffers格式的网络结构说明文件中读取
LayerParameter layer_param_;/** The phase: TRAIN or TEST */// 层状态,参与网络的训练还是测试
Phase phase_;/** The vector that stores the learnable parameters as a set of blobs. */// 层权值和偏置参数,使用向量是因为权值参数和偏置是分开保存在两个blob中的
vector<shared_ptr<Blob<Dtype> > > blobs_;/** Vector indicating whether to compute the diff of each param blob. */// 标志每个top blob是否需要计算反向传递的梯度值
vector<bool> param_propagate_down_;/** The vector that indicates whether each top blob has a non-zero weight in* the objective function. */// 非LossLayer为零,LossLayer中表示每个top blob计算的loss的权重
vector<Dtype> loss_;
构造和析构函数,
/*** You should not implement your own constructor. Any set up code should go* to SetUp(), where the dimensions of the bottom blobs are provided to the* layer.*/
// 显示的构造函数不需要重写,任何初始工作在SetUp()中完成
// 构造方法只复制层参数说明的值,如果层说明参数中提供了权值和偏置参数,也复制
explicit Layer(const LayerParameter& param): layer_param_(param) {// Set phase and copy blobs (if there are any).
phase_ = param.phase();if (layer_param_.blobs_size() > 0) {blobs_.resize(layer_param_.blobs_size());for (int i = 0; i < layer_param_.blobs_size(); ++i) {blobs_[i].reset(new Blob<Dtype>());blobs_[i]->FromProto(layer_param_.blobs(i));}}}
// 虚析构
virtual ~Layer() {}
初始化函数SetUp,每个Layer对象都必须遵循固定的调用模式,
/*** @brief Implements common layer setup functionality.* @brief 实现每个layer对象的setup函数* @param bottom the preshaped input blobs* @param bottom 层的输入数据,blob中的存储空间已申请* @param top* the allocated but unshaped output blobs, to be shaped by Reshape* @param top 层的输出数据,blob对象以构造但是其中的存储空间未申请,* 具体空间大小需根据bottom blob大小和layer_param_共同决定,具体在Reshape函数现实** Checks that the number of bottom and top blobs is correct.* Calls LayerSetUp to do special layer setup for individual layer types,* followed by Reshape to set up sizes of top blobs and internal buffers.* Sets up the loss weight multiplier blobs for any non-zero loss weights.* This method may not be overridden.* 1. 检查输入输出blob个数是否满足要求,每个层能处理的输入输出数据不一样* 2. 调用LayerSetUp函数初始化特殊的层,每个Layer子类需重写这个函数完成定制的初始化* 3. 调用Reshape函数为top blob分配合适大小的存储空间* 4. 为每个top blob设置损失权重乘子,非LossLayer为的top blob其值为零** 此方法非虚函数,不用重写,模式固定*/void SetUp(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) {CheckBlobCounts(bottom, top);LayerSetUp(bottom, top);Reshape(bottom, top);SetLossWeights(top);}
每个子类Layer必须重写的初始化函数LayerSetUp,
/*** @brief Does layer-specific setup: your layer should implement this function* as well as Reshape.* @brief 定制初始化,每个子类layer必须实现此虚函数** @param bottom* the preshaped input blobs, whose data fields store the input data for* this layer* @param bottom* 输入blob, 数据成员data_和diff_存储了相关数据* @param top* the allocated but unshaped output blobs* @param top* 输出blob, blob对象已构造但数据成员的空间尚未申请** This method should do one-time layer specific setup. This includes reading* and processing relevent parameters from the <code>layer_param_</code>.* Setting up the shapes of top blobs and internal buffers should be done in* <code>Reshape</code>, which will be called before the forward pass to* adjust the top blob sizes.* 此方法执行一次定制化的层初始化,包括从layer_param_读入并处理相关的层权值和偏置参数,* 调用Reshape函数申请top blob的存储空间*/virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) {}
每个子类Layer必须重写的Reshape函数,完成top blob形状的设置并为其分配存储空间,
/*** @brief Adjust the shapes of top blobs and internal buffers to accomodate* the shapes of the bottom blobs.* @brief 根据bottom blob的形状和layer_param_计算top blob的形状并为其分配存储空间** @param bottom the input blobs, with the requested input shapes* @param top the top blobs, which should be reshaped as needed** This method should reshape top blobs as needed according to the shapes* of the bottom (input) blobs, as well as reshaping any internal buffers* and making any other necessary adjustments so that the layer can* accomodate the bottom blobs.*/virtual void Reshape(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) = 0;
前向传播函数Forward和反向传播函数Backward,
inline Dtype Forward(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top);
inline void Backward(const vector<Blob<Dtype>*>& top,const vector<bool>& propagate_down,const vector<Blob<Dtype>*>& bottom);
这两个函数非虚函数,它们内部会调用如下虚函数完成数据前向传递和误差反向传播,根据执行环境的不同每个子类Layer必须重写CPU和GPU版本,
virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) = 0;
virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) {// LOG(WARNING) << "Using CPU code as backup.";
return Forward_cpu(bottom, top);}virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,const vector<bool>& propagate_down,const vector<Blob<Dtype>*>& bottom) = 0;virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,const vector<bool>& propagate_down,const vector<Blob<Dtype>*>& bottom) {// LOG(WARNING) << "Using CPU code as backup.";
Backward_cpu(top, propagate_down, bottom);}
Layer的序列化函数,将layer的层说明参数layer_param_,层权值和偏置参数blobs_复制到LayerParameter对象,便于写到磁盘,
// Serialize LayerParameter to protocol buffer
template <typename Dtype>
void Layer<Dtype>::ToProto(LayerParameter* param, bool write_diff) {param->Clear();param->CopyFrom(layer_param_); // 复制层说明参数layer_param_
param->clear_blobs();// 复制层权值和偏置参数blobs_
for (int i = 0; i < blobs_.size(); ++i) {blobs_[i]->ToProto(param->add_blobs(), write_diff);}
}
子类Data Layers
数据经过date layers进入Caffe的数据处理流程,他们位于网络Net最底层。数据可以来自高效的数据库(LevelDB或LMDB),直接来自内存,或者对效率不太关注时,可以来自HDF5格式的或常见图片格式的磁盘文件。Data Layers继承自Layer,继承关系如图所示,
最终的子类层包括DataLayer,ImageDataLayer,WindowDataLayer,MemoryDataLayer,HDF5DataLayer,HDF5OutputLayer,DummyDataLayer。这里只分析DataLayer,其它数据层类似。
首先,来看DataLayer的LayerSetUp
实现过程,DataLayer直接从父类BasePrefetchingDataLayer继承此方法,
// in base_data_layer.cpp
template <typename Dtype>
void BasePrefetchingDataLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {// 1. 调用父父类BaseDataLayer构造方法,
BaseDataLayer<Dtype>::LayerSetUp(bottom, top);// Now, start the prefetch thread. Before calling prefetch, we make two
// cpu_data calls so that the prefetch thread does not accidentally make
// simultaneous cudaMalloc calls when the main thread is running. In some
// GPUs this seems to cause failures if we do not so.
// 2. 访问预取数据空间,这里是为了提前分配预取数据的存储空间
this->prefetch_data_.mutable_cpu_data();if (this->output_labels_) {this->prefetch_label_.mutable_cpu_data();}// 3. 创建用于预取数据的线程
DLOG(INFO) << "Initializing prefetch";this->CreatePrefetchThread();DLOG(INFO) << "Prefetch initialized.";
}
执行流程大致为:
-
调用父父类BaseDataLayer构造方法,
// in base_data_layer.cpp template <typename Dtype> void BaseDataLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) {if (top.size() == 1) {output_labels_ = false;} else {output_labels_ = true;}// The subclasses should setup the size of bottom and top DataLayerSetUp(bottom, top);data_transformer_.reset(new DataTransformer<Dtype>(transform_param_, this->phase_));data_transformer_->InitRand(); }
根据top blob的个数判断是否输出数据的label,对
output_labels_
赋值,接下来调用自己的DataLayerSetUp
方法,// in data_layer.cpp template <typename Dtype> void DataLayer<Dtype>::DataLayerSetUp(const vector<Blob<Dtype>*>& bottom,const vector<Blob<Dtype>*>& top) {// Initialize DB // 打开源数据库 db_.reset(db::GetDB(this->layer_param_.data_param().backend()));db_->Open(this->layer_param_.data_param().source(), db::READ);cursor_.reset(db_->NewCursor());// Check if we should randomly skip a few data points if (this->layer_param_.data_param().rand_skip()) {unsigned int skip = caffe_rng_rand() %this->layer_param_.data_param().rand_skip();LOG(INFO) << "Skipping first " << skip << " data points.";while (skip-- > 0) {cursor_->Next();}}// Read a data point, and use it to initialize the top blob. // 读取一个数据对象, 用于分析数据对象的存储空间大小,并未输出到top blob Datum datum;datum.ParseFromString(cursor_->value());bool force_color = this->layer_param_.data_param().force_encoded_color();if ((force_color && DecodeDatum(&datum, true)) ||DecodeDatumNative(&datum)) {LOG(INFO) << "Decoding Datum";}// image // 对数据对象进行预处理 int crop_size = this->layer_param_.transform_param().crop_size();if (crop_size > 0) {// 为top blob分配存储空间,同时为预取数据分配存储空间 top[0]->Reshape(this->layer_param_.data_param().batch_size(),datum.channels(), crop_size, crop_size);this->prefetch_data_.Reshape(this->layer_param_.data_param().batch_size(),datum.channels(), crop_size, crop_size);this->transformed_data_.Reshape(1, datum.channels(), crop_size, crop_size);} else {top[0]->Reshape(this->layer_param_.data_param().batch_size(), datum.channels(),datum.height(), datum.width());this->prefetch_data_.Reshape(this->layer_param_.data_param().batch_size(),datum.channels(), datum.height(), datum.width());this->transformed_data_.Reshape(1, datum.channels(),datum.height(), datum.width());}LOG(INFO) << "output data size: " << top[0]->num() << ","<< top[0]->channels() << "," << top[0]->height() << ","<< top[0]->width();// label if (this->output_labels_) {vector<int> label_shape(1, this->layer_param_.data_param().batch_size());top[1]->Reshape(label_shape);this->prefetch_label_.Reshape(label_shape);} }
打开数据源数据库,读取一个数据对象,对数据对象进行预处理,为top blob分配存储空间,同时为预取数据分配存储空间。
- 访问预取数据空间,为了提前分配预取数据的存储空间。
- 调用
CreatePrefetchThread
方法,创建用于预取数据的线程。
层初始化的工作完成。接下来看DataLayer的Forward
实现过程,因为DataLayer位于网络最底层,因此无需实现Backward
。DataLayer直接从父类BasePrefetchingDataLayer继承Forward
方法,且只实现了CPU版本Forward_cpu
,
// in base_data_layer.cpp
template <typename Dtype>
void BasePrefetchingDataLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {// First, join the thread
// 等待线程的数据预取结束
JoinPrefetchThread();DLOG(INFO) << "Thread joined";// Reshape to loaded data.
top[0]->Reshape(this->prefetch_data_.num(), this->prefetch_data_.channels(),this->prefetch_data_.height(), this->prefetch_data_.width());// Copy the data
// 将预取的数据复制到top blobs
caffe_copy(prefetch_data_.count(), prefetch_data_.cpu_data(),top[0]->mutable_cpu_data());DLOG(INFO) << "Prefetch copied";if (this->output_labels_) {caffe_copy(prefetch_label_.count(), prefetch_label_.cpu_data(),top[1]->mutable_cpu_data());}// Start a new prefetch thread
// 创建新线程完成数据预取
DLOG(INFO) << "CreatePrefetchThread";CreatePrefetchThread();
}
可以看到,DataLayer的Forward_cpu
就是通过另一个线程预先取得数据源中的数据,需要时将预取的数据复制到top blobs,完成数据的前向传播。
P.S. 注意到在data_layer.cpp
文件的最后,有下面两句宏函数,
INSTANTIATE_CLASS(DataLayer);
REGISTER_LAYER_CLASS(Data);
它们被用来做什么了?看看它们的定义,
// ------ in common.hpp ------
// Instantiate a class with float and double specifications.
#define INSTANTIATE_CLASS(classname) \char gInstantiationGuard##classname; \template class classname<float>; \template class classname<double>
// ------ in common.hpp ------
// ------ in layer_factory.hpp ------
#define REGISTER_LAYER_CREATOR(type, creator) \static LayerRegisterer<float> g_creator_f_##type(#type, creator<float>); \static LayerRegisterer<double> g_creator_d_##type(#type, creator<double>) \#define REGISTER_LAYER_CLASS(type) \template <typename Dtype> \shared_ptr<Layer<Dtype> > Creator_##type##Layer(const LayerParameter& param) \{ \return shared_ptr<Layer<Dtype> >(new type##Layer<Dtype>(param)); \} \REGISTER_LAYER_CREATOR(type, Creator_##type##Layer)
// ------ in layer_factory.hpp ------
其中,INSTANTIATE_CLASS(DataLayer)
被用来实例化DataLayer的类模板,REGISTER_LAYER_CLASS(Data)
被用来向layer_factory注册DataLayer的构造方法,方便直接通过层的名称(Data)直接获取层的对象。Caffe中内置的层在实现的码的最后都会加上这两个宏。
子类Vision Layers
Vision Layers, 暂时将其翻译成特征表达层,它通常接收“图像”作为输入,输出结果也是“图像”。这里的“图像”可以是真实世界的单通道灰度图像,或RGB彩色图像, 或多通道2D矩阵。在Caffe的上下文环境下,“图像”的显著性特征是它的空间结构:宽w>1,高h>1,这个2D的性质导致Vision Layers具有局部区域操作的性质,例如卷积,池化等。Vision Layers继承自也Layer,继承关系如图所示,
最终的子类层包括ConvolutionLayer,CuDNNConvolutionLayer,PoolingLayer,CuDNNPoolingLayer,LRNLayer,DeconvolutionLayer,还有若干辅助的功能子类层Im2colLayer,SplitLayer。这里会详细分析ConvolutionLayer。