1. 程式人生 > >Caffe原始碼理解3:Layer基類與template method設計模式

Caffe原始碼理解3:Layer基類與template method設計模式

目錄

部落格:blog.shinelee.me | 部落格園 | CSDN

寫在前面

層的概念在深度神經網路中佔據核心位置,給定輸入,資料在層間運算流動,最終輸出結果。層定義了對資料如何操作,根據操作的不同,可以對層進行劃分(具體參見Caffe Layers):

  • Data Layers:跟據檔案型別和格式讀取和處理資料,給網路輸入
  • Vision Layers
    :輸入特徵圖輸出也是特徵圖,像卷積、池化等
  • Activation Layers:定義了逐元素的操作,輸入輸出shape相同,像ReLU、sigmoid等,
  • Loss Layers:比較網路最終輸出與目標的偏差,以縮小偏差為目的來驅動網路向目標學習,像Softmax with Loss等
  • Common Layers:全連線層、dropout等
  • Normalization Layers:歸一化層,像LRN、MVN、BN等
  • Utility Layers:特殊功能的層,像split、slice、concat等

注意,在Caffe中啟用是單獨的層,損失也是單獨的層。所有這些層,都從一個共同的基類Layer

繼承而來,Layer定義了這些類共有的行為和資料部分,這篇文章的重點就是介紹這個基類。

Layer採用了template method設計模式,因此先介紹template method。

template method設計模式

template method設計模式,即在父類中定義好流程的框架,而流程中的某些步驟在子類中具體實現。下面以開啟檔案為例(例子來自侯捷老師),所有客戶端軟體開啟檔案的流程都是類似的,如下圖所示,這個流程可以事先定義好,寫在SDK裡,但是,將來這個SDK要被用來開啟什麼型別的檔案是SDK的設計者無法完全預測的,因此具體某個型別的檔案該如何讀取應由SDK的使用者來編寫。

文件開啟流程

那麼,SDK設計者定義的流程如何在執行到檔案讀取步驟時使用“將來”SDK使用者編寫的程式?這就需要SDK的設計者將這個步驟設計為虛擬函式(關於虛擬函式可以檢視cppreference.com),將來SDK的使用者繼承這個類同時重寫對應的虛擬函式,這種實現方法就是template method設計模式,其呼叫順序如下圖所示。

template method檔案開啟呼叫流程

caffe中的基類Layer在設計時就採用了這種思想。

Layer 基類

Layer成員變數

先看一下Layer的成員變數,具體參看註釋。

LayerParameter layer_param_; // 將protobuf中定義的該層的超引數等物件化儲存
Phase phase_; // TRAIN or TEST,指示該層參與訓練還是測試
vector<shared_ptr<Blob<Dtype> > > blobs_; // 儲存可學習引數(權重)param blob
vector<bool> param_propagate_down_; // 指示每個param blob是否需要計算diff
vector<Dtype> loss_; // 儲存top blob在損失函式中的權重loss_weight(與top blob數量相同),在反向傳播時會作用在梯度上
// 對於損失層loss_weight預設為1(見LossLayer的LayerSetUp),其他層預設對損失函式沒有直接貢獻

層所擁有的是它的可學習引數部分,輸入輸出都不屬於層,因此輸入輸出blob並不是層的成員變數,而只出現在介面上層關注的是對資料的操作方式本身,這是設計時的考量。

構造與析構

構造與析構,Layer的子類不需要實現自己的建構函式,所有的set up操作應該在後面的SetUp函式中完成,建構函式中僅將納入LayerParameter、設定pahse_以及寫入初始網路權重(如果在protobuf檔案中指定了的話)。

explicit Layer(const LayerParameter& param)
  : layer_param_(param) {
    // Set phase and copy blobs (if there are any).
    phase_ = param.phase();
    if (layer_param_.blobs_size() > 0) {
      blobs_.resize(layer_param_.blobs_size());
      for (int i = 0; i < layer_param_.blobs_size(); ++i) {
        blobs_[i].reset(new Blob<Dtype>());
        blobs_[i]->FromProto(layer_param_.blobs(i));
      }
    }
  }
virtual ~Layer() {}

SetUp成員函式

SetUp是本文最為關注的成員函式,顧名思義,其負責完成層的基礎搭建工作。在Net初始化時會順序呼叫每個層的SetUp函式來搭建網路,見Net::InitNet::Init利用多型+template method在一個迴圈中完成所有層的搭建。

// in Net::Init
for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {
    // ……
    // After this layer is connected, set it up.
    layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);
    // ……
}

