簡介
CNN最經典的幾個層,這裏介紹一下卷積層。
這張圖總結的不太全面,但是基本表現出了卷積層的繼承關係。
BaseConvolutionLayer
其繼承自Layer,是一個卷積以及反捲積操作的基類,首先我們來看BaseConvolutionLayer的LayerSetUp函數
void BaseConvolutionLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
//首先這裏主要是在配置卷積kernel 的size,padding,stride以及inputs
ConvolutionParameter conv_param = this->layer_param_.convolution_param();
force_nd_im2col_ = conv_param.force_nd_im2col();
channel_axis_ = bottom[0]->CanonicalAxisIndex(conv_param.axis());
const int first_spatial_axis = channel_axis_ + 1;
const int num_axes = bottom[0]->num_axes();
num_spatial_axes_ = num_axes - first_spatial_axis;
CHECK_GE(num_spatial_axes_, 0);
vector<int> bottom_dim_blob_shape(1, num_spatial_axes_ + 1);
vector<int> spatial_dim_blob_shape(1, std::max(num_spatial_axes_, 1));
// 設置kernel的dimensions
kernel_shape_.Reshape(spatial_dim_blob_shape);
int* kernel_shape_data = kernel_shape_.mutable_cpu_data();
接着是設置相應的stride dimensions,對於2D,設置在h和w方向上的stride,代碼太長列出簡要的
pad_.Reshape(spatial_dim_blob_shape);
int* pad_data = pad_.mutable_cpu_data();
pad_data[0] = conv_param.pad_h();
pad_data[1] = conv_param.pad_w();
......一堆if else判斷
對於kernel的pad也做相應設置
pad_.Reshape(spatial_dim_blob_shape);
int* pad_data = pad_.mutable_cpu_data();
pad_data[0] = conv_param.pad_h();
pad_data[1] = conv_param.pad_w();
接下來是對weights 和bias設置和填充,其中blob[0]裏面存放的是filter weights,而blob[1]裏面存放的是biases,當然biases是可選的,也可以沒有
//設置相應的shape,並檢查
vector<int> weight_shape(2);
weight_shape[0] = conv_out_channels_;
weight_shape[1] = conv_in_channels_ / group_;
bias_term_ = this->layer_param_.convolution_param().bias_term();
vector<int> bias_shape(bias_term_, num_output_);
//填充權重
this->blobs_[0].reset(new Blob<Dtype>(weight_shape));
shared_ptr<Filler<Dtype> > weight_filler(GetFiller<Dtype>(
this->layer_param_.convolution_param().weight_filler()));
weight_filler->Fill(this->blobs_[0].get());
//填充偏置項
if (bias_term_) {
this->blobs_[1].reset(new Blob<Dtype>(bias_shape));
shared_ptr<Filler<Dtype> > bias_filler(GetFiller<Dtype>(
this->layer_param_.convolution_param().bias_filler()));
bias_filler->Fill(this->blobs_[1].get());
}
ConvolutionLayer
ConvolutionLayer繼承了BaseConvolutionLayer,主要作用就是將一副image做卷積操作,使用學到的filter的參數和biaes。同時在Caffe裏面,卷積操作做了優化,變成了一個矩陣相乘的操作。其中有兩個比較主要的函數是im2col以及col2im。
圖中上半部分是一個傳統卷積,下圖是一個矩陣相乘的版本
下圖是在一個卷積層中將卷積操作展開的具體操作過程,他裏面按照卷積核的大小取數據然後展開,在同一張圖裏的不同卷積核選取的逐行擺放,不同N的話,就在同一行後面繼續拼接,不同個可以是多個通道,但是需要注意的是同一行裏面每一段都應該對應的是原圖中中一個位置的卷積窗口。
對於卷積層中的卷積操作,還有一個group的概念要說明一下,groups是代表filter 組的個數。引入gruop主要是爲了選擇性的連接卷積層的輸入端和輸出端的channels,否則參數會太多。每一個group 和1/ group的input 通道和 1/group 的output通道進行卷積操作。比如有4個input, 8個output,那麼1-4屬於第一組,5-8屬於第二個gruop。
ConvolutionLayer裏面,主要重寫了Forward_cpu和Backward_cpu
void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
const Dtype* weight = this->blobs_[0]->cpu_data();
for (int i = 0; i < bottom.size(); ++i) {
const Dtype* bottom_data = bottom[i]->cpu_data();
Dtype* top_data = top[i]->mutable_cpu_data();
for (int n = 0; n < this->num_; ++n) {
this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight,
top_data + n * this->top_dim_);
if (this->bias_term_) {
const Dtype* bias = this->blobs_[1]->cpu_data();
this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
}
}
}
}
可以看到其實這裏面他調用了forward_cpu_gemm,而這個函數內部又調用了math_function裏面的caffe_cpu_gemm的通用矩陣相乘接口,GEMM的全稱是General Matrix Matrix Multiply。其基本形式如下:
注意,其中this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight,
在循環裏,
top_data + n * this->top_dim_);this->num_
指的是batch的個數,bottom_data + n * this->bottom_dim_
也就是跳到第i個圖來做矩陣運算。
反向傳播計算過程是殘差與卷積模板的轉置做卷積。因此代碼類似。
反向傳播代碼如下。
template <typename Dtype>
void ConvolutionLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
//反向傳播梯度誤差
const Dtype* weight = this->blobs_[0]->cpu_data();
Dtype* weight_diff = this->blobs_[0]->mutable_cpu_diff();
for (int i = 0; i < top.size(); ++i) {
const Dtype* top_diff = top[i]->cpu_diff();
const Dtype* bottom_data = bottom[i]->cpu_data();
Dtype* bottom_diff = bottom[i]->mutable_cpu_diff();
//如果有bias項,計算Bias導數
if (this->bias_term_ && this->param_propagate_down_[1]) {
Dtype* bias_diff = this->blobs_[1]->mutable_cpu_diff();
for (int n = 0; n < this->num_; ++n) {
this->backward_cpu_bias(bias_diff, top_diff + n * this->top_dim_);
}
}
//計算weight
if (this->param_propagate_down_[0] || propagate_down[i]) {
for (int n = 0; n < this->num_; ++n) {
// 計算weights權重的梯度
if (this->param_propagate_down_[0]) {
this->weight_cpu_gemm(bottom_data + n * this->bottom_dim_,
top_diff + n * this->top_dim_, weight_diff);
}
//計算botttom數據的梯度,下後傳遞
if (propagate_down[i]) {
this->backward_cpu_gemm(top_diff + n * this->top_dim_, weight,
bottom_diff + n * this->bottom_dim_);
}
}
}
}
}
CuDNNConvolutionLayer
直接調用了cudnnConvolutionForward來計算前向過程。
template <typename Dtype>
void CuDNNConvolutionLayer<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();
// Forward through cuDNN in parallel over groups.
for (int g = 0; g < this->group_; g++) {
// Filters.
CUDNN_CHECK(cudnnConvolutionForward(handle_[g],
cudnn::dataType<Dtype>::one,
bottom_descs_[i], bottom_data + bottom_offset_ * g,
filter_desc_, weight + this->weight_offset_ * g,
conv_descs_[i],
fwd_algo_[i], workspace[g], workspace_fwd_sizes_[i],
cudnn::dataType<Dtype>::zero,
top_descs_[i], top_data + top_offset_ * g));
// Bias.
if (this->bias_term_) {
const Dtype* bias_data = this->blobs_[1]->gpu_data();
CUDNN_CHECK(cudnnAddTensor(handle_[g],
cudnn::dataType<Dtype>::one,
bias_desc_, bias_data + bias_offset_ * g,
cudnn::dataType<Dtype>::one,
top_descs_[i], top_data + top_offset_ * g));
}
}
// Synchronize the work across groups, each of which went into its own
// stream, by launching an empty kernel into the default (null) stream.
// NOLINT_NEXT_LINE(whitespace/operators)
sync_conv_groups<<<1, 1>>>();
}
}
反向過程類似,略過。