convolutional layer 源代码

http://blog.csdn.net/xizero00/article/details/51049858


一、 卷积层的作用简介

卷积层是深度神经网络中的一个重要的层,该层实现了局部感受野,通过这种局部感受野,可以有效地降低参数的数目。
我们将结合caffe来讲解具体是如何实现卷积层的前传和反传的。至于是如何前传和反传的原理可以参考Notes on Convolutional Neural Networks,具体请百度或者谷歌,即可下载到。
Caffe中的master分支已经将vision_layers.hpp中的各个层分散到layers中去了,因此如果你是主分支的代码,请在include/layers中找BaseConvolutionLayer和ConvolutionLayer的头文件的定义。



二、卷积层的详细介绍


1)构造函数

[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. // 构造函数  
  2.   explicit BaseConvolutionLayer(const LayerParameter& param)  
  3.       : Layer<Dtype>(param) {}  


2)成员变量

[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. /// @brief The spatial dimensions of a filter kernel.  
  2.   // kernel的形状 = [kernel_h, kernel_w]  
  3.   Blob<int> kernel_shape_;  
  4.   
  5.   /// @brief The spatial dimensions of the stride.  
  6.   // 步长形状 = [stride_h, stride_w]  
  7.   Blob<int> stride_;  
  8.   
  9.   /// @brief The spatial dimensions of the padding.  
  10.   // pad的形状 = [pad_h, pad_w]  
  11.   Blob<int> pad_;  
  12.   
  13.   /// @brief The spatial dimensions of the convolution input.  
  14.   // 卷积的输入形状 = [输入图像通道数, 输入图像h,    输入图像w]  
  15.   Blob<int> conv_input_shape_;  
  16.   
  17.   /// @brief The spatial dimensions of the col_buffer.  
  18.   // col_buffer的形状 = [kernel_dim_, conv_out_spatial_dim_ ]  
  19.   vector<int> col_buffer_shape_;  
  20.   
  21.   /// @brief The spatial dimensions of the output.  
  22.   // 输出的形状  
  23.   vector<int> output_shape_;  
  24.   
  25.   // 输入的形状  
  26.   const vector<int>* bottom_shape_;  
  27.   
  28.   // 空间轴个数  
  29.   int num_spatial_axes_;  
  30.   
  31.   // 输入度维度 = 输入图像通道数*输入图像的h*输入图像w  
  32.   int bottom_dim_;  
  33.   
  34.   // 输出维度 = 输出通道数*输出h*输出w  
  35.   int top_dim_;  
  36.   
  37.   // 输入图像的第几个轴是通道  
  38.   int channel_axis_;  
  39.   
  40.   // batchsize  
  41.   int num_;  
  42.   
  43.   // 输入图像的通道数  
  44.   int channels_;  
  45.   
  46.   // 卷积组的大小  
  47.   int group_;  
  48.   
  49.   // 输出空间维度 = 卷积之后的图像长*卷积之后图像的宽  
  50.   int out_spatial_dim_;  
  51.   
  52.   // 使用卷积组用到的  
  53.   int weight_offset_;  
  54.   
  55.   // 卷积后的图像的通道数  
  56.   int num_output_;  
  57.   
  58.   // 是否启用偏置  
  59.   bool bias_term_;  
  60.   
  61.   // 是不是1x1卷积  
  62.   bool is_1x1_;  
  63.   
  64.   // 强制使用n维通用卷积  
  65.   bool force_nd_im2col_;  
  66.   
  67.   // conv_in_channels_ * conv_out_spatial_dim_  
  68.   int num_kernels_im2col_;  
  69.   // num_kernels_col2im_ = reverse_dimensions() ? top_dim_ : bottom_dim_  
  70.   int num_kernels_col2im_;  
  71.   
  72.   // 卷积的输出通道数 ,在参数配置文件中设置  
  73.   int conv_out_channels_;  
  74.   
  75.   // 卷积的输入通道数 (即输入图像的通道数)  
  76.   int conv_in_channels_;  
  77.   
  78.   // 卷积的输出的空间维度 = 卷积后图像h*卷积后图像w  
  79.   int conv_out_spatial_dim_;  
  80.   
  81.   // 卷积核的维度 = 输入图像的维度*卷积核的h*卷积核的w  
  82.   int kernel_dim_;  
  83.   
  84.   // 在使用gropu参数的时候使用的offset  
  85.   int col_offset_;  
  86.   int output_offset_;  
  87.     
  88.   // im2col的时候使用的存储空间  
  89.   Blob<Dtype> col_buffer_;  
  90.   
  91.   // 将偏置扩展成矩阵的东东  
  92.   Blob<Dtype> bias_multiplier_;  



为了更为准确地解释,我把代码中的各个变量的含义也贴进来:
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. channel_axis_ = bottom[0]->CanonicalAxisIndex(conv_aram.axis());  
  2.   num_spatial_axes_ = num_axes - first_spatial_axis;  
  3.   // 是否需要强制n维卷积  
  4.   force_nd_im2col_ = conv_param.force_nd_im2col();  
  5.   
  6.   // 如果是正方形的那么  
  7.   kernel_shape_data[0]和[1]=conv_param.kernel_size(0)  
  8.   stride_data[0]和[1] = conv_param.stride(0)  
  9.   pad_data[0]和[1] = conv_param.pad[0]  
  10.   
  11.   // 输入的图像的通道数  
  12.   conv_in_channels_ = channels_;  
  13.    
  14.   // 经过卷积之后的通道数   
  15.   conv_out_channels_ = num_output_;  
  16.   bias_term_ = 1或者0  
  17.     
  18.   // 一个kernel大小的图像块的维度是,输入图像的通道数乘以kernel的长度和宽度  
  19.   kernel_dim_ = input channels per-group x kernel height x kernel width  
  20.   
  21.   // 卷积进行分组的offset  
  22.   weight_offset_ = conv_out_channels_ * kernel_dim_ / group_;  
  23.   
  24.   // 批数  
  25.   num_ = batchsize  
  26.   
  27.   // 卷积层的输入的图像的形状,  batchsize x input image channel x input image height x input image width  
  28.   bottom_shape_ = &bottom[0]->shape();  
  29.   
  30.     
  31.   conv_out_spatial_dim_ = 输出图像的长度和宽度  
  32.   
  33.   // 就是一个kernel的图像块中像素的个数  
  34.   kernel_dim_ = input channels per-group x kernel height x kernel width    
  35.   
  36.   
  37.   // col_buffe_shape压入的是经过im2col处理之后的形状  
  38.   col_buf_shape = kernel_dim_ x   


输入的大小 bottom_dim_ = conv_in_channel X in_height X in_width
输出的大小 top_dim_ = conv_out_channel X out_height X out_width
卷积核输入的图像大小num_kernels_im2col_ = conv_in_channel X out_height X out_width
卷积核输出的图像大小num_kernels_col2im_ = conv_out_channel X out_height X out_width
将偏置扩展成矩阵的bias_multiplier_
weight_shape = [conv_out_channels_, conv_in_channels_/group, weight_h, weight_w]
conv_input_shape_data = [inchannel, in_height, in width]
kernel_shape_=[kernel_height, kernel_width]


3)成员函数

