基於CPU版本的Caffe推理框架

啊順發表於2020-11-16

最近一段時間,認真研究了一下caffe。但是,裡面內容過多,集合了CPU版本和GPU版本的程式碼,導致閱讀起來有些複雜。因此,特意對caffe程式碼進行了重構,搭建一個基於CPU版本的Caffe推理框架。

此簡化的Caffe推理框架具有以下特點:

  1. 只有CPU推理功能,無需GPU;
  2. 只有前向計算能力,無後向求導功能;
  3. 介面保持與原版的Caffe一致;
  4. 精簡了大部分程式碼,並進行了詳盡註釋。

通過對Caffe的重構,理解了如何搭建一個推理框架,如何從輸入一張圖片從而得到結果。注意:此框架只是用於教學使用,通過最簡單的圖片分類的方式來理解框架,不保證適合不同的任務。

專案地址為:https://gitee.com/dengshunge/simple-caffe-inference

 


一、SimpleCaffe的結構

SimpleCaffe中保持了與原版caffe同樣的程式碼結構,整體結構始終貫穿著Blob、Layer和Net這3大類,分別負責資料的儲存、網路的層次和網路骨架:

  • Blob:負責資料的傳輸,其中資料主要包括每層的輸入輸出資料、模型的權重等;
  • Layer:負責構建模型的每一層,主要處理如何將該層的輸入轉換成輸出;
  • Net:負責搭建網路的骨架,將每層Layer進行組合。

其主要結構圖如下所示

 

在SyncedMemory中,其主要負責CPU資料與GPU資料進行同步(此版本沒有GPU,只是保留介面),其主要包含4個函式,分別用於讀取CPU常量資料、設定CPU資料、獲取CPU可變資料和將資料同步至CPU。

在Blob中,有兩個重要的變數:$data\_$表示儲存的資料,包括模型權重和每層的計算結果;$shape\_$表示這些資料的形狀shape。而函式則包含:

  • $Reshape()$根據傳入的shape資訊,通過呼叫SyncedMemory來為$data\_$分配記憶體空間;
  • $count()$表示切片的容量,如果不傳入引數的話,返回的結果是$N \times C \times H \times W$;
  • $cpu\_data()$負責讀取cpu中的資料,為常量,只可讀,其實現就是呼叫SyncedMemory中的$cpu\_data()$;
  • $set\_cpu\_data()$是為cpu中的資料空間進行賦值,其實現就是呼叫SyncedMemory中的$set\_cpu\_data()$;
  • $mutable\_cpu\_data()$是獲取指向該資料的指標,可以通過該指標來修改內部資料。

在Layer_factory和Layer中,其主要是利用了工廠模式的方式來進行組合,將每個特定層Layer進行註冊,通過Layer_factory的多型性進行呼叫。在Layer中,$blob\_$表示模型的權重,即可學習的引數,例如卷積中的卷積核;而$SetUp()$函式是Layer中最重要的函式,其包含著檢查輸入輸出是否合規、構建特定層和為top層分配空間等作用;最後的$Forward\_cpu()$則是前向計算的介面,將bottom的資料進行計算,放在top層空間中。

在Net中,其主要的作用是將所有構建的層按照特定順序進行組合,並儲存每層計算結果,得到最終模型輸出結構。其中$layers$是一個vector,裡面儲存著指向在建立的layer,可以理解成指向每一層;$blobs$是一個vector,裡面儲存著指向blob的指標,代表著每層計算的結果;$bottom\_vecs$和$top\_vecs$裡面儲存在指向bottom層或者top層的指標。其餘成員函式的作用分別是:

  • $Init()$用於構建模型結構,可以理解成搭網路;
  • $Forward()$表示模型的前向計算,其主要就是呼叫每個layer的$Forward\_cpu()$;
  • $CopyTrainedLayersFrom()$的作用就是將訓練好的caffemodel檔案中的權重,拷貝到已經建立好的blob中。

 

二、SimpleCaffe的推理流程

接下來,結合具體的程式碼,講述一下SimpleCaffe如何進行推理的,以圖片分類為例。

main函式的入口為$tool/caffe.cpp$,其虛擬碼如下所示。這與人們常見的思維方式的流程一致,我寫出了裡面幾個比較重要的函式,分別用於構建網路、載入權重和前向計算。接下來將會對這幾部分進行注重講解。

int main()
{
    ///設定prototxt和caffemodel的路徑
    //..

    ///設定caffe的工作模型,cpu或者gpu
    //..

    ///根據prototxt檔案來建立網路,並將caffemodel中的權重載入進網路中
    Net<float> caffe_net(model, caffe::TEST, 0, nullptr);
    caffe_net.CopyTrainedLayersFrom(weights);
    //..

    ///對圖片進行處理,並將處理完的資料放入到網路的輸入層對應的blob中
    //..

    ///進行模型的前向計算,並對結果進行後處理
    caffe_net.Forward();
    //..
}

2.1 構建網路

對於網路的構建,是通過構建Net的物件來完成的,其流程圖如下所示。

