1. 程式人生 > >caffe程式碼淺析

caffe程式碼淺析

Caffe引數及儲存方式

引數型別
● 可學習引數,即權重,其值由模型初始化引數、誤差方向傳播過程決定。存放在.caffemodel檔案中。
● 結構引數,包括網路層數、層型別、卷積核數等,一旦設定好,訓練階段不能更改,注意:caffe中可以設定訓練階段結構引數和預測階段結構引數不同。存放在train.prototxt和test.prototxt中。
● 訓練超引數,包括訓練次數等,預測階段不需要該引數。存放在Solver.prototxt中

prototxt文字
protocol buffers用於序列化和檢索資料結構。caffe使用prototxt文字儲存輸入輸出引數描述檔案。
protocol buffers的序列化資料及讀取資料的步驟:
1. 在一個.proto檔案中定義Message格式。
2. 使用protoc編譯.proto檔案,編譯器會生成一個類,以便後續的計算單元中包含該類
3. 使用python/C++/java protocol buffer API 讀寫Message
caffe模型結構引數定義在prototxt檔案中,prototxt檔案的字尾是.prototxt。學習好的模型會被序列化地儲存在二進位制 protocol buffer (binaryproto) .caffemodel 檔案中。

輸入輸出檔案
輸入:
(1)slover.prototxt。描述網路訓練時的各種超引數檔案,如訓練的策略,學習率的變化率,模型儲存的頻率等引數
(2)train.prototxt。描述訓練網路的拓撲結構引數檔案,如卷積層/池化層個數,卷積核大小,卷積核數目。
(3)test.prototxt。描述測試網路的拓撲網路結構引數檔案。
輸出:
VGG16.caffemodel:儲存的訓練好的網路可學習引數檔案,如權重,偏置。

Caffe程式碼組成

一個Caffe由四個主要的大類構成,這些類分別是Blob類,Layer類,Net類,Solver類。
● Blob:是存放資料的結構,是用來儲存學習到的引數以及網路傳輸過程中產生資料的類,本質上是一個N維陣列。
● Layer:是網路的基本單元,由此派生出了各種層類。修改這部分的人主要是研究特徵表達方向的。
● Net:是網路的搭建,將Layer所派生出層類組合成網路。
● Solver:是Net的求解方法,管理並執行Net物件的訓練和測試。

Blob

Blob類原型(只擷取關鍵資料和函式)

//Blob是一個模板類
template <typename Dtype>
class Blob{
protected:
    shared_ptr<SyncedMemory> data_;
    shared_ptr<SyncedMemory> diff_;
    vector<int> shape_;
    int count_;
    int capacity_;
public:
    Blob():data_(),diff_(),count_(0),capacity_(0
){}//預設建構函式 //... };

宣告一個Blob物件如下:

Blob<float> input;

blob 表示一個4維陣列,常規的維數為影象數量 N*通道數 C*影象高度 H *影象寬度W。 Blob 按行為主( row-major) 進行儲存,所以一個 4 維 blob 中,索引為(n, c, h, w)的blob值轉換為一維陣列的索引值為((n* C+c) *H+h)*W+w。

Blob成員變數
Blob類中有兩個SyncedMemory型別資料物件指標——data_(資料或權值)和diff_(梯度),一個表示形狀的vector<int>,還有兩個用於指示大小和總容量的count_和capacity_變數。由於資料既可儲存在CPU上,也可儲存在GPU上。資料訪問有兩種方式:靜態方式,不改變資料;動態方式,改變資料。

const Dtype* cpu_data() const;
Dtype* mutable_cpu_data();

blob 使用了一個 SyncedMem 類來同步 CPU 和 GPU 上的數值, 以隱藏同步的細節和最小化傳送資料。一個經驗準則是,如果不想改變數值,就一直使用常量呼叫,而且絕不要在自定義類中儲存指標。 每次操作 blob 時, 呼叫相應的函式來獲取它的指標,因為 SyncedMem 需要用這種方式來確定何時需要複製資料。

Blob成員函式

Blob<float> input;//建立一個Blob物件
vector<int> a;
input.Reshape(1,2,3,4);//它通過整型陣列改變Blob的各個維度的長度
a=input.shape();//它返回vector<int>型別的shape
cout<<input.shape_string();//它返回string型別的shape
cout<<input.num();
cout<<input.channels();
cout<<input.height();
cout<<input.width();

//反序列化函式原型,從BlobProto中恢復一個物件
void FromProto(const BlobProto& proto,bool reshape = true);
//序列化函式原型,將記憶體中的Blob物件存在BlobProto中,以便於儲存在磁碟中
void ToProto(BlobProto* proto,bool write_diff = false)const;

Layer

Layer類原型(只擷取關鍵資料和函式)

//Lyaer本身是一個抽象模板類
template <typename Dtype>
class Layer{
protected:
    LayerParameter layer_param_;//它是一個Protocol buffer物件,包含層內結構引數
    Phase phase_;//階段,TRAIN或TEST
    vector<shared_ptr<Blob<Dtype>>> blobs_;//層的權值和偏置
    vector<Dtype> loss_;//當前層的loss值
    vector<bool> param_propagate_down_;//是否計算誤差梯度的標誌
public:
    //顯式建構函式
    explicit Layer(const LayerParameter& param):layer_param(param),is_shared(false){
    phase_ = param.phase();
    blobs_.resize(layer_param.blobs_size());按照layer_param_設定Blob物件個數
    for(int i = 0;i<layer_param.blobs_size();i++)
    {   
        //對blobs_中每個Blob物件調整尺寸,與layer_param_Blob物件尺寸一致
        blobs_[i].reset(new Blob<Dtype>());
    blobs_i]->FroProto(layer_param_.blobs(i));
    }
    }
//...
};

