caffe學(xué)習(xí)系列二:源碼深入解析-單步跟蹤調(diào)試指南

為了更好的學(xué)習(xí)caffe,我們利用上節(jié)安裝好的環(huán)境,進(jìn)行單步調(diào)試,以窺caffe全貌。

準(zhǔn)備工作:要在vs2013中單步跟蹤調(diào)試caffe,需要配置caffe工程,打開(kāi)【屬性】-【調(diào)試】-【命令行參數(shù)】中加入輸入?yún)?shù)。如下配置:

image.png

先貼一張caffe的整體處理流程:


image.png

一、函數(shù)入口

眾所周知,caffe由c++寫的,而c++的入口函數(shù)為main,我們?cè)赾affe.cpp文件中找到main函數(shù),關(guān)鍵代碼如下:

1 int main(int argc, char** argv) {
2       .....
3       caffe::GlobalInit(&argc, &argv);
4       ......
5       return GetBrewFunction(caffe::string(argv[1]))();
6       ....
7 }

函數(shù)進(jìn)來(lái)后首先進(jìn)行g(shù)flags的一些初始化,設(shè)置并打印版本信息,用戶信息等。
接著進(jìn)行的是GlobalInit函數(shù),主要作用是對(duì)gflags和glog的一些初始化,該函數(shù)定義在了caffe安裝目錄./src/caffe/common.cpp中。 其中g(shù)flags是google的一個(gè)開(kāi)源的處理命令行參數(shù)的庫(kù),而glog是google的開(kāi)源日志庫(kù)。
上面完成了一些初始化工作,而真正的程序入口就是下面這個(gè)GetBrewFunction函數(shù),GetBrewFunction函數(shù)的入?yún)閍rgv[1],也就是我們train字符串。而GetBrewFunction的返回值為g_brew_map。那么g_brew_map是怎么實(shí)現(xiàn)的呢?代碼如下:

1 typedef int (*BrewFunction)();
2 typedef std::map<caffe::string, BrewFunction> BrewMap;
3 BrewMap g_brew_map;

首先通過(guò) typedef定義函數(shù)指針 typedef int (*BrewFunction)(); 這個(gè)是用typedef定義函數(shù)指針?lè)椒?。然后再把輸入的字符串和以該字符串命名的函?shù)用map容器關(guān)聯(lián)起來(lái),這樣就實(shí)現(xiàn)了用戶輸入字符串調(diào)用相應(yīng)函數(shù)的功能。
在caffe.cpp 中 BrewFunction 作為GetBrewFunction()函數(shù)的返回類型,可以是 train(),test(),device_query(),time() 這四個(gè)函數(shù)指針的其中一個(gè)。四個(gè)函數(shù)功能如下:
(1)train: 訓(xùn)練或者調(diào)整一個(gè)模型
(2)test : 在測(cè)試集上測(cè)試一個(gè)模型
(3)device_query : 打印GPU的調(diào)試信息
(4)time: 壓測(cè)一個(gè)模型的執(zhí)行時(shí)間
caffe是通過(guò)RegisterBrewFunction()實(shí)現(xiàn)四個(gè)函數(shù)的注冊(cè)的,具體代碼如下:

1 #define RegisterBrewFunction(func) \
2 namespace { \
3 class __Registerer_##func { \
4 public: /* NOLINT */ \
5 __Registerer_##func() { \
6   g_brew_map[#func] = &func; \
7 } \
8 }; \
9 __Registerer_##func g_registerer_##func; \
10 }

這里有必要解釋下#的用法,相信很多人看這段宏定義代碼都懵吧。在C/C++的宏中,"#"的功能是將其后面的宏參數(shù)進(jìn)行字符串化操作(Stringfication),簡(jiǎn)單說(shuō)就是在對(duì)它所引用的宏變量通過(guò)替換后在其左右各加上一個(gè)雙引號(hào)。而”##”被稱為連接符(concatenator),用來(lái)將兩個(gè)子串Token連接為一個(gè)Token。注意這里連接的對(duì)象是Token就行,而不一定是宏的變量。 所謂的子串(token)就是指編譯器能夠識(shí)別的最小語(yǔ)法單元。這樣以上的宏定義代碼的功能就顯而易見(jiàn)了。如果需要,也可以增加你自己的函數(shù),然后通過(guò)RegisterBrewFunction()注冊(cè)一下即可使用。