在给出上述的变量以后,不得不介绍一下caffe里面究竟是如何实现卷积的,在介绍成员函数这一小节,我们从调用的顺序讲起是如何实现卷积以及反传的。
ConvolutionLayer是继承于BaseConvolutionLayer的,而BaseConvolutionLayer才是真正实现卷积及其反传的,而在BaseConvolutionLayer中的卷积的实现中有一个重要的函数就是im2col以及col2im,im2colnd以及col2imnd。前面的两个函数是二维卷积的正向和逆向过程,而后面的两个函数是n维卷积的正向和逆向过程。



首先放出conv_layer.cpp的前传代码
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template <typename Dtype>  
  2. void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,  
  3.       const vector<Blob<Dtype>*>& top) {  
  4.   const Dtype* weight = this->blobs_[0]->cpu_data();  
  5.   for (int i = 0; i < bottom.size(); ++i) {  
  6.     // bottom_data指向数组,此数组以一张图像为单位
  7.     const Dtype* bottom_data = bottom[i]->cpu_data();  
  8.     Dtype* top_data = top[i]->mutable_cpu_data();  
  9.     // num_ = batchsize  
  10.     for (int n = 0; n < this->num_; ++n) {  
  11.       // 基类的forward_cpu_gemm函数  
  12.       // 计算的是top_data[n * this->top_dim_] =  
  13.       // weights X bottom_data[n * this->bottom_dim_]  
  14.       // 输入的是(一个batch中)一幅图像的数据,对应的是这幅图像卷积之后的位置  
  15.       this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight,  
  16.           top_data + n * this->top_dim_);  
  17.       if (this->bias_term_) {  
  18.         const Dtype* bias = this->blobs_[1]->cpu_data();  
  19.         this->forward_cpu_bias(top_data + n * this->top_dim_, bias);  
  20.       }  
  21.     }  
  22.   }  
  23. }  
bottom_data与top_data中存放着输入和输出数据,blobs_[0]存放着权重数据,blobs_[1]存放着偏置信息。

