1. 程式人生 > >解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt

解析./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演算法計算最終的梯度; 
- 最後的最後把計算得到的最終梯度對權值進行更新。