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;
  }

以上。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章