caffe源碼學習

本文轉載自:https://buptldy.github.io/2016/10/09/2016-10-09-Caffe_Code/

Caffe簡介

Caffe做爲一個優秀的深度學習框架網上已經有不少內容介紹了,這裏就不在多說。做爲一個C++新手,斷斷續續看Caffe源碼一個月以來發現越看不懂的東西越多,所以在博客裏記錄和分享一下學習的過程。其中我把本身看源碼的一些註釋結合了網上一些同窗的註釋以及在學習源碼過程當中查到到的一些資源(包括怎麼使用IDE單步調試以及一些Caffe中使用的第三方庫的介紹)放在github上:Caffe_Code_Analysis,感興趣的同窗能夠看一看,但願能對你有幫助。html

通常在介紹Caffe代碼結構的時候,你們都會說Caffe主要由Blob Layer NetSolver這幾個部分組成。git

  • Blob 主要用來表示網絡中的數據,包括訓練數據,網絡各層自身的參數(包括權值、偏置以及它們的梯度),網絡之間傳遞的數據都是經過 Blob 來實現的,同時 Blob 數據也支持在 CPU 與 GPU 上存儲,可以在二者之間作同步。
  • Layer 是對神經網絡中各類層的一個抽象,包括咱們熟知的卷積層和下采樣層,還有全鏈接層和各類激活函數層等等。同時每種 Layer 都實現了前向傳播和反向傳播,並經過 Blob 來傳遞數據。
  • Net 是對整個網絡的表示,由各類 Layer 先後鏈接組合而成,也是咱們所構建的網絡模型。
  • Solver 定義了針對 Net 網絡模型的求解方法,記錄網絡的訓練過程,保存網絡模型參數,中斷並恢復網絡的訓練過程。自定義 Solver 可以實現不一樣的網絡求解方式。

不過在剛開始準備閱讀Caffe代碼的時候,就算知道了代碼是由上面四部分組成仍是感受會無從下手,下面咱們準備經過一個Caffe訓練LeNet的實例並結合代碼來解釋Caffe是如何初始化網絡,而後正向傳播、反向傳播開始訓練,最終獲得訓練好的模型這一過程。github

訓練LeNet

在Caffe提供的例子裏,訓練LeNet網絡的命令爲:算法

cd $CAFFE_ROOT
./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt

其中第一個參數是Caffe框架的主要框架,由文件編譯而來,第二個參數表示是要訓練網絡,第三個參數是 solver的protobuf描述文件。在Caffe中,網絡模型的描述及其求解都是經過 protobuf 定義的,並不須要經過敲代碼來實現。同時,模型的參數也是經過 protobuf 實現加載和存儲,包括 CPU 與 GPU 之間的無縫切換,都是經過配置來實現的,不須要經過硬編碼的方式實現,有關
protobuf的具體內容可參考這篇博文:http://alanse7en.github.io/caffedai-ma-jie-xi-2/
build/tools/caffetools/caffe.cpptrain

網絡初始化

下面咱們從caffe.cpp的main函數入口開始觀察Caffe是怎麼一步一步訓練網絡的。在caffe.cpp中main函數以外經過RegisterBrewFunction這個宏在每個實現主要功能的函數以後將這個函數的名字和其對應的函數指針添加到了g_brew_map中,具體分別爲train(),test(),device_query(),time()這四個函數。數組

在運行的時候,根據傳入的參數在main函數中,經過GetBrewFunction獲得了咱們須要調用的那個函數的函數指針,並完成了調用。網絡

// caffe.cpp
return GetBrewFunction(caffe::string(argv[1])) ();

在咱們上面所說的訓練LeNet的例子中,傳入的第二個參數爲train,因此調用的函數爲caffe.cpp中的int train()函數,接下來主要看這個函數的內容。在train函數中有下面兩行代碼,下面的代碼定義了一個指向Solver 的shared_ptr。其中主要是經過調用SolverRegistry這個類的靜態成員函數CreateSolver獲得一個指向Solver的指針來構造shared_ptr類型的solver。並且因爲C++多態的特性,儘管solver是一個指向基類Solver類型的指針,經過solver這個智能指針來調用各個成員函數會調用到各個子類(SGDSolver等)的函數。框架

