caffe源码vision_layers.hpp+各cpp

vision_layers.hpp:

ConvolutionLayer类,CuDNNConvolutionLayer类;

Im2colLayer类;

LRNLayer类;

PoolingLayer类,CuDNNPoolingLayer类。

在这个文件中一共包含了上面的4种层。因为在卷积的时候,caffe为了加速处理,使用了小技巧im2col,所以我们先来看看im2colLayer:

1. im2colLayer:

1.1. 原理介绍:

用两个图来进行说明:

如上图,相当于是把很多个小矩阵行化之后合并成一个大矩阵。

这个图演示了卷积的过程。可能稍微有些看不太明白,我用matlab简单的演示一下:

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. clear,clc  
  2. im = {[1,2,0;1,1,3;0,2,2;],[0,2,1;0,3,2;1,1,0;],[1,2,1;0,1,3;3,3,2]};  
  3. kel = {[1,1;2,2;],[1,1;1,1;],[0,1;1,0;];[1,0;0,1;],[2,1;2,1;],[1,2;2,0;]};  
  4. out1 = zeros(2,2);  
  5. out2 = zeros(2,2);  
  6. for i = 1:2  
  7.     for j = 1:2  
  8.         for k = 1:3  
  9.             out1(i,j) = out1(i,j) + sum(sum(kel{1,k}.*im{k}(i:i+1,j:j+1)));  
  10.             out2(i,j) = out2(i,j) + sum(sum(kel{2,k}.*im{k}(i:i+1,j:j+1)));  
  11.         end  
  12.     end  
  13. end  
  14. out1  
  15. out2  

上图中一共3个输入矩阵,6个卷积核。以上的代码演示的是传统卷积部分,也就是图片的上半部分:

然后,下半部分的卷积过程应该就好理解了。那么其实我们可以想象得到,两种方式做的乘法加法次数应该都是相同的,为什么下面版本的卷积会起到加速的作用呢?还多了一个转换的过程多麻烦啊!我暂时的理解是:这个应该与计算的多级缓存有关系,弄成一个大矩阵之后,换页的次数应该会大大降低,从而提高了速度。(后来发现还有另外一个好处,以后分析道全连接层的代码时会发现跟卷积层很像,卷积层的计算其实可以就看成一种局部全连接层嘛,而经过im2col转换之后,就变得和全连接层的计算一样了)

1.2. 源码分析:

A helper for image operations that rearranges image regions into column vectors.  Used by ConvolutionLayer to perform convolution by matrix multiplication.

1.2.1. 属性变量:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int kernel_h_, kernel_w_;  
  2. int stride_h_, stride_w_;  
  3. int channels_;  
  4. int height_, width_;  
  5. int pad_h_, pad_w_;  

对跑过caffe的人来说,这几个变量并不陌生吧。因为在配置文件(例如:train_val.prototxt)中常常见到这几个东西。但是我们一般可能设置的都是单个值,因为我们一般使用的都是方形。从这里来看,原来可以使用矩形的。其实这些东西在caffe.proto中也可以看得到。

1.2.2. 构造函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. explicit Im2colLayer(const LayerParameter& param)  
  2.     : Layer<Dtype>(param) {}  
  3. virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,  
  4.     vector<Blob<Dtype>*>* top);  
  5. virtual void Reshape(const vector<Blob<Dtype>*>& bottom,  
  6.     vector<Blob<Dtype>*>* top);  

虽然这里只有一个是构造函数, Im2colLayer()。感觉这三个放在这里一起介绍合适一些。

在im2col_layer.cpp中找到对应的实现。

其中Im2colLayer()直接继承使用了Layer工厂类中的Layer()

LayerSetUp()的实现如下:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void Im2colLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,  
  3.       vector<Blob<Dtype>*>* top) {  
  4.   ConvolutionParameter conv_param = this->layer_param_.convolution_param();  
  5.   CHECK(!conv_param.has_kernel_size() !=  
  6.       !(conv_param.has_kernel_h() && conv_param.has_kernel_w()))  
  7.       << "Filter size is kernel_size OR kernel_h and kernel_w; not both";  
  8.   CHECK(conv_param.has_kernel_size() ||  
  9.       (conv_param.has_kernel_h() && conv_param.has_kernel_w()))  
  10.       << "For non-square filters both kernel_h and kernel_w are required.";  
  11.   CHECK((!conv_param.has_pad() && conv_param.has_pad_h()  
  12.       && conv_param.has_pad_w())  
  13.       || (!conv_param.has_pad_h() && !conv_param.has_pad_w()))  
  14.       << "pad is pad OR pad_h and pad_w are required.";  
  15.   CHECK((!conv_param.has_stride() && conv_param.has_stride_h()  
  16.       && conv_param.has_stride_w())  
  17.       || (!conv_param.has_stride_h() && !conv_param.has_stride_w()))  
  18.       << "Stride is stride OR stride_h and stride_w are required.";  
  19.   if (conv_param.has_kernel_size()) {  
  20.     kernel_h_ = kernel_w_ = conv_param.kernel_size();  
  21.   } else {  
  22.     kernel_h_ = conv_param.kernel_h();  
  23.     kernel_w_ = conv_param.kernel_w();  
  24.   }  
  25.   CHECK_GT(kernel_h_, 0) << "Filter dimensions cannot be zero.";  
  26.   CHECK_GT(kernel_w_, 0) << "Filter dimensions cannot be zero.";  
  27.   if (!conv_param.has_pad_h()) {  
  28.     pad_h_ = pad_w_ = conv_param.pad();  
  29.   } else {  
  30.     pad_h_ = conv_param.pad_h();  
  31.     pad_w_ = conv_param.pad_w();  
  32.   }  
  33.   if (!conv_param.has_stride_h()) {  
  34.     stride_h_ = stride_w_ = conv_param.stride();  
  35.   } else {  
  36.     stride_h_ = conv_param.stride_h();  
  37.     stride_w_ = conv_param.stride_w();  
  38.   }  
  39. }  

该方法函数首先调用的是:layer_param_.convolution_param()

还记得在Layer工厂类中的属性变量就有layer_param_吗?其定义如下:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. LayerParameter layer_param_;  

那么LayerParameter是怎样的一个类型呢?可以在caffe.proto中找到(详见: caffe源码阅读4-layer.hpp)

那么这个类中的方法convolution_param()是个什么东西?

从上面的源码返回类型来看,返回值是ConvolutionParameter类型的:

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // Message that stores parameters used by ConvolutionLayer  
  2. message ConvolutionParameter {  
  3.   optional uint32 num_output = 1; // The number of outputs for the layer  
  4.   optional bool bias_term = 2 [default = true]; // whether to have bias terms  
  5.   // Pad, kernel size, and stride are all given as a single value for equal  
  6.   // dimensions in height and width or as Y, X pairs.  
  7.   optional uint32 pad = 3 [default = 0]; // The padding size (equal in Y, X)  
  8.   optional uint32 pad_h = 9 [default = 0]; // The padding height  
  9.   optional uint32 pad_w = 10 [default = 0]; // The padding width  
  10.   optional uint32 kernel_size = 4; // The kernel size (square)  
  11.   optional uint32 kernel_h = 11; // The kernel height  
  12.   optional uint32 kernel_w = 12; // The kernel width  
  13.   optional uint32 group = 5 [default = 1]; // The group size for group conv  
  14.   optional uint32 stride = 6 [default = 1]; // The stride (equal in Y, X)  
  15.   optional uint32 stride_h = 13; // The stride height  
  16.   optional uint32 stride_w = 14; // The stride width  
  17.   optional FillerParameter weight_filler = 7; // The filler for the weight  
  18.   optional FillerParameter bias_filler = 8; // The filler for the bias  
  19.   enum Engine {  
  20.     DEFAULT = 0;  
  21.     CAFFE = 1;  
  22.     CUDNN = 2;  
  23.   }  
  24.   optional Engine engine = 15 [default = DEFAULT];  
  25. }  

所以 convolution_param()差不多相当于返回了本层参数的详细信息。

