zoukankan      html  css  js  c++  java
  • 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的源码笔者是第一次阅读,一边阅读一边记录,对代码的理解和分析可能会存在错误或遗漏,希望各位读者批评指正,谢谢支持!

  • 相关阅读:
    《TCP/IP详解》读书笔记
    更改vsts源代码绑定服务器地址方法
    t-sql或mssql怎么用命令行导入数据脚本
    t-sql中字符串前加N代表什么意思
    C#连接mariadb代码及方式
    sp_executesql动态执行sql语句并将结果赋值给一变量
    mariadb配置允许远程访问方式
    将DataTable转换为List,将List转换为DataTable的实现类
    SQL update select语句
    怎么区分MSSQL中nvarchar和varchar的区别?
  • 原文地址:https://www.cnblogs.com/Relu110/p/12184777.html
Copyright © 2011-2022 走看看