caffe源码 base_conv_layer详解

看完了caffe中层的基础功能及构成后,我们通过base_conv_layer这个十分基础的卷积层来具体认识一下,caffe中层的定义,不多说,看源码。

1.头文件

先看头文件中,主要是一些声明和变量的定义:

#ifndef CAFFE_BASE_CONVOLUTION_LAYER_HPP_
#define CAFFE_BASE_CONVOLUTION_LAYER_HPP_

#include <vector>

#include "caffe/blob.hpp"
#include "caffe/layer.hpp"
#include "caffe/proto/caffe.pb.h"
#include "caffe/util/im2col.hpp"

namespace caffe {

/**
 * @brief Abstract base class that factors out the BLAS code common to
 *        ConvolutionLayer and DeconvolutionLayer.
 */
template <typename Dtype>
class BaseConvolutionLayer : public Layer<Dtype> {
 public:
  explicit BaseConvolutionLayer(const LayerParameter& param)
      : Layer<Dtype>(param) {}
  virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top);
  virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top);

  virtual inline int MinBottomBlobs() const { return 1; }
  virtual inline int MinTopBlobs() const { return 1; }
  virtual inline bool EqualNumBottomTopBlobs() const { return true; }

 protected:
  // Helper functions that abstract away the column buffer and gemm arguments.
  // The last argument in forward_cpu_gemm is so that we can skip the im2col if
  // we just called weight_cpu_gemm with the same input.
  void forward_cpu_gemm(const Dtype* input, const Dtype* weights,
      Dtype* output, bool skip_im2col = false);
  void forward_cpu_bias(Dtype* output, const Dtype* bias);
  void backward_cpu_gemm(const Dtype* input, const Dtype* weights,
      Dtype* output);
  void weight_cpu_gemm(const Dtype* input, const Dtype* output, Dtype*
      weights);
  void backward_cpu_bias(Dtype* bias, const Dtype* input);

#ifndef CPU_ONLY
  void forward_gpu_gemm(const Dtype* col_input, const Dtype* weights,
      Dtype* output, bool skip_im2col = false);
  void forward_gpu_bias(Dtype* output, const Dtype* bias);
  void backward_gpu_gemm(const Dtype* input, const Dtype* weights,
      Dtype* col_output);
  void weight_gpu_gemm(const Dtype* col_input, const Dtype* output, Dtype*
      weights);
  void backward_gpu_bias(Dtype* bias, const Dtype* input);