二、train函數(shù)

接著調(diào)用train()函數(shù),train函數(shù)中主要有三個(gè)方法ReadSolverParamsFromTextFileOrDie、CreateSolver、Solve。關(guān)鍵代碼如下所示:

 1 // Train / Finetune a model.
 2 int train() {
 3   ......
 4   caffe::SolverParameter solver_param;
 5   caffe::ReadSolverParamsFromTextFileOrDie(FLAGS_solver, &solver_param);//從-solver參數(shù)讀取solver_param
 6   ......
 7   shared_ptr<caffe::Solver<float> >
 8       solver(caffe::SolverRegistry<float>::CreateSolver(solver_param));//從參數(shù)創(chuàng)建solver,同樣采用string到函數(shù)指針的映射實(shí)現(xiàn),用到了工廠模式
 9 
10   if (FLAGS_snapshot.size()) {//迭代snapshot次后保存模型一次
11     LOG(INFO) << "Resuming from " << FLAGS_snapshot;
12     solver->Restore(FLAGS_snapshot.c_str());
13   } else if (FLAGS_weights.size()) {//若采用finetuning,則拷貝weight到指定模型
14     CopyLayers(solver.get(), FLAGS_weights);
15   }
16 
17   if (gpus.size() > 1) {
18     caffe::P2PSync<float> sync(solver, NULL, solver->param());
19     sync.Run(gpus);
20   } else {
21     LOG(INFO) << "Starting Optimization";
22     solver->Solve();//開(kāi)始訓(xùn)練網(wǎng)絡(luò)
23   }
24   LOG(INFO) << "Optimization Done.";
25   return 0;
26 }

1、初始化
train函數(shù)進(jìn)來(lái)后先進(jìn)行一些參數(shù)檢測(cè)工作,檢測(cè)FLAGS_solver.size()是否為零,為零的話表示用戶沒(méi)有傳入solver文件 ;接著再檢查參數(shù)里面--weights和--snapshot有沒(méi)有同時(shí)出現(xiàn),因?yàn)?-weights是 在從頭啟動(dòng)訓(xùn)練的時(shí)候需要的參數(shù),表示對(duì)模型的finetune,而--snapshot表示的是繼續(xù)訓(xùn)練模型, 這種情況對(duì)應(yīng)于用戶之前暫停了模型訓(xùn)練,現(xiàn)在繼續(xù)訓(xùn)練。因此不再需要weight參數(shù)。接著就是去獲取并解析用戶定義的solver.prototxt文件。
2、讀取solver參數(shù):ReadSolverParamsFromTextFileOrDie
caffe::ReadSolverParamsFromTextFileOrDie(FLAGS_solver, &solver_param)解析-solver指定的solver.prototxt的文件內(nèi)容到solver_param中
3、查詢GPU信息,并進(jìn)行GPU初始化,如果未配置,則直接跳過(guò)
查詢用戶配置的GPU信息,用戶可以在輸入命令行的時(shí)候配置gpu信息,也可以在solver.prototxt 文件中定義GPU信息,如果用戶在solver.prototxt里面配置了GPU的id,則將該id寫入FLAGS_gpu中,如果用戶只是說(shuō)明了使用gpu模式,而沒(méi)有詳細(xì)指定使用的gpu的id,則將gpu的id默認(rèn)為0。然后根據(jù)gpu的檢測(cè)結(jié)果,如果沒(méi)有g(shù)pu信息,那么則使用cpu訓(xùn)練,否則,就開(kāi)始一些GPU訓(xùn)練的初始化工作。
3、構(gòu)造網(wǎng)絡(luò)訓(xùn)練器:CreateSolver
CreateSolver函數(shù)實(shí)現(xiàn)細(xì)節(jié)如下:

1  static Solver<Dtype>* CreateSolver(const SolverParameter& param) {
2    const string& type = param.type();
3    CreatorRegistry& registry = Registry();
4    CHECK_EQ(registry.count(type), 1) << "Unknown solver type: " << type
5        << " (known types: " << SolverTypeListString() << ")";
6    return registry[type](param);
7  }

首先通過(guò)CreatorRegistry&registry = Registry()對(duì)caffe的所有求解器進(jìn)行注冊(cè),并通過(guò)map容器將求解器的名稱字符串和對(duì)應(yīng)函數(shù)指針聯(lián)結(jié)起來(lái)。最后返回registrytype;所以該段程序的核心功能是執(zhí)行了solver參數(shù)中的求解器對(duì)應(yīng)的構(gòu)造函數(shù),如Creator_SGDSolver(const SolverParameter& param)函數(shù)。

class SGDSolver : public Solver<Dtype>

構(gòu)建SGDSolver,首先執(zhí)行Solver類的構(gòu)造,該函數(shù)是初始化的入口,調(diào)用 void Solver<Dtype>::Init(const SolverParameter& param),該函數(shù)內(nèi)有InitTrainNet()、InitTestNets()。對(duì)于InitTrainNet函數(shù),首先進(jìn)行網(wǎng)絡(luò)的參數(shù)初始化,再調(diào)用Net類的構(gòu)造函數(shù)

1 net_.reset(new Net<Dtype>(net_param));

Net構(gòu)造中先執(zhí)行Init()操作,該函數(shù)具體的內(nèi)容如下圖和源碼所示:

 1 template <typename Dtype>
 2 void Net<Dtype>::Init(const NetParameter& in_param) {
 3   ........//過(guò)濾校驗(yàn)參數(shù)FilterNet
 4   FilterNet(in_param, &filtered_param);
 5   .........//插入Splits層
 6   InsertSplits(filtered_param, &param);
 7   .......// 構(gòu)建網(wǎng)絡(luò)中輸入輸出存儲(chǔ)結(jié)構(gòu)
 8   bottom_vecs_.resize(param.layer_size());
 9   top_vecs_.resize(param.layer_size());
10   bottom_id_vecs_.resize(param.layer_size());
11   param_id_vecs_.resize(param.layer_size());
12   top_id_vecs_.resize(param.layer_size());
13   bottom_need_backward_.resize(param.layer_size());
14 
15   for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {
16    ...//創(chuàng)建層
17  layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));
18     layer_names_.push_back(layer_param.name());
19     LOG_IF(INFO, Caffe::root_solver())
20         << "Creating Layer " << layer_param.name();
21     bool need_backward = false;
22 
23     // Figure out this layer's input and output
24     for (int bottom_id = 0; bottom_id < layer_param.bottom_size();
25          ++bottom_id) {
26       const int blob_id = AppendBottom(param, layer_id, bottom_id,
27                                        &available_blobs, &blob_name_to_idx);
28 
29 
30    ........//創(chuàng)建相關(guān)blob
31     // If the layer specifies that AutoTopBlobs() -> true and the LayerParameter
32     // specified fewer than the required number (as specified by
33     // ExactNumTopBlobs() or MinTopBlobs()), allocate them here.
34     Layer<Dtype>* layer = layers_[layer_id].get();
35     if (layer->AutoTopBlobs()) {
36       const int needed_num_top =
37           std::max(layer->MinTopBlobs(), layer->ExactNumTopBlobs());
38       for (; num_top < needed_num_top; ++num_top) {
39         // Add "anonymous" top blobs -- do not modify available_blobs or
40         // blob_name_to_idx as we don't want these blobs to be usable as input
41         // to other layers.
42         AppendTop(param, layer_id, num_top, NULL, NULL);
43       }
44     }
45 
46 
47     .....//執(zhí)行SetUp()
48     // After this layer is connected, set it up.
49     layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);
50     LOG_IF(INFO, Caffe::root_solver())
51         << "Setting up " << layer_names_[layer_id];
52     for (int top_id = 0; top_id < top_vecs_[layer_id].size(); ++top_id) {
53       if (blob_loss_weights_.size() <= top_id_vecs_[layer_id][top_id]) {
54         blob_loss_weights_.resize(top_id_vecs_[layer_id][top_id] + 1, Dtype(0));
55       }
56       blob_loss_weights_[top_id_vecs_[layer_id][top_id]] = layer->loss(top_id);
57       LOG_IF(INFO, Caffe::root_solver())
58           << "Top shape: " << top_vecs_[layer_id][top_id]->shape_string();
59       if (layer->loss(top_id)) {
60         LOG_IF(INFO, Caffe::root_solver())
61             << "    with loss weight " << layer->loss(top_id);
62       }
63       memory_used_ += top_vecs_[layer_id][top_id]->count();
64     }
65     LOG_IF(INFO, Caffe::root_solver())
66         << "Memory required for data: " << memory_used_ * sizeof(Dtype);
67     const int param_size = layer_param.param_size();
68     const int num_param_blobs = layers_[layer_id]->blobs().size();
69     CHECK_LE(param_size, num_param_blobs)
70         << "Too many params specified for layer " << 