// in net.hpp
/// @brief Individual layers in the net
vector<shared_ptr<Layer<Dtype> > > layers_;

SetUp在設計時就採用了template method設計思想,基類Layer為所有派生類的SetUp定義好了流程框架,先檢查bottom和top的blob數量是否正確,然後呼叫LayerSetUp為完成層“個性化”的搭建工作(如卷積層會設定pad、stride等引數),再根據層自己定義的操作以及bottom的shape去計算top的shape,最後根據loss_weight設定top blob在損失函式中的權重。其中,Reshape為純虛擬函式,子類必須自己實現,CheckBlobCountsLayerSetUp為虛擬函式,提供了預設實現,子類也可以定義自己的實現。一般,SetUp的執行順序為:

  • 進入父類的SetUp函式
  • 執行父類的CheckBlobCounts,在這個函式中會執行子類的ExactNumBottomBlobs等函式
  • 執行子類的LayerSetUp
  • 執行子類的Reshape
  • 執行父類的SetLossWeights
  • 退出父類的SetUp函式
void SetUp(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {
  CheckBlobCounts(bottom, top);
  LayerSetUp(bottom, top);
  Reshape(bottom, top);
  SetLossWeights(top);
}
virtual void CheckBlobCounts(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {
    // 實現具體省略
    /* check that the number of bottom and top Blobs provided as input 
     match the expected numbers specified 
     by the {ExactNum,Min,Max}{Bottom,Top}Blobs() functions
    */
    }

/* This method should do one-time layer specific setup. This includes reading
* and processing relevent parameters from the <code>layer_param_</code>.
* Setting up the shapes of top blobs and internal buffers should be done in
* <code>Reshape</code>, which will be called before the forward pass to
* adjust the top blob sizes.
*/
virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {}

/* This method should reshape top blobs as needed according to the shapes
* of the bottom (input) blobs, as well as reshaping any internal buffers
* and making any other necessary adjustments so that the layer can
* accommodate the bottom blobs.
*/
virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) = 0;

/**
* Called by SetUp to initialize the weights associated with any top blobs in
* the loss function. Store non-zero loss weights in the diff blob.
*/
inline void SetLossWeights(const vector<Blob<Dtype>*>& top) {
  const int num_loss_weights = layer_param_.loss_weight_size();
  if (num_loss_weights) {
    CHECK_EQ(top.size(), num_loss_weights) << "loss_weight must be "
        "unspecified or specified once per top blob.";
    for (int top_id = 0; top_id < top.size(); ++top_id) {
      const Dtype loss_weight = layer_param_.loss_weight(top_id);
      if (loss_weight == Dtype(0)) { continue; }
      this->set_loss(top_id, loss_weight);
      const int count = top[top_id]->count();
      Dtype* loss_multiplier = top[top_id]->mutable_cpu_diff();
      caffe_set(count, loss_weight, loss_multiplier);
    }
  }
}

Layer在設計之初無法料想到今天會有如此多各種各樣的層,但是這些層只需要繼承基類Layer,同時定義好各自個性化的LayerSetUpReshape等函式,就可以將自己納入到SetUp的搭建流程,並通過Net::Init進一步納入整個網路的搭建中。

前向傳播與反向傳播

Layer為所有層定義了前向傳播與反向傳播的通用介面ForwardBackward,實際上,ForwardBackwardForward_cpuForward_gpuBackward_cpuBackward_gpu包裝器,子類需要定義自己的Forward_cpuForward_gpuBackward_cpuBackward_gpu,比如,卷積層前向傳播要通過卷積操作,池化層前向傳播時要通過池化操作,而不需要重寫ForwardBackward。此外,如果子類不定義自己的gpu函式,預設的gpu函式實際呼叫的是cpu函式,如下面程式碼所示,所以如果要使用GPU,必須要自己實現Forward_gpuBackward_gpu

public:
    inline Dtype Forward(const vector<Blob<Dtype>*>& bottom, 
        const vector<Blob<Dtype>*>& top);
    inline void Backward(const vector<Blob<Dtype>*>& top,
        const vector<bool>& propagate_down,
        const vector<Blob<Dtype>*>& bottom);
protected:
    virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
        const vector<Blob<Dtype>*>& top) = 0;
    virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
        const vector<Blob<Dtype>*>& top) {
      // LOG(WARNING) << "Using CPU code as backup.";
      return Forward_cpu(bottom, top);
    }
    virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
        const vector<bool>& propagate_down,
        const vector<Blob<Dtype>*>& bottom) = 0;
    virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
        const vector<bool>& propagate_down,
        const vector<Blob<Dtype>*>& bottom) {
      // LOG(WARNING) << "Using CPU code as backup.";
      Backward_cpu(top, propagate_down, bottom);
    }