上述的代码中调用了基类的前传函数,那么我们就把基类的前传函数拿出来看看
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template <typename Dtype>  
  2. void BaseConvolutionLayer<Dtype>::forward_cpu_gemm(const Dtype* input,  
  3.     const Dtype* weights, Dtype* output, bool skip_im2col) {  
  4.   const Dtype* col_buff = input;  
  5.   if (!is_1x1_) {  
  6.     if (!skip_im2col) {  
  7.       // 如果没有1x1卷积,也没有skip_im2col  
  8.       // 则使用conv_im2col_cpu对使用卷积核滑动过程中的每一个kernel大小的图像块  
  9.       // 变成一个列向量,形成一个height=kernel_dim_的  
  10.       // width = 卷积后图像heght*卷积后图像width  
  11.       conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());  
  12.     }  
  13.     col_buff = col_buffer_.cpu_data();  
  14.   }  
  15.   
  16.   // 使用caffe的cpu_gemm来进行计算  
  17.   for (int g = 0; g < group_; ++g) {  
  18.       // 分组分别进行计算  
  19.       // conv_out_channels_ / group_是每个卷积组的输出的channel  
  20.       // kernel_dim_ = input channels per-group x kernel height x kernel width  
  21.       // 计算的是output[output_offset_ * g] =  
  22.       // weights[weight_offset_ * g] X col_buff[col_offset_ * g]  
  23.       // weights的形状是 [conv_out_channel x kernel_dim_]  
  24.       // col_buff的形状是[kernel_dim_ x (卷积后图像高度乘以卷积后图像宽度)]  
  25.       // 所以output的形状自然就是conv_out_channel X (卷积后图像高度乘以卷积后图像宽度)  
  26.     caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ /  
  27.         group_, conv_out_spatial_dim_, kernel_dim_,  
  28.         (Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g,  
  29.         (Dtype)0., output + output_offset_ * g);  
  30.   }  
  31. }  
  32.   
  33. template <typename Dtype>  
  34. void BaseConvolutionLayer<Dtype>::forward_cpu_bias(Dtype* output,  
  35.     const Dtype* bias) {  
  36.   // output = bias * bias_multiplier_  
  37.   // num_output 与 conv_out_channel是一样的  
  38.   // num_output_ X out_spatial_dim_ = num_output_ X 1    1 X out_spatial_dim_  
  39.   caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,  
  40.       out_spatial_dim_, 1, (Dtype)1., bias, bias_multiplier_.cpu_data(),  
  41.       (Dtype)1., output);  
  42. }  



而积累中又使用了conv_im2col_cpu将卷积核在图像上的滑动转换为了矩阵。

这就是真身:
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. inline void conv_im2col_cpu(const Dtype* data, Dtype* col_buff) {  
  2.     if (!force_nd_im2col_ && num_spatial_axes_ == 2) {  
  3.       im2col_cpu(data, conv_in_channels_,  
  4.           conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],  
  5.           kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],  
  6.           pad_.cpu_data()[0], pad_.cpu_data()[1],  
  7.           stride_.cpu_data()[0], stride_.cpu_data()[1], col_buff);  
  8.     } else {  
  9.       im2col_nd_cpu(data, num_spatial_axes_, conv_input_shape_.cpu_data(),  
  10.           col_buffer_shape_.data(), kernel_shape_.cpu_data(),  
  11.           pad_.cpu_data(), stride_.cpu_data(), col_buff);  
  12.     }  
  13.   }  



调用im2col_cpu的时候输入的参数为
im2col_cpu(一幅图像,输入图像的channel, 输入图像的height, 输入图像的width, kernel的height, kernel的width, pad的height, pad的width, stride的height, stride的width)

其函数原型如下
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. // 将输入的图像首先进行虚假pad(啥叫虚假填充,就是实际没填充,但是目标图像中有了填充的0)  
  2. // 填充这一步,我们在原图像上并没有做pad,只是在处理后的图像上加上了pad的值  
  3. // 然后按照channel*kernel_h*kernel_w一列,将一个channel x kernel_h x kernel_w 大小的图像块变成一个列。  
  4. // 有多少个这样的列呢,这就可以用公式进行计算  
  5. // 列数 = [(图像高度+2*填充高度-kernel高度)/stride高度+1] * [(图像宽度+2*填充宽度-kernel宽度)/stride宽度+1]  
  6. // 这个行数就是一个kernel大小的图像块的维度  
  7. // 这个列数实际上就是kernel在图像上滑动的次数  
  8. template <typename Dtype>  
  9. void im2col_cpu(const Dtype* data_im, const int channels,  
  10.     const int height, const int width, const int kernel_h, const int kernel_w,  
  11.     const int pad_h, const int pad_w,  
  12.     const int stride_h, const int stride_w,  
  13.     Dtype* data_col) {  
  14.   int height_col = (height + 2 * pad_h - kernel_h) / stride_h + 1;  
  15.   int width_col = (width + 2 * pad_w - kernel_w) / stride_w + 1;  
  16.   int channels_col = channels * kernel_h * kernel_w;  
  17.     
  18.   // 遍历一个kernel大小的图像  
  19.   for (int c = 0; c < channels_col; ++c) {  
  20.     // 下面三行是计算在kernel大小的图像上面的位置  
  21.     //  c_im     h_offset     w_offset      
  22.       
  23.     // 在当前的kernel大小的图像上的w  
  24.     int w_offset = c % kernel_w;  
  25.     // 在当前的kernel大小的图像上的h  
  26.     int h_offset = (c / kernel_w) % kernel_h;  
  27.    // 当前kernel大小的图像是第几个channel  
  28.     int c_im = c / kernel_h / kernel_w;  
  29.   
  30.     // 遍历卷积之后的图像的上面的每一个像素  
  31.     for (int h = 0; h < height_col; ++h) {  
  32.       for (int w = 0; w < width_col; ++w) {  
  33.          
  34.         // 计算卷积之后的图像与卷积之前的图像的位置  
  35.    
  36.         // 卷积之后的图像与卷积之前的图像像素所对应的位置  
  37.         // 卷积之后的像素为h和w那么所对应的原图像的位置为 [h * stride_h - pad_h,   h * stride_h - pad_h+kernel_h]以及  
  38.         // [w * stride_w - pad_w,   w * stride_w - pad_w+kernel_w]  
  39.         int h_pad = h * stride_h - pad_h + h_offset;  
  40.         int w_pad = w * stride_w - pad_w + w_offset;  
  41.         if (h_pad >= 0 && h_pad < height && w_pad >= 0 && w_pad < width)  
  42.           data_col[(c * height_col + h) * width_col + w] =  
  43.             data_im[(c_im * height + h_pad) * width + w_pad];  
  44.         else  
  45.           data_col[(c * height_col + h) * width_col + w] = 0;  
  46.       }  
  47.     }  
  48.   }  
  49. }  




