caffe源碼閱讀《六》base_conv_layer

BaseConvolutionLayer是所有卷積層的基類。爲什麼卷積層還需要基類呢?因爲再caffe裏除了 ConvolutionLayer還有 DeconvolutionLayer

BaseConvolutionLayer 類

BaseConvolutionLayer 構造函數

  explicit BaseConvolutionLayer(const LayerParameter& param)
      : Layer<Dtype>(param) {}

因爲它是一個基類,所以這個構造函數就寫成了空的,傳入了 param這一個參數,但是什麼都沒做,傳一個參數把方向確定了,具體內容留給子類去實現。

LayerSetUp 函數

這個 LayerSetUp函數是很關鍵的,因爲它實現了,而且實現的非常長。

  virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top);

第一步首先加載我們這一個卷積的參數

  ConvolutionParameter conv_param = this->layer_param_.convolution_param();

這個參數是從 layer_param_這個變量裏來的,而這個變量裏的參數又是從哪裏來的呢?這就要從 Layer這個類說起了,因爲我們這個BaseConvolutionLayer是繼承的Layer這個類。

template <typename Dtype>
class BaseConvolutionLayer : public Layer<Dtype>

所以一切的變量也是定義在Layer這個類裏面的,我們去Layer這個類裏面找一下。

  /** The protobuf that stores the layer parameters */
  LayerParameter layer_param_;

它定義的這個 LayerParameter是在 proto文件裏

class LayerParameter : public ::google::protobuf::Message /* @@protoc_insertion_point(class_definition:caffe.LayerParameter) */ 

convolution_param函數把參數變量的指針返回回來,實際上就是把參數返回到了這個類裏面。

inline const ::caffe::ConvolutionParameter& LayerParameter::convolution_param() const {
  // @@protoc_insertion_point(field_get:caffe.LayerParameter.convolution_param)
  return convolution_param_ != NULL ? *convolution_param_ : *default_instance_->convolution_param_;
}

接着問一下是不是要強制把矩陣轉成caffe的矩陣形式,就是我們說的 im2col

  force_nd_im2col_ = conv_param.force_nd_im2col();

這個 im2col真的非常重要,我覺得這個是caffe計算部分的核心,建議大家一定要認真的學習這一塊。
接着獲取bottom的 channel_axis

  channel_axis_ = bottom[0]->CanonicalAxisIndex(conv_param.axis());

這一步不是獲取 channel的數量,而是返回 channelshape裏是第幾位,比方說 shape3224224,那麼 channel就在第0位,這一步返回值就是0.
所以真正空間形狀的開始就是從第1位開始的,接下來這個函數就是返回的空間形狀開始的那個axis

  const int first_spatial_axis = channel_axis_ + 1;

然後是總共的axis的數量

  const int num_axes = bottom[0]->num_axes();

接下來是 num_spatial_axes_,這個代表的就是真正空間的axis的數量,也就是排除了channel那一層的數量

  num_spatial_axes_ = num_axes - first_spatial_axis;

檢查一下 num_spatial_axes_是不是大於0的

  CHECK_GE(num_spatial_axes_, 0);

因爲如果不是大於0的就說明空間的維度不存在,這顯然是不可能的,就會直接拋出異常。
然後聲明瞭一個變量,專門去存空間的形狀。

  vector<int> spatial_dim_blob_shape(1, std::max(num_spatial_axes_, 1));

所以剛纔纔要了空間形狀的起始位置,因爲在這裏等着,準備拷貝空間形狀。
因爲卷積核的空間維度數量要和圖片是一樣的,所以要把卷積核 Reshape

  // Setup filter kernel dimensions (kernel_shape_).
  kernel_shape_.Reshape(spatial_dim_blob_shape);

注意啊,這裏是維度是一樣的,並不是形狀是一樣的,也就是說假設圖片是個2維圖片,那麼卷積核也就必須是2維的。假設圖片大小是224224,那麼卷積核是33就可以和它匹配。假設圖片是三維的,比方說224224224,那麼卷積核也就必須是三維的,比方說555.
接着準備對 kernel_shape_賦值了

int* kernel_shape_data = kernel_shape_.mutable_cpu_data();