#endif

  /// @brief The spatial dimensions of the input.输入的空间维度。
  inline int input_shape(int i) {
    return (*bottom_shape_)[channel_axis_ + i];
  }
  // reverse_dimensions should return true iff we are implementing deconv, so
  // that conv helpers know which dimensions are which.
  virtual bool reverse_dimensions() = 0;
  // Compute height_out_ and width_out_ from other parameters.
  virtual void compute_output_shape() = 0;

  /// @brief The spatial dimensions of a filter kernel.
  // 卷积核的形状[kernel_h, kernel_w]
  Blob<int> kernel_shape_;
  /// @brief The spatial dimensions of the stride.
  // 步长形状[stride_h, stride_w]
  Blob<int> stride_;
  /// @brief The spatial dimensions of the padding.
  // padding形状[pad_h, pad_w]
  Blob<int> pad_;
  /// @brief The spatial dimensions of the dilation.
  // 扩张卷积的形状,就是镂空式的卷积
  Blob<int> dilation_;
  /// @brief The spatial dimensions of the convolution input.
  // 卷积的输入形状 = [输入图像通道数, 输入图像h, 输入图像w]
  Blob<int> conv_input_shape_;
  /// @brief The spatial dimensions of the col_buffer.
  // col_buffer的形状 = [kernel_dim_, conv_out_spatial_dim_ ]
  // 即将图像转化成利于卷积的展开体col形式(具体参考src/utils/im2col.cpp),存于col_buffer,将 卷积核权值×col_buffer=卷积输出,所以其形状为上述样子。
  vector<int> col_buffer_shape_;
  /// @brief The spatial dimensions of the output.层输出的形状,存在vector里
  vector<int> output_shape_;
  // 层输入的形状,存在vector里,返回指针,因为是别的层的输出,直接用指针指向之前已经存在的上一层的output_shape_。
  const vector<int>* bottom_shape_;
  // 空间轴个数,就是输入是几维图像
  int num_spatial_axes_;
  // 输入度维度 = 输入通道数*输入图像的h*输入图像的w
  int bottom_dim_;
  // 输出维度 = 输出通道数*输出图像的h*输出图像的w
  int top_dim_;
  // 输入图像的哪个axis是channel,一般是第二个维度
  int channel_axis_;
  // 堆大小
  int num_;
  // 通道数
  int channels_;
  // 卷积组的大小
  int group_;
  // 输出空间维度 = 卷积之后的图像长*卷积之后图像的宽
  int out_spatial_dim_;
  // 使用卷积组用到的权值偏置
  int weight_offset_;
  // 卷积后的图像的通道数
  int num_output_;
  // 是否启用偏置
  bool bias_term_;
  // 是否是1x1卷积
  bool is_1x1_;
  // 强制使用n维通用卷积,即im2col的n维形式,而不是更常用的二维形式。
  bool force_nd_im2col_;

 private:
  // wrap im2col/col2im so we don't have to remember the (long) argument lists
  // 将im2col/col2im封装,就不用自己来输入这么长的输入列表了。
  //void im2col_cpu(const Dtype* data_im, const int channels,
  //const int height, const int width, const int kernel_h, const int kernel_w,
  //const int pad_h, const int pad_w,
  //const int stride_h, const int stride_w,
  //const int dilation_h, const int dilation_w,
  //Dtype* data_col)
  inline void conv_im2col_cpu(const Dtype* data, Dtype* col_buff) {
    if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
      im2col_cpu(data, conv_in_channels_,
          conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
          kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
          pad_.cpu_data()[0], pad_.cpu_data()[1],
          stride_.cpu_data()[0], stride_.cpu_data()[1],
          dilation_.cpu_data()[0], dilation_.cpu_data()[1], col_buff);
    } else {
      im2col_nd_cpu(data, num_spatial_axes_, conv_input_shape_.cpu_data(),
          col_buffer_shape_.data(), kernel_shape_.cpu_data(),
          pad_.cpu_data(), stride_.cpu_data(), dilation_.cpu_data(), col_buff);
    }
  }
  inline void conv_col2im_cpu(const Dtype* col_buff, Dtype* data) {
    if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
      col2im_cpu(col_buff, conv_in_channels_,
          conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
          kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
          pad_.cpu_data()[0], pad_.cpu_data()[1],
          stride_.cpu_data()[0], stride_.cpu_data()[1],
          dilation_.cpu_data()[0], dilation_.cpu_data()[1], data);
    } else {
      col2im_nd_cpu(col_buff, num_spatial_axes_, conv_input_shape_.cpu_data(),
          col_buffer_shape_.data(), kernel_shape_.cpu_data(),
          pad_.cpu_data(), stride_.cpu_data(), dilation_.cpu_data(), data);
    }
  }
#ifndef CPU_ONLY
  inline void conv_im2col_gpu(const Dtype* data, Dtype* col_buff) {
    if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
      im2col_gpu(data, conv_in_channels_,
          conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
          kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
          pad_.cpu_data()[0], pad_.cpu_data()[1],
          stride_.cpu_data()[0], stride_.cpu_data()[1],
          dilation_.cpu_data()[0], dilation_.cpu_data()[1], col_buff);
    } else {
      im2col_nd_gpu(data, num_spatial_axes_, num_kernels_im2col_,
          conv_input_shape_.gpu_data(), col_buffer_.gpu_shape(),
          kernel_shape_.gpu_data(), pad_.gpu_data(),
          stride_.gpu_data(), dilation_.gpu_data(), col_buff);
    }
  }
  inline void conv_col2im_gpu(const Dtype* col_buff, Dtype* data) {
    if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
      col2im_gpu(col_buff, conv_in_channels_,
          conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
          kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
          pad_.cpu_data()[0], pad_.cpu_data()[1],
          stride_.cpu_data()[0], stride_.cpu_data()[1],
          dilation_.cpu_data()[0], dilation_.cpu_data()[1], data);
    } else {
      col2im_nd_gpu(col_buff, num_spatial_axes_, num_kernels_col2im_,
          conv_input_shape_.gpu_data(), col_buffer_.gpu_shape(),
          kernel_shape_.gpu_data(), pad_.gpu_data(), stride_.gpu_data(),
          dilation_.gpu_data(), data);
    }
  }