具體的步驟如下描述:

  1. 讀取並解析prototxt檔案。
  2. 根據規則,加入或者排除某些層。因為在prototxt檔案中裡面包含著某些只有訓練才用的層,例如loss層等。根據這些規則,在推理階段可以將這些層給排除掉。
  3. 加入Split層。因為在我們搭建prototxt中,經常沒有用split層,例如resnet中的的殘差結構,沒有專門寫一份split層。這個split層的主要作用是將一個blob的資料複製成N份,方便接下來的層使用。所以,在這裡需要判斷哪些層需要進行Split,並在這些層之後建立Split層。
  4. 通過第1/2/3步,就已經能知道我們在這次推理中,需要哪些層了。接下來就是需要迴圈的建立每一層。
  5. 在第4步迴圈建立每一層中,首先需要呼叫工廠模式來建立一個指向該層的指標,但該指標只是指向該例項。
  6. 呼叫$AppendBottom()$和$AppendTop()$來為blob建立指標,但未開闢空間。這兩個函式的配合方式是:在$AppendTop()$中建立指向blob的智慧指標$blob\_pointer$ ,並將此指標加入到$top\_vecs$中,並記錄下$blob\_name$,$layer\_id$和$blob\_id$;然後在$AppendBottom()$中,根據bottom的名字,找到上一層top的名字和上一層的$blob\_id$,由此得到在$AppendTop()$中的建立的$blob\_pointer$的指標,加入到$bottom\_vecs\_$。從而將同一層的bottom和top進行關聯起來。
  7. 當把該層的bottom和top關聯起來後,就需要呼叫在$include/caffe/layer.hpp$中的$SetUp()$函式,構建這一層,需要把bottom和top對應的指向blob的指標傳入進去。
  8. 在$CheckBlobCounts()$函式中,會首先檢查輸入blob的數量和輸出blob的數量是否滿足在該層的定義的數量要求。例如relu層,需要滿足一個輸入和一個輸入,如果你傳入兩個輸入,則會報錯。
  9. 呼叫$LayerSetUp()$函式,用於為為特定引數分配空間。例如在conv層中,需要記錄下stride\pad\dilation等引數,同時為kernel分配空間大小。
  10. 在$Reshape()$中,需要為傳入進行來top的指標(即top層對應的blob的指標)分配合適的空間。注意,這裡只為top層分配空間,因為下一層的bottom會指向上一層的top。

通過上面的10步操作,就完成了網路的搭建,將每層的bottom和top關聯起來,並分配了空間。

 

2.2 前向計算

當搭建完網路後,就需要進行前向計算。在前向計算中$net.cpp$會迴圈呼叫每一層的$Forward()$函式,在$layer.hpp$中的$Forward()$首先先進行$Reshape()$操作(即檢查空間大小),然後執行虛擬函式$Forward\_cpu()$,這個虛擬函式$Forward\_cpu()$需要在每個層檔案中自己定義如何進行實現,將bottom的資料計算得到top的資料。以普通的卷積conv層操作為例:

  1. 取出bottom和top的指標,迴圈每個batch size;
  2. 將特徵圖進行im2col轉換,提高cache的命中率,加快計算速度;
  3. 將卷積核和特徵圖進行矩陣相乘的操作$caffe\_cpu\_gemm$,並將計算後的結果放入top指向空間。

通過$Forward()$函式,就完成了該層的前向計算操作。由於是迴圈每一層的$Forward()$函式,最終就得到模型的輸出結果。

 

2.3 載入權重

載入模型權重是通過呼叫$net.cpp$檔案中的$CopyTrainedLayersFrom()$函式。其主要邏輯是:

  1. 解析caffemodel檔案,會得到一個字典(key為layer的名字,value為值);
  2. 根據caffemodel的key的名字來尋找已經建好的網路中對應的layer的名字;
  3. 如果找到相應的名字,則呼叫$blob.cpp$檔案中的$FromProto()$函式,檢查分配的空間大小是否一致,然後將caffemodel中該層的每個值複製到已經建好的blob中。

由於在實際空間中,資料是以一維的尺寸進行存放的,而且是連續的,所以能進行迴圈的複製。如此一來,網路中的每個blob就得到了訓練好的權重,可以進行推理了。

 

三、其餘輔助檔案

在完成上述操作的基礎上,還需要很多輔助函式的幫助,下面介紹一下:

  • $im2col$檔案,這個檔案的作用是將特徵圖進行im2col操作,提高cache命中率;
  • $io$檔案,主要負責讀取和解析prototxt與caffemodel;
  • $math\_functions$檔案和$mkl\_alternate$檔案,負責定義caffe中常用的數學操作,例如矩陣相乘,元素相加等;
  • $upgrade\_proto$檔案,負責相容舊的caffe版本的網路,用於將舊的層升級成新的層。

 

四、總結

上述只是以巨集觀的視角,大略介紹了一下SimpleCaffe的框架,具體更多的細節,需要仔細研讀下專案。然而,在本次專案中,存在著很多不懂的地方,有待補充:

  • cmake檔案的編寫;
  • cblas或者openblas的用法;
  • proto的編寫,和如何進行解析的。

 

參考資料:

  1. Caffe原始碼導讀
  2. caffe原始碼深入學習5:超級詳細的caffe卷積層程式碼解析
  3. caffe原始碼深入學習6:超級詳細的im2col繪圖解析,分析caffe卷積操作的底層實現
  4. Caffe原始碼(一):math_functions 分析

相關文章