目录
@(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);
小结
- 注意代码中
col2im_cpu()
函数与im2col_cpu()
函数不是严格的逆操作。如果图像的某个点在卷积时被多次使用过,那么在矩阵转为图像时该位置的图像值同样会被多次累加(应该是为了方便计算卷积层反传时的梯度,不过笔者还未看这部分),所以还原的图像并不是真实的卷积前的图像。im2col_nd_core_cpu()
函数中也是如此。 im2col_nd_core_cpu()
函数实现了高维卷积的数据转矩阵操作,高维卷积中除了用于计算卷积值的那几个维度(卷积核也在这些维度上移动),还有一个更高维的维度用于累加卷积核,类似于2维卷积中的channel维度。- im2col.cpp文件中的这几个函数与caffe关联较少,可自己写个demo测试各个函数的功能以及单步调试,方便理解。
参考
https://blog.csdn.net/jiongnima/article/details/69736844
Caffe的源码笔者是第一次阅读,一边阅读一边记录,对代码的理解和分析可能会存在错误或遗漏,希望各位读者批评指正,谢谢支持!
来源:https://www.cnblogs.com/Relu110/p/12184777.html