Caffe代码阅读
轉(zhuǎn)載自:
Caffe代碼閱讀——層次結(jié)構(gòu) - 無痛的機(jī)器學(xué)習(xí) - 知乎專欄 ?https://zhuanlan.zhihu.com/p/21796890
Caffe源碼閱讀——Net組裝 - 無痛的機(jī)器學(xué)習(xí) - 知乎專欄 ?https://zhuanlan.zhihu.com/p/21875025
Caffe代碼閱讀——Solver - 無痛的機(jī)器學(xué)習(xí) - 知乎專欄 ?https://zhuanlan.zhihu.com/p/21800004
1.Caffe代碼閱讀——層次結(jié)構(gòu)
作者:馮超鏈接:https://zhuanlan.zhihu.com/p/21796890
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
Caffe是一款優(yōu)秀的深度神經(jīng)網(wǎng)絡(luò)的開源軟件,下面我們來聊聊它的源代碼以及它的實(shí)現(xiàn)。Caffe的代碼整體上可讀性很好,架構(gòu)比較清晰,閱讀代碼并不算是一件很困難的事情。不過在閱讀代碼之前還是要回答兩個(gè)問題:
閱讀代碼大體上來說有下面幾個(gè)目的:
我們的目標(biāo)是擴(kuò)展代碼。Caffe中主要的擴(kuò)展點(diǎn)就是Layer和Solver,當(dāng)然其他的部分也可以擴(kuò)展,只不過要改動(dòng)的代碼會(huì)多一些。
當(dāng)確定了上面第一個(gè)問題,下面就是第二個(gè)問題了。讀代碼要讀到什么程度?一般來說,我覺得閱讀代碼這件事情可以用一個(gè)Logistic型的函數(shù)來表示:
這個(gè)圖上,橫軸是閱讀代碼花費(fèi)的時(shí)間,縱軸是閱讀代碼帶來的效果。對(duì)于代碼量比較大的項(xiàng)目,一開始閱讀肯定是蒙的,需要花一定的時(shí)間梳理清楚各個(gè)文件,各個(gè)模塊之間的關(guān)系。隨著結(jié)構(gòu)關(guān)系逐漸清晰,讀者開始領(lǐng)會(huì)代碼中所表達(dá)的含義,閱讀代碼的效果直線上升。然而當(dāng)我們把代碼主線和重要支線弄懂后,再讀一些小支線的收益就不會(huì)太大。所以根據(jù)閱讀代碼的性價(jià)比和Caffe代碼自身的特點(diǎn),我們只會(huì)將主線和一些重要支線閱讀完,估計(jì)也就是整體代碼量的一半。
Caffe代碼的主線結(jié)構(gòu)抽象
不同于其他的一些框架,Caffe沒有采用符號(hào)計(jì)算的模式進(jìn)行編寫,整體上的架構(gòu)以系統(tǒng)級(jí)的抽象為主。所謂的抽象,就是逐層地封裝一些細(xì)節(jié)問題,讓上層的代碼變得更加清晰。那么就讓我們來順著Caffe的抽象層級(jí)看看Caffe的主線結(jié)構(gòu):
SyncedMem:這個(gè)類的主要功能是封裝CPU和GPU的數(shù)據(jù)交互操作。一般來說,數(shù)據(jù)的流動(dòng)形式都是:硬盤->CPU內(nèi)存->GPU內(nèi)存->CPU內(nèi)存->(硬盤),所以在寫代碼的過程中經(jīng)常會(huì)寫CPU/GPU之間數(shù)據(jù)傳輸?shù)拇a,同時(shí)還要維護(hù)CPU和GPU兩個(gè)處理端的內(nèi)存指針。這些事情處理起來不會(huì)很難,但是會(huì)很繁瑣。因此SyncedMem的出現(xiàn)就是把CPU/GPU的數(shù)據(jù)傳輸操作封裝起來,只需要調(diào)用簡(jiǎn)單的接口就可以獲得兩個(gè)處理端同步后的數(shù)據(jù)。
Blob:這個(gè)類做了兩個(gè)封裝:一個(gè)是操作數(shù)據(jù)的封裝。在這里使用Blob,我們可以操縱高維的數(shù)據(jù),可以快速訪問其中的數(shù)據(jù),變換數(shù)據(jù)的維度等等;另一個(gè)是對(duì)原始數(shù)據(jù)和更新量的封裝。每一個(gè)Blob中都有data和diff兩個(gè)數(shù)據(jù)指針,data用于存儲(chǔ)原始數(shù)據(jù),diff用于存儲(chǔ)反。向傳播的梯度更新值。Blob使用了SyncedMem,這樣也得到了不同處理端訪問的便利。這樣Blob就基本實(shí)現(xiàn)了整個(gè)Caffe數(shù)據(jù)部分結(jié)構(gòu)的封裝,在Net類中可以看到所有的前后向數(shù)據(jù)和參數(shù)都用Blob來表示就足夠了。
數(shù)據(jù)的抽象到這個(gè)就可以了,接下來是層級(jí)的抽象。前面我們也分析過,神經(jīng)網(wǎng)絡(luò)的前后向計(jì)算可以做到層與層之間完全獨(dú)立,那么每個(gè)層只要依照一定的接口規(guī)則實(shí)現(xiàn),就可以確保整個(gè)網(wǎng)絡(luò)的正確性。
Layer:Caffe實(shí)現(xiàn)了一個(gè)基礎(chǔ)的層級(jí)類Layer,對(duì)于一些特殊種類還會(huì)有自己的抽象類(比如base_conv_layer),這些類主要采用了模板的設(shè)計(jì)模式(Template),也就是說一些必須的代碼在基類寫好,一些具體的內(nèi)容在子類中實(shí)現(xiàn)。比方說在Layer的Setup中,函數(shù)中包括Setup的幾個(gè)步驟,其中的一些步驟由基類完成,一些步驟由子類完成。還有十分重要的Forward和Backward,基類實(shí)現(xiàn)了其中需要的一些邏輯,但是真正的運(yùn)算部分則交給了子類。這樣當(dāng)我們需要實(shí)現(xiàn)一個(gè)新的層時(shí),我們不需要管理瑣碎的事物,只要關(guān)系好層的初始化和前后向即可。
Net:Net將數(shù)據(jù)和層組合起來做進(jìn)一步的封裝,對(duì)外暴露了初始化和前后向的接口,使得整體看上去和一個(gè)層的功能類似,但內(nèi)部的組合可以是多種多樣。同時(shí)值得一提的是,每一層的輸入輸出數(shù)據(jù)統(tǒng)一保存在Net中,同時(shí)每個(gè)層內(nèi)的參數(shù)指針也保存在Net中,不同的層可以通過WeightShare共享相同的參數(shù),所以我們可以通過配置實(shí)現(xiàn)多個(gè)神經(jīng)網(wǎng)絡(luò)層之間共享參數(shù)的功能,這也增強(qiáng)了我們對(duì)網(wǎng)絡(luò)結(jié)構(gòu)的想象力。
Solver:有了Net我們實(shí)際上就可以進(jìn)行網(wǎng)絡(luò)的前向后向計(jì)算了,但是關(guān)于網(wǎng)絡(luò)的學(xué)習(xí)訓(xùn)練的功能還有些缺乏,于是在此之上,Solver類進(jìn)一步封裝了訓(xùn)練和預(yù)測(cè)相關(guān)的一些功能。與此同時(shí),它還開放了兩類接口:一個(gè)是更新參數(shù)的接口,繼承Solver可以實(shí)現(xiàn)不同的參數(shù)更新方法,如大家喜聞樂見的Momentum,Nesterov,Adagrad等。這樣使得不同的優(yōu)化算法能夠應(yīng)用其中。另外一個(gè)是訓(xùn)練過程中每一輪特定狀態(tài)下的可注入的一些回調(diào)函數(shù),在代碼中這個(gè)回調(diào)點(diǎn)的直接使用者就是多卡訓(xùn)練算法。
IO:有了上面的東西就夠了?還不夠,我們還需要輸入數(shù)據(jù)和參數(shù),正所謂巧婦難為無米之炊,沒有數(shù)據(jù)都是白搭。DataReader和DataTransformer幫助準(zhǔn)備輸入數(shù)據(jù),Filler對(duì)參數(shù)進(jìn)行初始化。一些Snapshot方法幫助模型的持久化,這樣模型和數(shù)據(jù)的IO問題也解決了。
多卡:對(duì)于單GPU訓(xùn)練來說,基本的層次關(guān)系到這里也就結(jié)束了,如果要進(jìn)行多GPU訓(xùn)練,那么上層還會(huì)有InternalThread和P2PSync兩個(gè)類,這兩個(gè)類屬于最上層的類了,而他們所調(diào)用的也只有Solver和一些參數(shù)類。
其實(shí)到這里,Caffe的主線也就基本走完了。我們可以畫一張圖把Caffe的整體層次關(guān)系展示出來:
如果對(duì)這張圖和圖中的一些細(xì)節(jié)比較清楚的話,那么你對(duì)Caffe的了解應(yīng)該已經(jīng)不錯(cuò)了。后面關(guān)于Caffe源碼分析的文章就可以不看了。如果沒有,那么我們還是可以繼續(xù)關(guān)注一下。當(dāng)然如果想真正理解這張圖中所表達(dá)的含義,還是要真正地讀一下代碼,去理解一些細(xì)節(jié)。但是有些細(xì)節(jié)這里就不做詳細(xì)的分析了,下一回我們會(huì)站在Layer的角度去看一個(gè)Layer在訓(xùn)練過程的全部經(jīng)歷。
2.
Caffe源碼閱讀——Net組裝
最近忙著看TI沒有及時(shí)寫文章,今天趕緊補(bǔ)一篇……
Net是Caffe代碼中一個(gè)比較核心的類,往下看它封裝了所有的Layer,構(gòu)建起了整個(gè)神經(jīng)網(wǎng)絡(luò);往上看它對(duì)外提供了前向后向計(jì)算,以及核心數(shù)據(jù)結(jié)構(gòu)的訪問結(jié)構(gòu),使得再上層的Solver可以利用Net比較輕松地實(shí)現(xiàn)Train和Test的策略。當(dāng)然,正是因?yàn)樗闹匾?#xff0c;組裝Net是一個(gè)比較復(fù)雜的部分。這一回我們就來看看Net的內(nèi)容。當(dāng)然,說在前面,看Net組裝的代碼有兩個(gè)目的:
首先,為了使問題不那么復(fù)雜,我們先從訓(xùn)練模型時(shí)輸出的log看看Net組裝的幾個(gè)關(guān)鍵步驟,然后再把這個(gè)過程慢慢展開,了解組裝的所有細(xì)節(jié)。
Log眼中的Net組裝
為了更好地展示Net組裝的一些細(xì)節(jié),我們?cè)谶@里選取了一個(gè)實(shí)際例子,就是Caffe的examples里面的siamese model。關(guān)于這個(gè)model的細(xì)節(jié)這里就不多說了,感興趣的可以去看官方或者非官方的文檔,這里只提一點(diǎn):這個(gè)網(wǎng)絡(luò)除了包含其他正常網(wǎng)絡(luò)中的一些特性之外,還具有網(wǎng)絡(luò)參數(shù)復(fù)用的特點(diǎn),在后面的分析中我們會(huì)用到。
下面我們要看的就是Net組裝的Log。這段Log一般都是大家在訓(xùn)練網(wǎng)絡(luò)時(shí)一閃而過的大段Log,當(dāng)然如果它沒有一閃而過而是停下來了,有可能是你的網(wǎng)絡(luò)定義有問題爆出了錯(cuò)誤。這段Log內(nèi)容比較多,總體來說就是Train階段和Test階段的兩個(gè)網(wǎng)絡(luò)組裝起來。我們重點(diǎn)關(guān)注其中的幾個(gè)片段,來大概了解Net組裝的一些核心內(nèi)容,也是那些比較值得打印出來的內(nèi)容。
首先是一個(gè)正常的卷積層conv1,Log如下所示(以下代碼的行號(hào)可能會(huì)有不同,但位置是相近的):
layer_factory.hpp:77] Creating layer conv1 net.cpp:92] Creating Layer conv1 net.cpp:428] conv1 <- data net.cpp:402] conv1 -> conv1 net.cpp:144] Setting up conv1 net.cpp:151] Top shape: 64 20 24 24 (737280) net.cpp:159] Memory required for data: 3752192這其中第一行是創(chuàng)建這個(gè)Layer實(shí)例的代碼,具體的創(chuàng)建過程在layer_factory里面。為了方便創(chuàng)建Layer,Caffe采用了工廠方法的設(shè)計(jì)模式,只要提供Layer的名字(在配置文件中參數(shù)叫type),就可以根據(jù)名字和對(duì)應(yīng)參數(shù)實(shí)例化一個(gè)Layer。這部分的細(xì)節(jié)只要認(rèn)真看一下就會(huì)明白。
第3,4行顯示了創(chuàng)建當(dāng)前層的bottom和top數(shù)據(jù)的過程。這里涉及到net.cpp中的AppendBottom和AppendTop兩個(gè)方法,因?yàn)槊恳粋€(gè)bottom blob和top blob都有名字,這里就將他們之間的關(guān)系輸出在了這里。
第5行看上去沒什么干貨,但是它代表了Layer的Setup函數(shù)已經(jīng)調(diào)用完成(或者Layer被share)。Layer的Setup函數(shù)是Layer初始化的關(guān)鍵函數(shù),這里面涉及到以下幾個(gè)具體的操作:
CheckBlobCounts(bottom, top); LayerSetUp(bottom, top); Reshape(bottom, top); SetLossWeights(top);總結(jié)地說,這四句完成了:
好了回到上面的log。接下來的那一句告訴了我們top層應(yīng)該輸出的維度。這里輸出了維度就是為了讓不放心的朋友算一下,看看和你想的是否一樣。當(dāng)然,輸出這句log的循環(huán)不是只做了這件事,它的主要工作就是設(shè)置top blob的loss_weight。
最后一句計(jì)算了該層top blob所占用的內(nèi)存。可以看出截至到這一層,內(nèi)存消耗大約是3M多,還不算大。
好,這就是一個(gè)最典型的Layer的初始化,下面這個(gè)ReLU層就稍微有些不同了:
layer_factory.hpp:77] Creating layer relu1 net.cpp:92] Creating Layer relu1 net.cpp:428] relu1 <- ip1 net.cpp:389] relu1 -> ip1 (in-place) net.cpp:144] Setting up relu1 net.cpp:151] Top shape: 64 500 (32000) net.cpp:159] Memory required for data: 5769472這里面最不同的就是第4行結(jié)尾的(in-place),這說明relu的bottom blob和top blob是同一個(gè)數(shù)據(jù),這和我們?cè)诰W(wǎng)絡(luò)中的定義是一樣的。in-place的好處就是減少內(nèi)存的操作,但是這里在統(tǒng)計(jì)內(nèi)存消耗時(shí)并沒有考慮in-place帶來的節(jié)省。
接下來就是共享網(wǎng)絡(luò)的conv1_p了:
layer_factory.hpp:77] Creating layer conv1_p net.cpp:92] Creating Layer conv1_p net.cpp:428] conv1_p <- data_p net.cpp:402] conv1_p -> conv1_p net.cpp:144] Setting up conv1_p net.cpp:151] Top shape: 64 20 24 24 (737280) net.cpp:159] Memory required for data: 8721664 net.cpp:488] Sharing parameters 'conv1_w' owned by layer 'conv1', param index 0 net.cpp:488] Sharing parameters 'conv1_b' owned by layer 'conv1', param index 1這一段最有特點(diǎn)的是最后兩句“Sharing”,因?yàn)閟iamese model中擁有參數(shù)完全相同的兩個(gè)網(wǎng)絡(luò),所以在構(gòu)建時(shí)候,第二個(gè)網(wǎng)絡(luò)檢測(cè)到參數(shù)名字已經(jīng)存在,說明該層的參數(shù)和其他層共享,于是在這里打印出來告訴用戶這一點(diǎn)。當(dāng)然,這一句之前沒有打印出來的內(nèi)容告訴了我們,實(shí)際上Net類中還負(fù)責(zé)了參數(shù)相關(guān)的初始化。這部分的內(nèi)容實(shí)際上還挺多,除了參數(shù)共享,還有對(duì)參數(shù)learning rate,weight decay的設(shè)定。
最后是最特別的一層:loss層
net.cpp:92] Creating Layer loss net.cpp:428] loss <- feat net.cpp:428] loss <- feat_p net.cpp:428] loss <- sim net.cpp:402] loss -> loss net.cpp:144] Setting up loss net.cpp:151] Top shape: (1) net.cpp:154] with loss weight 1 net.cpp:159] Memory required for data: 10742020這一層看上去沒有什么特別,該有的和前面一樣,但是唯一不同的就是它的倒數(shù)第二行,這說明這一層是有l(wèi)oss weight的。至于有l(wèi)oss weight有什么用,以后我們會(huì)詳細(xì)說這個(gè)事情。這里簡(jiǎn)單說一下,有l(wèi)oss weight表示這個(gè)blob會(huì)被用于計(jì)算loss。
前面的log主要解決了網(wǎng)絡(luò)的組裝和前向的一些計(jì)算,從log中,我們可以看出Net完成了以下的事情:
從上面的過程也可以看出,整個(gè)網(wǎng)絡(luò)中所有的流動(dòng)性變量(bottom blob,top blob)都保存在Net中,同時(shí)對(duì)于各層的參數(shù),根據(jù)各層的共享關(guān)系做了標(biāo)記。這樣好處是集中管理了網(wǎng)絡(luò)中的數(shù)據(jù),方便對(duì)數(shù)據(jù)進(jìn)行操作。
再往下面,我們可以截取一小段log來:
net.cpp:220] pool1 needs backward computation. net.cpp:220] conv1 needs backward computation. net.cpp:222] slice_pair does not need backward computation. net.cpp:222] pair_data does not need backward computation. net.cpp:264] This network produces output loss net.cpp:277] Network initialization done.接下來是統(tǒng)計(jì)一個(gè)層次是否需要進(jìn)行反向傳播的計(jì)算。一般來說我們的層是都需要計(jì)算的,但是也會(huì)有一些層不需要計(jì)算,比方說數(shù)據(jù)層,就像上面的log那樣,還有就是一些希望固定的層,這個(gè)一般在finetune網(wǎng)絡(luò)的時(shí)候用的上。因?yàn)榉聪蛴?jì)算一般比前向計(jì)算慢,如果有不需要計(jì)算的Layer,直接跳過計(jì)算是可以節(jié)省時(shí)間的。
最后是整個(gè)網(wǎng)絡(luò)產(chǎn)生的輸出,這個(gè)輸出會(huì)在訓(xùn)練迭代中顯示出來的。
了解了這些,我們就對(duì)Net裝載有了大概的了解,再去看它的代碼就會(huì)輕松些。
最后,關(guān)于Net類中所有的成員變量與它們之間的關(guān)系,我們可以用下面的一張圖來理解就好:
把Net的初始化理解后,其實(shí)Net以下的架構(gòu)方面的問題就不多了。下面我再看看Net以上的東西,Solver以及Caffe里“簡(jiǎn)單”的多卡訓(xùn)練。
3.
Caffe代碼閱讀——Solver
前面我們聊了Net組裝的內(nèi)容,接下來我們來看看Solver的內(nèi)容。Solver主體有兩部分:初始化和訓(xùn)練。初始化內(nèi)容相對(duì)比較簡(jiǎn)單,這里就不說了;下面我們來說說訓(xùn)練中的幾個(gè)關(guān)鍵函數(shù)。核心函數(shù):Step
真正的訓(xùn)練在Step函數(shù)內(nèi),這里有多卡訓(xùn)練的關(guān)鍵回調(diào)函數(shù):on_start()和on_gradient_ready(),具體的調(diào)用方法我們后面再說,在這兩個(gè)回調(diào)函數(shù)中間有兩個(gè)重要的過程:ForwardBackward和UpdateSmoothedLoss。在on_gradient_ready之后有一個(gè)關(guān)鍵函數(shù)ApplyUpdate(),這里面的代碼在Sgd_solver中。下面我們?cè)敿?xì)看一下。
ForwardBackward
這里主要調(diào)用了Net中的代碼,主要完成了前向后向的計(jì)算,前向用于計(jì)算模型的最終輸出和Loss,后向用于計(jì)算每一層網(wǎng)絡(luò)和參數(shù)的梯度。對(duì)于前向后向的具體內(nèi)容這里需要詳細(xì)敘述了,唯一值得一提的是前向的Loss計(jì)算,這部分代碼實(shí)際上實(shí)在Layer里面,具體涉及到loss_weight這個(gè)參數(shù)相關(guān)的初始化和loss()的判斷,同時(shí)還有Loss_Layer在Setup函數(shù)中的初始化。
UpdateSmoothedLoss
這個(gè)函數(shù)主要做Loss的平滑。由于Caffe的訓(xùn)練方式是SGD,我們無法把所有的數(shù)據(jù)同時(shí)放入模型進(jìn)行訓(xùn)練,那么部分?jǐn)?shù)據(jù)產(chǎn)生的Loss就可能會(huì)和全樣本的平均Loss不同,在必要時(shí)候?qū)oss和歷史過程中更新的Loss求平均就可以減少Loss的震蕩問題。代碼中的平滑方法比較簡(jiǎn)單,大家一看便知。
下面就是ApplyUpdate函數(shù),這個(gè)函數(shù)真正完成了參數(shù)更新的任務(wù)。Caffe的參數(shù)更新只利用了模型的梯度信息,沒有利用二階信息。下面就詳細(xì)介紹下更新參數(shù)的幾個(gè)過程:
- GetLearningRate
- ClipGradients
- Normalize
- Regularize
- ComputeUpdateValue
GetLearningRate
learning rate的故事我們前面已經(jīng)聊過了,在CNN訓(xùn)練中這確實(shí)是個(gè)大問題。Caffe為了讓learning rate的設(shè)計(jì)更靈活,提供了一系列的learning rate方案:
- fixed:lr永遠(yuǎn)不變
- step:
- exp:
- inv:
- multistep:直接寫iter在某個(gè)范圍內(nèi)時(shí)lr應(yīng)該是多少
- poly:
- sigmoid:
這些方案各有優(yōu)劣,選擇自己順手的就好。
ClipGradients
這一步主要是對(duì)梯度值做一個(gè)限制,如果梯度值過大,那么這里就會(huì)對(duì)梯度做一個(gè)修剪,對(duì)所有的參數(shù)乘以一個(gè)縮放因子,使得所有參數(shù)的平方和不超過參數(shù)中設(shè)定的梯度總值。這個(gè)功能感覺上像是對(duì)全局函數(shù)設(shè)置了一個(gè)Trust Region,可以防止更新的量過大二導(dǎo)致梯度發(fā)散。我認(rèn)為這一步的想法是很好的,但是實(shí)際操作中可能會(huì)有問題。實(shí)際中可能只有部分參數(shù)的梯度比較大,而其他參數(shù)的梯度本身比較小,那么對(duì)所有的參數(shù)乘以相同的因子會(huì)讓一些本來比較小的參數(shù)變得更小,這樣會(huì)帶來一些不公平。
Normalize
這一步同樣考慮了一些單一Batch不足以完成訓(xùn)練的問題,通過限制每個(gè)Batch的更新量來控制更新總量,代碼比較簡(jiǎn)單。
Regularize
到這一步終于要計(jì)算正則項(xiàng)的梯度了。Caffe提供兩種正則方法——L2和L1,其中L2采用了標(biāo)準(zhǔn)的梯度下降方法,L1采用了sub-gradient的計(jì)算方法。L2的優(yōu)化計(jì)算比較簡(jiǎn)單,沒有什么好說的,但是L1的計(jì)算還是有點(diǎn)值得玩味的地方的。這里采用的sub-gradient方法其實(shí)本身沒有什么問題,不過Lasso的優(yōu)化還可以有其他的方法,這個(gè)問題以后可以再細(xì)聊。
ComputeUpdateValue
到這里,我們終于來到了梯度計(jì)算的最后一站,這時(shí)候我們終于完成了對(duì)梯度的計(jì)算,下面該考慮lr和梯度結(jié)合起來如何計(jì)算最終的梯度優(yōu)化值了。sgd方法主要采用momentum加梯度的優(yōu)化方法。關(guān)于momentum的優(yōu)勢(shì)我們前面已經(jīng)聊過了。除此之外,Caffe還提供了一系列的梯度計(jì)算方法,這些優(yōu)化方法各有特點(diǎn),以后我們可以慢慢來看。
當(dāng)計(jì)算完這一步,我們就可以調(diào)用Blob中的Update把每個(gè)參數(shù)的data和diff進(jìn)行相加,計(jì)算出最終的結(jié)果。這樣,整個(gè)優(yōu)化過程就完成了。至于剩下的一些內(nèi)容都不是核心過程,就略去不看了。
如果我們采用單卡訓(xùn)練的策略,那么閱讀代碼到這里也差不多了。不過多卡訓(xùn)練對(duì)于大規(guī)模的訓(xùn)練任務(wù)來說是必不可少的,所以我們接下來趁熱打鐵地看看Caffe的多卡訓(xùn)練。
多卡訓(xùn)練算法
Caffe的多卡訓(xùn)練算法總體思路是數(shù)據(jù)并行,我們用不同的GPU處理不同的數(shù)據(jù),然后將所有的梯度更新匯總。由于Solver在訓(xùn)練中給了兩個(gè)回調(diào)函數(shù),多卡訓(xùn)練也主要利用了這兩個(gè)回調(diào)函數(shù)進(jìn)行:
其中第2步由每一個(gè)CPU線程和自己的GPU并行完成,第4步由匯總的CPU和自己的GPU完成,剩下的1,3兩步主要是完成數(shù)據(jù)傳輸?shù)娜蝿?wù),也是多卡計(jì)算中主要完成的部分。
Caffe采用樹型結(jié)構(gòu)進(jìn)行參數(shù)傳遞,其中一個(gè)CPU線程和GPU作為樹型結(jié)構(gòu)的根,其他的則作為根下面的節(jié)點(diǎn)。為了更快地傳輸GPU數(shù)據(jù),樹型結(jié)構(gòu)的構(gòu)建要考慮GPU之間是否相近,比方說兩個(gè)GPU之間是否可以進(jìn)行P2P的直傳。在前面的翻譯博客中我們已經(jīng)聊過GPU之間數(shù)據(jù)傳輸?shù)膯栴}了,這里的樹形結(jié)構(gòu)也主要以此做考慮。
我們假設(shè)4塊GPU的拓?fù)浣Y(jié)構(gòu)如下:
nvidia-smi topo -mGPU0 GPU1 GPU2 GPU3 GPU0 X PHB SOC SOC GPU1 PHB X SOC SOC GPU2 SOC SOC X PHB GPU3 SOC SOC PHB X
那么我們構(gòu)造出的樹型結(jié)構(gòu)如下所示,數(shù)據(jù)傳輸也是按照這樣的結(jié)構(gòu)傳輸:
這樣1,3的數(shù)據(jù)傳遞就解決了,具體的過程請(qǐng)?jiān)敿?xì)閱讀代碼,這里就不敘述了。
對(duì)Caffe代碼的基本介紹就到這里了,我們對(duì)代碼的整體結(jié)構(gòu)有了比較清晰的認(rèn)識(shí),下面我們將分析模型中各個(gè)部分的特性。
總結(jié)
- 上一篇: Android Studio如何发布AP
- 下一篇: 利用TinyXML读取VOC2012数据