1. 程式人生 > >深度學習筆記4:卷積層的實現

深度學習筆記4:卷積層的實現

卷積層的推導

卷積層的前向計算

如下圖,卷積層的輸入來源於輸入層或者pooling層。每一層的多個卷積核大小相同,在這個網路中,我使用的卷積核均為5*5。


如圖輸入為28*28的影象,經過5*5的卷積之後,得到一個(28-5+1)*(28-5+1) = 24*24、的map。卷積層2的每個map是不同卷積核在前一層每個map上進行卷積,並將每個對應位置上的值相加然後再加上一個偏置項。


每次用卷積核與map中對應元素相乘,然後移動卷積核進行下一個神經元的計算。如圖中矩陣C的第一行第一列的元素2,就是卷積核在輸入map左上角時的計算結果。在圖中也很容易看到,輸入為一個4*4的map,經過2*2的卷積核卷積之後,結果為一個(4-2+1) *(4-2+1) = 3*3的map。

卷積層的後向計算

之前的筆記中我有寫到:在反向傳播過程中,若第x層的a節點通過權值W對x+1層的b節點有貢獻,則在反向傳播過程中,梯度通過權值W從b節點傳播回a節點。不管下面的公式推導,還是後面的卷積神經網路,在反向傳播的過程中,都是遵循這樣的一個規律。

卷積層的反向傳播過程也是如此,我們只需要找出卷積層L中的每個單元和L+1層中的哪些單元相關聯即可。我們還用上邊的圖片舉例子。

在上圖中,我們的矩陣A11通過權重B11與C11關聯。而A12與2個矩陣C中2個元素相關聯,分別是通過權重B12和C11關聯,和通過權重B11和C12相關聯。矩陣A中其他元素也類似。

那麼,我們有沒有簡單的方法來實現這樣的關聯呢。答案是有的。可以通過將卷積核旋轉180度,再與擴充後的梯度矩陣進行卷積。擴充的過程如下:如果卷積核為k*k,待卷積矩陣為n*n,需要以n*n原矩陣為中心擴充套件到(n+2(k-1))*(n+2(k-1))。具體過程如下:

假設D為反向傳播到卷積層的梯度矩陣,則D應該與矩陣C的大小相等,在這裡為3*3。我們首先需要將它擴充到(3+2*(2-1))* (3+2*(2-1)) = 5*5大小的矩陣,


同時將卷積核B旋轉180度:


將旋轉後的卷積核與擴充後的梯度矩陣進行卷積:


Caffe中卷積層的實現

在caffe的配置檔案中,我們的網路定義了2個卷積層,下面是第二個卷積層的配置資訊:

layer {
  name: "conv2"
  type: "Convolution"
  bottom: "pool1"
  top: "conv2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 50
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

我們可以看到,該層的型別為Convolution,即卷積層,bottom表示上一層為pool1,是一個池化層,top表示該層的輸出為conv2,即本層卷積層的輸出。

lr_mult是2個學習速率,這個在權值更新部分在說。

接下來可以看到num_output表示該層有50個輸出map,kernel_size卷積核大小為5*5,stride表示卷積步長為1,weight_filler表示權值初始化方式, 預設為“constant",值全為0,很多時候我們也可以用"xavier"或者”gaussian"來進行初始化。bias_filler表示偏置值的初始化方式,該引數的值和weight_filler類似,一般設定為"constant",值全為0。


前向過程

在看程式碼前,我們先看一下caffe中卷積的實現。

下面是一張論文中的圖片,看這張圖片可以很清楚理解。從圖中可以看出,卷積之前將輸入的多個矩陣和多個卷積核先展開再組合成2個大的矩陣,用展開後的矩陣相乘。


假設我們一次訓練16張圖片(即batch_size為16)。通過之前的推導,我們知道該層的輸入為20個12*12的特徵圖,所以bottom的維度16*20*12*12,則該層的輸出top的維度為16*50*8*8。

下面我們來看一下caffe中對於卷積層的實現程式碼。在caffe中,GPU上的卷積層對應的檔案為\src\caffe\layers\conv_layer.cu

我們先看一下前向過程的程式碼:

void ConvolutionLayer<Dtype>::Forward_gpu(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top) {
  const Dtype* weight = this->blobs_[0]->gpu_data();
  for (int i = 0; i < bottom.size(); ++i) {
    const Dtype* bottom_data = bottom[i]->gpu_data();
    Dtype* top_data = top[i]->mutable_gpu_data();
    for (int n = 0; n < this->num_; ++n) {
	//bottom_data為上一層傳入的資料,與weight作卷積,結果儲存到top_data中
      this->forward_gpu_gemm(bottom_data + n * this->bottom_dim_, weight,
          top_data + n * this->top_dim_);
      if (this->bias_term_) {
	  //加上偏置值
        const Dtype* bias = this->blobs_[1]->gpu_data();
        this->forward_gpu_bias(top_data + n * this->top_dim_, bias);
      }
    }
  }
}

其中,卷積的運算用到了這個函式forward_gpu_gemm(),我們展開看一下這個函式的程式碼:

void BaseConvolutionLayer<Dtype>::forward_gpu_gemm(const Dtype* input,
       const Dtype* weights, Dtype* output, bool skip_im2col) {
    const Dtype* col_buff = input;
    //若為1x1,不進行卷積操作
    if (!is_1x1_) {
		if (!skip_im2col) {
                //將輸入矩陣展開
                conv_im2col_gpu(input, col_buffer_.mutable_gpu_data());
              }
        col_buff = col_buffer_.gpu_data();
    }
    //對weights與col_buffer作卷積,卷積的結果放入output
    caffe_gpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ /
            group_, conv_out_spatial_dim_, kernel_dim_,
            (Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g,
            (Dtype)0., output + output_offset_ * g);
        }
}

反向傳播

Caffe中反向傳播的程式碼如下:

void ConvolutionLayer<Dtype>::Backward_gpu(const vector<Blob<Dtype>*>& top,
      const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
  const Dtype* weight = this->blobs_[0]->gpu_data();
  Dtype* weight_diff = this->blobs_[0]->mutable_gpu_diff();
  for (int i = 0; i < top.size(); ++i) {
    const Dtype* top_diff = top[i]->gpu_diff();
    // Bias gradient, if necessary.
    if (this->bias_term_ && this->param_propagate_down_[1]) {
      Dtype* bias_diff = this->blobs_[1]->mutable_gpu_diff();
      for (int n = 0; n < this->num_; ++n) {
		  //對一個batch中每一個map,計算其偏置的偏導
        this->backward_gpu_bias(bias_diff, top_diff + n * this->top_dim_);
      }
    }
    if (this->param_propagate_down_[0] || propagate_down[i]) {
      const Dtype* bottom_data = bottom[i]->gpu_data();
      Dtype* bottom_diff = bottom[i]->mutable_gpu_diff();
      for (int n = 0; n < this->num_; ++n) {
        // gradient w.r.t. weight. Note that we will accumulate diffs.
        if (this->param_propagate_down_[0]) {
          this->weight_gpu_gemm(bottom_data + n * this->bottom_dim_,
              top_diff + n * this->top_dim_, weight_diff);
        }
        // gradient w.r.t. bottom data, if necessary.
        if (propagate_down[i]) {
          this->backward_gpu_gemm(top_diff + n * this->top_dim_, weight,
              bottom_diff + n * this->bottom_dim_);
        }
      }
    }
  }
}