我們知道 mutable_cpu_data存的是最新修改的數據的指針,所以這裏取了這個地址,就準備開始賦值。

  if (conv_param.has_kernel_h() || conv_param.has_kernel_w()) {
    CHECK_EQ(num_spatial_axes_, 2)
        << "kernel_h & kernel_w can only be used for 2D convolution.";
    CHECK_EQ(0, conv_param.kernel_size_size())
        << "Either kernel_size or kernel_h/w should be specified; not both.";
    kernel_shape_data[0] = conv_param.kernel_h();
    kernel_shape_data[1] = conv_param.kernel_w();
  } else {
    const int num_kernel_dims = conv_param.kernel_size_size();
    CHECK(num_kernel_dims == 1 || num_kernel_dims == num_spatial_axes_)
        << "kernel_size must be specified once, or once per spatial dimension "
        << "(kernel_size specified " << num_kernel_dims << " times; "
        << num_spatial_axes_ << " spatial dims).";
      for (int i = 0; i < num_spatial_axes_; ++i) {
        kernel_shape_data[i] =
            conv_param.kernel_size((num_kernel_dims == 1) ? 0 : i);
      }
  }

這裏判斷了,如果是二維的話,那麼就從參數裏面讀取高和寬,如果是多維的話,就一維一維的從參數裏面去讀取。
最後檢查一下空間的維度有沒有存在0

  for (int i = 0; i < num_spatial_axes_; ++i) {
    CHECK_GT(kernel_shape_data[i], 0) << "Filter dimensions must be nonzero.";
  }

這一步很好理解,你總不能有高或者寬是0的圖吧
接下來把 stride_這個blob也給它 Reshape一下,因爲這個 stride_ blob它是記錄步長的一個blob,所以它的維度也得和我們這個圖片是一致的。

  // Setup stride dimensions (stride_).
  stride_.Reshape(spatial_dim_blob_shape);
  int* stride_data = stride_.mutable_cpu_data();
  if (conv_param.has_stride_h() || conv_param.has_stride_w()) {
    CHECK_EQ(num_spatial_axes_, 2)
        << "stride_h & stride_w can only be used for 2D convolution.";
    CHECK_EQ(0, conv_param.stride_size())
        << "Either stride or stride_h/w should be specified; not both.";
    stride_data[0] = conv_param.stride_h();
    stride_data[1] = conv_param.stride_w();
  } else {
    const int num_stride_dims = conv_param.stride_size();
    CHECK(num_stride_dims == 0 || num_stride_dims == 1 ||
          num_stride_dims == num_spatial_axes_)
        << "stride must be specified once, or once per spatial dimension "
        << "(stride specified " << num_stride_dims << " times; "
        << num_spatial_axes_ << " spatial dims).";
    const int kDefaultStride = 1;
    for (int i = 0; i < num_spatial_axes_; ++i) {
      stride_data[i] = (num_stride_dims == 0) ? kDefaultStride :
          conv_param.stride((num_stride_dims == 1) ? 0 : i);
      CHECK_GT(stride_data[i], 0) << "Stride dimensions must be nonzero.";
    }
  }

和上面的卷積核是一樣的。
然後是 pad_,和 stride_一模一樣,也是給他形狀賦一個值。padding就是卷積邊界處理的長度。

  // Setup pad dimensions (pad_).
  pad_.Reshape(spatial_dim_blob_shape);
  int* pad_data = pad_.mutable_cpu_data();
  if (conv_param.has_pad_h() || conv_param.has_pad_w()) {
    CHECK_EQ(num_spatial_axes_, 2)
        << "pad_h & pad_w can only be used for 2D convolution.";
    CHECK_EQ(0, conv_param.pad_size())
        << "Either pad or pad_h/w should be specified; not both.";
    pad_data[0] = conv_param.pad_h();
    pad_data[1] = conv_param.pad_w();
  } else {
    const int num_pad_dims = conv_param.pad_size();
    CHECK(num_pad_dims == 0 || num_pad_dims == 1 ||
          num_pad_dims == num_spatial_axes_)
        << "pad must be specified once, or once per spatial dimension "
        << "(pad specified " << num_pad_dims << " times; "
        << num_spatial_axes_ << " spatial dims).";
    const int kDefaultPad = 0;
    for (int i = 0; i < num_spatial_axes_; ++i) {
      pad_data[i] = (num_pad_dims == 0) ? kDefaultPad :
          conv_param.pad((num_pad_dims == 1) ? 0 : i);
    }
  }