LayerSetUp()中接着就是检测本层初始化的时候,该有的属性变量是否都具有,是否正确,例如需要包含kernel_size的话,就不用kernel_h和kernel_w了。

然后就开始对这些属性变量进行赋值。有点奇怪的是,LayerSetUp()代码中根本没有使用参数const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top啊!这是什么情况?可能是为了以后需要的时候便于扩展而不需要修改接口吧。

下来来看看Reshape()函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void Im2colLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom,  
  3.       vector<Blob<Dtype>*>* top) {  
  4.   channels_ = bottom[0]->channels();  
  5.   height_ = bottom[0]->height();  
  6.   width_ = bottom[0]->width();  
  7.   (*top)[0]->Reshape(  
  8.       bottom[0]->num(), channels_ * kernel_h_ * kernel_w_,  
  9.       (height_ + 2 * pad_h_ - kernel_h_) / stride_h_ + 1,  
  10.       (width_ + 2 * pad_w_ - kernel_w_) / stride_w_ + 1);  
  11. }  

该函数实现的是将本层的图像(或者说矩阵吧)大小改成和bottom一样。将top的也修改的和bottom一致,不过是做了变换之后的,与卷积核大小,步长等等有关。

1.2.3. 前馈反馈函数:

vision_layers.hpp中对这里的声明是:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. protected:  
  2.  virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,  
  3.      vector<Blob<Dtype>*>* top);  
  4.  virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,  
  5.      vector<Blob<Dtype>*>* top);  
  6.  virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,  
  7.      const vector<bool>& propagate_down, vector<Blob<Dtype>*>* bottom);  
  8.  virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,  
  9.      const vector<bool>& propagate_down, vector<Blob<Dtype>*>* bottom);  

而在im2col_layer.cpp中的实现是:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void Im2colLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,  
  3.       vector<Blob<Dtype>*>* top) {  
  4.   const Dtype* bottom_data = bottom[0]->cpu_data();  
  5.   Dtype* top_data = (*top)[0]->mutable_cpu_data();  
  6.   for (int n = 0; n < bottom[0]->num(); ++n) {  
  7.     im2col_cpu(bottom_data + bottom[0]->offset(n), channels_, height_,  
  8.         width_, kernel_h_, kernel_w_, pad_h_, pad_w_,  
  9.         stride_h_, stride_w_, top_data + (*top)[0]->offset(n));  
  10.   }  
  11. }  
  12.   
  13. template <typename Dtype>  
  14. void Im2colLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,  
  15.       const vector<bool>& propagate_down, vector<Blob<Dtype>*>* bottom) {  
  16.   const Dtype* top_diff = top[0]->cpu_diff();  
  17.   Dtype* bottom_diff = (*bottom)[0]->mutable_cpu_diff();  
  18.   for (int n = 0; n < top[0]->num(); ++n) {  
  19.     col2im_cpu(top_diff + top[0]->offset(n), channels_, height_, width_,  
  20.         kernel_h_, kernel_w_, pad_h_, pad_w_,  
  21.         stride_h_, stride_w_, bottom_diff + (*bottom)[0]->offset(n));  
  22.   }  
  23. }  

这两个代码看起来很简单,因为具体的操作过程都在 im2col_cpu()col2im_cpu()中。这里有没有感觉到有点奇怪?奇怪的有两个地方:

1 为什么只有cpu的计算呢?不应该也有gpu的计算吗?难道这里的计算不需要gpu?其实不然,在im2col_layer.cu中实现了gpu的部分,和这里cpu的代码基本长得一样。只是调用的是im2col_gpu()col2im_gpu()

2 为什么只对bottom[0]和top[0]操作呢?不应该可能有多个bottom和多个top吗?呃,这里我也懵了,先不管,继续往后面看吧。

然后关于im2col_layer中的其余几个函数,呃,暂时看上去没有什么特别的用处一样。

1.3 im2col, col2im的实现

在im2col.hpp+cpp中,一共包含4个函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #ifndef _CAFFE_UTIL_IM2COL_HPP_  
  2. #define _CAFFE_UTIL_IM2COL_HPP_  
  3.   
  4. namespace caffe {  
  5.   
  6. template <typename Dtype>  
  7. void im2col_cpu(const Dtype* data_im, const int channels,  
  8.     const int height, const int width, const int kernel_h, const int kernel_w,  
  9.     const int pad_h, const int pad_w, const int stride_h,  
  10.     const int stride_w, Dtype* data_col);  
  11.   
  12. template <typename Dtype>  
  13. void col2im_cpu(const Dtype* data_col, const int channels,  
  14.     const int height, const int width, const int patch_h, const int patch_w,  
  15.     const int pad_h, const int pad_w, const int stride_h,  
  16.     const int stride_w, Dtype* data_im);  
  17.   
  18. template <typename Dtype>  
  19. void im2col_gpu(const Dtype* data_im, const int channels,  
  20.     const int height, const int width, const int kernel_h, const int kernel_w,  
  21.     const int pad_h, const int pad_w, const int stride_h,  
  22.     const int stride_w, Dtype* data_col);  
  23.   
  24. template <typename Dtype>  
  25. void col2im_gpu(const Dtype* data_col, const int channels,  
  26.     const int height, const int width, const int patch_h, const int patch_w,  
  27.     const int pad_h, const int pad_w, const int stride_h,  
  28.     const int stride_w, Dtype* data_im);  
  29.   
  30. }  // namespace caffe  
  31.   
  32. #endif  // CAFFE_UTIL_IM2COL_HPP_  

gpu和cpu的实现基本一样,所以我们只看cpu的实现吧:

1.3.1 im2col_cpu():

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void im2col_cpu(const Dtype* data_im, const int channels,  
  3.     const int height, const int width, const int kernel_h, const int kernel_w,  
  4.     const int pad_h, const int pad_w,  
  5.     const int stride_h, const int stride_w,  
  6.     Dtype* data_col) {  
  7.   int height_col = (height + 2 * pad_h - kernel_h) / stride_h + 1;  
  8.   int width_col = (width + 2 * pad_w - kernel_w) / stride_w + 1;  
  9.   int channels_col = channels * kernel_h * kernel_w;  
  10.   for (int c = 0; c < channels_col; ++c) {  
  11.     int w_offset = c % kernel_w;  
  12.     int h_offset = (c / kernel_w) % kernel_h;  
  13.     int c_im = c / kernel_h / kernel_w;  
  14.     for (int h = 0; h < height_col; ++h) {  
  15.       for (int w = 0; w < width_col; ++w) {  
  16.         int h_pad = h * stride_h - pad_h + h_offset;  
  17.         int w_pad = w * stride_w - pad_w + w_offset;  
  18.         if (h_pad >= 0 && h_pad < height && w_pad >= 0 && w_pad < width)  
  19.           data_col[(c * height_col + h) * width_col + w] =  
  20.             data_im[(c_im * height + h_pad) * width + w_pad];  
  21.         else  
  22.           data_col[(c * height_col + h) * width_col + w] = 0;  
  23.       }  
  24.     }  
  25.   }  
  26. }  

原理已经在最开始介绍过了,那么这里的代码读起来应该不是那么费劲。

这里是将输入数据data_im经过im2col之后,将新的数据存入data_col中。至于说data_col的通道数,高,宽的计算,自己稍微推理一下吧。

需要注意的是:pad_h乘了2,pad_w也乘了2,我们首先需要明确pad_h的意思是在输入数据的高度方向两边各增加pad_h个单位长度,所以乘了2。那么pad_w也是一样的道理。