// caffe.cpp
// 其中輸入參數solver_param就是上面所說的第三個參數:網絡的模型及求解文件
shared_ptr<caffe::Solver<float> >
solver(caffe::SolverRegistry<float>::CreateSolver(solver_param);

由於在caffe.proto文件中默認的優化typeSGD,因此上面的代碼會實例化一個SGDSolver的對象,’SGDSolver’類繼承於Solver類,在新建SGDSolver對象時會調用其構造函數以下所示:函數

//sgd_solvers.hpp
explicit SGDSolver(const SolverParameter& param)
: Solver<Dtype>(param) { PreSolve(); }

從上面代碼能夠看出,會先調用父類Solver的構造函數,以下所示。Solver類的構造函數經過Init(param)函數來初始化網絡。學習

//solver.cpp
template <typename Dtype>
Solver<Dtype>::Solver(const SolverParameter& param, const Solver* root_solver)
: net_(), callbacks_(), root_solver_(root_solver),requested_early_exit_(false)
{
Init(param);
}

而在Init(paran)函數中,又主要是經過InitTrainNet()InitTestNets()函數分別來搭建訓練網絡結構和測試網絡結構。測試

訓練網絡只能有一個,在InitTrainNet()函數中首先會設置一些基本參數,包括設置網絡的狀態爲TRAIN,肯定訓練網絡只有一個等,然會會經過下面這條語句新建了一個Net對象。InitTestNets()函數和InitTrainNet()函數基本相似,再也不贅述。

//solver.cpp
net_.reset(new Net<Dtype>(net_param));

上面語句新建了Net對象以後會調用Net類的構造函數,以下所示。能夠看出構造函數是經過Init(param)函數來初始化網絡結構的。

//net.cpp
template <typename Dtype>
Net<Dtype>::Net(const NetParameter& param, const Net* root_net)
: root_net_(root_net) {
Init(param);
}

下面是net.cpp文件裏Init()函數的主要內容(忽略具體細節),其中LayerRegistry<Dtype>::CreateLayer(layer_param)主要是經過調用LayerRegistry這個類的靜態成員函數CreateLayer獲得一個指向Layer類的shared_ptr類型指針。並把每一層的指針存放在vector<shared_ptr<Layer<Dtype> > > layers_這個指針容器裏。這裏至關於根據每層的參數layer_param實例化了對應的各個子類層,好比conv_layer(卷積層)和pooling_layer(池化層)。實例化了各層就會調用每一個層的構造函數,但每層的構造函數都沒有作什麼大的設置。

接下來在Init()函數中主要由四部分組成:

  • AppendBottom:設置每一層的輸入數據
  • AppendTop:設置每一層的輸出數據
  • layers_[layer_id]->SetUp:對上面設置的輸入輸出數據計算分配空間,並設置每層的可學習參數(權值和偏置),下面會詳細降到這個函數
  • AppendParam:對上面申請的可學習參數進行設置,主要包括學習率和正則率等。
//net.cpp Init()
for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {//param是網絡參數,layer_size()返回網絡擁有的層數
const LayerParameter& layer_param = param.layer(layer_id);//獲取當前layer的參數
layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));//根據參數實例化layer


//下面的兩個for循環將此layer的bottom blob的指針和top blob的指針放入bottom_vecs_和top_vecs_,bottom blob和top blob的實例全都存放在blobs_中。相鄰的兩層,前一層的top blob是後一層的bottom blob,因此blobs_的同一個blob既多是bottom blob,也可能使top blob。
for (int bottom_id = 0; bottom_id < layer_param.bottom_size();++bottom_id) {
const int blob_id=AppendBottom(param,layer_id,bottom_id,&available_blobs,&blob_name_to_idx);
}

for (int top_id = 0; top_id < num_top; ++top_id) {
AppendTop(param, layer_id, top_id, &available_blobs, &blob_name_to_idx);
}

// 調用layer類的Setup函數進行初始化,輸入參數:每一個layer的輸入blobs以及輸出blobs,爲每一個blob設置大小
layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);