還有決定這個圖將被放大多少倍的 dilation_也是一樣的。

  // Setup dilation dimensions (dilation_).
  dilation_.Reshape(spatial_dim_blob_shape);
  int* dilation_data = dilation_.mutable_cpu_data();
  const int num_dilation_dims = conv_param.dilation_size();
  CHECK(num_dilation_dims == 0 || num_dilation_dims == 1 ||
        num_dilation_dims == num_spatial_axes_)
      << "dilation must be specified once, or once per spatial dimension "
      << "(dilation specified " << num_dilation_dims << " times; "
      << num_spatial_axes_ << " spatial dims).";
  const int kDefaultDilation = 1;
  for (int i = 0; i < num_spatial_axes_; ++i) {
    dilation_data[i] = (num_dilation_dims == 0) ? kDefaultDilation :
                       conv_param.dilation((num_dilation_dims == 1) ? 0 : i);
  }

接着就要開始轉成計算矩陣了。
首先判斷一下圖是不是11的,因爲如果是11的話,那麼這個計算就會省很多事情。

  // Special case: im2col is the identity for 1x1 convolution with stride 1
  // and no padding, so flag for skipping the buffer and transformation.
  is_1x1_ = true;
  for (int i = 0; i < num_spatial_axes_; ++i) {
    is_1x1_ &=
        kernel_shape_data[i] == 1 && stride_data[i] == 1 && pad_data[i] == 0;
    if (!is_1x1_) { break; }
  }

接着獲取 bottom層的通道數,也就是輸入的通道數,還有 output的數量,也就是輸出的通道數,還有 group的數量,分 group的目的是爲了指定哪些通道只能哪些卷積核做卷積,不相干的就不會卷積。

  // Configure output channels and groups.
  channels_ = bottom[0]->shape(channel_axis_);
  num_output_ = this->layer_param_.convolution_param().num_output();
  CHECK_GT(num_output_, 0);
  group_ = this->layer_param_.convolution_param().group();
  CHECK_EQ(channels_ % group_, 0);
  CHECK_EQ(num_output_ % group_, 0)
      << "Number of output should be multiples of group.";

因爲分組是均分的,所以判斷了一下 group的數量是不是能被 channel的數量整除。
之後判斷一下輸入和輸出順序是不是設定了需要反過來。

  if (reverse_dimensions()) {
    conv_out_channels_ = channels_;
    conv_in_channels_ = num_output_;
  } else {
    conv_out_channels_ = num_output_;
    conv_in_channels_ = channels_;
  }

如果是反捲積這種的話就要把輸入和輸出的channel給反過來了。
定義一下權值的形狀

  // Handle the parameters: weights and biases.
  // - blobs_[0] holds the filter weights
  // - blobs_[1] holds the biases (optional)
  vector<int> weight_shape(2);
  weight_shape[0] = conv_out_channels_;
  weight_shape[1] = conv_in_channels_ / group_;
  for (int i = 0; i < num_spatial_axes_; ++i) {
    weight_shape.push_back(kernel_shape_data[i]);
  }

然後是 bias,因爲這個 bias是可以選擇開關的,所以首先判斷了一下,如果開的話就定義一下這個偏置的形狀.

  bias_term_ = this->layer_param_.convolution_param().bias_term();
  vector<int> bias_shape(bias_term_, num_output_);

