【caffe】卷积层代码解析

1.Forward_cpu

conv_layer.cpp

template <typename Dtype>
void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top) {
        // blobs_声明在 layer.hpp 中,vector<shared_ptr<Blob<Dtype> > > blobs_;
        // 用于存放学习得到的参数权值 weight 和偏置参数 bias
        // weight(blobs_[0])和bias(blobs_[1])分别存放在两个blob中
  const Dtype* weight = this->blobs_[0]->cpu_data();

  // 对bottom中所有blob进行前向卷积运算
  for (int i = 0; i < bottom.size(); ++i) {
    const Dtype* bottom_data = bottom[i]->cpu_data();
    Dtype* top_data = top[i]->mutable_cpu_data();

    // 对一个Batch的每一张图片进行前向计算
    // num_定义在caffe.proto中,即在caffe.pb.h中,为BatchSize大小
    for (int n = 0; n < this->num_; ++n) {

        // 基类的forward_cpu_gemm函数 base_conv_layer.cpp
        // 计算的是top_data[n * this->top_dim_] =
        // weights * bottom_data[n * this->bottom_dim_]

        // bottom_dim_, bias_term和top_dim定义在base_conv_layer.hpp中,
        // int bottom_dim_; 大小默认为 C_in*H_in*W_in
        // int top_dim_; 大小默认为 C_out*H_out*W_out
        // bool bias_term_; 是否使用偏置项
      this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight,
          top_data + n * this->top_dim_);
      if (this->bias_term_) {
        const Dtype* bias = this->blobs_[1]->cpu_data();
        this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
      }
    }
  }
}

base_conv_layer.cpp

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;
  if (!is_1x1_) {
    if (!skip_im2col) {
        // 如果没有1x1卷积,也没有skip_im2col
        // 则使用conv_im2col_cpu对使用卷积核滑动过程中的
        // 每一个kernel大小的三维图像块
        // 变成一个列向量,形成一个
        // height = C_in * kernel_h * kernel_w
        // width = output_h * output_w 的矩阵
      conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
    }
    // 获取im2col后得到的矩阵。注意col_buff和col_buffer_不同
    col_buff = col_buffer_.cpu_data();
  }

  // 使用caffe的cpu_gemm来进行计算
  for (int g = 0; g < group_; ++g) {
          //  g=0,group_=1,所以传递的参数为:
          //  conv_out_channels_ :卷积层的输出通道
          //  conv_out_spatial_dim_: H_out*W_out
          //  kernel_dim_: C_in*H_k*W_k
          //  weight: 卷积核参数指针
          //  col_offset:图片展成的列向量
          //  output: 输出
          /*
          功能: C = alpha*A*B+beta*C
          A,B,C 是输入矩阵(一维数组格式)
          所以为:output = 1.*weights*col_buff+0*output
          其中weights矩阵的维数为 [C_out,C_in*H_k*W_k]
          col_buff矩阵的维数为 [C_in*H_k*W_K, H_out*W_out]
          所以得到的结果output的维数为 [C_out, H_out*W_out]
      */

          /*
           * 滤波器权值没有经行相应的转换是因为,权值和数据都是一维数组存储的
           * 但数据是按行存储的,所以需要im2col,而滤波器权值本来就是那样存放的
           * 区别在于滤波器权值是学习得到的,而图像是给定的
           */
    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);
  }
}

  

base_conv_layer.hpp

  inline void conv_im2col_cpu(const Dtype* data, Dtype* col_buff) {
        if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
                // 如果不是计算n维通用卷积且计算的是二维卷积
                // num_spatial_axes定义在base_conv_layer.hpp中
                // 含义是计算二维卷积还是三维卷积,
                // 对于(N, C, H, W)的输入结果为2
                // 对于(N, C, D, H, W)的输入,结果为3
        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);
    }
  }

  

im2col.cpp

