原文來自:http://www.shwley.com/index.php/archives/68/
前言
老實說,caffe中的layer層代碼比較多,各種抽象看起來比較繞。官方關於Layer的教程寫的很清楚,我根據這個文檔,簡單畫了個圖,再理解起來就方便了一些。
layer.hpp
和layer相關的標頭檔有:
common_layers.hppdata_layers.hpplayer.hpploss_layers.hppneuron_layers.hppvision_layers.hpp
其中``layer.hpp是抽象出來的基類,其他都是在其基礎上的繼承,也即剩下的五個標頭檔和上圖中的五個部分。在layer.hpp`標頭檔裡,包含了這幾個標頭檔:
#include "caffe/blob.hpp"#include "caffe/common.hpp"#include "caffe/proto/caffe.pb.h"#include "caffe/util/device_alternate.hpp"
在device_alternate.hpp中,通過#ifdef CPU_ONLY定義了一些宏來取消GPU的調用:
#define STUB_GPU(classname)#define STUB_GPU_FORWARD(classname, funcname)#define STUB_GPU_BACKWARD(classname, funcname)
layer中有這三個主要參數:
LayerParameter layer_param_; // 這個是protobuf檔案中儲存的layer參數vector<share_ptr<Blob<Dtype>>> blobs_; // 這個儲存的是layer的參數,在程式中用的vector<bool> param_propagate_down_; // 這個bool表示是否計算各個blob參數的diff,即傳播誤差
Layer類的構建函數explicit Layer(const LayerParameter& param) : layer_param_(param)會嘗試從protobuf檔案讀取參數。其三個主要介面:
virtual void SetUp(const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top)inline Dtype Forward(const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top);inline void Backward(const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down, const <Blob<Dtype>*>* bottom);
SetUp函數需要根據實際的參數設定進行實現,對各種類型的參數初始化;Forward和Backward對應前向計算和反向更新,輸入統一都是bottom,輸出為top,其中Backward裡面有個propagate_down參數,用來表示該Layer是否反向傳播參數。
在Forward和Backward的具體實現裡,會根據Caffe::mode()進行對應的操作,即使用cpu或者gpu進行計算,兩個都實現了對應的介面Forward_cpu、Forward_gpu和Backward_cpu、Backward_gpu,這些介面都是virtual,具體還是要根據layer的類型進行對應的計算(注意:有些layer並沒有GPU計算的實現,所以封裝時加入了CPU的計算作為後備)。另外,還實現了ToProto的介面,將Layer的參數寫入到protocol buffer檔案中。 data_layers.hpp
data_layers.hpp這個標頭檔包含了這幾個標頭檔:
#include "boost/scoped_ptr.hpp"#include "hdf5.h"#include "leveldb/db.h"#include "lmdb.h"#include "caffe/blob.hpp"#include "caffe/common.hpp"#include "caffe/filler.hpp"#include "caffe/internal_thread.hpp"#include "caffe/layer.hpp"#include "caffe/proto/caffe.pb.h"
看到hdf5、leveldb、lmdb,確實是與具體資料相關了。data_layer作為未經處理資料的輸入層,處於整個網路的最底層,它可以從資料庫leveldb、lmdb中讀取資料,也可以直接從記憶體中讀取,還可以從hdf5,甚至是原始的映像讀入資料。
關於這幾個資料庫,簡介如下:
LevelDB是Google公司搞的一個高效能的key/value存放庫,調用簡單,資料是被Snappy壓縮,據說效率很多,可以減少磁碟I/O,具體例子可以看看維基百科。
而LMDB(Lightning Memory-Mapped Database),是個和levelDB類似的key/value存放庫,但效果似乎更好些,其首頁上寫道“ultra-fast,ultra-compact”,這個有待進一步學習啊~~
HDF(Hierarchical Data Format)是一種為儲存和處理大容量科學資料而設計的檔案格式及相應的庫檔案,當前最流行的版本是HDF5,其檔案包含兩種基本資料對象: 群組(group):類似檔案夾,可以包含多個資料集或下級群組; 資料集(dataset):資料內容,可以是多維陣列,也可以是更複雜的資料類型。
以上內容來自維基百科,關於使用可以參考[HDF5 小試——高大上的多個物件檔案格式](HDF5 小試——高大上的多個物件檔案格式),後續會再詳細的研究下怎麼用。
caffe/filler.hpp的作用是在網路初始化時,根據layer的定義進行初始參數的填充,下面的代碼很直觀,根據FillerParameter指定的類型進行對應的參數填充。
// A function to get a specific filler from the specification given in// FillerParameter. Ideally this would be replaced by a factory pattern,// but we will leave it this way for now.template <typename Dtype>Filler<Dtype>* GetFiller(const FillerParameter& param) { const std::string& type = param.type(); if (type == "constant") { return new ConstantFiller<Dtype>(param); } else if (type == "gaussian") { return new GaussianFiller<Dtype>(param); } else if (type == "positive_unitball") { return new PositiveUnitballFiller<Dtype>(param); } else if (type == "uniform") { return new UniformFiller<Dtype>(param); } else if (type == "xavier") { return new XavierFiller<Dtype>(param); } else { CHECK(false) << "Unknown filler name: " << param.type(); } return (Filler<Dtype>*)(NULL);}
internal_thread.hpp裡面封裝了pthread函數,繼承的子類可以得到一個單獨的線程,主要作用是在計算當前的一批資料時,在後台擷取新一批的資料。
關於data_layer,基本要注意的我都在圖片上標註了。 neuron_layers.hpp
輸入了data後,就要計算了,比如常見的sigmoid、tanh等等,這些都計算操作被抽象成了neuron_layers.hpp裡面的類NeuronLayer,這個層只負責具體的計算,因此明確定義了輸入ExactNumBottomBlobs()和ExactNumTopBlobs()都是常量1,即輸入一個blob,輸出一個blob。 common_layers.hpp
NeruonLayer僅僅負責簡單的一對一計算,而剩下的那些複雜的計算則通通放在了common_layers.hpp中。像ArgMaxLayer、ConcatLayer、FlattenLayer、SoftmaxLayer、SplitLayer和SliceLayer等各種對blob增減修改的操作。 loss_layers.hpp
前面的data layer和common layer都是中間計算層,雖然會涉及到反向傳播,但傳播的源頭來自於loss_layer,即網路的最終端。這一層因為要計算誤差,所以輸入都是2個blob,輸出1個blob。 vision_layers.hpp
vision_layer主要是映像卷積的操作,像convolusion、pooling、LRN都在裡面,按官方文檔的說法,是可以輸出映像的,這個要看具體實現代碼了。裡面有個im2col的實現,看caffe作者的解釋,主要是為了加速卷積的,這個具體是怎麼實現的要好好研究下~~ 後語
結合官方文檔,再加畫圖和看代碼,終於對整個layer層有了個基本認識:data負責輸入,vision負責卷積相關的計算,neuron和common負責中間部分的資料計算,而loss是最後一部分,負責計算反向傳播的誤差。具體的實現都在src/caffe/layers裡面,慢慢再研究研究。
在這些抽象的基類標頭檔裡,看起來挺累,好在各種搜尋,也能學到一些技巧,如,巧用宏定義來簡寫C,C++代碼,使用模板方法,將有大量重複介面和參數的類抽象為一個宏定義,達到簡化代碼的目的。