#endif

  int num_kernels_im2col_;
  int num_kernels_col2im_;
  int conv_out_channels_;
  int conv_in_channels_;
  int conv_out_spatial_dim_;
  int kernel_dim_;
  int col_offset_;
  int output_offset_;

  Blob<Dtype> col_buffer_;
  Blob<Dtype> bias_multiplier_;
};

}  // namespace caffe

#endif  // CAFFE_BASE_CONVOLUTION_LAYER_HPP_

这里着重介绍了变量的意义,便于后面cpp的阅读,这里着重介绍一下比较陌生的caffe group参数:

caffe Convolution层的convolution_param参数字典中有一个group参数,其意思是将输入channel和该层卷积核进行分组,每个group中的卷积核只对本group对应channel的特征图进行卷积操作:

比如输入数据大小为128x32x100x100,其中128是batchsize,100x100是图像高、宽,32是channel数量,要经过一个3x3x48的卷积,group默认是1,就是48个卷积滤波器都直接在32个通道上操作,即每个滤波器的权值是3x3x32。

但当group>1,那么限制每个滤波器只与输入的子集连接,比如说,group是2时,也就是将输入的32个通道分成2个16的通道,将输出的48个通道分成2个24的通道,并且第一组24个卷积滤波器与输入的第一个16通道进行卷积,同样第二组24个卷积滤波器与输入的第一个16通道进行卷积,也就是说这时每个滤波器权值为3x3x16。

If g > 1, we restrict the connectivity of each filter to a subset of the input. Specifically, the input and output channels are separated into g groups, and the i-th output group channels will be only connected to the i-th input group channels.

2.im2col

此外,这里简要解释一下caffe的im2col方法,将图像矩阵的卷积运算通过im2col转化为高效的矩阵乘法运算。

简单来讲,图像的卷积操作,就是滤波器算子逐个区域扫描图像的过程,在每个区域内都是滤波器权值和图像块像素值之间的简单的数乘,因此,可以把每个图像块展成一列,将滤波器展成一行,进行矩阵乘法,实现卷积操作,如下图所示:


基本上就是Filter Matrix乘以Feature Matrix的转置,得到输出矩阵Cout x (H x W),就可以解释为输出的三维Blob(Cout x H x W)

这张图可能更清楚:


上图中,三通道的input featurs的每个被卷积的图像块按照卷积扫描顺序,展开成一个矩阵,通用,卷积核滤波器也是如此,然后进行矩阵乘法,得到输出特征图。

3.LayerSetup

#include <algorithm>
#include <vector>

#include "caffe/filler.hpp"
#include "caffe/layers/base_conv_layer.hpp"
#include "caffe/util/im2col.hpp"
#include "caffe/util/math_functions.hpp"