下面我画一个图来便于理解:(参考https://www.zhihu.com/question/28385679)

在源码当中还有一个小细节:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // Explicit instantiation  
  2. template void im2col_cpu<float>(const float* data_im, const int channels,  
  3.     const int height, const int width, const int kernel_h, const int kernel_w,  
  4.     const int pad_h, const int pad_w, const int stride_h,  
  5.     const int stride_w, float* data_col);  
  6. template void im2col_cpu<double>(const double* data_im, const int channels,  
  7.     const int height, const int width, const int kernel_h, const int kernel_w,  
  8.     const int pad_h, const int pad_w, const int stride_h,  
  9.     const int stride_w, double* data_col);  

扎眼一看,还以为两个长一样,其实不然。

第一个的data_im, data_col是float*类型的;第一个的data_im, data_col是double*类型的;同时也说明了不支持整型。

1.3.2 col2im_cpu():

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void col2im_cpu(const Dtype* data_col, const int channels,  
  3.     const int height, const int width, const int patch_h, const int patch_w,  
  4.     const int pad_h, const int pad_w,  
  5.     const int stride_h, const int stride_w,  
  6.     Dtype* data_im) {  
  7.   caffe_set(height * width * channels, Dtype(0), data_im);  
  8.   int height_col = (height + 2 * pad_h - patch_h) / stride_h + 1;  
  9.   int width_col = (width + 2 * pad_w - patch_w) / stride_w + 1;  
  10.   int channels_col = channels * patch_h * patch_w;  
  11.   for (int c = 0; c < channels_col; ++c) {  
  12.     int w_offset = c % patch_w;  
  13.     int h_offset = (c / patch_w) % patch_h;  
  14.     int c_im = c / patch_h / patch_w;  
  15.     for (int h = 0; h < height_col; ++h) {  
  16.       for (int w = 0; w < width_col; ++w) {  
  17.         int h_pad = h * stride_h - pad_h + h_offset;  
  18.         int w_pad = w * stride_w - pad_w + w_offset;  
  19.         if (h_pad >= 0 && h_pad < height && w_pad >= 0 && w_pad < width)  
  20.           data_im[(c_im * height + h_pad) * width + w_pad] +=  
  21.               data_col[(c * height_col + h) * width_col + w];  
  22.       }  
  23.     }  
  24.   }  
  25. }  

有了前面的介绍,加上我画的那个图,反过来的过程就很容易理解了吧。

2 ConvolutionLayer:

param provides ConvolutionParameter convolution_param, with ConvolutionLayer options:
  - num_output. The number of filters.
  - kernel_size / kernel_h / kernel_w. The filter dimensions, given by kernel_size for square filters or kernel_h and kernel_w for rectangular filters.
  - stride / stride_h / stride_w (\b optional, default 1). The filter stride, given by stride_size for equal dimensions or stride_h and stride_w for different strides. By default the convolution is dense with stride 1.
  - pad / pad_h / pad_w (\b optional, default 0). The zero-padding for convolution, given by pad for equal dimensions or pad_h and pad_w for different padding. Input padding is computed implicitly instead of actually padding.
  - group (\b optional, default 1). The number of filter groups. Group convolution is a method for reducing parameterization by selectively connecting input and output channels. The input and output channel dimensions must be divisible by the number of groups. For group @f$ \geq 1 @f$, the convolutional filters' input and output channels are separated s.t. each group takes 1 / group of the input channels and makes 1 / group of the output channels. Concretely 4 input channels, 8 output channels, and 2 groups separate input channels 1-2 and output channels 1-4 into the first group and input channels 3-4 and output channels 5-8 into the second group.
  - bias_term (\b optional, default true). Whether to have a bias.
  - engine: convolution has CAFFE (matrix multiplication) and CUDNN (library kernels + stream parallelism) engines.

以上是ConvolutionLayer类的源码注释:主要介绍各参数,其中有一个参数需要注意一下:group,如果该参数赋值为2,假设输入的channel有4个,而输出的channel为8,则输出的8个channel中的前4个来源于输入的前2个,输出的后4个来源于输入的后2个。

2.1 原理介绍:

可以参考:

caffe源码阅读(2)卷积层

cnn_tutorial

前馈:y = w*x + b

反馈:

卷积层参数传递求解:

但实际上经过im2col之后转换成了全连接的计算方式:(其实卷积层的参数传递计算与全连接层的计算本质上是一样的)

2.2 源码分析:

2.2.1 属性变量:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int kernel_h_, kernel_w_;  
  2. int stride_h_, stride_w_;  
  3. int num_;  
  4. int channels_;  
  5. int pad_h_, pad_w_;  
  6. int height_, width_;  
  7. int group_;  
  8. int num_output_;  
  9. int height_out_, width_out_;  
  10. bool bias_term_;  
  11.   
  12. /// M_ is the channel dimension of the output for a single group, which is the  
  13. /// leading dimension of the filter matrix.  
  14. int M_;  
  15. /// K_ is the dimension of an unrolled input for a single group, which is the  
  16. /// leading dimension of the data matrix.  
  17. int K_;  
  18. /// N_ is the spatial dimension of the output, the H x W, which are the last  
  19. /// dimensions of the data and filter matrices.  
  20. int N_;  
  21. Blob<Dtype> col_buffer_;  
  22. Blob<Dtype> bias_multiplier_;  

前面一堆就没什么好说的了吧,关键来看看后面的几个:( http://blog.csdn.net/xiaoyezi_1834/article/details/50786363)

M_:相当于是卷积核的数量;

K_:kernel_h × kernel_w,相当于是卷积核元素个数

N_:((height + 2*pad_h – kernel_h)/stride_h+ 1)*((weight +2*pad_w – kernel_w)/stride_w + 1),而不是简单的height×weight

col_buffer_:存储在前面介绍的im2col时的结果

2.2.2 前馈和反馈:

构造函数,SetUp()Reshpe()这里就不做说明了。直接看前馈和反馈吧:

首先来看前馈函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,  
  3.       vector<Blob<Dtype>*>* top) {  
  4.   for (int i = 0; i < bottom.size(); ++i) {  
  5.     const Dtype* bottom_data = bottom[i]->cpu_data();  
  6.     Dtype* top_data = (*top)[i]->mutable_cpu_data();  
  7.     Dtype* col_data = col_buffer_.mutable_cpu_data();  
  8.     const Dtype* weight = this->blobs_[0]->cpu_data();  
  9.     int weight_offset = M_ * K_;  // number of filter parameters in a group  
  10.     int col_offset = K_ * N_;  // number of values in an input region / column  
  11.     int top_offset = M_ * N_;  // number of values in an output region / column  
  12.     for (int n = 0; n < num_; ++n) {  
  13.       // im2col transformation: unroll input regions for filtering  
  14.       // into column matrix for multiplication.  
  15.       im2col_cpu(bottom_data + bottom[i]->offset(n), channels_, height_,  
  16.           width_, kernel_h_, kernel_w_, pad_h_, pad_w_, stride_h_, stride_w_,  
  17.           col_data);  
  18.       int offsetN = (*top)[i]->offset(n);  
  19.       // Take inner products for groups.  
  20.       for (int g = 0; g < group_; ++g) {  
  21.         caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, M_, N_, K_,  
  22.           (Dtype)1., weight + weight_offset * g, col_data + col_offset * g,  
  23.           (Dtype)0., top_data + (*top)[i]->offset(n) + top_offset * g);  
  24.       }  
  25.       // Add bias.  
  26.       if (bias_term_) {  
  27.         caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,  
  28.             N_, 1, (Dtype)1., this->blobs_[1]->cpu_data(),  
  29.             bias_multiplier_.cpu_data(),  
  30.             (Dtype)1., top_data + (*top)[i]->offset(n));  
  31.       }  
  32.     }  
  33.   }  
  34. }  

这个原理已经在上面介绍过了,然后在代码中用到了 im2col_cpu(),这个函数已经在前面介绍过了。接下来代码中先做的事情是:y = y + w * x,最后的if判断是否需要加上偏置,如果要加上,则再进行 y = y + b。