5大Layer派生類
● 神經元類,定義於neuron_layers.hpp中,其派生類主要是元素級別的運算(比如Dropout運算,啟用函式ReLu,Sigmoid等),運算均為同址計算(in-place computation,返回值覆蓋原值而佔用新的記憶體)。
● LossLayer類,定義於loss_layers.hpp中,其派生類會產生loss,只有這些層能夠產生loss。
● 資料層類,定義於data_layer.hpp中,作為網路的最底層,主要實現資料格式的轉換。
● 視覺層類,定義於vision_layers.hpp,實現抽取特徵功能,具體地說包含卷積操作,Pooling操作,他們基本都會產生新的記憶體佔用(Pooling相對較小)。
● 通用層類,定義於common_layers.hpp,Caffe提供了單個層與多個層的連線,並在這個標頭檔案中宣告。包括了常用的全連線層InnerProductLayer類。

Layer成員資料
1、loss
每一層又有一個loss值,大多數Layer都是0,只有LossLayer才可能產生非0的loss。計算loss是會把所有層的loss_相加。
2、權值和偏置
放在vector<shared_ptr<Blob<Dtype>>>型別的blobs_中,每層可能不止有一個Blob物件,對於大多數Layer來說輸入和輸出都各連線只有一個Blob,但是對於某些層,如下圖所示,LossLayer有兩個輸入Blob。在網路結構定義檔案(*.proto)中每一層的引數bottom和top數目就決定了vector中Blob物件的數目。

Layer成員函式
Layer是Caffe模型的本質內容和執行計算的基本單元。每一個 layer 都定義了 3 種重要的運算: setup(初始化設定), forward(前向傳播),backward(反向傳播)。CPU模式下,前向和反向傳播函式名分別為Forward_cpu()和Backward_cpu;GPU模式下,分別為Forward_gpu()和Backward_gpu,這四個函式都是虛擬函式,在不同Layer的派生類中需要重寫。
● Setup: 在模型初始化時重置 layers 及其相互之間的連線 ;
● Forward: 從bottom blob中接收資料,進行計算後將輸出送入到top blob中;
● Backward: 給定top blob的diff,計算其相對於輸入的diff,並傳遞到bottom blob。一個有引數的layer需要計算相對於各個引數的梯度值並存儲在內部。
Layer類派生出來的層類通過這實現這兩個虛擬函式,產生了各式各樣功能的層類。Forward是從根據bottom計算top的過程,Backward則相反(根據top計算bottom)。

Net

Net類原型(只擷取關鍵資料和函式)

template <typename Dtype>
class Net{
protected:
    string name_;//網路名稱
    Phase phase_;//當前階段

