博客: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;
}
以上。