其中调用了caffe_cpu_gemm()来实现代数运算:(源码实现在math_functions.cpp中)

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template<>  
  2. void caffe_cpu_gemm<float>(const CBLAS_TRANSPOSE TransA,  
  3.     const CBLAS_TRANSPOSE TransB, const int M, const int N, const int K,  
  4.     const float alpha, const float* A, const float* B, const float beta,  
  5.     float* C) {  
  6.   int lda = (TransA == CblasNoTrans) ? K : M;  
  7.   int ldb = (TransB == CblasNoTrans) ? N : K;  
  8.   cblas_sgemm(CblasRowMajor, TransA, TransB, M, N, K, alpha, A, lda, B,  
  9.       ldb, beta, C, N);  
  10. }  
  11.   
  12. template<>  
  13. void caffe_cpu_gemm<double>(const CBLAS_TRANSPOSE TransA,  
  14.     const CBLAS_TRANSPOSE TransB, const int M, const int N, const int K,  
  15.     const double alpha, const double* A, const double* B, const double beta,  
  16.     double* C) {  
  17.   int lda = (TransA == CblasNoTrans) ? K : M;  
  18.   int ldb = (TransB == CblasNoTrans) ? N : K;  
  19.   cblas_dgemm(CblasRowMajor, TransA, TransB, M, N, K, alpha, A, lda, B,  
  20.       ldb, beta, C, N);  
  21. }  

这里提供了两个版本的caffe_cpu_gemm(),这个时候你会发现,原来代数运算真正的实现者是BLAS:

官方API:

http://www.netlib.org/lapack/explore-html/db/d66/cblas__sgemm_8c_a584e7569aee83c27b2b2bf22ea4f9f23.html#a584e7569aee83c27b2b2bf22ea4f9f23

博客园的介绍:(尽管这里是MKL,其实他们的参数,功能都是一样的,估计实现代码也差不多)

http://www.cnblogs.com/darkknightzh/p/5553336.html

CSDN某人的介绍:(比较详细)

http://blog.csdn.net/cleverysm/article/details/1925549

再来看反馈部分:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void ConvolutionLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,  
  3.       const vector<bool>& propagate_down, vector<Blob<Dtype>*>* bottom) {  
  4.   const Dtype* weight = NULL;  
  5.   Dtype* weight_diff = NULL;  
  6.   if (this->param_propagate_down_[0]) {  
  7.     weight = this->blobs_[0]->cpu_data();  
  8.     weight_diff = this->blobs_[0]->mutable_cpu_diff();  
  9.     caffe_set(this->blobs_[0]->count(), Dtype(0), weight_diff);  
  10.   }  
  11.   Dtype* bias_diff = NULL;  
  12.   if (bias_term_ && this->param_propagate_down_[1]) {  
  13.     bias_diff = this->blobs_[1]->mutable_cpu_diff();  
  14.     caffe_set(this->blobs_[1]->count(), Dtype(0), bias_diff);  
  15.   }  
  16.   const int weight_offset = M_ * K_;  
  17.   const int col_offset = K_ * N_;  
  18.   const int top_offset = M_ * N_;  
  19.   for (int i = 0; i < top.size(); ++i) {  
  20.     const Dtype* top_diff = NULL;  
  21.     // Bias gradient, if necessary.  
  22.     if (bias_term_ && this->param_propagate_down_[1]) {  
  23.       top_diff = top[i]->cpu_diff();  
  24.       for (int n = 0; n < num_; ++n) {  
  25.         caffe_cpu_gemv<Dtype>(CblasNoTrans, num_output_, N_,  
  26.             1., top_diff + top[0]->offset(n),  
  27.             bias_multiplier_.cpu_data(), 1.,  
  28.             bias_diff);  
  29.       }  
  30.     }  
  31.     if (this->param_propagate_down_[0] || propagate_down[i]) {  
  32.       if (!top_diff) {  
  33.         top_diff = top[i]->cpu_diff();  
  34.       }  
  35.       Dtype* col_data = col_buffer_.mutable_cpu_data();  
  36.       Dtype* col_diff = col_buffer_.mutable_cpu_diff();  
  37.       const Dtype* bottom_data = (*bottom)[i]->cpu_data();  
  38.       Dtype* bottom_diff = (*bottom)[i]->mutable_cpu_diff();  
  39.       for (int n = 0; n < num_; ++n) {  
  40.         // Since we saved memory in the forward pass by not storing all col  
  41.         // data, we will need to recompute them.  
  42.         im2col_cpu(bottom_data + (*bottom)[i]->offset(n), channels_, height_,  
  43.                    width_, kernel_h_, kernel_w_, pad_h_, pad_w_,  
  44.                    stride_h_, stride_w_, col_data);  
  45.         // gradient w.r.t. weight. Note that we will accumulate diffs.  
  46.         if (this->param_propagate_down_[0]) {  
  47.           for (int g = 0; g < group_; ++g) {  
  48.             caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, M_, K_, N_,  
  49.                 (Dtype)1., top_diff + top[i]->offset(n) + top_offset * g,  
  50.                 col_data + col_offset * g, (Dtype)1.,  
  51.                 weight_diff + weight_offset * g);  
  52.           }  
  53.         }  
  54.         // gradient w.r.t. bottom data, if necessary.  
  55.         if (propagate_down[i]) {  
  56.           if (weight == NULL) {  
  57.             weight = this->blobs_[0]->cpu_data();  
  58.           }  
  59.           for (int g = 0; g < group_; ++g) {  
  60.             caffe_cpu_gemm<Dtype>(CblasTrans, CblasNoTrans, K_, N_, M_,  
  61.                 (Dtype)1., weight + weight_offset * g,  
  62.                 top_diff + top[i]->offset(n) + top_offset * g,  
  63.                 (Dtype)0., col_diff + col_offset * g);  
  64.           }  
  65.           // col2im back to the data  
  66.           col2im_cpu(col_diff, channels_, height_, width_,  
  67.               kernel_h_, kernel_w_, pad_h_, pad_w_,  
  68.               stride_h_, stride_w_, bottom_diff + (*bottom)[i]->offset(n));  
  69.         }  
  70.       }  
  71.     }  
  72.   }  
  73. }  

该函数最开始的部分对需要计算的参数(权重,偏置)的指针初始化,相当于是指向了需要存储的位置。

for循环,才是真正计算的主体:

其中用到了caffe_cpu_gemv(),可以查看文档,相当于实现了:y = alpha*A*x + beta*y,其中x, y表示向量,而A表示矩阵。

那么for循环的开始处理偏置的问题,根据计算原理,直接将top_diff+top[0]->offset(n)累加到bias_diff中即可。程序中实现的是:

bias_diff = 1.0 * [top_diff+top[0]->offset(n)] * bias_multiplier_.cpu_data() + 1.0 * bias_diff

而bias_mutiplier_在caffe中默认被设置为1,所以相当于是实现了一个累加的过程。

for循环后面就接着处理权重的问题了,这个时候因为可能批量处理时有很多个图,所以又涉及到一个for循环。

根据计算原理,在程序中对应的相当于是:

weight_diff+weight_offset*g = 1.0 * [top_diff+top[i]->offset(n)+top_offset*g] * [col_data+col_offset*g] + 1.0 * [weight_diff+weight_offset*g]

其中的g表示分组处理。

最后将误差传递到低层:(相当于是delta(l) = W * delta(l+1))

在程序中的体现是:col_diff+col_offset*g = 1.0 * [weight + weight_offset*g] * [top_diff + top[i]->offset(n) + top_offset * g] + 0.0 * [col_diff+col_offset*g]


引用caffe源码阅读(2)卷积层的一句话:

卷积运算是先要将kernel旋转180度之后再扫过去的。可以看出Caffe源码是没有这一步的,所以最后学出来的“kernel”实际上是应该还要旋转回来才是正确的卷积核。


CuDNNConvolutionLayer就跳过了,其实现的方式大致和ConvolutionLayer差不多。下面来看LRNLayer

3 LRNLayer:

Normalize the input in a local region across or within feature maps.

3.1 原理介绍:

局部相应归一化(local response normalization):每个输入的值都除以:

其中alpha, beta都在源码和配置文件中都有定义;

而n则表示局部相应范围的大小,在源码中使用size_来表达(在配置文件中用local_size),实际上n和size_是一种函数映射关系:(where n is the size of each local region)

如果在通道间计算归一化,则n=size_;

如果是通道内计算归一化,则n=size_*size_;

从配置文件说起,:

一半我们的网络配置文件中的LRN层参数如下:(caffeNet中截取的一段)

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. layer {  
  2.   name: "norm1"  
  3.   type: "LRN"  
  4.   bottom: "pool1"  
  5.   top: "norm1"  
  6.   lrn_param {  
  7.     local_size: 5  
  8.     alpha: 0.0001  
  9.     beta: 0.75  
  10.   }  
  11. }  
那么其中的local_size:(在源码中用size_)

1)通道间归一化时表示求和的通道数;

2)通道内归一化时表示求和区间的边长;

默认值为5。

alpha,beta也就是上面公式中的两个参数。

其实在写这个配置文件的时候还有一个可选参数:

norm_region: 选择对相邻通道间归一化还是通道内空间区域归一化,默认为ACROSS_CHANNELS,即通道间归一化;

在通道间归一化模式中,局部区域范围在相邻通道间,但没有空间扩展(即尺寸为 local_size x 1 x 1);

在通道内归一化模式中,局部区域在空间上扩展,但只针对独立通道进行(即尺寸为 1 x local_size x local_size);

3.2 源码分析:

3.2.1 属性变量:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int size_;  
  2. int pre_pad_;  
  3. Dtype alpha_;  
  4. Dtype beta_;  
  5. int num_;  
  6. int channels_;  
  7. int height_;  
  8. int width_;  

上面的变量当中,基本都是熟悉的部分。其余的变量在原理介绍中已经说过了。

再来看看别的变量:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // Fields used for normalization ACROSS_CHANNELS  
  2. // scale_ stores the intermediate summing results  
  3. Blob<Dtype> scale_;  
  4.   
  5. // Fields used for normalization WITHIN_CHANNEL  
  6. shared_ptr<SplitLayer<Dtype> > split_layer_;  
  7. vector<Blob<Dtype>*> split_top_vec_;  
  8. shared_ptr<PowerLayer<Dtype> > square_layer_;  
  9. Blob<Dtype> square_input_;  
  10. Blob<Dtype> square_output_;  
  11. vector<Blob<Dtype>*> square_bottom_vec_;  
  12. vector<Blob<Dtype>*> square_top_vec_;  
  13. shared_ptr<PoolingLayer<Dtype> > pool_layer_;  
  14. Blob<Dtype> pool_output_;  
  15. vector<Blob<Dtype>*> pool_top_vec_;  
  16. shared_ptr<PowerLayer<Dtype> > power_layer_;  
  17. Blob<Dtype> power_output_;  
  18. vector<Blob<Dtype>*> power_top_vec_;  
  19. shared_ptr<EltwiseLayer<Dtype> > product_layer_;  
  20. Blob<Dtype> product_input_;  
  21. vector<Blob<Dtype>*> product_bottom_vec_;  

看上去很多东西,但实际上根据我们的配置文件,基本不会用到WITHIN_CHANNEL,所以下面一堆变量暂时不用关心。

3.2.2 构造函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void LRNLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,  
  3.       vector<Blob<Dtype>*>* top) {  
  4.   size_ = this->layer_param_.lrn_param().local_size();  
  5.   CHECK_EQ(size_ % 2, 1) << "LRN only supports odd values for local_size";  
  6.   pre_pad_ = (size_ - 1) / 2;  
  7.   alpha_ = this->layer_param_.lrn_param().alpha();  
  8.   beta_ = this->layer_param_.lrn_param().beta();  
  9.   if (this->layer_param_.lrn_param().norm_region() ==  
  10.       LRNParameter_NormRegion_WITHIN_CHANNEL) {  
  11.     // Set up split_layer_ to use inputs in the numerator and denominator.  
  12.     split_top_vec_.clear();  
  13.     split_top_vec_.push_back(&product_input_);  
  14.     split_top_vec_.push_back(&square_input_);  
  15.     LayerParameter split_param;  
  16.     split_layer_.reset(new SplitLayer<Dtype>(split_param));  
  17.     split_layer_->SetUp(bottom, &split_top_vec_);  
  18.     // Set up square_layer_ to square the inputs.  
  19.     square_bottom_vec_.clear();  
  20.     square_top_vec_.clear();  
  21.     square_bottom_vec_.push_back(&square_input_);  
  22.     square_top_vec_.push_back(&square_output_);  
  23.     LayerParameter square_param;  
  24.     square_param.mutable_power_param()->set_power(Dtype(2));  
  25.     square_layer_.reset(new PowerLayer<Dtype>(square_param));  
  26.     square_layer_->SetUp(square_bottom_vec_, &square_top_vec_);  
  27.     // Set up pool_layer_ to sum over square neighborhoods of the input.  
  28.     pool_top_vec_.clear();  
  29.     pool_top_vec_.push_back(&pool_output_);  
  30.     LayerParameter pool_param;  
  31.     pool_param.mutable_pooling_param()->set_pool(  
  32.         PoolingParameter_PoolMethod_AVE);  
  33.     pool_param.mutable_pooling_param()->set_pad(pre_pad_);  
  34.     pool_param.mutable_pooling_param()->set_kernel_size(size_);  
  35.     pool_layer_.reset(new PoolingLayer<Dtype>(pool_param));  
  36.     pool_layer_->SetUp(square_top_vec_, &pool_top_vec_);  
  37.     // Set up power_layer_ to compute (1 + alpha_/N^2 s)^-beta_, where s is  
  38.     // the sum of a squared neighborhood (the output of pool_layer_).  
  39.     power_top_vec_.clear();  
  40.     power_top_vec_.push_back(&power_output_);  
  41.     LayerParameter power_param;  
  42.     power_param.mutable_power_param()->set_power(-beta_);  
  43.     power_param.mutable_power_param()->set_scale(alpha_);  
  44.     power_param.mutable_power_param()->set_shift(Dtype(1));  
  45.     power_layer_.reset(new PowerLayer<Dtype>(power_param));  
  46.     power_layer_->SetUp(pool_top_vec_, &power_top_vec_);  
  47.     // Set up a product_layer_ to compute outputs by multiplying inputs by the  
  48.     // inverse demoninator computed by the power layer.  
  49.     product_bottom_vec_.clear();  
  50.     product_bottom_vec_.push_back(&product_input_);  
  51.     product_bottom_vec_.push_back(&power_output_);  
  52.     LayerParameter product_param;  
  53.     EltwiseParameter* eltwise_param = product_param.mutable_eltwise_param();  
  54.     eltwise_param->set_operation(EltwiseParameter_EltwiseOp_PROD);  
  55.     product_layer_.reset(new EltwiseLayer<Dtype>(product_param));  
  56.     product_layer_->SetUp(product_bottom_vec_, top);  
  57.   }  
  58. }  

看上去这么大一堆,实际上如果我们忽略if里面的内容,只有很少一部分的初始化过程。因为if里面的一堆设置都是在通道内做归一化,而我们通常是做通道间归一化的,所以暂时也不用关心。

值得注意的是,在最开始对size_做了一个奇偶性的判断,只能够接受奇数!

Reshape()这里就暂时过掉了,基本都是那么回事。

3.2.3 前馈函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void LRNLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,  
  3.     vector<Blob<Dtype>*>* top) {  
  4.   switch (this->layer_param_.lrn_param().norm_region()) {  
  5.   case LRNParameter_NormRegion_ACROSS_CHANNELS:  
  6.     CrossChannelForward_cpu(bottom, top);  
  7.     break;  
  8.   case LRNParameter_NormRegion_WITHIN_CHANNEL:  
  9.     WithinChannelForward(bottom, top);  
  10.     break;  
  11.   default:  
  12.     LOG(FATAL) << "Unknown normalization region.";  
  13.   }  
  14. }  

后面的前馈反馈函数都涉及到两个模式,我们这里都只看通道间归一化模式:

通道间归一化的前馈函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void LRNLayer<Dtype>::CrossChannelForward_cpu(  
  3.     const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top) {  
  4.   const Dtype* bottom_data = bottom[0]->cpu_data();  
  5.   Dtype* top_data = (*top)[0]->mutable_cpu_data();  
  6.   Dtype* scale_data = scale_.mutable_cpu_data();  
  7.   // start with the constant value  
  8.   for (int i = 0; i < scale_.count(); ++i) {  
  9.     scale_data[i] = 1.;  
  10.   }  
  11.   Blob<Dtype> padded_square(1, channels_ + size_ - 1, height_, width_);  
  12.   Dtype* padded_square_data = padded_square.mutable_cpu_data();  
  13.   caffe_set(padded_square.count(), Dtype(0), padded_square_data);  
  14.   Dtype alpha_over_size = alpha_ / size_;  
  15.   // go through the images  
  16.   for (int n = 0; n < num_; ++n) {  
  17.     // compute the padded square  
  18.     caffe_sqr(channels_ * height_ * width_,  
  19.         bottom_data + bottom[0]->offset(n),  
  20.         padded_square_data + padded_square.offset(0, pre_pad_));  
  21.     // Create the first channel scale  
  22.     for (int c = 0; c < size_; ++c) {  
  23.       caffe_axpy<Dtype>(height_ * width_, alpha_over_size,  
  24.           padded_square_data + padded_square.offset(0, c),  
  25.           scale_data + scale_.offset(n, 0));  
  26.     }  
  27.     for (int c = 1; c < channels_; ++c) {  
  28.       // copy previous scale  
  29.       caffe_copy<Dtype>(height_ * width_,  
  30.           scale_data + scale_.offset(n, c - 1),  
  31.           scale_data + scale_.offset(n, c));  
  32.       // add head  
  33.       caffe_axpy<Dtype>(height_ * width_, alpha_over_size,  
  34.           padded_square_data + padded_square.offset(0, c + size_ - 1),  
  35.           scale_data + scale_.offset(n, c));  
  36.       // subtract tail  
  37.       caffe_axpy<Dtype>(height_ * width_, -alpha_over_size,  
  38.           padded_square_data + padded_square.offset(0, c - 1),  
  39.           scale_data + scale_.offset(n, c));  
  40.     }  
  41.   }  
  42.   
  43.   // In the end, compute output  
  44.   caffe_powx<Dtype>(scale_.count(), scale_data, -beta_, top_data);  
  45.   caffe_mul<Dtype>(scale_.count(), top_data, bottom_data, top_data);  
  46. }  

改程序前面会涉及到一个变量scale_data,这个数据来源于属性变量中的scale_,该变量初始化在 Reshape()中,相当于是创建了一个跟bottom[0]一样尺寸大小的Blob。

然后接着对scale_data全部赋初值1,注意这里为什么要赋初值为1!!这是因为在原理介绍中的公式括号里面的1.往后看完应该会明白。

接着创建了一个对象padded_square,它的大小和bottom一样,除了在通道维上比bottom多了size_-1。为什么要多size_-1呢?

还是否记得前面介绍过做局部归一化的时候,每个点要除以一个值(暂时我们设为x_centre),而这个值得计算当中涉及到一个求和,这个求和的范围是以x_centre为中心,边长为size_的区域。如果是对通道间做归一化,那么这个区域相当于可以理解成1维的;如果是通道内做归一化,那么这个区域可以理解成2维的。

因为这里的代码是通道间的,我们就以1维的为例来说明为什么要多size_-1:


通过上面这个图来解释,应该就相当清楚了,图中的蓝色线条相当于是源码中的channels_,红色线条相当于是channels_ + size_ - 1.

源码接着对对象padded_square的数据部分赋初值0。

这里涉及到一些数学函数,在math_function.cpp中能够找到:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void caffe_set(const int N, const Dtype alpha, Dtype* Y) {  
  3.   if (alpha == 0) {  
  4.     memset(Y, 0, sizeof(Dtype) * N);  // NOLINT(caffe/alt_fn)  
  5.     return;  
  6.   }  
  7.   for (int i = 0; i < N; ++i) {  
  8.     Y[i] = alpha;  
  9.   }  
  10. }  

这个就不做说明了吧。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <>  
  2. void caffe_sqr<float>(const int n, const float* a, float* y) {  
  3.   vsSqr(n, a, y);  
  4. }  
  5.   
  6. template <>  
  7. void caffe_sqr<double>(const int n, const double* a, double* y) {  
  8.   vdSqr(n, a, y);  
  9. }  

如果光看sqr还真不知道是计算什么,可能是平方,也可能是开根。然后网上也没有找到vsSqr(),不知道是什么库里面的。

Caffe源码(一):math_functions分析中基本将里面的函数全部都讲了,暂时无法考证是否全部正确,就默认他是对的吧。根据这篇博客的介绍,这里是计算平方的,也就是实现了:y = a^2

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <>  
  2. void caffe_axpy<float>(const int N, const float alpha, const float* X,  
  3.     float* Y) { cblas_saxpy(N, alpha, X, 1, Y, 1); }  
  4.   
  5. template <>  
  6. void caffe_axpy<double>(const int N, const double alpha, const double* X,  
  7.     double* Y) { cblas_daxpy(N, alpha, X, 1, Y, 1); }  

可以在BLAS API中查到,这里实现的是:Y = alpha*X + Y   (X, Y都是向量,alpha是一个值)

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void caffe_copy(const int N, const Dtype* X, Dtype* Y) {  
  3.   if (X != Y) {  
  4.     if (Caffe::mode() == Caffe::GPU) {  
  5. #ifndef CPU_ONLY  
  6.       // NOLINT_NEXT_LINE(caffe/alt_fn)  
  7.       CUDA_CHECK(cudaMemcpy(Y, X, sizeof(Dtype) * N, cudaMemcpyDefault));  
  8. #else  
  9.       NO_GPU;  
  10. #endif  
  11.     } else {  
  12.       memcpy(Y, X, sizeof(Dtype) * N);  // NOLINT(caffe/alt_fn)  
  13.     }  
  14.   }  
  15. }  
  16.   
  17. template void caffe_copy<int>(const int N, const int* X, int* Y);  
  18. template void caffe_copy<unsigned int>(const int N, const unsigned int* X,  
  19.     unsigned int* Y);  
  20. template void caffe_copy<float>(const int N, const float* X, float* Y);  
  21. template void caffe_copy<double>(const int N, const double* X, double* Y);  

这里有几种copy函数,相当于是将X复制到Y里面去。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <>  
  2. void caffe_powx<float>(const int n, const float* a, const float b,  
  3.     float* y) {  
  4.   vsPowx(n, a, b, y);  
  5. }  
  6.   
  7. template <>  
  8. void caffe_powx<double>(const int n, const double* a, const double b,  
  9.     double* y) {  
  10.   vdPowx(n, a, b, y);  
  11. }  

相当于是计算幂:y = a^b,其中y, a是向量,b是一个值

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <>  
  2. void caffe_mul<float>(const int n, const float* a, const float* b,  
  3.     float* y) {  
  4.   vsMul(n, a, b, y);  
  5. }  
  6.   
  7. template <>  
  8. void caffe_mul<double>(const int n, const double* a, const double* b,  
  9.     double* y) {  
  10.   vdMul(n, a, b, y);  
  11. }  

相当于是向量乘法,按元素乘,也就是:y[i] = a[i]*b[i]

涉及到的这个函数介绍完了,再来接着看前馈函数CrossChannelForward_cpu()

计算alpha_over_size好理解,也就是原理介绍中的alpha/n。

接下来的大for循环:

将bottom中的数据平方之后存入padded_square_data中;相当于是原理介绍中的x_i^2

接下来的两个for循环,想画图来说明,还真不知道怎么画合适:


尽量用语言描述清楚吧:

第一个for注释上说的是计算第一个通道的scale:

我将大for看完之后,才明白scale是原理介绍中公式部分的括号部分,也就是(1 + alpha/n * sum(x_i^2))。

而scale已经赋值为1了,所以剩下的部分就是累加 “加号” 后面的部分。

大for遍历的是图像个数,第一个小for遍历的是size_也就是原理介绍中的求和范围大小。

需要注意的是,这里的计算直接作用于每个通道的所有数据(height_*weight_),最后累加到scale_data里面,注意scale_data的尺寸大小和bottom大小是一样的,所以我这里的描述为了不引起歧义,再啰嗦一点,尽管是对每个通道的所有数据都操作了,但是也分别累加到scale_data对应的位置的。