最後檢查 blobsize

  if (this->blobs_.size() > 0) {
    CHECK_EQ(1 + bias_term_, this->blobs_.size())
        << "Incorrect number of weight blobs.";
    if (weight_shape != this->blobs_[0]->shape()) {
      Blob<Dtype> weight_shaped_blob(weight_shape);
      LOG(FATAL) << "Incorrect weight shape: expected shape "
          << weight_shaped_blob.shape_string() << "; instead, shape was "
          << this->blobs_[0]->shape_string();
    }
    if (bias_term_ && bias_shape != this->blobs_[1]->shape()) {
      Blob<Dtype> bias_shaped_blob(bias_shape);
      LOG(FATAL) << "Incorrect bias shape: expected shape "
          << bias_shaped_blob.shape_string() << "; instead, shape was "
          << this->blobs_[1]->shape_string();
    }
    LOG(INFO) << "Skipping parameter initialization";
  } else {
    if (bias_term_) {
      this->blobs_.resize(2);
    } else {
      this->blobs_.resize(1);
    }

如果沒有開 bias的話,那麼 blob的size就是1,只存了權值,如果開了的話就是2,所以它和這個 1+bias_term做比較看看是否相等。其實這樣寫是個非常差勁的寫法,很具有迷惑性,但是結果肯定是對的,因爲布爾變量爲true就是1.
然後初始化權值和bias

    // Initialize and fill the weights:
    // output channels x input channels per-group x kernel height x kernel width
    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 necessary, initialize and fill the biases.
    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());
    }
  }

從文件中讀取權值,然後填給blob裏面。
接着獲取一下卷積核的總大小

  kernel_dim_ = this->blobs_[0]->count(1);

這個 count就是總的大小,比方說卷積核形狀是355,那麼 count的結果就是75,而加了個參數就是從第幾位開始,那麼 count(1)就是5*5=25
因爲第0位就是輸入的channel,這個和卷積核沒什麼關係,所以從1開始數,用來分配矩陣的大小。
接着根據分組情況找一下權值的指針偏移量,方便換組的時候知道往後數多少位

  weight_offset_ = conv_out_channels_ * kernel_dim_ / group_;

最後在 blob裏給反向傳播騰個位置就結束了

  // Propagate gradients to the parameters (as directed by backward pass).
  this->param_propagate_down_.resize(this->blobs_.size(), true);

Reshape 函數

  virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top);

Reshape函數傳入了兩個參數,一個 bottom一個 top
一開始還是獲取了 first_spatial_axis

  const int first_spatial_axis = channel_axis_ + 1;

然後開始檢查 spatial_axisbottomnum_axes是不是一致的。

  CHECK_EQ(bottom[0]->num_axes(), first_spatial_axis + num_spatial_axes_)
      << "bottom num_axes may not change.";

如果不一致的話他就會拋出異常,然後告訴你這個維度可能被改變過。
接下來獲取輸入的維度數量

  num_ = bottom[0]->count(0, channel_axis_);
  CHECK_EQ(bottom[0]->shape(channel_axis_), channels_)
      << "Input size incompatible with convolution kernel.";

然後寫了個循環遍歷所有的bottom,因爲這些bottom可能來自不同的blob,所以我們挨個去遍歷他。

  // TODO: generalize to handle inputs of different shapes.
  for (int bottom_id = 1; bottom_id < bottom.size(); ++bottom_id) {
    CHECK(bottom[0]->shape() == bottom[bottom_id]->shape())
        << "shape mismatch - bottom[0]: " << bottom[0]->shape_string()
        << " vs. bottom[" << bottom_id << "]: "
        << bottom[bottom_id]->shape_string();
  }

如果發現有某一個bottom blob形狀不一樣的話,它也會拋出異常,因爲這樣就沒法統一進行下去了。
然後這些檢查完畢之後,就獲取了bottom的形狀,然後去計算輸出的形狀。

  // Shape the tops.
  bottom_shape_ = &bottom[0]->shape();
  compute_output_shape();

它計算輸出的形狀是根據不同層使用了不同的算法的,所以這裏也沒有實現,只有一個虛函數放在這裏,我們可以來看一下。

  // Compute height_out_ and width_out_ from other parameters.
  virtual void compute_output_shape() = 0;

接着定義了top_shape這個vector,注意,這裏還是shapevector,不是top這個blobvector

  vector<int> top_shape(bottom[0]->shape().begin(),
      bottom[0]->shape().begin() + channel_axis_);

首先把輸出的個數賦值給它

  top_shape.push_back(num_output_);

然後把各個維度的大小寫個循環賦值

  for (int i = 0; i < num_spatial_axes_; ++i) {
    top_shape.push_back(output_shape_[i]);
  }