//接下來的工做是將每層的parameter的指針塞進params_,尤爲是learnable_params_。
const int num_param_blobs = layers_[layer_id]->blobs().size();
for (int param_id = 0; param_id < num_param_blobs; ++param_id) {
AppendParam(param, layer_id, param_id);
//AppendParam負責具體的dirtywork
}
}

通過上面的過程,Net類的初始化工做基本就完成了,接着咱們具體來看看上面所說的layers_[layer_id]->SetUp對每一具體的層結構進行設置,咱們來看看Layer類的Setup()函數,對每一層的設置主要由下面三個函數組成:
LayerSetUp(bottom, top):由Layer類派生出的特定類都須要重寫這個函數,主要功能是設置權值參數(包括偏置)的空間以及對權值參數經行隨機初始化。
Reshape(bottom, top):根據輸出blob和權值參數計算輸出blob的維數,並申請空間。

//layer.hpp
// layer 初始化設置
void SetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
InitMutex();
CheckBlobCounts(bottom, top);
LayerSetUp(bottom, top);
Reshape(bottom, top);
SetLossWeights(top);
}

通過上述過程基本上就完成了初始化的工做,整體的流程大概就是新建一個Solver對象,而後調用Solver類的構造函數,而後在Solver的構造函數中又會新建Net類實例,在Net類的構造函數中又會新建各個Layer的實例,一直具體到設置每一個Blob,大概就介紹完了網絡初始化的工做,固然裏面還有不少具體的細節,但大概的流程就是這樣。

訓練過程

上面介紹了網絡初始化的大概流程,如上面所說的網絡的初始化就是從下面一行代碼新建一個solver指針開始一步一步的調用SolverNet,Layer,Blob類的構造函數,完成整個網絡的初始化。

//caffe.cpp
shared_ptr<caffe::Solver<float> > //初始化
solver(caffe::SolverRegistry<float>::CreateSolver(solver_param));

完成初始化以後,就能夠開始對網絡經行訓練了,開始訓練的代碼以下所示,指向Solver類的指針solver開始調用Solver類的成員函數Solve(),名稱比較繞啊。

// 開始優化
solver->Solve();

接下來咱們來看看Solver類的成員函數Solve(),Solve函數其實主要就是調用了Solver的另外一個成員函數Step()來完成實際的迭代訓練過程。

//solver.cpp
template <typename Dtype>
void Solver<Dtype>::Solve(const char* resume_file) {
...
int start_iter = iter_;
...
// 而後調用了'Step'函數,這個函數執行了實際的逐步的迭代過程
Step(param_.max_iter() - iter_);
...
LOG(INFO) << "Optimization Done.";
}

順着來看看這個Step()函數的主要代碼,首先是一個大循環設置了總的迭代次數,在每次迭代中訓練iter_size x batch_size個樣本,這個設置是爲了在GPU的顯存不夠的時候使用,好比我原本想把batch_size設置爲128,iter_size是默認爲1的,可是會out_of_memory,藉助這個方法,能夠設置batch_size=32,iter_size=4,那實際上每次迭代仍是處理了128個數據。

//solver.cpp
template <typename Dtype>
void Solver<Dtype>::Step(int iters) {
...
//迭代
while (iter_ < stop_iter) {
...
// iter_size也是在solver.prototxt裏設置,實際上的batch_size=iter_size*網絡定義裏的batch_size,
// 所以每一次迭代的loss是iter_size次迭代的和,再除以iter_size,這個loss是經過調用`Net::ForwardBackward`函數獲得的
// accumulate gradients over `iter_size` x `batch_size` instances
for (int i = 0; i < param_.iter_size(); ++i) {
/*
* 調用了Net中的代碼,主要完成了前向後向的計算,
* 前向用於計算模型的最終輸出和Loss,後向用於
* 計算每一層網絡和參數的梯度。
*/
loss += net_->ForwardBackward();
}

...

/*
* 這個函數主要作Loss的平滑。因爲Caffe的訓練方式是SGD,咱們沒法把全部的數據同時
* 放入模型進行訓練,那麼部分數據產生的Loss就可能會和全樣本的平均Loss不一樣,在必要
* 時候將Loss和歷史過程當中更新的Loss求平均就能夠減小Loss的震盪問題。
*/
UpdateSmoothedLoss(loss, start_iter, average_loss);


...
// 執行梯度的更新,這個函數在基類`Solver`中沒有實現,會調用每一個子類本身的實現
//,後面具體分析`SGDSolver`的實現
ApplyUpdate();

// 迭代次數加1
++iter_;
...

}
}