namespace caffe {

template <typename Dtype>
void BaseConvolutionLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top) {
  // Configure the kernel size, padding, stride, and inputs.
  // 根据protobuf中的层参数设置,配置卷积核的大小,padding,步长和输入等等。
  ConvolutionParameter conv_param = this->layer_param_.convolution_param();
  force_nd_im2col_ = conv_param.force_nd_im2col();//根据层参数设置设置是否强制进行n维im2col
  //下面的函数定义于blob.hpp,用于检查输入index是否在范围内,并支持输入负数反向索引,输出正数正向索引,
  //确定哪个维度是channel,一般是,batch*channel*height*width,第二个维度。
  channel_axis_ = bottom[0]->CanonicalAxisIndex(conv_param.axis());
  //channel后紧跟的就是图像高、宽这种空间轴,第一个空间轴是第几维自然就是+1
  const int first_spatial_axis = channel_axis_ + 1;
  const int num_axes = bottom[0]->num_axes();//数据的axis数量
  num_spatial_axes_ = num_axes - first_spatial_axis;//图像空间轴(h,w)的个数,即是几维图像
  CHECK_GE(num_spatial_axes_, 0);
  //当num_spatial_axes_==2时,spatial_dim_blob_shape这个vector只包含一个元素且值为2
  vector<int> spatial_dim_blob_shape(1, std::max(num_spatial_axes_, 1));
  // Setup filter kernel dimensions (kernel_shape_).
  // 调用blob.cpp里的 void Blob<Dtype>::Reshape(const vector<int>& shape)
  //以spatial_dim_blob_shape为参数来构造一个Blob,即kernel_shape_,则这个Blob的维度信息只包含一个维度,值为2,
  //也就是说这个Blob的count_==2。尽管这个Blob的维度信息只包含一个维度,只有两个数。
  //因为在后续的计算(Im2col)中,我只关心这个Blob中的数据的值,而不关心这个Blob的shape信息.
  //例如在Im2col()中,只要取出相应数值即可kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1]。
  kernel_shape_.Reshape(spatial_dim_blob_shape);
  int* kernel_shape_data = kernel_shape_.mutable_cpu_data();
  // 若层参数中有定义卷积核宽或高,则将其存入kernel_shape_data这一blob中
  if (conv_param.has_kernel_h() || conv_param.has_kernel_w()) {
    CHECK_EQ(num_spatial_axes_, 2)
        << "kernel_h & kernel_w can only be used for 2D convolution.";
    CHECK_EQ(0, conv_param.kernel_size_size())
        << "Either kernel_size or kernel_h/w should be specified; not both.";
    kernel_shape_data[0] = conv_param.kernel_h();
    kernel_shape_data[1] = conv_param.kernel_w();
  } else {
	// 若层参数中没有定义卷积核宽和高,则根据卷积核的维度数来确定。哪一维核大小就附几
    const int num_kernel_dims = conv_param.kernel_size_size();
    CHECK(num_kernel_dims == 1 || num_kernel_dims == num_spatial_axes_)
        << "kernel_size must be specified once, or once per spatial dimension "
        << "(kernel_size specified " << num_kernel_dims << " times; "
        << num_spatial_axes_ << " spatial dims).";
      for (int i = 0; i < num_spatial_axes_; ++i) {
        kernel_shape_data[i] =
            conv_param.kernel_size((num_kernel_dims == 1) ? 0 : i);
      }
  }
  for (int i = 0; i < num_spatial_axes_; ++i) {
    CHECK_GT(kernel_shape_data[i], 0) << "Filter dimensions must be nonzero.";
  }
  // 接下来的stride_,pad_,dilation_这些blob的设定也类似。
  // Setup stride dimensions (stride_).
  stride_.Reshape(spatial_dim_blob_shape);
  int* stride_data = stride_.mutable_cpu_data();
  if (conv_param.has_stride_h() || conv_param.has_stride_w()) {
    CHECK_EQ(num_spatial_axes_, 2)
        << "stride_h & stride_w can only be used for 2D convolution.";
    CHECK_EQ(0, conv_param.stride_size())
        << "Either stride or stride_h/w should be specified; not both.";
    stride_data[0] = conv_param.stride_h();
    stride_data[1] = conv_param.stride_w();
  } else {
    const int num_stride_dims = conv_param.stride_size();
    CHECK(num_stride_dims == 0 || num_stride_dims == 1 ||
          num_stride_dims == num_spatial_axes_)
        << "stride must be specified once, or once per spatial dimension "
        << "(stride specified " << num_stride_dims << " times; "
        << num_spatial_axes_ << " spatial dims).";
    const int kDefaultStride = 1;
    for (int i = 0; i < num_spatial_axes_; ++i) {
      stride_data[i] = (num_stride_dims == 0) ? kDefaultStride :
          conv_param.stride((num_stride_dims == 1) ? 0 : i);
      CHECK_GT(stride_data[i], 0) << "Stride dimensions must be nonzero.";
    }
  }
  // Setup pad dimensions (pad_).
  pad_.Reshape(spatial_dim_blob_shape);
  int* pad_data = pad_.mutable_cpu_data();
  if (conv_param.has_pad_h() || conv_param.has_pad_w()) {
    CHECK_EQ(num_spatial_axes_, 2)
        << "pad_h & pad_w can only be used for 2D convolution.";
    CHECK_EQ(0, conv_param.pad_size())
        << "Either pad or pad_h/w should be specified; not both.";
    pad_data[0] = conv_param.pad_h();
    pad_data[1] = conv_param.pad_w();
  } else {
    const int num_pad_dims = conv_param.pad_size();
    CHECK(num_pad_dims == 0 || num_pad_dims == 1 ||
          num_pad_dims == num_spatial_axes_)
        << "pad must be specified once, or once per spatial dimension "
        << "(pad specified " << num_pad_dims << " times; "
        << num_spatial_axes_ << " spatial dims).";
    const int kDefaultPad = 0;
    for (int i = 0; i < num_spatial_axes_; ++i) {
      pad_data[i] = (num_pad_dims == 0) ? kDefaultPad :
          conv_param.pad((num_pad_dims == 1) ? 0 : i);
    }
  }
  // Setup dilation dimensions (dilation_).
  dilation_.Reshape(spatial_dim_blob_shape);
  int* dilation_data = dilation_.mutable_cpu_data();
  const int num_dilation_dims = conv_param.dilation_size();
  CHECK(num_dilation_dims == 0 || num_dilation_dims == 1 ||
        num_dilation_dims == num_spatial_axes_)
      << "dilation must be specified once, or once per spatial dimension "
      << "(dilation specified " << num_dilation_dims << " times; "
      << num_spatial_axes_ << " spatial dims).";
  const int kDefaultDilation = 1;
  for (int i = 0; i < num_spatial_axes_; ++i) {
    dilation_data[i] = (num_dilation_dims == 0) ? kDefaultDilation :
                       conv_param.dilation((num_dilation_dims == 1) ? 0 : i);
  }
  // Special case: im2col is the identity for 1x1 convolution with stride 1
  // and no padding, so flag for skipping the buffer and transformation.
  is_1x1_ = true;
  for (int i = 0; i < num_spatial_axes_; ++i) {
    is_1x1_ &=
        kernel_shape_data[i] == 1 && stride_data[i] == 1 && pad_data[i] == 0;
    if (!is_1x1_) { break; }
  }
  // Configure output channels and groups.
  channels_ = bottom[0]->shape(channel_axis_);//根据channel的维度索引找数据blob的channel数。
  num_output_ = this->layer_param_.convolution_param().num_output();
  CHECK_GT(num_output_, 0);
  group_ = this->layer_param_.convolution_param().group();
  //输入、输出 channel 个数必须为group的整数倍,每个group中的卷积核只对本group对应频道的特征图进行卷积操作
  CHECK_EQ(channels_ % group_, 0);
  CHECK_EQ(num_output_ % group_, 0)
      << "Number of output should be multiples of group.";
  //是否反转维度,输入频道数变输出频道数。
  if (reverse_dimensions()) {
    conv_out_channels_ = channels_;
    conv_in_channels_ = num_output_;
  } else {
    conv_out_channels_ = num_output_;
    conv_in_channels_ = channels_;
  }
  // Handle the parameters: weights and biases.
  // - blobs_[0] holds the filter weights
  // - blobs_[1] holds the biases (optional)
  //权值形状:(conv_out_channels_,conv_in_channels_ / group_,kernel_h, kernel_w),如:256*256*3*3
  vector<int> weight_shape(2);
  weight_shape[0] = conv_out_channels_;
  //每个group中的卷积核只对本group对应频道的特征图进行卷积操作,即上文中描述的3x3x32和3x3x16的区别(group的影响)
  weight_shape[1] = conv_in_channels_ / group_;
  for (int i = 0; i < num_spatial_axes_; ++i) {
    weight_shape.push_back(kernel_shape_data[i]);
  }
  bias_term_ = this->layer_param_.convolution_param().bias_term();
  vector<int> bias_shape(bias_term_, num_output_);
  if (this->blobs_.size() > 0) {
    CHECK_EQ(1 + bias_term_, this->blobs_.size())
        << "Incorrect number of weight blobs.";
    if (weight_shape != this->blobs_[0]->shape()) {
      Blob<Dtype> weight_shaped_blob(weight_shape);
      LOG(FATAL) << "Incorrect weight shape: expected shape "
          << weight_shaped_blob.shape_string() << "; instead, shape was "
          << this->blobs_[0]->shape_string();
    }
    if (bias_term_ && bias_shape != this->blobs_[1]->shape()) {
      Blob<Dtype> bias_shaped_blob(bias_shape);
      LOG(FATAL) << "Incorrect bias shape: expected shape "
          << bias_shaped_blob.shape_string() << "; instead, shape was "
          << this->blobs_[1]->shape_string();
    }
    LOG(INFO) << "Skipping parameter initialization";
  } else {
    if (bias_term_) {
      this->blobs_.resize(2);
    } else {
      this->blobs_.resize(1);
    }
    // Initialize and fill the weights:
    // output channels x input channels per-group x kernel height x kernel width
    // 按照获取的形状初始化并填入权值,形状:输出通道数×每组输入通道数×卷积核高度×卷积核宽度
    this->blobs_[0].reset(new Blob<Dtype>(weight_shape));
    // 根据protobuf里设置的滤波器权值初始方法(constant,xavier等)进行初始化。
    shared_ptr<Filler<Dtype> > weight_filler(GetFiller<Dtype>(
        this->layer_param_.convolution_param().weight_filler()));
    weight_filler->Fill(this->blobs_[0].get());//将blob_中的参数填入weight_filler
    // If necessary, initialize and fill the biases.
    if (bias_term_) {
      this->blobs_[1].reset(new Blob<Dtype>(bias_shape));
      shared_ptr<Filler<Dtype> > bias_filler(GetFiller<Dtype>(
          this->layer_param_.convolution_param().bias_filler()));
      bias_filler->Fill(this->blobs_[1].get());
    }
  }
  kernel_dim_ = this->blobs_[0]->count(1);//从第一个维度开始统计权值数量,即每个滤波器的大小:每组输入通道数×卷积核高度×卷积核宽度
  weight_offset_ = conv_out_channels_ * kernel_dim_ / group_;//写成(conv_out_channels_ / group_) * kernel_dim_更直观。
  // Propagate gradients to the parameters (as directed by backward pass).
  this->param_propagate_down_.resize(this->blobs_.size(), true);
}