    vector<shared_ptr<Layer<Dtype>>> layers_;//網路中間的層
    vector<string> layer_names_;

    vector<shared_ptr<Blob<Dtype>>> blobs_;//層之間的輸入輸出blobs
    vector<string> blob_names_;

    vector<shared_ptr<Blob<Dtype>>> params_;//網路權值

    vector<float> params_lr;//學習率
public:
    //顯式建構函式
    //NetParameter物件初始化Net
    explicit Net(const NetParameter& param,const Net* root_net = NULL);
    //用網路描述檔案初始化Net
    explicit Net(const string& param_file,Phase phase, const Net* root_net= NULL);
    //以輸入Blob為引數進行前向傳播,返回輸出Blob
    const vector<Blob<Dtype>*>& Forward(const vector<Blob<Dtype>*>& bottom,Dtype* loss = NULL);
    void Backward();
    void Update();
//...
};

Net成員資料
一個Net物件包含的三種物件,分別是層陣列(layers_)、Blob陣列(blobs_)和權重陣列(params_)。layers_是Layer的vector容器,其自身實現的功能主要是對逐層Layer進行初始化,以及提供Update( )的介面(更新網路引數)。
Net 是由一系列層組成的有向無環( DAG)計算圖, Caffe 保留了計算圖中所有的中間值以確保前向和反向迭代的準確性。一個典型的 Net 開始於 data layer——從磁碟中載入資料,終止於 loss layer——計算如分類和重構這些任務的目標函式。

Net成員函式
Forward(…)和Backward()這兩個函式對整個網路的前向和方向傳播,各呼叫一次就可以計算出網路的loss了。
Net::Init()進行模型的初始化。初始化主要實現兩個操作:建立 blobs 和 layers 以搭建整 個網路 DAG 圖,以及呼叫layers 的 SetUp()函式。
網路構建完之後,通過設定 Caffe::mode()函式中的 Caffe::set_mode(), 即可實現在 CPU 或 GPU 上的執行。

Solver

Solver負責整個模型的優化,讓全域性loss達到最小。
Solver的主要工作:
1、呼叫Net::Forward=>呼叫Net::Backward=>呼叫Net::Update
2、週期性測試網路。

Solver類原型

//Solver是一個抽象模板類
template <typename Dtype>
class Slover{
protected:
    SolverParameter param_;//用於從prototxt中提取引數
    int iter_;//迭代次數
    shared_ptr<Net<Dtype> > net_;//指向一個Net物件的指標,用於訓練
    vector<shared_ptr<Net<Dtype>>> test_nets;//Net物件陣列,用於測試
    virtual void ApplyUpdate() = 0;//更新權值
    void TestAll();//每隔一定週期對訓練的網路進行一次評估
public:
    //顯示建構函式
    //以SolverParameter物件構造Solver
    explicit Slover(const SolverParameter& param,const Solver* root_solver =NULL);
    //以Solver描述檔案構造Solver
    explicit Solver(const string& param_file,const Solver* root_solver =NULL);
    void Init(const SolverParameter& param);//初始化
    virtual void Solve(const char* resume_file =NULL);//從一個resume_file恢復訓練,預設是從iter 0開始訓練
    void Step(int iters);//進行第iter次迭代
//...
};

Solver成員資料
Solver包含一個訓練網路net_物件和若干個預測網路test_nets_物件。由Solver管理它們的初始化和執行。

Solver成員函式
根據訓練方法的不同定義不同的Solver的派生類,不同的派生類都有一個ComputeUpdateValue( )實現計算update引數的核心功能。
最後當進行整個網路訓練過程(也就是你執行Caffe訓練某個模型)的時候,實際上是在執行caffe.cpp中的train( )函式,而這個函式實際上是例項化一個Solve的派生類物件,初始化後呼叫Solve( )函式。而這個Solve( )函式又呼叫派生類中的ApplyUpdate()函式,ApplyUpdate()函式就包含了ComputeUpdateValue()和net_->Update()兩個函式的呼叫。

至此,以物件的角度分析了caffe深度學習框架原始碼的組成,希望給初學者一個整體的認識。如有錯誤,請聯絡博主~