上面Step()函數主要分爲三部分:

loss += net_->ForwardBackward();

這行代碼經過Net類的net_指針調用其成員函數ForwardBackward(),其代碼以下所示,分別調用了成員函數Forward(&loss)和成員函數Backward()來進行前向傳播和反向傳播。

// net.hpp
// 進行一次正向傳播,一次反向傳播
Dtype ForwardBackward() {
Dtype loss;
Forward(&loss);
Backward();
return loss;
}

前面的Forward(&loss)函數最終會執行到下面一段代碼,Net類的Forward()函數會對網絡中的每一層執行Layer類的成員函數Forward(),而具體的每一層Layer的派生類會重寫Forward()函數來實現不一樣層的前向計算功能。上面的Backward()反向求導函數也和Forward()相似,調用不一樣層的Backward()函數來計算每層的梯度。

//net.cpp
for (int i = start; i <= end; ++i) {
// 對每一層進行前向計算,返回每層的loss,其實只有最後一層loss不爲0
Dtype layer_loss = layers_[i]->Forward(bottom_vecs_[i], top_vecs_[i]);
loss += layer_loss;
if (debug_info_) { ForwardDebugInfo(i); }
}

UpdateSmoothedLoss();

這個函數主要作Loss的平滑。因爲Caffe的訓練方式是SGD,咱們沒法把全部的數據同時放入模型進行訓練,那麼部分數據產生的Loss就可能會和全樣本的平均Loss不一樣,在必要時候將Loss和歷史過程當中更新的Loss求平均就能夠減小Loss的震盪問題

ApplyUpdate();

這個函數是Solver類的純虛函數,須要派生類來實現,好比SGDSolver類實現的ApplyUpdate();函數以下,主要內容包括:設置參數的學習率;對梯度進行Normalize;對反向求導獲得的梯度添加正則項的梯度;最後根據SGD算法計算最終的梯度;最後的最後把計算獲得的最終梯度對權值進行更新。

template <typename Dtype>
void SGDSolver<Dtype>::ApplyUpdate() {
CHECK(Caffe::root_solver());

// GetLearningRate根據設置的lr_policy來計算當前迭代的learning rate的值
Dtype rate = GetLearningRate();

// 判斷是否須要輸出當前的learning rate
if (this->param_.display() && this->iter_ % this->param_.display() == 0) {
LOG(INFO) << "Iteration " << this->iter_ << ", lr = " << rate;
}

// 避免梯度爆炸,若是梯度的二範數超過了某個數值則進行scale操做,將梯度減少
ClipGradients();

// 對全部可更新的網絡參數進行操做
for (int param_id = 0; param_id < this->net_->learnable_params().size();
++param_id) {
// 將第param_id個參數的梯度除以iter_size,
// 這一步的做用是保證明際的batch_size=iter_size*設置的batch_size
Normalize(param_id);

// 將正則化部分的梯度降入到每一個參數的梯度中
Regularize(param_id);

// 計算SGD算法的梯度(momentum等)
ComputeUpdateValue(param_id, rate);
}
// 調用`Net::Update`更新全部的參數
this->net_->Update();
}

等進行了全部的循環,網絡的訓練也算是完成了。上面大概說了下使用Caffe進行網絡訓練時網絡初始化以及前向傳播、反向傳播、梯度更新的過程,其中省略了大量的細節。上面還有不少東西都沒提到,好比說Caffe中Layer派生類的註冊及各個具體層前向反向的實現、Solver派生類的註冊、網絡結構的讀取、模型的保存等等大量內容。

相關文章
相關標籤/搜索