看起来这段代码很难理解,其实你不妨把计算卷积之后的图像与卷积之前的图像的位置看成是一个函数find_src_location
该函数输入的是h_offset,w_offset
该函数主要就是将每一个子矩形(与kernel一样大小的图像)的(h_offset,w_offset )的位置的像素取出来放到第c行去
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. void find_src_location(h_offset, w_offset)  
  2. {  
  3.      // 以下是遍历im2col处理之后的图像的一行的像素的位置  
  4.     // 因为卷积之后的图像的列数就是height_col*width_col  
  5.     // 这里两个for循环其实只是处理之后的矩阵的一行  
  6.     for (int h = 0; h < height_col; ++h) {  
  7.       for (int w = 0; w < width_col; ++w) {  
  8.   
  9.           // 将处理后图像的每一个像素找到位于输入图像中的像素值  
  10.         int h_pad = h * stride_h - pad_h + h_offset;  
  11.         int w_pad = w * stride_w - pad_w + w_offset;  
  12.         if (h_pad >= 0 && h_pad < height && w_pad >= 0 && w_pad < width)  
  13.           data_col[(c * height_col + h) * width_col + w] =  
  14.             data_im[(c_im * height + h_pad) * width + w_pad];  
  15.         else  
  16.           data_col[(c * height_col + h) * width_col + w] = 0;  
  17.       }  
  18.     }  
  19. }  




这个函数具体是做了啥事了呢,我来画个图
如下图所示,左边的是输入的原始图像,该图像需要进行卷积,右图是经过im2col处理的得到的图像
结合图来说这个函数find_src_location内部的两个for循环干的事情就是将每个不同颜色的小矩形框内部(h_offset,w_offset )位置的像素值复制到
第c行的位置去。比如下图中的右图,第一行的1,就是左图中的每一个不同颜色的小矩形框内的(0,0)位置的1全部复制过来的。


图1 解释im2col的工作过程


上图中输入图像的channel=1,长im_w是9,宽im_h是9,
kernel的h和w都是3,因为kernel是在图像上面滑动,所以kernel_channel=1
假设padding的h和w都是0,也就是不填充
stride_h为3,stide_w=3

那么通过该函数,就可以获得新的矩形
长是:channel*kernel_h*kernel_w = 1*3*3=9
宽是:[(im_h+2*pad_h-kernel_h)/stride_h+1] * [(im_w+2*pad_w-kernel_w)/stride_w+1]=[(9+2*0-3)/3+1]*[(9+2*0-3)/3+1]=9
注意:这个宽呢,就是经过卷积之后的图像的长和宽相乘

含义是什么:
将一个kernel大小的图像块变成图中新的矩形的一个列,一列的长是channel*kernel_h*kernel_w = 1*3*3=9
而宽(列数)则是在图像上的滑动的次数,这里是9,之所以是9并不是巧合,而是取了stride_h和stride_w都为3了。

可是这么干是为了什么呢?
这可以将滑动的卷积变成两个矩形相乘。
怎么讲,假设一个kernel输出的channel=output channel
那么对应的卷积权重W矩阵的形状即为outputchannle x [channel*kernel_h*kernel_w]
而图像经过im2col处理之后的原始图像srcimg变成了[channel*kernel_h*kernel_w] x [卷积之后的图像的长和宽相乘 ]
W x srcimg = outputchannle x 卷积之后的图像的长和宽相乘
怎么样,这下理解了吧
很机智地将卷积变成了两个矩阵相乘了。。。