这个函数主要是根据protobuf中的层参数设置,配置卷积核的大小,padding,步长和输入、权值参数初始化等等。是层建立的基础,是大多层建立的第一步,层参数设置及初始化。

5.Reshape

template <typename Dtype>
void BaseConvolutionLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top) {
  const int first_spatial_axis = channel_axis_ + 1;
  CHECK_EQ(bottom[0]->num_axes(), first_spatial_axis + num_spatial_axes_)
      << "bottom num_axes may not change.";
  num_ = bottom[0]->count(0, channel_axis_);//统计输入特征图数量batch_size*channel_num
  CHECK_EQ(bottom[0]->shape(channel_axis_), channels_)
      << "Input size incompatible with convolution kernel.";
  // TODO: generalize to handle inputs of different shapes.一般化来适应不同形状的输入。
  // 检查所有bottom blob形状一致
  for (int bottom_id = 1; bottom_id < bottom.size(); ++bottom_id) {
    CHECK(bottom[0]->shape() == bottom[bottom_id]->shape())
        << "shape mismatch - bottom[0]: " << bottom[0]->shape_string()
        << " vs. bottom[" << bottom_id << "]: "
        << bottom[bottom_id]->shape_string();
  }
  // Shape the tops.根据bottom形状,计算输出top blob形状(batch_size, channel_out, out_h, out_w,...)
  bottom_shape_ = &bottom[0]->shape();
  compute_output_shape();//虚函数,需要重写来确定具体输出形状
  //复制[begin,end)区间内另一个数组(描述bottom形状)的元素到该vector中,左闭右开,如果是blob是b*c*h*w,就只复制过来了b。
  vector<int> top_shape(bottom[0]->shape().begin(),
      bottom[0]->shape().begin() + channel_axis_);
  top_shape.push_back(num_output_);
  for (int i = 0; i < num_spatial_axes_; ++i) {
    top_shape.push_back(output_shape_[i]);
  }
  // 按得到的top_shape创建top blob,并调整其形状,为其开辟空间等。
  for (int top_id = 0; top_id < top.size(); ++top_id) {
    top[top_id]->Reshape(top_shape);
  }
  // 从channel后开始统计输出特征图大小,h*w*...
  if (reverse_dimensions()) {
    conv_out_spatial_dim_ = bottom[0]->count(first_spatial_axis);
  } else {
    conv_out_spatial_dim_ = top[0]->count(first_spatial_axis);
  }
  //卷积窗口在输入“图像”上按步长滑动,形成了多个子图;然后将所有子图拉成一列,列的长度就是col_offset_。
  //col_offset_与im2col_cpu()函数中channels_col的计算是相似的,但是值并不相等,
  //原因在于:channels_col是将卷积层输入的通道数conv_in_channels_用于相乘,
  //但kernel_dim_只用到了一部分channel,即conv_in_channels_/group_ 。
  col_offset_ = kernel_dim_ * conv_out_spatial_dim_;
  //卷积层的输出特征图也要分组,当然group_默认为1。写成(conv_out_channels_ / group_) * conv_out_spatial_dim_更直观
  output_offset_ = conv_out_channels_ * conv_out_spatial_dim_ / group_;
  // Setup input dimensions (conv_input_shape_).
  vector<int> bottom_dim_blob_shape(1, num_spatial_axes_ + 1);//获取每个图的维度数,二维图有三个维度:channel,h,w
  conv_input_shape_.Reshape(bottom_dim_blob_shape);
  int* conv_input_shape_data = conv_input_shape_.mutable_cpu_data();
  for (int i = 0; i < num_spatial_axes_ + 1; ++i) {
    if (reverse_dimensions()) {
      conv_input_shape_data[i] = top[0]->shape(channel_axis_ + i);
    } else {
      conv_input_shape_data[i] = bottom[0]->shape(channel_axis_ + i);
    }
  }
  // The im2col result buffer will only hold one image at a time to avoid
  // overly large memory usage. In the special case of 1x1 convolution
  // it goes lazily unused to save memory.
  // 每次只im2col转化一张图。
  col_buffer_shape_.clear();//col_buffer_shape_是一个vector
  col_buffer_shape_.push_back(kernel_dim_ * group_);
  for (int i = 0; i < num_spatial_axes_; ++i) {
    if (reverse_dimensions()) {
      col_buffer_shape_.push_back(input_shape(i + 1));
    } else {
      col_buffer_shape_.push_back(output_shape_[i]);
    }
  }
  //可以认为col_buffer_内所存储的数据的维度为:(kernel_dim_ * group_) × H × W.
  col_buffer_.Reshape(col_buffer_shape_);
  bottom_dim_ = bottom[0]->count(channel_axis_);
  top_dim_ = top[0]->count(channel_axis_);
  num_kernels_im2col_ = conv_in_channels_ * conv_out_spatial_dim_;
  num_kernels_col2im_ = reverse_dimensions() ? top_dim_ : bottom_dim_;
  // Set up the all ones "bias multiplier" for adding biases by BLAS
  out_spatial_dim_ = top[0]->count(first_spatial_axis);
  if (bias_term_) {
    vector<int> bias_multiplier_shape(1, out_spatial_dim_);
    //bias_multiplier_这个Blob的count_为out_spatial_dim_,是输出特征图的H×W
    bias_multiplier_.Reshape(bias_multiplier_shape);
    caffe_set(bias_multiplier_.count(), Dtype(1),
        bias_multiplier_.mutable_cpu_data());
  }
}
reshape函数主要是:根据bottom形状,计算输出top blob形状;计算im2col及反向的一些参数。