緊接着我們把傳進來的這個topblob挨個的reshape成我們想要的這個shape

  for (int top_id = 0; top_id < top.size(); ++top_id) {
    top[top_id]->Reshape(top_shape);
  }

同時這裏也是要判斷一下是不是要反過來,就是給反捲積用的,如果是的話,就要把輸出和輸入的位置交換

  if (reverse_dimensions()) {
    conv_out_spatial_dim_ = bottom[0]->count(first_spatial_axis);
  } else {
    conv_out_spatial_dim_ = top[0]->count(first_spatial_axis);
  }

接下來算出原圖的計算矩陣的空間大小

  col_offset_ = kernel_dim_ * conv_out_spatial_dim_;

因爲這個矩陣的行數就是kernel_dim_,而列數呢就是要卷積多少次,也就是conv_out_spatial_dim_,比如說輸出是3*3維的,那麼就有9列
這個col_offset_caffe_cpu_gemm中用到了,方便做矩陣運算的之後指針切換到下一張圖用的偏移量。
然後是output_offset_就是算出結果的輸出的矩陣,就是output的輸出的數量。

  output_offset_ = conv_out_channels_ * conv_out_spatial_dim_ / group_;

它最後還除了一個group就是假設有分組的情況下,它是一組一組的計算的,所以每次的偏移量都是這個組內的數量。
接着定義了一個輸入維度的vector

  // Setup input dimensions (conv_input_shape_).
  vector<int> bottom_dim_blob_shape(1, num_spatial_axes_ + 1);

這裏容易讓人產生誤解,其實它是一個維度爲1的vector,其實完全可以寫成一個變量的,他的值是num_spatial_axes_ + 1
爲什麼是+1呢?因爲這裏這個變量的作用是用來初始化形狀數組用的,形狀數組除了記錄了空間維度以外,還要記錄channel的數量,所以多留了一位存channel用的。這裏用一個vector表示而不是用一個int變量表示完全是爲了接口統一,方便下一步的處理。
下一步就開始reshape形狀的數組了。

  conv_input_shape_.Reshape(bottom_dim_blob_shape);

緊接着定義了一個指針指向了它,這樣做的目的是爲了方便下一步去用這個指針去修改它的值。

  int* conv_input_shape_data = conv_input_shape_.mutable_cpu_data();

接着把各個維度的真實大小一一賦值給它

  for (int i = 0; i < num_spatial_axes_ + 1; ++i) {
    if (reverse_dimensions()) {
      conv_input_shape_data[i] = top[0]->shape(channel_axis_ + i);
    } else {
      conv_input_shape_data[i] = bottom[0]->shape(channel_axis_ + i);
    }
  }

接着開始reshapeim2col結果的一個buffer

  // The im2col result buffer will only hold one image at a time to avoid
  // overly large memory usage. In the special case of 1x1 convolution
  // it goes lazily unused to save memory.
  col_buffer_shape_.clear();

這裏是im2col的結果,而不是我們最終矩陣計算後的結果,這裏其實就是得到一個計算矩陣用的。所以他的形狀就是kernel_dim_* group_

  col_buffer_shape_.push_back(kernel_dim_ * group_);
  for (int i = 0; i < num_spatial_axes_; ++i) {
    if (reverse_dimensions()) {
      col_buffer_shape_.push_back(input_shape(i + 1));
    } else {
      col_buffer_shape_.push_back(output_shape_[i]);
    }
  }
  col_buffer_.Reshape(col_buffer_shape_);

接着獲取它輸入的大小和輸出的大小

  bottom_dim_ = bottom[0]->count(channel_axis_);
  top_dim_ = top[0]->count(channel_axis_);

後面一步求了num_kernels_im2col_,它的算法是

num_kernels_im2col_ = conv_in_channels_ * conv_out_spatial_dim_;

這裏我沒有看懂是什麼意思,這兩個數字相乘讓人很迷,而且它實際的用途只有在im2col_nd_gpu這一個函數中才用到了,其他的都沒有用到,這個函數是什麼意思我也沒有搞明白。
後面還有一個它反向操作的變量

num_kernels_col2im_ = reverse_dimensions() ? top_dim_ : bottom_dim_;

