Caffe源码-im2col操作

心已入冬 提交于 2020-01-12 22:32:32

@(Caffe源码-im2col操作)

im2col简介

caffe的卷积操作中使用im2col来加速,im2col将卷积核中的每个点在图像上的对应点全都提取出来按行排列,得到一个矩阵,这样就将卷积操作转化为矩阵进行操作。

如上图所示的,假设输入图像的形状为channels=1, height=width=5,并且pad_w=pad_h=1, kernel_h=kernel_w=3, stride_h=stride_w=2, dilation_w=dilation_h=1。左侧图中蓝色为padding补充的边界,值均为0,绿色为实际图像的数据。其中卷积核中\(k_{00}\)位置在整个卷积操作中共计算了output_h*output_w=9次,每次的位置在左侧图中用黑色实心圆标注出来。而im2col操作即是将卷积核上的每个点的这些对应位置上的值都提取出来,按照右侧黄色方格的形式存放起来。这样卷积操作可简单地通过将卷积核(中间的红色方格)展成一个向量,然后与右侧的黄色方格矩阵中的每一列点乘来实现。更详细的说明可查看后面列出来的参考博客。
与im2col对应的是col2im操作,即是将矩阵还原成卷积前的图像的形状,不过caffe代码中的col2im_cpu()函数还稍微有些改动。

im2col.cpp源码

// Function uses casting from int to unsigned to compare if value of
// parameter a is greater or equal to zero and lower than value of
// parameter b. The b parameter is of type signed and is always positive,
// therefore its value is always lower than 0x800... where casting
// negative value of a parameter converts it to value higher than 0x800...
// The casting allows to use one condition instead of two.
inline bool is_a_ge_zero_and_a_lt_b(int a, int b) {
  return static_cast<unsigned>(a) < static_cast<unsigned>(b);
}

// data_im为输入的图像数据,单个图像数据,num=1, data_col为转化后的矩阵
// channels/height/width为图像的通道数/高度/宽度
// kernel_h/kernel_w为卷积核的高度/宽度
// pad_h/pad_w为卷积时图像的高度和宽度方向的边界补充大小
// stride_h/stride_w为卷积时高度和宽度方向的步进大小
// dilation_h/dilation_w为卷积时卷积核的空洞系数
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) {
  //(dilation_h * (kernel_h - 1) + 1)和(dilation_w * (kernel_w - 1) + 1)为带上空洞系数的卷积核的尺寸 
  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;    //输入图像的每个通道的大小
  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++) {   //处理卷积核的每列
        //开始计算卷积核的(kernel_row, kernel_col)点在输入图像的所有对应位置(input_row, input_col),
        //并将输入图像该位置的值存入data_col中,如果(kernel_row, kernel_col)点对应输入图像的padding位置,则存入0
        //卷积核上的每个点都有 output_h * output_w 个对应位置,输入图像的每行有output_w个对应位置,共output_h行

        int input_row = -pad_h + kernel_row * dilation_h;   //第一次卷积时卷积核的该点对应输入图像的第input_row行
        // output_rows在循环体中并没有使用,所以此处是从output_h减至0还是从0增至output_h的效果是一样的
        for (int output_rows = output_h; output_rows; output_rows--) {  //处理该点在输入图像每一行的对应位置
          //不满足0 ≤ input_row < height,则在此处卷积时卷积核的第kernel_row行对应着输入图像的边界之外的第input_row行
          if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {
            //卷积核的该点对应输入图像的边界外的行,则计算输出图像时,整行的对应位置都应在边界外,整行一共有output_w个对应位置
            for (int output_cols = output_w; output_cols; output_cols--) {
              *(data_col++) = 0;    //全部置为0
            }
          } else {    //卷积核的该点在图像内部
            int input_col = -pad_w + kernel_col * dilation_w; //第一次卷积时卷积核的该点对应输入图像的第input_col列
            for (int output_col = output_w; output_col; output_col--) { //处理该点在输入图像每一列的对应位置
              if (is_a_ge_zero_and_a_lt_b(input_col, width)) {    //同样判断对应位置的列是否在图像边界外
                *(data_col++) = data_im[input_row * width + input_col]; //图像内部,则将输入图像(input_row, input_col)处的值存入
              } else {
                *(data_col++) = 0;    //(input_row, input_col)在图像外,存入0
              }
              input_col += stride_w;  //循环,宽度方向上的移动,卷积核的该点每次对应输入图像的(input_row, input_col)位置
            }
          }
          input_row += stride_h;      //循环,高度方向上的移动,每次对应输入图像的(input_row, input_col)位置
        }
      }
    }
  }
}

