转载请注明出处:
https://www.cnblogs.com/darkknightzh/p/10486686.html
conv总体调用流程如下图所示:
说明:带o的为输出,如Wo代表输出宽度;带i的为输入,如Hi代表输入高度
1. 前向传播的计算ConvolutionLayer<Dtype>::Forward_cpu
注:不考虑反向传播的计算过程…
前向传播时,分别调用base_conv_layer.cpp中的BaseConvolutionLayer<Dtype>::forward_cpu_gemm和base_conv_layer.cpp中的BaseConvolutionLayer<Dtype>::forward_cpu_bias
1 template <typename Dtype> 2 void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom, 3 const vector<Blob<Dtype>*>& top) 4 { 5 const Dtype* weight = this->blobs_[0]->cpu_data(); // weight参数 6 for (int i = 0; i < bottom.size(); ++i) { // 多少个输入。一般1个的比较常见吧 7 const Dtype* bottom_data = bottom[i]->cpu_data(); // 第i个输入:NCHiWi 8 Dtype* top_data = top[i]->mutable_cpu_data(); // 第i个输出:NCHoWo 9 for (int n = 0; n < this->num_; ++n) { // batchsize 10 //forward_cpu_gemm输入为第n个channel的起始位置(C*Hi*Wi),及权重参数(No*Ni*Kh*Kw),输出为第n个channel的起始位置,(C*Ho*Wo) 11 this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight, top_data + n * this->top_dim_); 12 if (this->bias_term_) { // 含有bias 13 const Dtype* bias = this->blobs_[1]->cpu_data(); // bias参数 14 this->forward_cpu_bias(top_data + n * this->top_dim_, bias); // 计算增加bias后的输出 15 } 16 } 17 } 18 }
在forward之前,计算输出特征的尺寸函数为compute_output_shape
1 template <typename Dtype> 2 void ConvolutionLayer<Dtype>::compute_output_shape() { 3 const int* kernel_shape_data = this->kernel_shape_.cpu_data(); 4 const int* stride_data = this->stride_.cpu_data(); 5 const int* pad_data = this->pad_.cpu_data(); 6 const int* dilation_data = this->dilation_.cpu_data(); // 卷积核膨胀的宽高,默认为1;核膨胀,即在核中间加0 7 this->output_shape_.clear(); 8 for (int i = 0; i < this->num_spatial_axes_; ++i) { // HW总共维度,num_spatial_axes_=2 9 // i + 1 to skip channel axis 10 const int input_dim = this->input_shape(i + 1); //inline int input_shape(int i) {return (*bottom_shape_)[channel_axis_ + i];} 11 const int kernel_extent = dilation_data[i] * (kernel_shape_data[i] - 1) + 1; //得到膨胀之后的核的尺寸 12 const int output_dim = (input_dim + 2 * pad_data[i] - kernel_extent) / stride_data[i] + 1; //得到输出特征的尺寸 13 this->output_shape_.push_back(output_dim); // 输出特征宽高 14 } 15 }
2. forward_cpu_gemm
该函数首先判断是否为1*1的卷积,如果不是,则调用conv_im2col_cpu函数,将输入ChiWi变换成(C*Kh*Kw)*Ho*Wo的临时矩阵col_buffer_。
之后调用caffe_cpu_gemm,每次计算一部分输出,如果group_为1,则一次计算完:output(Co*(Ho*Wo))=1* weights(Co*(Ci*Kh*Kw))* col_buff((Ci*Kh*Kw)*(Ho*Wo)) + 0* output
1 template <typename Dtype> 2 void BaseConvolutionLayer<Dtype>::forward_cpu_gemm(const Dtype* input, 3 const Dtype* weights, Dtype* output, bool skip_im2col) { //bool skip_im2col = false 4 const Dtype* col_buff = input; 5 if (!is_1x1_) { // 不是1*1卷积 6 if (!skip_im2col) 7 { 8 // 调用base_conv_layer.hpp中的im2col_cpu,将输入CiHiWi变换成(Ci*Kh*Kw)*Ho*Wo的临时变量 9 // 由于调用本函数的函数ConvolutionLayer<Dtype>::Forward_cpu中调用batchsize次本函数,因而本函数内部不包含batchsize 10 conv_im2col_cpu(input, col_buffer_.mutable_cpu_data()); 11 } 12 col_buff = col_buffer_.cpu_data(); 13 } 14 for (int g = 0; g < group_; ++g) { // group_默认为1 15 caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ / group_, // Co 16 conv_out_spatial_dim_, kernel_dim_, // Ho*Wo // 卷积核的Ci*Kh*Kw 17 (Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g, 18 (Dtype)0., output + output_offset_ * g); 19 } 20 }
3. conv_im2col_cpu
该函数为内联函数,对im2col_cpu进行了封装,方便调用,如下:
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], 8 dilation_.cpu_data()[0], dilation_.cpu_data()[1], col_buff); 9 } 10 else { 11 im2col_nd_cpu(data, num_spatial_axes_, conv_input_shape_.cpu_data(), 12 col_buffer_shape_.data(), kernel_shape_.cpu_data(), 13 pad_.cpu_data(), stride_.cpu_data(), dilation_.cpu_data(), col_buff); 14 } 15 }
4. im2col_cpu
该函数用于将图像转换成卷积所需的列格式。a中黑色实线方框中为特征(或像素),虚线中为边界填充的0,红色虚线框为3*3的卷积核大小。如对于a所示的7*9输入图像(为方便b中的显示,因而a中值为1—63),四个边界各填充一个0后,通过该函数,得到的col格式如b所示,其中红色虚线为a中的位置对应的列格式的像素。b中…代表依次递增的5个特征。可以认为b中矩阵为一个kernel_h*kernel_w*output_h*output_w的行向量,也可以认为是一个(kernel_h*kernel_w)*(output_h*output_w)的2维的矩阵(每一行的长度为output_h*output_w)。通过这种方式得到的col格式数据,与卷积核可通过矩阵相乘,提高运算速度。
该函数代码如下。其中output_rows的for循环对应b中的蓝色箭头范围,output_col的for循环对应b中的橙色半框范围。
1 template <typename Dtype> 2 void im2col_cpu(const Dtype* data_im, const int channels, // 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, // 特征边界填充的宽高 5 const int stride_h, const int stride_w, // 间隔的宽高 6 const int dilation_h, const int dilation_w, // 卷积核膨胀的宽高,默认为1;核膨胀,即在核中间加0 // https://blog.csdn.net/wangyuxi__/article/details/83003357 7 Dtype* data_col) { // 为(kernel_h*kernel_w)*(output_h*output_w)的缓冲区。每一行为滑动窗口的某个位置对应的所有特征 8 const int output_h = (height + 2 * pad_h - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1; // 输出特征宽高 9 const int output_w = (width + 2 * pad_w - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1; 10 const int channel_size = height * width; // 输入特征的每个通道的总特征数 11 for (int channel = channels; channel--; data_im += channel_size) // 每次循环完毕,输入特征偏移一个通道 12 { 13 for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) 14 { 15 for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) 16 { 17 int input_row = -pad_h + kernel_row * dilation_h; // 每次核在特征上的起始行坐标 18 for (int output_rows = output_h; output_rows; output_rows--) // 遍历输入特征每行 19 { 20 if (!is_a_ge_zero_and_a_lt_b(input_row, height)) // a<0 或者 a>=b,即当前行超出输入边界 21 { 22 for (int output_cols = output_w; output_cols; output_cols--) // 每列填0 23 { 24 *(data_col++) = 0; 25 } 26 } 27 else { // 当前行在输入边界内 28 int input_col = -pad_w + kernel_col * dilation_w; // 每次核在特征上的起始列坐标 29 for (int output_col = output_w; output_col; output_col--) // 遍历输入特征每列 30 { 31 if (is_a_ge_zero_and_a_lt_b(input_col, width)) // 当前列在输入边界内 32 { 33 *(data_col++) = data_im[input_row * width + input_col]; // 将输入特征赋值给data_col 34 } 35 else // 当前列超出输入边界 36 { 37 *(data_col++) = 0; 38 } 39 input_col += stride_w; // 输入特征位置增加stride_w 40 } 41 } 42 input_row += stride_h; // 输入特征位置增加stride_h 43 } 44 } 45 } 46 } 47 }
5. BaseConvolutionLayer<Dtype>::forward_cpu_bias
该函数为output =1*bias(C*1)* bias_multiplier_(1*(H*W))+ 1*output。其中C为输出特征的通道数No,H为特征高Ho,W为特征宽Wo,最终得到某个batch中CoHoWo的特征。
1 template <typename Dtype> 2 void BaseConvolutionLayer<Dtype>::forward_cpu_bias(Dtype* output, 3 const Dtype* bias) { 4 caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_, //输出特征维度No 5 out_spatial_dim_, 1, (Dtype)1., bias, bias_multiplier_.cpu_data(), // Wo*Ho 6 (Dtype)1., output); 7 }
bias_multiplier_为1*(Wo*Ho)的向量,在void BaseConvolutionLayer<Dtype>::Reshape中将其所有的值均设置为1:
1 out_spatial_dim_ = top[0]->count(first_spatial_axis); // Wo*Ho 2 if (bias_term_) { 3 vector<int> bias_multiplier_shape(1, out_spatial_dim_); 4 bias_multiplier_.Reshape(bias_multiplier_shape); 5 caffe_set(bias_multiplier_.count(), Dtype(1), // bias_multiplier_为1*(Wo*Ho)的向量,所有元素值为1 6 bias_multiplier_.mutable_cpu_data()); 7 }
6. caffe_cpu_gemm
该函数调用cblas_sgemm,实现矩阵相乘:
1 template<> 2 void caffe_cpu_gemm<float>(const CBLAS_TRANSPOSE TransA, 3 const CBLAS_TRANSPOSE TransB, const int M, const int N, const int K, 4 const float alpha, const float* A, const float* B, const float beta, 5 float* C) { 6 int lda = (TransA == CblasNoTrans) ? K : M; 7 int ldb = (TransB == CblasNoTrans) ? N : K; 8 cblas_sgemm(CblasRowMajor, TransA, TransB, M, N, K, alpha, A, lda, B, 9 ldb, beta, C, N); 10 }
cblas_sgemm具体见:http://www.cnblogs.com/darkknightzh/p/5553336.html