第二个for:

有了前面的那张图 + 前面第一个for的解释,我想应该能够理解了。

经过这个大for之后,相当于是把原理介绍中的括号部分计算完了。

这个前馈函数的最后两行:第一个是计算幂,注意参数是 -beta。为什么要用负的呢?还记得原理介绍中是这样说的:“每个输入的值都除以”,所以为了将除转换成乘,就用负的幂。

最后一行也就是实现乘法,也就是原始数据乘以scale_data。

哎,终于把这个前馈弄完了,下面接着研究它的反馈函数。

3.2.4 反馈函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void LRNLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,  
  3.     const vector<bool>& propagate_down, vector<Blob<Dtype>*>* bottom) {  
  4.   switch (this->layer_param_.lrn_param().norm_region()) {  
  5.   case LRNParameter_NormRegion_ACROSS_CHANNELS:  
  6.     CrossChannelBackward_cpu(top, propagate_down, bottom);  
  7.     break;  
  8.   case LRNParameter_NormRegion_WITHIN_CHANNEL:  
  9.     WithinChannelBackward(top, propagate_down, bottom);  
  10.     break;  
  11.   default:  
  12.     LOG(FATAL) << "Unknown normalization region.";  
  13.   }  
  14. }  

同样也只看通道间的反馈:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void LRNLayer<Dtype>::CrossChannelBackward_cpu(  
  3.     const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down,  
  4.     vector<Blob<Dtype>*>* bottom) {  
  5.   const Dtype* top_diff = top[0]->cpu_diff();  
  6.   const Dtype* top_data = top[0]->cpu_data();  
  7.   const Dtype* bottom_data = (*bottom)[0]->cpu_data();  
  8.   const Dtype* scale_data = scale_.cpu_data();  
  9.   Dtype* bottom_diff = (*bottom)[0]->mutable_cpu_diff();  
  10.   Blob<Dtype> padded_ratio(1, channels_ + size_ - 1, height_, width_);  
  11.   Blob<Dtype> accum_ratio(1, 1, height_, width_);  
  12.   Dtype* padded_ratio_data = padded_ratio.mutable_cpu_data();  
  13.   Dtype* accum_ratio_data = accum_ratio.mutable_cpu_data();  
  14.   // We hack a little bit by using the diff() to store an additional result  
  15.   Dtype* accum_ratio_times_bottom = accum_ratio.mutable_cpu_diff();  
  16.   caffe_set(padded_ratio.count(), Dtype(0), padded_ratio_data);  
  17.   Dtype cache_ratio_value = 2. * alpha_ * beta_ / size_;  
  18.   
  19.   caffe_powx<Dtype>(scale_.count(), scale_data, -beta_, bottom_diff);  
  20.   caffe_mul<Dtype>(scale_.count(), top_diff, bottom_diff, bottom_diff);  
  21.   
  22.   // go through individual data  
  23.   int inverse_pre_pad = size_ - (size_ + 1) / 2;  
  24.   for (int n = 0; n < num_; ++n) {  
  25.     int block_offset = scale_.offset(n);  
  26.     // first, compute diff_i * y_i / s_i  
  27.     caffe_mul<Dtype>(channels_ * height_ * width_,  
  28.         top_diff + block_offset, top_data + block_offset,  
  29.         padded_ratio_data + padded_ratio.offset(0, inverse_pre_pad));  
  30.     caffe_div<Dtype>(channels_ * height_ * width_,  
  31.         padded_ratio_data + padded_ratio.offset(0, inverse_pre_pad),  
  32.         scale_data + block_offset,  
  33.         padded_ratio_data + padded_ratio.offset(0, inverse_pre_pad));  
  34.     // Now, compute the accumulated ratios and the bottom diff  
  35.     caffe_set(accum_ratio.count(), Dtype(0), accum_ratio_data);  
  36.     for (int c = 0; c < size_ - 1; ++c) {  
  37.       caffe_axpy<Dtype>(height_ * width_, 1.,  
  38.           padded_ratio_data + padded_ratio.offset(0, c), accum_ratio_data);  
  39.     }  
  40.     for (int c = 0; c < channels_; ++c) {  
  41.       caffe_axpy<Dtype>(height_ * width_, 1.,  
  42.           padded_ratio_data + padded_ratio.offset(0, c + size_ - 1),  
  43.           accum_ratio_data);  
  44.       // compute bottom diff  
  45.       caffe_mul<Dtype>(height_ * width_,  
  46.           bottom_data + top[0]->offset(n, c),  
  47.           accum_ratio_data, accum_ratio_times_bottom);  
  48.       caffe_axpy<Dtype>(height_ * width_, -cache_ratio_value,  
  49.           accum_ratio_times_bottom, bottom_diff + top[0]->offset(n, c));  
  50.       caffe_axpy<Dtype>(height_ * width_, -1.,  
  51.           padded_ratio_data + padded_ratio.offset(0, c), accum_ratio_data);  
  52.     }  
  53.   }  
  54. }  

这里相当于是在计算反函数!具体的自己研究吧~

这里的解释应该有误,误差传递通常应该是遵循:


4 PoolingLayer:

Pools the input image by taking the max, average, etc. within regions.

4.1 原理介绍:

在官网的介绍中,说提供了三种方式:MAX, AVG, STOCHASTIC. 但实际上在源码中只看到前两种,最后一种随机的没有。

前馈的过程比较简单:MAX就是将“卷积核”(过滤器)覆盖范围最大的那个值传递下去;而AVG就是将覆盖范围的平均值传递下去。

反馈的过程:

MAX:这个在前馈的时候需要记录传递下去的值的坐标,所以反馈的时候也只反馈给对应的坐标,其余的位置误差设定为0.

AVG:直接将top层的误差分成N份,反馈回去。N通常等于过滤器的大小(kernel_h * kernel_w)。边角部分的N可能会小一些,这是因为当过滤器去覆盖的时候,可能超出了边界。

4.2 源码分析:

LayerSetUp()Reshape()都基本差不多的。

4.2.1 属性变量:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int kernel_h_, kernel_w_;  
  2. int stride_h_, stride_w_;  
  3. int pad_h_, pad_w_;  
  4. int channels_;  
  5. int height_, width_;  
  6. int pooled_height_, pooled_width_;  
  7. Blob<Dtype> rand_idx_;  
  8. Blob<int> max_idx_;  

重点来看看最后三行变量。pooled_height_, pooled_width_这两个好理解吧,也就是降采样(或者说池化)之后的大小。

rand_idx_应该是随机降采样时用的,可惜在源码中没有看到有这种降采样的实现。

那么max_idx_就是用于max pooling咯?

4.2.2 前馈反馈函数:

前馈函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // TODO(Yangqing): Is there a faster way to do pooling in the channel-first  
  2. // case?  
  3. template <typename Dtype>  
  4. void PoolingLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,  
  5.       vector<Blob<Dtype>*>* top) {  
  6.   const Dtype* bottom_data = bottom[0]->cpu_data();  
  7.   Dtype* top_data = (*top)[0]->mutable_cpu_data();  
  8.   const int top_count = (*top)[0]->count();  
  9.   // We'll output the mask to top[1] if it's of size >1.  
  10.   const bool use_top_mask = top->size() > 1;  
  11.   int* mask = NULL;  // suppress warnings about uninitalized variables  
  12.   Dtype* top_mask = NULL;  
  13.   // Different pooling methods. We explicitly do the switch outside the for  
  14.   // loop to save time, although this results in more code.  
  15.   switch (this->layer_param_.pooling_param().pool()) {  
  16.   case PoolingParameter_PoolMethod_MAX:  
  17.     // Initialize  
  18.     if (use_top_mask) {  
  19.       top_mask = (*top)[1]->mutable_cpu_data();  
  20.       caffe_set(top_count, Dtype(-1), top_mask);  
  21.     } else {  
  22.       mask = max_idx_.mutable_cpu_data();  
  23.       caffe_set(top_count, -1, mask);  
  24.     }  
  25.     caffe_set(top_count, Dtype(-FLT_MAX), top_data);  
  26.     // The main loop  
  27.     for (int n = 0; n < bottom[0]->num(); ++n) {  
  28.       for (int c = 0; c < channels_; ++c) {  
  29.         for (int ph = 0; ph < pooled_height_; ++ph) {  
  30.           for (int pw = 0; pw < pooled_width_; ++pw) {  
  31.             int hstart = ph * stride_h_ - pad_h_;  
  32.             int wstart = pw * stride_w_ - pad_w_;  
  33.             int hend = min(hstart + kernel_h_, height_);  
  34.             int wend = min(wstart + kernel_w_, width_);  
  35.             hstart = max(hstart, 0);  
  36.             wstart = max(wstart, 0);  
  37.             const int pool_index = ph * pooled_width_ + pw;  
  38.             for (int h = hstart; h < hend; ++h) {  
  39.               for (int w = wstart; w < wend; ++w) {  
  40.                 const int index = h * width_ + w;  
  41.                 if (bottom_data[index] > top_data[pool_index]) {  
  42.                   top_data[pool_index] = bottom_data[index];  
  43.                   if (use_top_mask) {  
  44.                     top_mask[pool_index] = static_cast<Dtype>(index);  
  45.                   } else {  
  46.                     mask[pool_index] = index;  
  47.                   }  
  48.                 }  
  49.               }  
  50.             }  
  51.           }  
  52.         }  
  53.         // compute offset  
  54.         bottom_data += bottom[0]->offset(0, 1);  
  55.         top_data += (*top)[0]->offset(0, 1);  
  56.         if (use_top_mask) {  
  57.           top_mask += (*top)[0]->offset(0, 1);  
  58.         } else {  
  59.           mask += (*top)[0]->offset(0, 1);  
  60.         }  
  61.       }  
  62.     }  
  63.     break;  
  64.   case PoolingParameter_PoolMethod_AVE:  
  65.     for (int i = 0; i < top_count; ++i) {  
  66.       top_data[i] = 0;  
  67.     }  
  68.     // The main loop  
  69.     for (int n = 0; n < bottom[0]->num(); ++n) {  
  70.       for (int c = 0; c < channels_; ++c) {  
  71.         for (int ph = 0; ph < pooled_height_; ++ph) {  
  72.           for (int pw = 0; pw < pooled_width_; ++pw) {  
  73.             int hstart = ph * stride_h_ - pad_h_;  
  74.             int wstart = pw * stride_w_ - pad_w_;  
  75.             int hend = min(hstart + kernel_h_, height_ + pad_h_);  
  76.             int wend = min(wstart + kernel_w_, width_ + pad_w_);  
  77.             int pool_size = (hend - hstart) * (wend - wstart);  
  78.             hstart = max(hstart, 0);  
  79.             wstart = max(wstart, 0);  
  80.             hend = min(hend, height_);  
  81.             wend = min(wend, width_);  
  82.             for (int h = hstart; h < hend; ++h) {  
  83.               for (int w = wstart; w < wend; ++w) {  
  84.                 top_data[ph * pooled_width_ + pw] +=  
  85.                     bottom_data[h * width_ + w];  
  86.               }  
  87.             }  
  88.             top_data[ph * pooled_width_ + pw] /= pool_size;  
  89.           }  
  90.         }  
  91.         // compute offset  
  92.         bottom_data += bottom[0]->offset(0, 1);  
  93.         top_data += (*top)[0]->offset(0, 1);  
  94.       }  
  95.     }  
  96.     break;  
  97.   case PoolingParameter_PoolMethod_STOCHASTIC:  
  98.     NOT_IMPLEMENTED;  
  99.     break;  
  100.   default:  
  101.     LOG(FATAL) << "Unknown pooling method.";  
  102.   }  
  103. }  

看着这么大一堆,别被吓着,其实仔细看看内容,里面实现了MAX  和 AVG的降采样,都比较好理解,具体的坐标对应关系有兴趣的自己研究研究。

反馈函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. template <typename Dtype>  
  2. void PoolingLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,  
  3.       const vector<bool>& propagate_down, vector<Blob<Dtype>*>* bottom) {  
  4.   if (!propagate_down[0]) {  
  5.     return;  
  6.   }  
  7.   const Dtype* top_diff = top[0]->cpu_diff();  
  8.   Dtype* bottom_diff = (*bottom)[0]->mutable_cpu_diff();  
  9.   // Different pooling methods. We explicitly do the switch outside the for  
  10.   // loop to save time, although this results in more codes.  
  11.   caffe_set((*bottom)[0]->count(), Dtype(0), bottom_diff);  
  12.   // We'll output the mask to top[1] if it's of size >1.  
  13.   const bool use_top_mask = top.size() > 1;  
  14.   const int* mask = NULL;  // suppress warnings about uninitialized variables  
  15.   const Dtype* top_mask = NULL;  
  16.   switch (this->layer_param_.pooling_param().pool()) {  
  17.   case PoolingParameter_PoolMethod_MAX:  
  18.     // The main loop  
  19.     if (use_top_mask) {  
  20.       top_mask = top[1]->cpu_data();  
  21.     } else {  
  22.       mask = max_idx_.cpu_data();  
  23.     }  
  24.     for (int n = 0; n < top[0]->num(); ++n) {  
  25.       for (int c = 0; c < channels_; ++c) {  
  26.         for (int ph = 0; ph < pooled_height_; ++ph) {  
  27.           for (int pw = 0; pw < pooled_width_; ++pw) {  
  28.             const int index = ph * pooled_width_ + pw;  
  29.             const int bottom_index =  
  30.                 use_top_mask ? top_mask[index] : mask[index];  
  31.             bottom_diff[bottom_index] += top_diff[index];  
  32.           }  
  33.         }  
  34.         bottom_diff += (*bottom)[0]->offset(0, 1);  
  35.         top_diff += top[0]->offset(0, 1);  
  36.         if (use_top_mask) {  
  37.           top_mask += top[0]->offset(0, 1);  
  38.         } else {  
  39.           mask += top[0]->offset(0, 1);  
  40.         }  
  41.       }  
  42.     }  
  43.     break;  
  44.   case PoolingParameter_PoolMethod_AVE:  
  45.     // The main loop  
  46.     for (int n = 0; n < top[0]->num(); ++n) {  
  47.       for (int c = 0; c < channels_; ++c) {  
  48.         for (int ph = 0; ph < pooled_height_; ++ph) {  
  49.           for (int pw = 0; pw < pooled_width_; ++pw) {  
  50.             int hstart = ph * stride_h_ - pad_h_;  
  51.             int wstart = pw * stride_w_ - pad_w_;  
  52.             int hend = min(hstart + kernel_h_, height_ + pad_h_);  
  53.             int wend = min(wstart + kernel_w_, width_ + pad_w_);  
  54.             int pool_size = (hend - hstart) * (wend - wstart);  
  55.             hstart = max(hstart, 0);  
  56.             wstart = max(wstart, 0);  
  57.             hend = min(hend, height_);  
  58.             wend = min(wend, width_);  
  59.             for (int h = hstart; h < hend; ++h) {  
  60.               for (int w = wstart; w < wend; ++w) {  
  61.                 bottom_diff[h * width_ + w] +=  
  62.                   top_diff[ph * pooled_width_ + pw] / pool_size;  
  63.               }  
  64.             }  
  65.           }  
  66.         }  
  67.         // offset  
  68.         bottom_diff += (*bottom)[0]->offset(0, 1);  
  69.         top_diff += top[0]->offset(0, 1);  
  70.       }  
  71.     }  
  72.     break;  
  73.   case PoolingParameter_PoolMethod_STOCHASTIC:  
  74.     NOT_IMPLEMENTED;  
  75.     break;  
  76.   default:  
  77.     LOG(FATAL) << "Unknown pooling method.";  
  78.   }  
  79. }  

因为反馈的原理在前面介绍过了,所以这个代码看起来也不是那么复杂。

1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值