而对应的col2im的代码就很类似了与im2col 的代码几乎没有啥差别就是这个下面的赋值语句的位置颠倒了一下
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template <typename Dtype>  
  2. void col2im_cpu(const Dtype* data_col, const int channels,  
  3.     const int height, const int width, const int patch_h, const int patch_w,  
  4.     const int pad_h, const int pad_w,  
  5.     const int stride_h, const int stride_w,  
  6.     Dtype* data_im) {  
  7.   caffe_set(height * width * channels, Dtype(0), data_im);  
  8.   int height_col = (height + 2 * pad_h - patch_h) / stride_h + 1;  
  9.   int width_col = (width + 2 * pad_w - patch_w) / stride_w + 1;  
  10.   int channels_col = channels * patch_h * patch_w;  
  11.   for (int c = 0; c < channels_col; ++c) {  
  12.     int w_offset = c % patch_w;  
  13.     int h_offset = (c / patch_w) % patch_h;  
  14.     int c_im = c / patch_h / patch_w;  
  15.     for (int h = 0; h < height_col; ++h) {  
  16.       for (int w = 0; w < width_col; ++w) {  
  17.         int h_pad = h * stride_h - pad_h + h_offset;  
  18.         int w_pad = w * stride_w - pad_w + w_offset;  
  19.         if (h_pad >= 0 && h_pad < height && w_pad >= 0 && w_pad < width)  
  20.           data_im[(c_im * height + h_pad) * width + w_pad] +=  
  21.               data_col[(c * height_col + h) * width_col + w];  
  22.       }  
  23.     }  
  24.   }  
  25. }  


上面介绍了二维卷积,那么我们就乘热打铁,再看看n维通用卷积是如何实现的
接下来介绍n维通用的卷积的具体实现
n维卷积的实现与二维卷积的实现很类似,只不过对应的变量进行了变化,你只需要找到对应就可以很快理解

d_offset 对应于im2col中的h_offset和w_offset是一个输入图像的channel 乘以kernel_size大小的图像块的偏移量(kernel_size下面的代码有定义)
d_iter对应于im2col中内层for循环的h和w,是经过im2colnd处理过的col_buff中的偏移
d_pad对应于im2col中内层for循环的h_pad和w_pad,是输入的原始图像中的偏移

作者还将im2colnd和col2imnd合并到一起实现了,通过const bool im2col来判断是im2col还是col2im