在下面程式碼中,注意Forward中的loss_weight的來源以及損失的計算。

template <typename Dtype>
inline Dtype Layer<Dtype>::Forward(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {
  Dtype loss = 0;
  Reshape(bottom, top);
  switch (Caffe::mode()) {
  case Caffe::CPU:
    Forward_cpu(bottom, top);
    for (int top_id = 0; top_id < top.size(); ++top_id) {
      if (!this->loss(top_id)) { continue; }
      const int count = top[top_id]->count();
      const Dtype* data = top[top_id]->cpu_data();
      const Dtype* loss_weights = top[top_id]->cpu_diff(); // 在損失函式中的權重
      loss += caffe_cpu_dot(count, data, loss_weights);
    }
    break;
  case Caffe::GPU:
    Forward_gpu(bottom, top);
#ifndef CPU_ONLY
    for (int top_id = 0; top_id < top.size(); ++top_id) {
      if (!this->loss(top_id)) { continue; }
      const int count = top[top_id]->count();
      const Dtype* data = top[top_id]->gpu_data();
      const Dtype* loss_weights = top[top_id]->gpu_diff();
      Dtype blob_loss = 0;
      caffe_gpu_dot(count, data, loss_weights, &blob_loss);
      loss += blob_loss;
    }
#endif
    break;
  default:
    LOG(FATAL) << "Unknown caffe mode.";
  }
  return loss;
}

template <typename Dtype>
inline void Layer<Dtype>::Backward(const vector<Blob<Dtype>*>& top,
    const vector<bool>& propagate_down,
    const vector<Blob<Dtype>*>& bottom) {
  switch (Caffe::mode()) {
  case Caffe::CPU:
    Backward_cpu(top, propagate_down, bottom);
    break;
  case Caffe::GPU:
    Backward_gpu(top, propagate_down, bottom);
    break;
  default:
    LOG(FATAL) << "Unknown caffe mode.";
  }
}

其他成員函式

首先是成員變數的setget函式:

virtual inline const char* type() const { return ""; } // return the layer type
inline void SetPhase(Phase p) { phase_ = p;} 
vector<shared_ptr<Blob<Dtype> > >& blobs() { return blobs_;} 
vector<Blob<Dtype>*> GetBlobs(); 
void SetBlobs(const vector<Blob<Dtype>*>& weights); 
inline Dtype loss(const int top_index) const; 
inline void set_loss(const int top_index, const Dtype value); 
const LayerParameter& layer_param() const { return layer_param_; } 
inline bool param_propagate_down(const int param_id);
inline void set_param_propagate_down(const int param_id, const bool value);

ToProto將該層的引數設定以及學習到的權重序列化輸出。

// Serialize LayerParameter to protocol buffer
template <typename Dtype>
void Layer<Dtype>::ToProto(LayerParameter* param, bool write_diff) {
  param->Clear();
  param->CopyFrom(layer_param_);
  param->clear_blobs();
  for (int i = 0; i < blobs_.size(); ++i) {
    blobs_[i]->ToProto(param->add_blobs(), write_diff);
  }
}

下面為供CheckBlobCounts使用的函式,根據層的需要自行定義,預設狀態對top和bottom的blob數量不做要求。可見,其實CheckBlobCounts也採用了template method設計思想,只是這個函式沒那麼重要,按下不表。

virtual inline int ExactNumBottomBlobs() const { return -1; }
virtual inline int MinBottomBlobs() const { return -1; }
virtual inline int MaxBottomBlobs() const { return -1; }
virtual inline int MaxBottomBlobs() const { return -1; }
virtual inline int ExactNumTopBlobs() const { return -1; }
virtual inline int MinTopBlobs() const { return -1; }
virtual inline int MaxTopBlobs() const { return -1; }
virtual inline bool EqualNumBottomTopBlobs() const { return false; }

其他成員函式

/* If this method returns true, Net::Init will create enough "anonymous" top
 * blobs to fulfill the requirement specified by ExactNumTopBlobs() or
 * MinTopBlobs().
 */
virtual inline bool AutoTopBlobs() const { return false; }
/* If AllowForceBackward(i) == false, we will ignore the force_backward
 * setting and backpropagate to blob i only if it needs gradient information
 * (as is done when force_backward == false).
 */
virtual inline bool AllowForceBackward(const int bottom_index) const {
    return true;
  }

以上。

參考