5.forward_cpu_gemm

template <typename Dtype>
void BaseConvolutionLayer<Dtype>::forward_cpu_gemm(const Dtype* input,
    const Dtype* weights, Dtype* output, bool skip_im2col) {
  const Dtype* col_buff = input;
  // 如果没有1x1卷积,也没有skip_im2col
  // 则使用conv_im2col_cpu对使用卷积核滑动过程中的每一个kernel大小的图像块
  // 变成一个列向量,形成一个height=kernel_dim_的
  // width = 卷积后图像height*卷积后图像width
  if (!is_1x1_) {
    if (!skip_im2col) {
      conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
    }
    col_buff = col_buffer_.cpu_data();
  }
  // caffe_cpu_gemm就是调用cblas_sgemm函数进行矩阵乘法运算
  for (int g = 0; g < group_; ++g) {
      // conv_out_channels_ / group_是每个卷积组的输出的channel
      // kernel_dim_ = input channels per-group x kernel height x kernel width
      // 计算的是output[output_offset_ * g]= weights[weight_offset_ * g] X col_buff[col_offset_ * g]
      // weights的维度为(conv_out_channels_ /group_) x kernel_dim_
      // weights的形状是 [conv_out_channel x kernel_dim_]
      // col_buff相当于数据,它的形状是[kernel_dim_ x (卷积后图像高度*卷积后图像宽度)]=
      //    kernel_dim_ x conv_out_spatial_dim_
      // 所以output的形状自然就是conv_out_channel X (卷积后图像高度*卷积后图像宽度)=
      //       (conv_out_channels_ /group_) x conv_out_spatial_dim_
    caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ /
        group_, conv_out_spatial_dim_, kernel_dim_,
        (Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g,
        (Dtype)0., output + output_offset_ * g);
  }
}

前向、反向传播计算大致就两步:用im2col函数展成矩阵,调用cblas_sgemm函数进行矩阵乘法运算。

之前的所有都是准备参数,用于输出这两个函数的。这里仅仅用一个做例子,其他都类似。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值