解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt
轉自:http://blog.csdn.net/wuqingshan2010/article/details/71211467
解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt
第一個引數build/tools/caffe是Caffe框架的主要框架,由tools/caffe.cpp檔案編譯而來,第二個引數train表示是要訓練網路,第三個引數是 solver的protobuf描述檔案。
在Caffe中,網路模型的描述及其求解都是通過 protobuf 定義的,模型的引數也是通過 protobuf 實現載入和儲存,包括 CPU 與 GPU 之間的無縫切換,不需要通過硬編碼的方式實現。在caffe.cpp中main函式之外,通過巨集RegisterBrewFunction將train(),test(),device_query(),time()等函式及其對應的函式指標新增到了g_brew_map中, 通過GetBrewFunction可以得到需要呼叫的函式的函式指標。
1.網路初始化過程
第二個引數train呼叫caffe.cpp中的int train()函式。在train函式中:
// caffe.cpp
shared_ptr<caffe::Solver<float>> solver(caffe::SolverRegistry<float>::CreateSolver(solver_param);
- 1
- 2
首先定義了一個指向Solver的shared_ptr,然後其通過呼叫SolverRegistry類的靜態成員函式CreateSolver得到一個指向Solver的指標來構造shared_ptr型別的solver。
由於C++多型性,儘管solver是一個指向基類Solver型別的指標,通過solver這個智慧指標來呼叫各個子類(SGDSolver等)的函式。在caffe.proto檔案中預設的優化type為SGD,所以上面的程式碼會例項化一個SGDSolver的物件,SGDSolver類繼承於Solver類,在新建SGDSolver物件時會呼叫其建構函式如下所示:
//sgd_solvers.hpp
explicit SGDSolver(const SolverParameter& param)
: Solver<Dtype>(param) { PreSolve(); }
- 1
- 2
- 3
其中會先呼叫父類的Solver的建構函式。
//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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Solver類的建構函式通過Init(param)函式來初始化網路。在Init(param)函式中,又主要是通過InitTrainNet()和InitTestNets()函式分別來搭建訓練網路結構和測試網路結構。訓練網路只能有一個,在InitTrainNet()函式中首先會設定一些基本引數,包括設定網路的狀態為TRAIN,確定訓練網路只有一個等,然後會通過net_.reset(new
Net<Dtype>(net_param))
新建了一個Net物件。新建了Net物件之後會呼叫Net類的建構函式:
//net.cpp
template <typename Dtype>
Net<Dtype>::Net(const NetParameter& param, const Net* root_net)
: root_net_(root_net) { Init(param); }
- 1
- 2
- 3
- 4
Net類的建構函式是通過Init(param)函式來初始化網路結構的。
在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
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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
經過上述過程基本上就完成了初始化的工作,總體的流程是新建一個Solver物件,然後呼叫Solver類的建構函式,然後在Solver的建構函式中又會新建Net類例項,在Net類的建構函式中又會新建各個Layer的例項,一直具體到設定每個Blob。
2.訓練過程
網路的初始化即建立一個solver指標並逐步呼叫Solver、Net、Layer、Blob類的建構函式,完成整個網路的初始化。完成初始化工作之後,指向Solver類的指標solver開始呼叫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.";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Step()函式主要分為三個部分,首先是一個大迴圈設定了總的迭代次數,在每次迭代中訓練iter_size * 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函式得到
for (int i = 0; i < param_.iter_size(); ++i)
{
//主要完成了前向後向的計算,
//前向用於計算模型的最終輸出和Loss,後向用於計算每一層網路和引數的梯度。
loss += net_->ForwardBackward();
}
...
//主要對Loss進行平滑。由於Caffe的訓練方式是SGD,無法把所有的資料同時放入模型進行訓練,那麼部分資料產生的Loss就可能會和全樣本的平均Loss不同,在必要時候將Loss和歷史過程中更新的Loss求平均就可以減少Loss的振盪問題。
UpdateSmoothedLoss(loss, start_iter, average_loss);
...
// 執行梯度的更新,這個函式在基類Solver中沒有實現,會呼叫每個子類自己的實現
ApplyUpdate();
// 迭代次數加1
++iter_;
...
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
ForwardBackward()函式如下:
// net.hpp
// 進行一次正向傳播,一次反向傳播
Dtype ForwardBackward() {
Dtype loss;
Forward(&loss);
Backward();
return loss;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
Forward(&loss)函式最終會執行到如下程式碼:
//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); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
具體的每一層Layer的派生類均會重寫Forward()函式來實現不同層的前向計算功能。Backward()反向求導函式也和Forward()類似,呼叫不同層的Backward()函式來計算每層的梯度。
ApplyUpdate()函式是Solver類的純虛擬函式,需要派生類來實現。SGDSolver類實現的ApplyUpdate()函式:
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個引數的梯度除以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();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
ApplyUpdate()函式主要完成以下工作:
- 設定引數的學習率;
- 對梯度進行Normalize;
- 對反向求導得到的梯度新增正則項的梯度;
- 最後根據SGD演算法計算最終的梯度;
- 最後的最後把計算得到的最終梯度對權值進行更新。