接下來設置了一下輸出的空間大小

  // Set up the all ones "bias multiplier" for adding biases by BLAS
  out_spatial_dim_ = top[0]->count(first_spatial_axis);

如果設置了bias的話,我們就要reshape這個bias_multiplier_的大小。

  if (bias_term_) {
    vector<int> bias_multiplier_shape(1, out_spatial_dim_);
    bias_multiplier_.Reshape(bias_multiplier_shape);
    caffe_set(bias_multiplier_.count(), Dtype(1),
        bias_multiplier_.mutable_cpu_data());
  }

它最後使用的是caffe_set這個函數,把所有的bias先全部初始化爲1

MinBottomBlobs 函數

virtual inline int MinBottomBlobs() const { return 1; }

這個函數是個虛函數,也就是留給子類來實現的,也就是把BottomBlobs最小值返回回去

forward_cpu_gemm 函數

使用cpu的前傳函數

  // Helper functions that abstract away the column buffer and gemm arguments.
  // The last argument in forward_cpu_gemm is so that we can skip the im2col if
  // we just called weight_cpu_gemm with the same input.
  void forward_cpu_gemm(const Dtype* input, const Dtype* weights,
      Dtype* output, bool skip_im2col = false);

第一步先生成im2col的矩陣

  const Dtype* col_buff = input;
  if (!is_1x1_) {
    if (!skip_im2col) {
      conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
    }
    col_buff = col_buffer_.cpu_data();
  }

然後根據分組來進行卷積計算

  for (int g = 0; g < group_; ++g) {
    caffe_cpu_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);
  }

forward_cpu_bias 函數

計算偏移量,因爲caffe的前傳把計算權值和偏移量分開算了,主要就是方便矩陣計算的操作,沒有什麼難懂的地方。
它用的函數是一樣的,但是這次權值那個位置都設置的1,只計算bias

  caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,
      out_spatial_dim_, 1, (Dtype)1., bias, bias_multiplier_.cpu_data(),
      (Dtype)1., output);

backward_cpu_bias 函數

這個函數求出當前層bias的梯度
因爲y=w*x+b,那麼可以得知bias的梯度就是1乘以上一層傳下來的梯度

  caffe_cpu_gemv<Dtype>(CblasNoTrans, num_output_, out_spatial_dim_, 1.,
      input, bias_multiplier_.cpu_data(), 1., bias);

因爲我們bias_multiplier_設置的就是1,所以這個函數翻譯過來就是bias = bias *1 + input * bias_multiplier_,所以 bias = bias + input
這裏這個加號是什麼意思呢?因爲一個batch裏面是所有的圖加起來求得平均值,所以要先把這個batch裏的數據都先加起來,方便之後求平均。

weight_cpu_gemm

求權重的梯度
根據公式y=w*x+b,那麼可以得知weight的梯度就是x乘以上一層傳下來的梯度

  const Dtype* col_buff = input;
  if (!is_1x1_) {
    conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
    col_buff = col_buffer_.cpu_data();
  }
  for (int g = 0; g < group_; ++g) {
    caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, conv_out_channels_ / group_,
        kernel_dim_, conv_out_spatial_dim_,
        (Dtype)1., output + output_offset_ * g, col_buff + col_offset_ * g,
        (Dtype)1., weights + weight_offset_ * g);
  }

最終算出來的值就給了weights + weight_offset_ * g這個位置的數組

backward_cpu_gemm 函數

這個函數是由損失函數那裏傳來的這一層的梯度,這一層的梯度其實就是y對於x的求導
因爲y=wx+b,所以y對於x的求導其實就是w
那麼總的梯度就應該是上一層的梯度w

  Dtype* col_buff = col_buffer_.mutable_cpu_data();
  if (is_1x1_) {
    col_buff = input;
  }
  for (int g = 0; g < group_; ++g) {
    caffe_cpu_gemm<Dtype>(CblasTrans, CblasNoTrans, kernel_dim_,
        conv_out_spatial_dim_, conv_out_channels_ / group_,
        (Dtype)1., weights + weight_offset_ * g, output + output_offset_ * g,
        (Dtype)0., col_buff + col_offset_ * g);
  }
  if (!is_1x1_) {
    conv_col2im_cpu(col_buff, input);
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章