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設計模式,其呼叫順序如下圖所示。
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::Init
,Net::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
為純虛擬函式,子類必須自己實現,CheckBlobCounts
和LayerSetUp
為虛擬函式,提供了預設實現,子類也可以定義自己的實現。一般,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
,同時定義好各自個性化的LayerSetUp
和Reshape
等函式,就可以將自己納入到SetUp
的搭建流程,並通過Net::Init
進一步納入整個網路的搭建中。
前向傳播與反向傳播
Layer
為所有層定義了前向傳播與反向傳播的通用介面Forward
和Backward
,實際上,Forward
和Backward
是Forward_cpu
、Forward_gpu
和Backward_cpu
、Backward_gpu
的包裝器,子類需要定義自己的Forward_cpu
、Forward_gpu
和Backward_cpu
、Backward_gpu
,比如,卷積層前向傳播要通過卷積操作,池化層前向傳播時要通過池化操作,而不需要重寫Forward
和Backward
。此外,如果子類不定義自己的gpu
函式,預設的gpu
函式實際呼叫的是cpu
函式,如下面程式碼所示,所以如果要使用GPU,必須要自己實現Forward_gpu
和Backward_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.";
}
}
其他成員函式
首先是成員變數的set
和get
函式:
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;
}
以上。