讀caffe源碼(未完待續)
阿新 • • 發佈:2017-11-25
樣本 源碼解析 單元 最小 size 傳遞 strong 統一 news
caffe源碼閱讀雜記
準備
- 一些參考網頁
- Neural Networks and Deep Learning
- TUTORIAL ON DEEP LEARNING FOR VISION
- Deep Learning Tutorial
- 知乎-深度學習caffe的代碼怎麽讀
- Caffe源碼解析
- caffe源碼結構
- 官方代碼結構doxygen
- 官方Caffe Tutorial
- 以C++源碼形式配置debug&CPU版的caffe,便於閱讀源碼與單步調試【參考】
- 參考官方的文檔,先了解某個模塊的作用
- 為了簡化學習,先不考慮任何與GPU相關的問題
- 我在讀代碼的過程中在代碼中添加了一些自己的註釋,代碼保存在github上
我的閱讀過程
大致的過程
- 讀caffe前對深度學習沒有什麽概念所以先通讀了上面參考網頁中的第一個並詳細閱讀了其中的network1代碼,閱讀過程在這裏
Caffe中幾個主要的類
- Blob: 數據的保存與傳遞都使用的類(神經網絡中主要的參數就是神經元之間的權值與偏置值,其均使用Blob對象保存與交互)
- Layer: Blob數據處理方式的封裝,不同layer對Blob data進行不同的運算,如卷積、池化、全連接等
- Net: 描述網絡的結構,Net中以指定的結構保存不同的Layer
- Solver: 網絡求解類,一般使用的方法是梯度下降法
讀caffe.proto
google/protobuf 簡介
- protobuf是一個數據持久化與序列化的工具,具體教程參考1、2、官方C++版教程(C++教程國內免FQ版)。
- 數據存儲和交換(包括通過網絡交換數據) 是 Protobuf 最有效的應用領域。caffe中主要用protobuf實現網絡模型的結構定義、存儲和讀取(網絡結構映射到內存中就是對應對象之間的結構,對象初始化時需要參數。caffe使用protobuf定義內存中對象的結構並保存相應的數據,protobuf將管理這些參數在文件與內存中的映射關系,例如我們使用caffe框架是一般需要網絡文件與對應的訓練好的模型文件,他們是一一對應的,模型文件中保存了網絡對象中的參數)。
- 使用流程如下:
- 首先我們需要編寫一個 proto 文件,定義我們程序中需要處理的結構化數據,在 protobuf 的術語中,結構化數據被稱為 Message;
- 寫好 proto 文件之後就可以用 Protobuf 編譯器將該文件編譯成目標語言了,對於C++而言就是生成一個hpp和cpp文件;
- 在生成的頭文件中,有一個 C++ 類,使用這個類可以對消息進行操作。諸如對消息的成員進行賦值,將消息序列化、反序列化等等。
- proto格式說明(見本文下方)
caffe.proto
- caffe.proto中定義了很多數據結構(這些結構都會轉化為對應C++類並在caffe代碼中使用),數據結構的具體內容參考caffe.proto中的定義
blob、layer、net、solver的大致結構
blob.hpp
- blob類用來保存學習到的參數以及網絡傳輸過程中產生的數據,bolb是caffe數據保存與交換的核心
- 一層一層往下分BLOB的基本元素分別為:圖像->通道->行->列(這裏已經是最小的元素了,其類型就是DType,常見的有float和double)。
( (n * channels() + c) * height() + h) * width() + w;
表示的是元素(n,c,h,w)在blob中的索引。n*channels()+c
表示的是當前元素所處的通道的索引((n * channels() + c) * height() + h)
當前元素所處的行所在的索引- 最後求的就是當前元素的索引了
以RGB圖像為例描述BLOB在內存中的格式,假設當前BLOB中有N張圖,圖的高為H,寬為W像素:
|------------------------img0-----------------------|...img1~imgN-1...| |<------channel R-------> <-channel G-><-channel B->|.................. |<row0--row2-...row(H-1)>...........................|.................. |對應的R、或G、或B的值......
- blob類的成員如下(部分)
成員變量
//指向存儲數據的內存或顯存塊,一般是圖像數據、權值或偏置值數據 shared_ptr<SyncedMemory> data_; shared_ptr<SyncedMemory> diff_;//保存反向傳遞時的梯度信息
成員函數
void Reshape (const vector<int>& shape); //用於調整下標。C++數組不支持負數下標,當前函數的功能就是將負數轉化為對應的正數下標 inline int CanonicalAxisIndex (int axis_index) const; //blob中還有一些對數據進行簡單計算的方法
SyncedMemory
- SyncedMemory類主要用於數據在CPU和GPU之間的交換,本文不深入探索當前類。
SyncedMemory中的部分成員
const void* cpu_data(); void set_cpu_data (void* data); const void* gpu_data(); void set_gpu_data (void* data); void* mutable_cpu_data();//與上面的cpu_data和gpu_data相對應,只不過內容可改 void* mutable_gpu_data();
layer.hpp
- layer是caffe中網絡的基礎,是基本的計算單元與連接單元。卷積、池化、內積、sigmoid等神經元運算都在layer中完成。caffe中的layer不但實現前向運算,同時也實現反向運算,即檢測與訓練運算都包含在同一個層中。
- caffe中所有的層都直接或間接繼承於layer這個類
- 每一層的layer從底層連接獲得輸入數據,計算之後通過頂層連接輸出計算結果。
- 每一層layer都必須實現3個關鍵函數:setup、forward、backward
- Setup: 在初始化模型的的時候初始化層的參數和連接
- Forward: 通過底層(bottom)給出的數據計算結果並傳遞給頂層(top)
- Backward: 通過頂層(top)給出的數據計算梯度並傳遞給底層(bottom)。 A layer with parameters computes the gradient w.r.t. to its parameters and stores it internally.
- forward和backward函數有對應的CPU與GPU版,如果沒有定義GPU版函數,則退化使用CPU函數
- layer類中的成員有
數據成員
//當前層的參數,LayerParameter的結構定義在caffe.proto中 LayerParameter layer_param_; Phase phase_;//用於測試還是訓練,有些層只能用於測試,而有些是測試訓練均可 vector<shared_ptr<Blob<Dtype> > > blobs_;//指向需要訓練的參數 /** Vector indicating whether to compute the diff of each param blob. */ vector<bool> param_propagate_down_; /** The vector that indicates whether each top blob has a non-zero weight in the objective function. */ vector<Dtype> loss_;//損失函數值
函數成員
virtual void LayerSetUp (...); inline Dtype Forward (...); inline void Backward (...); //Returns the vector of learnable parameter blobs. vector<shared_ptr<Blob<Dtype> > >& blobs(); //Writes the layer parameter to a protocol buffer virtual void ToProto (...);
net.hpp
net類定義了網絡的結構,即各個層(layers)之間的組合(連接)方式。網絡的結構在caffe中是定義在prototxt文件中的,下面以lenet網絡的部分結構為例來說明(caffe中的網絡層可以並列,下面使用deepid網絡為例來說明)
name: "LeNet"#網絡結構的名稱 layer { name: "data" #當前層的名稱 type: "Input"#當前層的類型 top: "data" #輸出層的名稱,一般每個layer都有輸入與輸出兩個“子層”,Input層比較特殊,不用寫bottom input_param { shape: { dim: 64 dim: 1 dim: 28 dim: 28 } } #當前層的參數 } layer { name: "conv1" type: "Convolution" bottom: "data" #指明當前層將從“data”層獲得輸入數據 top: "conv1" #指明輸出層的名稱,一般與當前層的名稱相同 param {... #因為篇幅,具體參數這裏忽略 } ################################################################### # 以deepid網絡為例,caffe中的層可以連接多個層也可以將多個層合並為一個輸出 ... layer { name: "pool3" ... } # 下面兩層的輸入都取自pool3,且這兩層的類別也不同 layer { name: "deepid_match_conv" type: "Pooling" bottom: "pool3" # 從pool3中獲得輸入數據 ... } layer { name: "conv4" type: "Convolution" bottom: "pool3" # 從pool3中獲得輸入數據 ... } ... layer { name: "pool4" type: "Pooling" bottom: "norm4" top: "pool4" ... } # 下面這一層的輸入數據來自於兩個層:pool4和上面的deepid_match_conv,並只有一個輸出 layer { name: "deepid_concat" type: "Concat" bottom: "deepid_match_conv" bottom: "pool4" top: "deepid_concat" } ...
- net類中的成員
數據成員
string name; //網絡的名稱 //各個層的信息 vector<shared_ptr<Layer<Dtype> > > layers_; vector<string> layer_names_; map<string, int> layer_names_index_; //保存層層之間交互的信息 vector<shared_ptr<Blob<Dtype> > > blobs_; vector<string> blob_names_; map<string, int> blob_names_index_;
函數成員
//前向傳輸,前向傳輸還有其他形式的函數如:ForwardFromTo、ForwardFrom等 const vector<Blob<Dtype>*>& Forward (Dtype* loss = NULL); //有前向亦有反向傳播函數 void Backward();// void BackwardFrom (int start);等形式 //前向傳輸後進行反向傳播並返回前向傳輸時的損失函數輸出 Dtype ForwardBackward(); //設置層數據的函數 void CopyTrainedLayersFrom (const NetParameter& param);//亦有其他形式 //其他形式的help函數,例如ToHDF5、ToProto等
solver.hpp
- solver用於訓練網絡(優化權值和偏置值使得損失函數的輸出最小)。caffe中實現了以下幾種算法用於網絡的訓練【參考】
- Stochastic Gradient Descent (type: "SGD"),
- AdaDelta (type: "AdaDelta"),
- Adaptive Gradient (type: "AdaGrad"),
- Adam (type: "Adam"),
- Nesterov’s Accelerated Gradient (type: "Nesterov") and
- RMSprop (type: "RMSProp")
- SolverParameter類中的數據成員(讀caffe.proto)有很多,大部分都是關於訓練方法參數設置的,例如網絡結構文件、學習率、叠代次數等
- solver.hpp只是所有solver的父類,具體的solver在其他文件中
以lenet網絡的訓練solver為例說明solver需要的參數
net: "./lenet_train_test.prototxt" #網絡結構文件 #指明測試時前向傳輸的次數(每次batch size個樣本) #平均這些結果即為網絡的當前對test數據集的精度 test_iter: 100 test_interval: 100 # 指明進行多少次訓練後進行測試 base_lr: 0.01 #基本的學習率 momentum: 0.9 #加速訓練的一個參數 weight_decay: 0.0005 #目的是正則化,降低過擬合的風險 # 學習率更新的策略,一般在整個訓練過程中學習率是會變化的 lr_policy: "inv" gamma: 0.0001 power: 0.75 # 訓練多少次顯示一次信息 # 信息中包括叠代次數、時間、當前網絡對測試集與訓練集的損失函數輸出 display: 100 max_iter: 1000 #最大的訓練次數,達到這個次數時caffe會停止訓練並保存模型 snapshot: 500 #多久保存一次當前訓練模型的快照,caffe可以從指定的快照處開始訓練 snapshot_prefix: "./snapshot/lenet" # 快照文件保存的位置 solver_mode: GPU #設置solver運行的模式GPU or CPU
LayerParameter
LayerParameter中定義了如下參數(這裏只有部分,具體可參考caffe.proto文件)
optional string name; // the layer name optional string type; // the layer type repeated string bottom; // the name of each bottom blob repeated string top; // the name of each top blob // The train / test phase for computation. 有些層只用於TEST optional Phase phase; // The blobs containing the numeric parameters of the layer. //BlobProto也是一種結構,同樣定義在caffe.proto中,這個和blob.hpp中定義的結構不同 //在內存中使用後者的結構,在硬盤中使用前者的結構 repeated BlobProto blobs; // Rules controlling whether and when a layer is included in the network repeated NetStateRule include; repeated NetStateRule exclude; optional LossParameter loss_param;// Parameters shared by loss layers. // Layer type-specific parameters. optional AccuracyParameter accuracy_param; optional ArgMaxParameter argmax_param; optional BatchNormParameter batch_norm_param; ...
input_layer(所有網絡的第一層,以代碼中的lenet為例)
proto詳解
proto 格式示例與解釋
syntax = "proto2";//指明所使用的協議版本號 //名字空間,自動生成代碼時所有代碼均在這個名字空間內,在C++中會像這樣:namespace tutorial {...} package tutorial; //結構化數據被稱為 Message //在hpp文件中會生成如下類和結構: //enum Person_PhoneType; //class Person_PhoneNumber : public ::google::protobuf::Message; //class Person :public ::google::protobuf::Message; //class AddressBook : public ::google::protobuf::Message; message Person { // 字段(類成員)定義格式:限定修飾符① | 數據類型② | 字段名稱③ | = | 字段編碼值④ | [字段默認值⑤] // 每一個變量前都需要使用下面三個詞進行修飾:required、optional、repeated // required表示是一個必須字段,必須相對於發送方(對應於數據的序列化方),在發送消息之前必須設置該字段的值, // 對於接收方(對應於數據的反序列化方),必須能夠識別該字段的意思。 // 發送之前沒有設置required字段或者無法識別required字段都會引發編解碼異常,導致消息被丟棄。 required string name = 1; // 這裏的數值"=1、=2..."是編碼值,編碼值用於通信雙方互相識別對方的字段 // 其中 1~15的編碼時間和空間效率都是最高的,編碼值越大,其編碼的時間和空間效率就越低 // 1900~2000編碼值為Google protobuf 系統內部保留值,建議不要在自己的項目中使用 required int32 id = 2; // optional表示是一個可選字段,可選對於發送方,在發送消息時,可以有選擇性的設置或者不設置該字段的值 // 對於接收方,如果能夠識別可選字段就進行相應的處理,如果無法識別,則忽略該字段,消息中的其它字段正常處理 // 項目投入運營以後涉及到版本升級時的新增消息字段全部使用optional或者repeated,盡量不使用required // 如果使用了required,需要全網統一升級,如果使用optional或者repeated可以平滑升級 optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } // 表示該字段可以包含0~N個元素。其特性和optional一樣,但是每一次可以包含多個值 // 可以看作是在傳遞一個數組的值 repeated PhoneNumber phones = 4; } message AddressBook { repeated Person people = 1; }
The Protocol Buffer API(通過 Protobuf 編譯器編譯上面的proto文件所獲得的)
// name inline bool has_name() const;//判斷信息中是否包含當前元素 inline void clear_name(); inline const ::std::string& name() const;//下面是getter和setter inline void set_name(const ::std::string& value); inline void set_name(const char* value); inline ::std::string* mutable_name(); ...
Writing A Message & Reading A Message(使用上面API的示例)
//create Message cout << "Enter person ID number: "; int id; cin >> id; person->set_id(id); cin.ignore(256, ‘\n‘); cout << "Enter name: "; getline(cin, *person->mutable_name()); ... //read massage cout << "Person ID: " << person.id() << endl; cout << " Name: " << person.name() << endl; if (person.has_email()) { cout << " E-mail address: " << person.email() << endl; }
讀caffe源碼(未完待續)