Caffe源码学习系列二----卷积层

来源:互联网 发布:淘宝上怎么秒杀东西 编辑:程序博客网 时间:2024/05/16 14:34


本文转自:http://zhangliliang.com/2015/02/11/about-caffe-code-convolutional-layer/

感谢博主!

一开始笔者先看了卷积层的梯度传导公式,参考了这两篇:

  1. http://ufldl.stanford.edu/tutorial/supervised/ConvolutionalNeuralNetwork/
  2. http://cogprints.org/5869/1/cnn_tutorial.pdf

卷积层的参数的梯度可以这样来求:

 W (l) k  J(W,b;x,y) b (l) k  J(W,b;x,y) = i=1 m (a (l) i )rot90(δ (l+1) k ,2),= a,b (δ (l+1) k ) a,b .  

看上去比全连接层复杂多了,但其实,他们本质上基本是一样的,依然可以套回全连接层的参数求导公式:
 W (l)  J(W,b;x,y) b (l)  J(W,b;x,y) =δ (l+1) (a (l) ) T ,=δ (l+1) .  

只需要额外增加一步im2col。这一步的意思是将首先将整张图片按照卷积的窗口大小切好(按照stride来切,可以有重叠),然后各自拉成一列。
为啥要怎样做,因为对于这个小窗口内拉成一列的神经元来说来说,它们跟下一层神经元就是全连接了,所以这个小窗口里面的梯度计算就可以按照全连接来计算就可以了。

如果对照着Caffe的卷积层源码来看,就很清晰了。
forward的代码如下,假设没有分group,这段代码的意思是对于一个大小为num_的batch里面的任意一张图片,首先通过im2col展开成多个列向量,之后直接就用wx+b的方式就能够算到输出了。

1234567891011121314151617181920212223242526272829303132
void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,      vector<Blob<Dtype>*>* top) {  for (int i = 0; i < bottom.size(); ++i) {    const Dtype* bottom_data = bottom[i]->cpu_data();    Dtype* top_data = (*top)[i]->mutable_cpu_data();    Dtype* col_data = col_buffer_.mutable_cpu_data();    const Dtype* weight = this->blobs_[0]->cpu_data();    int weight_offset = M_ * K_;  // number of filter parameters in a group    int col_offset = K_ * N_;  // number of values in an input region / column    int top_offset = M_ * N_;  // number of values in an output region / column    for (int n = 0; n < num_; ++n) {      // im2col transformation: unroll input regions for filtering      // into column matrix for multplication.      im2col_cpu(bottom_data + bottom[i]->offset(n), channels_, height_,          width_, kernel_h_, kernel_w_, pad_h_, pad_w_, stride_h_, stride_w_,          col_data);      // Take inner products for groups.      for (int g = 0; g < group_; ++g) {        caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, M_, N_, K_,          (Dtype)1., weight + weight_offset * g, col_data + col_offset * g,          (Dtype)0., top_data + (*top)[i]->offset(n) + top_offset * g);      }      // Add bias.      if (bias_term_) {        caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,            N_, 1, (Dtype)1., this->blobs_[1]->cpu_data(),            bias_multiplier_.cpu_data(),            (Dtype)1., top_data + (*top)[i]->offset(n));      }    }  }}

所以卷积看成是多个局部的全连接。笔者记得在某个地方看到过,实现卷积的方式有两个,一个是像caffe里面的im2col,另外一个是傅里叶变换。而后者的速度比较快,那么看来facebook给出的加速代码应该是用了傅里叶变换咯:)
言归正传,如果能够理解到im2col的作用,那么backward的代码也很容易理解了。
对于bias,直接就是delta(可能还要乘以bias_multiplier_,这个是Caffe自己的功能,默认不开启,即bias_multiplier_=1)

12345678910
// Bias gradient, if necessary.if (bias_term_ && this->param_propagate_down_[1]) {  top_diff = top[i]->cpu_diff();  for (int n = 0; n < num_; ++n) {caffe_cpu_gemv<Dtype>(CblasNoTrans, num_output_, N_,1., top_diff + top[0]->offset(n),bias_multiplier_.cpu_data(), 1.,bias_diff);  }}

对于weights,batch中的每张图片,首先还是用im2col展开,之后用矩阵乘法表示累加:

1234567891011121314
// Since we saved memory in the forward pass by not storing all col// data, we will need to recompute them.im2col_cpu(bottom_data + (*bottom)[i]->offset(n), channels_, height_,   width_, kernel_h_, kernel_w_, pad_h_, pad_w_,   stride_h_, stride_w_, col_data);// gradient w.r.t. weight. Note that we will accumulate diffs.if (this->param_propagate_down_[0]) {  for (int g = 0; g < group_; ++g) {caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, M_, K_, N_,(Dtype)1., top_diff + top[i]->offset(n) + top_offset * g,col_data + col_offset * g, (Dtype)1.,weight_diff + weight_offset * g);  }}

最后是计算需要传回前一层的delta,也是先看成多个独立的全连接,但是最后需要还原成带有空间结构的形状,需要调用逆过程col2im,其实也是一个累加的过程,让每个空间位置的delta累加起来。

12345678910111213141516
// gradient w.r.t. bottom data, if necessary.if (propagate_down[i]) {  if (weight == NULL) {weight = this->blobs_[0]->cpu_data();  }  for (int g = 0; g < group_; ++g) {caffe_cpu_gemm<Dtype>(CblasTrans, CblasNoTrans, K_, N_, M_,(Dtype)1., weight + weight_offset * g,top_diff + top[i]->offset(n) + top_offset * g,(Dtype)0., col_diff + col_offset * g);  }  // col2im back to the data  col2im_cpu(col_diff, channels_, height_, width_,  kernel_h_, kernel_w_, pad_h_, pad_w_,  stride_h_, stride_w_, bottom_diff + (*bottom)[i]->offset(n));}

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

0 0
原创粉丝点击