SetUp是怎么構(gòu)建的呢?

1 virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
 2       const vector<Blob<Dtype>*>& top) {}
 3 
 4  void SetUp(const vector<Blob<Dtype>*>& bottom,
 5       const vector<Blob<Dtype>*>& top) {
 6     InitMutex();
 7     CheckBlobCounts(bottom, top);
 8     LayerSetUp(bottom, top);
 9     Reshape(bottom, top);
10     SetLossWeights(top);
11   }
image.png

初始化的總體流程大概就是新建一個(gè)Solver對(duì)象,然后調(diào)用Solver類的構(gòu)造函數(shù),然后在Solver的構(gòu)造函數(shù)中又會(huì)新建Net類實(shí)例,在Net類的構(gòu)造函數(shù)中又會(huì)新建各個(gè)layer的實(shí)例,一直具體到設(shè)置每個(gè)Blob,大概就完成了網(wǎng)絡(luò)初始化的工作了。
(3)Solve
train函數(shù)中CreateSolver()執(zhí)行完成后,接下來(lái)是具體訓(xùn)練過(guò)程,執(zhí)行Solve()函數(shù)---->Step()--->結(jié)束

Solve的具體內(nèi)容和代碼:

 1 template <typename Dtype>
 2 void Solver<Dtype>::Solve(const char* resume_file) {
 3   CHECK(Caffe::root_solver());
 4   LOG(INFO) << "Solving " << net_->name();
 5   LOG(INFO) << "Learning Rate Policy: " << param_.lr_policy();
 6 
 7   // For a network that is trained by the solver, no bottom or top vecs
 8   // should be given, and we will just provide dummy vecs.
 9   int start_iter = iter_;
10   Step(param_.max_iter() - iter_);
11   
12   // overridden by setting snapshot_after_train := false
13   if (param_.snapshot_after_train()
14       && (!param_.snapshot() || iter_ % param_.snapshot() != 0)) {
15     Snapshot();
16   }
17  
18   // display loss
19   if (param_.display() && iter_ % param_.display() == 0) {
20     int average_loss = this->param_.average_loss();
21     Dtype loss;
22     net_->Forward(&loss);
23 
24     UpdateSmoothedLoss(loss, start_iter, average_loss);
25 
26     
27   if (param_.test_interval() && iter_ % param_.test_interval() == 0) {
28     TestAll();
29   }
30 }

然后開(kāi)始執(zhí)行Step函數(shù),具體內(nèi)容和代碼:

1 template <typename Dtype>  
 2 void Solver<Dtype>::Step(int iters)  
 3 {  
 4     // 起始迭代步數(shù)  
 5     const int start_iter = iter_;  
 6     // 終止迭代步數(shù)  
 7     const int stop_iter = iter_ + iters;  
 8 
 9     // 判斷是否已經(jīng)完成設(shè)定步數(shù)  
10     while (iter_ < stop_iter)  
11     {  
12         // 將net_中的Bolb梯度參數(shù)置為零  
13         net_->ClearParamDiffs();  
14 
15         ...  
16 
17         // accumulate the loss and gradient  
18         Dtype loss = 0;  
19         for (int i = 0; i < param_.iter_size(); ++i)  
20         {  
21             // 正向傳導(dǎo)和反向傳導(dǎo),并計(jì)算loss  
22             loss += net_->ForwardBackward();  
23         }  
24         loss /= param_.iter_size();  
25 
26         // 為了輸出結(jié)果平滑,將臨近的average_loss個(gè)loss數(shù)值進(jìn)行平均,存儲(chǔ)在成員變量smoothed_loss_中  
27         UpdateSmoothedLoss(loss, start_iter, average_loss);  
28 
29         // BP算法更新權(quán)重  
30         ApplyUpdate();  
31 
32         // Increment the internal iter_ counter -- its value should always indicate  
33         // the number of times the weights have been updated.  
34         ++iter_;  
35     }  
36 }  

while循環(huán)中先調(diào)用了網(wǎng)絡(luò)類Net::ForwardBackward()成員函數(shù)進(jìn)行正向傳導(dǎo)和反向傳導(dǎo),并計(jì)算loss

1 Dtype ForwardBackward() {
2     Dtype loss;
3     //正向傳導(dǎo)
4     Forward(&loss);
5     //反向傳導(dǎo)
6     Backward();
7     return loss;
8   }

而Fordward函數(shù)中調(diào)用了ForwardFromTo,而FordwardFromTo又調(diào)用了每個(gè)layer的Fordward。反向傳導(dǎo)函數(shù)Backward()調(diào)用了BackwardFromTo(int start, int end)函數(shù)。正向傳導(dǎo)和反向傳導(dǎo)結(jié)束后,再調(diào)用SGDSolver::ApplyUpdate()成員函數(shù)進(jìn)行權(quán)重更新。

(1)ForwardBackward:按順序調(diào)用了Forward和Backward。
(2)ForwardFromTo(int start, int end):執(zhí)行從start層到end層的前向傳遞,采用簡(jiǎn)單的for循環(huán)調(diào)用。,forward只要計(jì)算損失loss
(3)BackwardFromTo(int start, int end):和前面的ForwardFromTo函數(shù)類似,調(diào)用從start層到end層的反向傳遞。backward主要根據(jù)loss來(lái)計(jì)算梯度,caffe通過(guò)自動(dòng)求導(dǎo)并反向組合每一層的梯度來(lái)計(jì)算整個(gè)網(wǎng)絡(luò)的梯度。
(4)ToProto函數(shù)完成網(wǎng)絡(luò)的序列化到文件,循環(huán)調(diào)用了每個(gè)層的ToProto函數(shù)

1 template <typename Dtype>  
 2 void SGDSolver<Dtype>::ApplyUpdate()  
 3 {  
 4     // 獲取當(dāng)前學(xué)習(xí)速率  
 5     Dtype rate = GetLearningRate();  
 6     if (this->param_.display() && this->iter_ % this->param_.display() == 0)  
 7     {  
 8         LOG(INFO) << "Iteration " << this->iter_ << ", lr = " << rate;  
 9     }  
10 
11     // 在計(jì)算當(dāng)前梯度的時(shí)候,如果該值超過(guò)了閾值clip_gradients,則將梯度直接設(shè)置為該閾值  
12     // 此處閾值設(shè)為-1,即不起作用  
13     ClipGradients();  
14 
15     // 逐層更新網(wǎng)絡(luò)中的可學(xué)習(xí)層  
16     for (int param_id = 0; param_id < this->net_->learnable_params().size();  
17        ++param_id)  
18     {  
19         // 歸一化  
20         Normalize(param_id);  
21         // L2范數(shù)正則化添加衰減權(quán)重  
22         Regularize(param_id);  
23         // 隨機(jī)梯度下降法計(jì)算更新值  
24         ComputeUpdateValue(param_id, rate);  
25     }  
26     // 更新權(quán)重  
27     this->net_->Update();  
28 } 

最后將迭代次數(shù)++iter_,繼續(xù)while循環(huán),直到迭代次數(shù)完成。 這就是整個(gè)網(wǎng)絡(luò)的訓(xùn)練過(guò)程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容