// Explicit instantiation
template void im2col_cpu<float>(const float* 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,
    float* data_col);
template void im2col_cpu<double>(const double* 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,
    double* data_col);

//col_shape的值为[k_dim0*k_dim1*...*channel_in, col_dim0, col_dim1, ...]
//[col_dim0, col_dim1, ...]为卷积操作之后的图像的各个维度的大小
template <typename Dtype>
inline void im2col_nd_core_cpu(const Dtype* data_input, const bool im2col,
    const int num_spatial_axes, const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, Dtype* data_output) {
  //*_dim0表示第0维的大小,dim0_?表示第0维上的位置?
  if (!im2col) {    //不是image to column,则是column to image
    int im_size = im_shape[0];
    for (int i = 0; i < num_spatial_axes; ++i) {
      im_size *= im_shape[1 + i];   //计算图像的大小,im_dim0*im_dim1*...
    }
    caffe_set(im_size, Dtype(0), data_output);    //数据先清空
  }
  //kernel_shape中存放着卷积核中参与卷积的各个维度的值[k_dim0,k_dim1,...]
  //在2Dconv中, num_spatial_axes=2, H*W维度参与卷积, 卷积核在C维度上累加, 则kernel_shape为H*W
  int kernel_size = 1;
  for (int i = 0; i < num_spatial_axes; ++i) {
    kernel_size *= kernel_shape[i]; //单个通道的卷积核的大小k_dim0*k_dim1*...
  }
  //col_buf中的第0维等于卷积核的大小乘上输入图像的通道数,后面几维为输出图像参与卷积的那几维的大小
  const int channels_col = col_shape[0];      //col_buf的第0维的大小col_dim0
  //col_buf中的(c_col, out_dim0?, out_dim1?, ...)的位置存放着卷积核的(out_num?, im_channel?, d_offset[0], d_offset[1], ...)点对应的所有输入图像的值
  vector<int> d_offset(num_spatial_axes, 0);  //num_spatial_axes大小的向量,初始为0
  vector<int> d_iter(num_spatial_axes, 0);
  for (int c_col = 0; c_col < channels_col; ++c_col) {      //c_col即为单个卷积核上的每一个点
    // Loop over spatial axes in reverse order to compute a per-axis offset.
    int offset = c_col;
    for (int d_i = num_spatial_axes - 1; d_i >= 0; --d_i) { //从末尾维(如2D卷积的W维)开始计算
      if (d_i < num_spatial_axes - 1) {
        offset /= kernel_shape[d_i + 1];  //除以第d_i + 1维的大小,得到点c_col在第0维到第d_i维之间的索引
      }
      d_offset[d_i] = offset % kernel_shape[d_i]; //得到点c_col在第d_i维的位置,存入d_offset中
    }
    //卷积核的(out_num?, im_channel?, d_offset[0], d_offset[1], ...)点对应卷积核的点c_col,
    //但是此处还只是计算了参与卷积的几个维度d_offset[...], 点c_col中还包含了在卷积核累加的维度上的索引im_channel?
    for (bool incremented = true; incremented; ) {
      // Loop over spatial axes in forward order to compute the indices in the
      // image and column, and whether the index lies in the padding.
      int index_col = c_col;
      //判断index_col的含义时可将下面的代码单独抽离出来, index_col = (...((c_col * col_dim1 + d0) * col_dim2 + d1) * ... + ...)
      // for (int d_i = 0; d_i < num_spatial_axes; ++d_i) {
      //   const int d = d_iter[d_i];
      //   index_col *= col_shape[d_i + 1];
      //   index_col += d;
      // }

      int index_im = c_col / kernel_size;   //得到点c_col中在卷积核累加的维度上的索引im_channel的确切值
      bool is_padding = false;
      for (int d_i = 0; d_i < num_spatial_axes; ++d_i) {
        const int d = d_iter[d_i];          //整个卷积核在第d_i维度的移动位置
        //得到点c_col在卷积输入图像中第d_i维度上的索引d_im
        const int d_im = d * stride[d_i] - pad[d_i] + d_offset[d_i] * dilation[d_i];
        is_padding |= d_im < 0 || d_im >= im_shape[d_i + 1];  //存在任何超出边界的点,则is_padding为true
        index_col *= col_shape[d_i + 1];  //col_shape[1],col_shape[2]...为col_buf中图像的维度的大小
        index_col += d;                   //再加上位置,最终index_col为在d_iter表示的图像位置卷积时卷积核上的点c_col在col_buf中的索引
        index_im *= im_shape[d_i + 1];
        index_im += d_im;   //最终index_im为在d_iter表示的图像位置卷积时卷积核上的点c_col对应的图像点的索引
      }
      if (im2col) {         //图像转矩阵
        if (is_padding) {
          data_output[index_col] = 0;   //点c_col此次卷积时超出图像,则col_buf中置为0
        } else {
          data_output[index_col] = data_input[index_im];  //设置col_buf的值
        }
      } else if (!is_padding) {  // col2im  //矩阵转图像,并且未在图像边界外,则设置im_buf的值
        data_output[index_im] += data_input[index_col];
      }
      // Loop over spatial axes in reverse order to choose an index, like counting.
      incremented = false;
      //判断下一次卷积位置在各维度中的值,即d_iter中的值.如果卷积位置到了某一维度的末尾,则重新置为0,并且在下一维度上的值自增.
      //如果下一维度同样已经到了末尾,则在下下一维自增,如此重复,直至最终某一维位置自增了.
      //如果所有的维度都已经到了末尾位置,则自增标志incremented为false,则说明点c_col对应的各个图像位置都已经判断完毕
      for (int d_i = num_spatial_axes - 1; d_i >= 0; --d_i) {
        const int d_max = col_shape[d_i + 1];   //col_shape的第d_i + 1维对应卷积输出图像的第d_i维的大小
        //d_iter是卷积核在第d_i维度的移动位置,每个位置也即是输出图像上的一个点
        DCHECK_LT(d_iter[d_i], d_max);    //小于该维度的最大值
        if (d_iter[d_i] == d_max - 1) {
          d_iter[d_i] = 0;                //到了末尾,则该维度重新置为0
        } else {  // d_iter[d_i] < d_max - 1
          ++d_iter[d_i];                  //该维度不在末尾,则该维度自增
          incremented = true;             //设置标志,已自增.如果
          break;                          //退出
        }
      }
    }  // while(incremented) {
  }  // for (int c = 0; c < channels_col; ++c) {
}

