最近一段時間,認真研究了一下caffe。但是,裡面內容過多,集合了CPU版本和GPU版本的程式碼,導致閱讀起來有些複雜。因此,特意對caffe程式碼進行了重構,搭建一個基於CPU版本的Caffe推理框架。
此簡化的Caffe推理框架具有以下特點:
- 只有CPU推理功能,無需GPU;
- 只有前向計算能力,無後向求導功能;
- 介面保持與原版的Caffe一致;
- 精簡了大部分程式碼,並進行了詳盡註釋。
通過對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的物件來完成的,其流程圖如下所示。
具體的步驟如下描述:
- 讀取並解析prototxt檔案。
- 根據規則,加入或者排除某些層。因為在prototxt檔案中裡面包含著某些只有訓練才用的層,例如loss層等。根據這些規則,在推理階段可以將這些層給排除掉。
- 加入Split層。因為在我們搭建prototxt中,經常沒有用split層,例如resnet中的的殘差結構,沒有專門寫一份split層。這個split層的主要作用是將一個blob的資料複製成N份,方便接下來的層使用。所以,在這裡需要判斷哪些層需要進行Split,並在這些層之後建立Split層。
- 通過第1/2/3步,就已經能知道我們在這次推理中,需要哪些層了。接下來就是需要迴圈的建立每一層。
- 在第4步迴圈建立每一層中,首先需要呼叫工廠模式來建立一個指向該層的指標,但該指標只是指向該例項。
- 呼叫$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進行關聯起來。
- 當把該層的bottom和top關聯起來後,就需要呼叫在$include/caffe/layer.hpp$中的$SetUp()$函式,構建這一層,需要把bottom和top對應的指向blob的指標傳入進去。
- 在$CheckBlobCounts()$函式中,會首先檢查輸入blob的數量和輸出blob的數量是否滿足在該層的定義的數量要求。例如relu層,需要滿足一個輸入和一個輸入,如果你傳入兩個輸入,則會報錯。
- 呼叫$LayerSetUp()$函式,用於為為特定引數分配空間。例如在conv層中,需要記錄下stride\pad\dilation等引數,同時為kernel分配空間大小。
- 在$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層操作為例:
- 取出bottom和top的指標,迴圈每個batch size;
- 將特徵圖進行im2col轉換,提高cache的命中率,加快計算速度;
- 將卷積核和特徵圖進行矩陣相乘的操作$caffe\_cpu\_gemm$,並將計算後的結果放入top指向空間。
通過$Forward()$函式,就完成了該層的前向計算操作。由於是迴圈每一層的$Forward()$函式,最終就得到模型的輸出結果。
2.3 載入權重
載入模型權重是通過呼叫$net.cpp$檔案中的$CopyTrainedLayersFrom()$函式。其主要邏輯是:
- 解析caffemodel檔案,會得到一個字典(key為layer的名字,value為值);
- 根據caffemodel的key的名字來尋找已經建好的網路中對應的layer的名字;
- 如果找到相應的名字,則呼叫$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的編寫,和如何進行解析的。