[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. // n维通用im2col以及col2im的实现  
  2. // 作者两个功能一起实现了  
  3. template <typename Dtype>  
  4. inline void im2col_nd_core_cpu(const Dtype* data_input, const bool im2col,  
  5.     const int num_spatial_axes, const int* im_shape, const int* col_shape,  
  6.     const int* kernel_shape, const int* pad, const int* stride,  
  7.     Dtype* data_output) {  
  8.   
  9.   // 如果不是im2col则表明是col2im,也就是说data_output是需要输出的原始图像大小的数据  
  10.   if (!im2col) {  
  11.     int im_size = im_shape[0];  
  12.     for (int i = 0; i < num_spatial_axes; ++i) {  
  13.       im_size *= im_shape[1 + i];  
  14.     }  
  15.     caffe_set(im_size, Dtype(0), data_output);  
  16.   }  
  17.   
  18.   // 一个kernel大小的块有多大  
  19.   int kernel_size = 1;  
  20.   for (int i = 0; i < num_spatial_axes; ++i) {  
  21.     kernel_size *= kernel_shape[i];  
  22.   }  
  23.   
  24.   // channels_col = inputchannel(输入图像的channel)*kernel_size  
  25.   const int channels_col = col_shape[0];  
  26.   
  27.   // 类似于im2col中的w_offset和h_offset,只不过因为这里是n维,所以用数组表示  
  28.   vector<int> d_offset(num_spatial_axes, 0);  
  29.   
  30.   // 类似于im2col中w和h,是col_buff中的偏移  
  31.   vector<int> d_iter(num_spatial_axes, 0);  
  32.   
  33.   
  34.   for (int c = 0; c < channels_col; ++c) {  
  35.   
  36.     // Loop over spatial axes in reverse order to compute a per-axis offset.  
  37.     // 计算n维kernel上的offset,与im2col中对应的代码一样的道理  
  38.     // 只不过这里是n维了,所以用d_offset来表示  
  39.     // 注意,这里用逆序来进行计算得到每个轴的偏移  
  40.     int offset = c;  
  41.     for (int d_i = num_spatial_axes - 1; d_i >= 0; --d_i) {  
  42.       if (d_i < num_spatial_axes - 1) {  
  43.         offset /= kernel_shape[d_i + 1];  
  44.       }  
  45.       d_offset[d_i] = offset % kernel_shape[d_i];  
  46.     }  
  47.   
  48.   
  49.     for (bool incremented = true; incremented; ) {  
  50.       // Loop over spatial axes in forward order to compute the indices in the  
  51.       // image and column, and whether the index lies in the padding.  
  52.       // 是经过im2colnd变换之后的索引  
  53.       int index_col = c;  
  54.   
  55.       // index_im是原始图像中的channel  
  56.       // c = channel * kernel_size  
  57.       int index_im = c / kernel_size;  
  58.   
  59.   
  60.       bool is_padding = false;  
  61.       for (int d_i = 0; d_i < num_spatial_axes; ++d_i) {  
  62.   
  63.         // d是col_buff上的偏移,与d_pad相对(d_pad是原始图像上的偏移)  
  64.         const int d = d_iter[d_i];  
  65.   
  66.         // 在d_pad是经过pad之后的col_buff中的坐标经过转换成原图中的坐标  
  67.         const int d_pad = d * stride[d_i] - pad[d_i] + d_offset[d_i];  
  68.   
  69.         // 判断经过im2colnd处理的图像上的像素是否位于输入的n维图像的上的pad的那个部分  
  70.         is_padding |= d_pad < 0 || d_pad >= im_shape[d_i + 1];  
  71.   
  72.         // 计算位于col_buff中的位置(就是经过im2colnd变换之后的)  
  73.         index_col *= col_shape[d_i + 1];  
  74.         index_col += d;  
  75.   
  76.         // 计算位于原始图像中的位置  
  77.         index_im *= im_shape[d_i + 1];  
  78.         index_im += d_pad;  
  79.       }  
  80.       if (im2col) {  
  81.         if (is_padding) {// 如果是位于pad的部分则设置为0  
  82.           data_output[index_col] = 0;  
  83.         } else {  
  84.           data_output[index_col] = data_input[index_im];  
  85.         }  
  86.       } else if (!is_padding) {  // col2im  
  87.         data_output[index_im] += data_input[index_col];  
  88.       }  
  89.   
  90.       // 更新位于col_buff上的偏移d(d_iter就是所有的d存进去的)  
  91.       // Loop over spatial axes in reverse order to choose an index,  
  92.       // like counting.  
  93.       incremented = false;  
  94.       for (int d_i = num_spatial_axes - 1; d_i >= 0; --d_i) {  
  95.         const int d_max = col_shape[d_i + 1];  
  96.         DCHECK_LT(d_iter[d_i], d_max);  
  97.         if (d_iter[d_i] == d_max - 1) {  
  98.           d_iter[d_i] = 0;  
  99.         } else {  // d_iter[d_i] < d_max - 1  
  100.           ++d_iter[d_i];  
  101.           incremented = true;  
  102.           break;  
  103.         }  
  104.       }  
  105.     }  // while(incremented) {  
  106.   }  // for (int c = 0; c < channels_col; ++c) {  
  107. }  



给出包裹im2col_nd_core_cpu 的im2col_nd_cpu 函数
kIm2Col=true,输入是data_im,输出是data_col
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. // im2col_nd_cpu只是将kIm2Col=true然后调用im2col_nd_core_cpu  
  2. template <typename Dtype>  
  3. void im2col_nd_cpu(const Dtype* data_im, const int num_spatial_axes,  
  4.     const int* im_shape, const int* col_shape,  
  5.     const int* kernel_shape, const int* pad, const int* stride,  
  6.     Dtype* data_col) {  
  7.   const bool kIm2Col = true;  
  8.   im2col_nd_core_cpu(data_im, kIm2Col, num_spatial_axes, im_shape, col_shape,  
  9.                   kernel_shape, pad, stride, data_col);  
  10. }  



给出包裹im2col_nd_core_cpu的col2im_nd_cpu函数
一个德行,只不过kIm2Col = false了,此外输入的书data_col而输出的是data_im
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template <typename Dtype>  
  2. void col2im_nd_cpu(const Dtype* data_col, const int num_spatial_axes,  
  3.     const int* im_shape, const int* col_shape,  
  4.     const int* kernel_shape, const int* pad, const int* stride,  
  5.     Dtype* data_im) {  
  6.   const bool kIm2Col = false;  
  7.   im2col_nd_core_cpu(data_col, kIm2Col, num_spatial_axes, im_shape, col_shape,  
  8.                      kernel_shape, pad, stride, data_im);  
  9. }  




还有几个显式的声明,定义float和double类型的函数,下面一并贴出
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. // 显式声明float和double类型的im2col_cpu  
  2. template void im2col_cpu<float>(const float* data_im, const int channels,  
  3.     const int height, const int width, const int kernel_h, const int kernel_w,  
  4.     const int pad_h, const int pad_w, const int stride_h,  
  5.     const int stride_w, float* data_col);  
  6. template void im2col_cpu<double>(const double* data_im, const int channels,  
  7.     const int height, const int width, const int kernel_h, const int kernel_w,  
  8.     const int pad_h, const int pad_w, const int stride_h,  
  9.     const int stride_w, double* data_col);  




// 显式声明float和double类型的im2col_nd_cpu
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template void im2col_nd_cpu<float>(const float* data_im,  
  2.     const int num_spatial_axes,  
  3.     const int* im_shape, const int* col_shape,  
  4.     const int* kernel_shape, const int* pad, const int* stride,  
  5.     float* data_col);  
  6. template void im2col_nd_cpu<double>(const double* data_im,  
  7.     const int num_spatial_axes,  
  8.     const int* im_shape, const int* col_shape,  
  9.     const int* kernel_shape, const int* pad, const int* stride,  
  10.     double* data_col);  




// 显式声明float和double类型的col2im_cpu
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template void col2im_cpu<float>(const float* data_col, const int channels,  
  2.     const int height, const int width, const int patch_h, const int patch_w,  
  3.     const int pad_h, const int pad_w, const int stride_h,  
  4.     const int stride_w, float* data_im);  
  5. template void col2im_cpu<double>(const double* data_col, const int channels,  
  6.     const int height, const int width, const int patch_h, const int patch_w,  
  7.     const int pad_h, const int pad_w, const int stride_h,  
  8.     const int stride_w, double* data_im);  



// 显式声明float和double类型的col2im_nd_cpu
[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. template void col2im_nd_cpu<float>(const float* data_col,  
  2.     const int num_spatial_axes,  
  3.     const int* im_shape, const int* col_shape,  
  4.     const int* kernel_shape, const int* pad, const int* stride,  
  5.     float* data_im);  
  6. template void col2im_nd_cpu<double>(const double* data_col,  
  7.     const int num_spatial_axes,  
  8.     const int* im_shape, const int* col_shape,  
  9.     const int* kernel_shape, const int* pad, const int* stride,  
  10.     double* data_im);  




三、总结

一开始看的时候,看懵了,没看懂im2col,后来查了下matlab的im2col,发现原来做了这么一件事情,就是给定一个大小的窗口之后,im2col会将窗口内的元素flat成一个列向量,而列向量的个数则是这个大小的窗口在图像上的滑动的次数(这个滑动的次数实际上也是经过卷积之后的图像中的像素个数,不知道你理解没:))。

窥一斑难见全豹,详细的注释请在http://download.csdn.net/detail/xizero00/9480181下载

参考:

[1] 别人写的一个博客介绍im2col这个函数具体是怎么实现的,但是我觉得没写明白
http://blog.csdn.net/ayst123/article/details/43924151
[2] matlab中im2col的解释,这给了我启发
http://blog.163.com/liwei_ie/blog/static/20931113220143954730342/
4

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313182.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在.net core3.0中使用SignalR实现实时通信

最近用.net core3.0重构网站&#xff0c;老大想做个站内信功能&#xff0c;就是有些耗时的后台任务的结果需要推送给用户。一开始我想简单点&#xff0c;客户端每隔1分钟调用一下我的接口&#xff0c;看看是不是有新消息&#xff0c;有的话就告诉用户有新推送&#xff0c;但老大…

活动最后72小时:购书优惠劵,折后再折,赶紧来抢啊

1024程序员节当当网计算机图书每满100减50&#xff01;满200减100&#xff01;满300-150&#xff01;机械工业出版社华章公司联合当当网特意为【DotNET技术圈】用户申请了一批可与满减叠加使用的“满200减30”的图书优惠码&#xff0c;优惠码使用后相当于&#xff1a;400减230 …

C# 8 新特性 - using 声明

using语句块 尽管.NET Core运行时有垃圾收集器&#xff08;GC&#xff09;来负责内存清理工作&#xff0c;但是我们还是要自己确保当非托管资源不再使用的时候应该被清理掉。以前针对实现了IDisposable接口的对象&#xff0c;我们经常会使用using 语句块来这样做&#xff1a; 这…

.Net Core3.0依赖注入DI

构建ASP.NET Core应用程序的时候&#xff0c;依赖注入已成为了.NET Core的核心&#xff0c;这篇文章&#xff0c;我们理一理依赖注入的使用方法。不使用依赖注入首先&#xff0c;我们创建一个ASP.NET Core Mvc项目&#xff0c;定义个表达的爱服务接口&#xff0c;中国小伙类实现…

.Net轻量状态机Stateless

很多业务系统开发中&#xff0c;不可避免的会出现状态变化&#xff0c;通常采用的情形可能是使用工作流去完成&#xff0c;但是对于简单场景下&#xff0c;用工作流有点大财小用感觉&#xff0c;比如订单业务中&#xff0c;订单状态的变更&#xff0c;涉及到的状态量不是很多&a…

Asp.net Core全局异常监控和记录日志

前言系统异常监控可以说是重中之重&#xff0c;系统不可能一直运行良好&#xff0c;开发和运维也不可能24小时盯着系统&#xff0c;系统抛异常后我们应当在第一时间收到异常信息。在Asp.net Core里我使用拦截器和中间件两种方式来监控异常。全局异常监控的数据最好还是写入数据…

SiteServer CMS 新版本 V6.13(2019年11月1日发布)

欢迎来到 SiteServer CMS V6.13 版本&#xff0c;经过两个月的连续迭代开发&#xff0c;V6.13版本新增了几项重要功能&#xff0c;我们希望你会喜欢&#xff0c;一些关键的亮点包括&#xff1a;。新增功能及BUG 修复经过两个月的连续迭代开发&#xff0c;V6.13 版本新增了部分功…

CUDA的global内存访问的问题

http://blog.csdn.net/OpenHero/article/details/3520578 关于CUDA的global内存访问的问题&#xff0c;怎么是访问的冲突&#xff0c;怎样才能更好的访问内存&#xff0c;达到更高的速度。下面先看几张图&#xff0c;这些图都是CUDA编程手册上的图&#xff0c;然后分别对这些…

C# 8 新特性 - 异步流 Asynchronous Streams

异步流 Asynchronous Streams例子 这是一个很简单的控制台程序。它有一个NumberFactory&#xff0c;它可以根据传递的参数来产生一串数字&#xff08;IEnumerable<int>&#xff09;。然后在这个程序中把每个数字都打印出来&#xff0c;同时在前边显示出当前的线程ID。 这…

__syncthreads()

http://www.cnblogs.com/dwdxdy/p/3215136.html __syncthreads()是cuda的内建函数&#xff0c;用于块内线程通信. __syncthreads() is you garden variety thread barrier. Any thread reaching the barrier waits until all of the other threads in that block also reach i…

互联网50周年!这有它的一张“出生证明”

2019 年 10 月 29 日是互联网的 50 周年&#xff0c;50 年前(1969 年 10 月 29 日)&#xff0c;加州大学洛杉矶分校的计算机将一个只有两个字母(LO)的数据包发送到斯坦福研究所的计算机上&#xff0c;这是互联网史上的第一个数据包&#xff0c;从此开启互联网时代的第一步。 当…

Eltwise_layer简介

http://www.voidcn.com/blog/thy_2014/article/p-6117416.html common_layer&#xff1a; ArgMaxLayer类&#xff1b; ConcatLayer类&#xff1a; EltwiseLayer类&#xff1b; FlattenLayer类&#xff1b; InnerProductLayer类&#xff1b; MVNLayer类&#xff1b; SilenceLaye…

PowerBI 秒级实时大屏展示方案 全面助力双十一

双十一来了&#xff0c;你准备好了吗&#xff1f;不管你是否准备完毕&#xff0c;我们带来了全网首发的 PowerBI 秒级实时大屏展示方案&#xff0c;你可以直接用来展示双十一的实时状况。我们一步步来说明这个套件模板教程。真实效果功能如下&#xff1a;全实时展示 双十一 当天…

优化 .net core 应用的 dockerfile

优化 .net core 应用的 dockerfileIntro在给 .net core 应用的写 dockerfile 的时候一直有个苦恼&#xff0c;就是如果有很多个项目&#xff0c;在 dockerfile 里写起来就会很繁琐&#xff0c;有很多项目文件要 copy&#xff0c;dockerfile 还不支持直接批量复制项目文件&#…

C# 8 新特性 - 静态本地方法

从C# 8 开始&#xff0c;本地方法就可以是静态的了。 与其他的本地方法不同&#xff0c;静态的本地方法无法捕获任何本地状态量。 直接看例子&#xff1a; 这段代码里有两个本地方法&#xff0c;他们分别对实例的一个字段和方法里的一个本地变量进行了修改操作&#xff0c;也就…

​.NET手撸2048小游戏

前言2048是一款益智小游戏&#xff0c;得益于其规则简单&#xff0c;又和 2的倍数有关&#xff0c;因此广为人知&#xff0c;特别是广受程序员的喜爱。本文将再次使用我自制的“准游戏引擎” FlysEngine&#xff0c;从空白窗口开始&#xff0c;演示如何“手撸” 2048小游戏&…

自行实现高性能MVC

wcf虽然功能多、扩展性强但是也面临配置忒多&#xff0c;而且restful的功能相当怪异&#xff0c;并且目前没法移植。asp.net core虽然支持webapi&#xff0c;但是功能也相对繁多、配置复杂。就没有一个能让码农们安安心心的写webapi&#xff0c;无需考虑性能、配置、甚至根据问…

caffe matio问题

http://blog.csdn.net/houqiqi/article/details/46469981 注&#xff1a;如果指令行模式实在解决不了/lib/libcaffe.so: undefined reference to Mat_VarReadDataLinear问题&#xff0c;可以尝试在QT下进行训练和测试。 1&#xff0c; 下载matio(http://sourceforge.NET/pro…

技术管理者怎样跳出“泥潭”

近几年面试了不少新人&#xff0c;当问到职业规划时&#xff0c;大多都会说先积累技术&#xff0c;然后往架构师的方向发展。这可能是技术人的一个特质&#xff0c;喜欢跟机器相处&#xff0c;沉浸在代码之中&#xff0c;而不喜欢跟人打交道。现实的情况是&#xff0c;一些中小…

你或许以为你不需要领域驱动设计

作者&#xff1a;邹溪源&#xff0c;长沙资深互联网从业者&#xff0c;架构师社区合伙人&#xff01;一犹记得刚刚参加工作时&#xff0c;是地图厂商四维图新集团旗下的一家子公司&#xff0c;主要从事规划测绘相关软件研发的公司。当时我的项目是为勘测设计院提供相对应的应用…