template <typename Dtype>
void im2col_nd_cpu(const Dtype* data_im, const int num_spatial_axes,
    const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, Dtype* data_col) {
  const bool kIm2Col = true;
  im2col_nd_core_cpu(data_im, kIm2Col, num_spatial_axes, im_shape, col_shape,
                  kernel_shape, pad, stride, dilation, data_col);
}

// Explicit instantiation
template void im2col_nd_cpu<float>(const float* data_im,
    const int num_spatial_axes,
    const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, float* data_col);
template void im2col_nd_cpu<double>(const double* data_im,
    const int num_spatial_axes,
    const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, double* data_col);

//矩阵转图像,data_col为矩阵,形状为[kernel_h*kernel_w*channels, output_h*output_w]
//data_im为卷积前的图像,形状为[channels, height, width]
template <typename Dtype>
void col2im_cpu(const Dtype* data_col, 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_im) {
  caffe_set(height * width * channels, Dtype(0), data_im);    //先将图像数据清零
  //计算卷积后的图像的宽高
  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;    //卷积前图像的单个通道的大小
  
  for (int channel = channels; channel--; data_im += channel_size) {    //处理每个通道
    for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {     //处理卷积核的第kernel_row行
      for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {   //处理卷积核的第kernel_col列
        //data_col的第0维的大小维kernel_h*kernel_w*channels,所以此处的三个循环相当于是处理data_col的第0维的每个数据
        //假设是处理data_col的第0维的第kernel_idx个数据, kernel_idx = (channel * kernel_h + kernel_row) * kernel_w + kernel_col
        //同时第kernel_idx个数据也对应卷积核中的点(1, channel, kernel_row, kernel_col)点
        int input_row = -pad_h + kernel_row * dilation_h;   //卷积核的该点在初次卷积时对应卷积前图像的第input_row行
        for (int output_rows = output_h; output_rows; output_rows--) {
          if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {  //input_row不在[0,height)之间,即对应图像的padding位置
            data_col += output_w;   //则一整列都会在图像边界外,直接跳过整行的数据
          } else {
            int input_col = -pad_w + kernel_col * dilation_w; //卷积核的该点在初次卷积时对应卷积前图像的第input_col列
            for (int output_col = output_w; output_col; output_col--) {
              if (is_a_ge_zero_and_a_lt_b(input_col, width)) {  //input_col不在[0,width)之间,直接跳过,否则将
                //注意,此处是累加.所以如果卷积前图像的某个点被多次用于卷积操作时,其数值是会累加的
                data_im[input_row * width + input_col] += *data_col;
              }
              data_col++;   //下一个
              input_col += stride_w;  //卷积核的该点在下一次卷积时的图像位置
            }
          }
          input_row += stride_h;  //卷积核的该点在下一次卷积时的图像位置
        }
      }
    }
  }
}