template <typename Dtype>
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) {
  // 通用公式,其中包含了卷积核的膨胀操作
  const int output_h = (height + 2 * pad_h -
    (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1;
  const int output_w = (width + 2 * pad_w -
    (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1;
  const int channel_size = height * width;
  // 先遍历每一个通道的图像矩阵(不是顺序遍历像素)
  // 然后遍历每一个kernel(遍历kernel,而不是遍历kernel内的元素)
  // 不是依次遍历kernel中的所有元素,而是每次只取kernel中某一个位置的元素
  // 需要遍历多次所有的kernel,每遍历一次所有的kernel
  // 所有kernel中的某个位置的元素被遍历,假如kernel大小为3*3,
  // 那么需要遍历9次所有的kernel才能遍历完图像矩阵的所有像素
  // 因为kernel有9个元素,每次kernel的遍历只取一个位置,
  // 那么如果遍历某个kernel中的所有元素,那么就需要9次遍历
  for (int channel = channels; channel--; data_im += channel_size) {
    for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {
      for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {
        // 计算将要访问的元素在输入矩阵中的行数(此矩阵为pad操作后的矩阵)
        int input_row = -pad_h + kernel_row * dilation_h;
        for (int output_rows = output_h; output_rows; output_rows--) {
          // is_a_ge_zero_and_a_lt_b(a, b) 含义为
          // 判断 0<= a < b 是否成立
          // 如果input_row和height不满足条件,说明input_row位于pad行
          if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {
            for (int output_cols = output_w; output_cols; output_cols--) {
              *(data_col++) = 0;
            }
          } else {
            // 计算将要访问的元素在输入矩阵中的列数(此矩阵为pad操作后的矩阵)
            int input_col = -pad_w + kernel_col * dilation_w;
            for (int output_col = output_w; output_col; output_col--) {
              if (is_a_ge_zero_and_a_lt_b(input_col, width)) {
                // 依次复制该行所有kernel中的固定位置的元素
                *(data_col++) = data_im[input_row * width + input_col];
              } else {
                // 如果input_col和width不满足条件,说明input_col位于pad列
                *(data_col++) = 0;
              }
              // 依次遍历input_row行中所有kernel
              input_col += stride_w;
            }
          }
          // 遍历下一行的kernel组
          input_row += stride_h;
        }
      }
    }
  }
}

  

math_functions.cpp

/*
功能: 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的列数(不做转置)行数(做转置)
A:[M,K]
B:[K,N]
 */
template<>
void caffe_cpu_gemm<float>(const CBLAS_TRANSPOSE TransA,
    const CBLAS_TRANSPOSE TransB, const int M, const int N, const int K,
    const float alpha, const float* A, const float* B, const float beta,
    float* C) {
  int lda = (TransA == CblasNoTrans) ? K : M;
  int ldb = (TransB == CblasNoTrans) ? N : K;
  cblas_sgemm(CblasRowMajor, TransA, TransB, M, N, K, alpha, A, lda, B,
      ldb, beta, C, N);
}

  

cblas

void cblas_sgemm(const enum CBLAS_ORDER Order, const enum CBLAS_TRANSPOSE TransA,
        const enum CBLAS_TRANSPOSE TransB, const int M, const int N,
        const int K, const float alpha, const float  *A,
        const int lda, const float  *B, const int ldb,
        const float beta, float  *C, const int ldc)
/*
 * 功能为计算 C = alpha*op(A)*op(B) + beta*C
 *
 * const enum CBLAS_ORDER Order,这是指的数据的存储形式,
 * 在CBLAS的函数中无论一维还是二维数据都是用一维数组存储,
 * 这就要涉及是行主序还是列主序,在C语言中数组是用行主序,fortran中是列主序
 *
 * CblasNoTrans表示矩阵是否转置
 * 不转置则 op(A) = A
 * 转置则   op(A) = A'
 *
 * const int M,矩阵A的行,矩阵C的行
 * const int N,矩阵B的列,矩阵C的列
 * const int K,矩阵A的列,矩阵B的行
 *
 * const float alpha,const float beta,计算公式中的两个参数值
 *
 * const float* A, const float* B, const float* C,矩阵A、B、C的数据
 *
 * const int lda, const int ldb, const int ldc
 * 不转置 ld* = max(1, 列数)
 * 转置   ld* = max(1, 行数)
 */

  

2.Backward_cpu

conv_layer.cpp

template <typename Dtype>
void ConvolutionLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
      const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {

    const Dtype* weight = this->blobs_[0].cpu_data();
    Dtype* weight_diff = this->blobs_[0].mutable_cpu_diff();

    for (int i = 0; i < top.size(); i++) {

        // 上一层传下来的导数
        const Dtype* top_diff = top[i].cpu_diff();
        const Dtype* bottom_data = bottom[i].cpu_data();

        //传给下一层的导数
        Dtype* bottom_diff = bottom[i].mutable_cpu_diff();

        // 如果有bias项,计算Bias导数
        if (this->bias_term_ && this->param_propagate_down_[1]) {
                for (int n = 0; n < this->num_; ++n) {
                        this->Backward_cpu_bias(bias_diff, top_diff + n * this->top_dim_);
                }
        }

        // 计算weight
        // param_propagate_down_定义在layer.hpp中,vector<bool> param_propagate_down_;
        // 标志该层每个可学习参数blob是否需要计算反向传递的梯度值
        // param_propagate_down_[0] - weight; param_propagate_down_[1] - bias

        if (this->param_propagate_down_[0] || propagate_down[i]) {
                for (int n = 0; n < this->num_; ++n) {
                        // 计算对于权值的梯度
                        if (this->param_propagate_down_[0]) {
                                // weight_diff = bottom_data * top_diff
                                this->weight_cpu_gemm(bottom_data + n * this->bottom_dim_,
                                        top_diff + n * this->top_dim_, weight_diff);
                        }
                        // 计算传播到下一层的梯度,即对于下一层来说是top_diff
                        // 对于这个层来说是 bottom_diff
                        if (propagate_down[i]) {
                                // bottom_diff = top_diff * weight
                                // bottom_diff [C_in*H_k*W_k, H_out*W_out]
                                // top_diff    [C_out, H_out*W_out]
                                // weight      [C_out, C_in*H_k*W_K]
                                this->backward_cpu_gemm(top_diff + n * this->top_dim_, weight, 
                                        bottom_diff _ n * this->bottom_dim_);
                        }
                }
        }
    }
}

  

base_conv_layer.cpp

template <typename Dtype>
void BaseConvolutionLayer<Dtype>::weight_cpu_gemm(const Dtype* input,
    const Dtype* output, Dtype* weights) {
  const Dtype* col_buff = input;

  // 不使用 1*1 卷积
  if (!is_1x1_) {
        // 矩阵转换
    conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
    col_buff = col_buffer_.cpu_data();
  }
  // 前向:output = weights * col_buff
  // 反向:weights = output * col_buff^T
  // 这是对滤波器权值求导
  for (int g = 0; g < group_; ++g) {
    caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, conv_out_channels_ / group_,
        kernel_dim_, conv_out_spatial_dim_,
        (Dtype)1., output + output_offset_ * g, col_buff + col_offset_ * g,
        (Dtype)1., weights + weight_offset_ * g);
  }
}

  

base_conv_layer.cpp

template <typename Dtype>
void BaseConvolutionLayer<Dtype>::backward_cpu_gemm(const Dtype* output,
    const Dtype* weights, Dtype* input) {
  Dtype* col_buff = col_buffer_.mutable_cpu_data();

  // 使用1*1卷积
  if (is_1x1_) {
    col_buff = input;
  }

  // 前向:output = weights * col_buff
  // 反向:col_buff = weights^T * output
  // 这是对bottom_data即输入数据求梯度
  for (int g = 0; g < group_; ++g) {
    caffe_cpu_gemm<Dtype>(CblasTrans, CblasNoTrans, kernel_dim_,
        conv_out_spatial_dim_, conv_out_channels_ / group_,
        (Dtype)1., weights + weight_offset_ * g, output + output_offset_ * g,
        (Dtype)0., col_buff + col_offset_ * g);
  }
  if (!is_1x1_) {
        // Blob中数据是row-major存储的,W(列)是变化最快的维度
    conv_col2im_cpu(col_buff, input);
  }
}