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