// Explicit instantiation
template void col2im_cpu<float>(const float* data_col, 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,
    float* data_im);
template void col2im_cpu<double>(const double* data_col, 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,
    double* data_im);

template <typename Dtype>
void col2im_nd_cpu(const Dtype* data_col, const int num_spatial_axes,
    const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, Dtype* data_im) {
  const bool kIm2Col = false;
  im2col_nd_core_cpu(data_col, kIm2Col, num_spatial_axes, im_shape, col_shape,
                     kernel_shape, pad, stride, dilation, data_im);
}

// Explicit instantiation
template void col2im_nd_cpu<float>(const float* data_col,
    const int num_spatial_axes,
    const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, float* data_im);
template void col2im_nd_cpu<double>(const double* data_col,
    const int num_spatial_axes,
    const int* im_shape, const int* col_shape,
    const int* kernel_shape, const int* pad, const int* stride,
    const int* dilation, double* data_im);

小结

  1. 注意代码中col2im_cpu()函数与im2col_cpu()函数不是严格的逆操作。如果图像的某个点在卷积时被多次使用过,那么在矩阵转为图像时该位置的图像值同样会被多次累加(应该是为了方便计算卷积层反传时的梯度,不过笔者还未看这部分),所以还原的图像并不是真实的卷积前的图像。im2col_nd_core_cpu()函数中也是如此。
  2. im2col_nd_core_cpu()函数实现了高维卷积的数据转矩阵操作,高维卷积中除了用于计算卷积值的那几个维度(卷积核也在这些维度上移动),还有一个更高维的维度用于累加卷积核,类似于2维卷积中的channel维度。
  3. im2col.cpp文件中的这几个函数与caffe关联较少,可自己写个demo测试各个函数的功能以及单步调试,方便理解。

参考

https://blog.csdn.net/jiongnima/article/details/69736844

Caffe的源码笔者是第一次阅读,一边阅读一边记录,对代码的理解和分析可能会存在错误或遗漏,希望各位读者批评指正,谢谢支持!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!