从零开始山寨Caffe·壹:仰望星空与脚踏实地
請(qǐng)以“仰望星空與腳踏實(shí)地”作為題目,寫(xiě)一篇不少于800字的文章。除詩(shī)歌外,文體不限。
——2010·北京卷
仰望星空
規(guī)范性
Caffe誕生于12年末,如果偏要形容一下這個(gè)框架,可以用"須敬如師長(zhǎng)"。
這是一份相當(dāng)規(guī)范的代碼,這個(gè)規(guī)范,不應(yīng)該是BAT規(guī)范,那得是Google規(guī)范。
很多自稱碼農(nóng)的人應(yīng)該好好學(xué)習(xí)這份代碼,改改自己丑陋的C++編程習(xí)慣。
下面列出幾條重要的規(guī)范準(zhǔn)則:
★const
先說(shuō)說(shuō)const問(wèn)題,Google為了增加代碼的可讀性,明確要求:
不做修改的量(涵蓋函數(shù)體內(nèi)、函數(shù)參數(shù)列表),必須以const標(biāo)記。
相對(duì)的,對(duì)于那些改變的量,可選擇用mutable標(biāo)記。
因?yàn)閙utable關(guān)鍵詞不是很常用,所以一般在自設(shè)函數(shù)中使用。
嚴(yán)格的const不在于擔(dān)心變量是否被誤修改,而在于給代碼閱讀者一個(gè)清晰的思路:
這個(gè)值不會(huì)改變,這個(gè)值肯定要改變。
★引用
"引用"是C/C++設(shè)計(jì)的一個(gè)敗筆,因?yàn)镃/C++默認(rèn)是深拷貝,這在大內(nèi)存數(shù)據(jù)結(jié)構(gòu)操作的時(shí)候,
容易讓新手程序員寫(xiě)出弱智低能的代碼。假設(shè)Datum結(jié)構(gòu)A使用了2G內(nèi)存,令:
Datum B=A;
那么,內(nèi)存會(huì)占用4G空間,而且,我們大概需要幾秒的時(shí)間去拷貝A的2G內(nèi)存。
這個(gè)幾秒看起來(lái)不是很成問(wèn)題,但是在多線程編程中,兩個(gè)異步線程共享數(shù)據(jù):
如果你不用引用會(huì)怎么樣?
很有趣,這個(gè)復(fù)制再賦值的操作會(huì)被CPU中斷,變成無(wú)效指令。
這在Caffe的多線程I/O設(shè)計(jì)架構(gòu)中,是個(gè)關(guān)鍵點(diǎn)。
另外,對(duì)于基本數(shù)據(jù)類(lèi)型(char/int/float/double),引用是沒(méi)用必要的。
但是,string、vector<int>等容器,引用就相當(dāng)有必要了。
★const引用
const引用最常見(jiàn)于函數(shù)參數(shù)列表,用于傳遞常、大數(shù)據(jù)結(jié)構(gòu)量。
與此相對(duì)的,如果你要修改一個(gè)大數(shù)據(jù)結(jié)構(gòu)量,應(yīng)當(dāng)在參數(shù)列表中傳入指針,而不是引用。
傳入引用來(lái)修改是C規(guī)范,傳入指針來(lái)修改是C++規(guī)范,Caffe嚴(yán)格遵照C++規(guī)范,這點(diǎn)要明確。
★常成員函數(shù)
常成員函數(shù),在OO里通常容易被新手忽略掉。(Java就沒(méi)那么復(fù)雜),通常寫(xiě)作:
void xxx() const,目的是:
const標(biāo)記住傳入成員函數(shù)的this指針。
常成員函數(shù)其實(shí)不是必要的,但是在一定情況下,就會(huì)變成必要的。
這個(gè)情況相當(dāng)有趣,而且在Caffe中也經(jīng)常發(fā)生:
void xxx(const Blob& blob){blob.count(); }如果我們遵照Google的編程規(guī)范,用const引用鎖定傳入的Blob。
那么,blob.count()這個(gè)成員函數(shù)的調(diào)用就會(huì)被編譯器的語(yǔ)義分析為:成員變量不可修改。
如果你的代碼寫(xiě)成這樣,那就會(huì)被編譯器攔下,錯(cuò)誤信息為:this指針不一致。
class Blob{ public:int count() {} //錯(cuò)誤int count() const {} //正確 };★public、private、protected
OO的封裝性是比較難定位的一個(gè)規(guī)范,成員變量及成員函數(shù)如何訪問(wèn)權(quán)限是個(gè)問(wèn)題。
Caffe嚴(yán)格遵照標(biāo)準(zhǔn)的OO封裝概念:方法是public,變量是private或者是protected。
區(qū)別private和protected就一句話:
private成員變量或是函數(shù),不可能被繼承。通常只用在本Class獨(dú)有,而派生類(lèi)不直使用的函數(shù)/變量上。
比如im2col和col2im,這兩個(gè)為卷積做Patch預(yù)變換的函數(shù)。
protected和private的成員函數(shù)和成員變量都不可能從外部被訪問(wèn),應(yīng)當(dāng)在public里專(zhuān)門(mén)設(shè)置訪問(wèn)接口。
并且接口根據(jù)需要,恰當(dāng)使用const標(biāo)記,避免越權(quán)訪問(wèn)。
有趣的是,如果這么做,會(huì)增加相當(dāng)多的代碼量,而且都是一些復(fù)制粘貼的廢品代碼。
為了避免這種情況,Google開(kāi)發(fā)了Protocol Buffer,將數(shù)據(jù)結(jié)構(gòu)大部分訪問(wèn)接口自動(dòng)生成,且獨(dú)立安排。
這樣,在主體代碼里,我們不會(huì)因?yàn)閿?shù)據(jù)的訪問(wèn)接口的規(guī)范,而導(dǎo)致閱讀代碼十分頭疼(想想那一掃下來(lái)的廢代碼)。
獨(dú)立性
如果你研究過(guò)Word2Vec的源碼,應(yīng)該就知道,為什么Word2Vec必須跑在Linux下。。
因?yàn)镸ikolov同學(xué)在寫(xiě)代碼的時(shí)候,用了POSIX OS的API函數(shù)pThread,來(lái)實(shí)現(xiàn)內(nèi)核級(jí)線程。
這為跨平臺(tái)帶來(lái)麻煩,一份優(yōu)秀的跨平臺(tái)代碼,必須具有相當(dāng)出色的平臺(tái)獨(dú)立性。
在這點(diǎn)上,Caffe使用了C++最強(qiáng)大的Boost庫(kù),來(lái)避免對(duì)OS API函數(shù)的使用。
?
Boost庫(kù),又稱為C++三千佳麗的后宮,內(nèi)涵1W+頭文件,完整編譯完大小達(dá)3.3G,相當(dāng)龐大。
它的代碼來(lái)自世界上頂級(jí)的C++開(kāi)發(fā)者,是C++最忠實(shí)的第三方庫(kù),并且是ISO C++新規(guī)范的唯一來(lái)源。
Boost在Caffe中的主要作用是提供OS獨(dú)立的內(nèi)核級(jí)線程。
當(dāng)然,已經(jīng)于C++11中被列入規(guī)范的boost::shared_ptr其實(shí)也算。
還有一個(gè)十分精彩的boost::thread_specific_ptr,也在Caffe中起到了核心作用。
?
不足之處也有,而且其中一處還成了Bug,那就是API函數(shù)之一的open。
Linux的open默認(rèn)是以二進(jìn)制打開(kāi)的,而Windows則是以文本形式打開(kāi)的。
移植到Windows時(shí),需要補(bǔ)上 O_BINARY作為flag。
異構(gòu)性
大家都知道Caffe能跑GPU,一個(gè)關(guān)鍵點(diǎn)是:
它是在何處,又是怎么進(jìn)行CPU與GPU分離的?
這個(gè)模型實(shí)際上應(yīng)當(dāng)算是CUDA標(biāo)準(zhǔn)模型。
由于內(nèi)存顯存不能跨著訪問(wèn)(一個(gè)在北橋,一個(gè)在南橋),又要考慮的CPU和GPU的平衡。
所以,數(shù)據(jù)的讀取、轉(zhuǎn)換不僅要被平攤到CPU上,而且應(yīng)當(dāng)設(shè)計(jì)成多線程,多線程的生產(chǎn)者消費(fèi)者模型。
并且具有一定的多重緩沖能力,這樣保證最大化CPU/GPU的計(jì)算力。
在一個(gè)機(jī)器學(xué)習(xí)系統(tǒng)當(dāng)中,我們要珍惜計(jì)算設(shè)備的每一個(gè)時(shí)鐘周期,切實(shí)做到計(jì)算力的最大化利用。
設(shè)計(jì)模式
實(shí)際使用的設(shè)計(jì)模式只有兩個(gè)。
第一個(gè)是MVC,這個(gè)其實(shí)是迫不得已。
異構(gòu)編程決定著,數(shù)據(jù)、視圖、控制三大塊必須獨(dú)立開(kāi)來(lái)。
但視圖和控制并不是很明顯,在設(shè)計(jì)接口/可視化GUI的時(shí)候,將凸顯重要性。
?
第二個(gè)稱為工廠模式,這是一個(gè)存在于Java的概念,盡管C++也可以模仿。
具體來(lái)說(shuō),工廠模式是為了彌補(bǔ)面向?qū)ο笮途幾g語(yǔ)言的不足,會(huì)被OO的多態(tài)所需要。
以Caffe為例:
我們當(dāng)前有一個(gè)基類(lèi)指針Layer* layer;
在程序運(yùn)行之前,計(jì)算機(jī)并不知道這個(gè)指針究竟要指向何種派生類(lèi)。是卷積層?Pooling層?ReLU層?
鬼才知道。一個(gè)愚蠢的方法:
if(type==CONV) {....} else if(type==POOLING) {....} else if(type==RELU} {.....} else {ERROR}看起來(lái),還是可以接受的,但是在軟件工程專(zhuān)業(yè)看來(lái),這種模式相當(dāng)?shù)么馈?/p>
工廠模式借鑒了工廠管理產(chǎn)品的經(jīng)驗(yàn),將各種類(lèi)型存在數(shù)據(jù)庫(kù)中,需要時(shí),拿出來(lái)看看。
這種模式相當(dāng)?shù)渺`活,當(dāng)然,在Caffe中作用不是很大,僅僅是為了花式好看。
要實(shí)現(xiàn)這個(gè)模式,你只需要一個(gè)關(guān)聯(lián)容器(C++/JAVA),字典容器(Python)。
將string與創(chuàng)建指針綁定即可。
C/C++中有函數(shù)指針的說(shuō)法,如:
typedef boost::shared_ptr< Layer<Dtype> > (*NEW_FUNC)(const LayerParameter& );經(jīng)過(guò)typdef之后,NEW_FUNC就可以指向函數(shù):
boost::shared_ptr< Layer<Dtype> > xxx(const LayerParameter& x); NEW_FUNC yyy=boost::shared_ptr< Layer<Dtype> > xxx(const LayerParameter& x);yyy(); //相當(dāng)于xxx() xxx();需要訪問(wèn)工廠時(shí),我們只需要訪問(wèn)這個(gè)代替工廠管理數(shù)據(jù)庫(kù)的容器,而不是幼稚地使用if(.....)
序列化與反序列化
如果Caffe不使用Protocol Buffer,那么代碼量將擴(kuò)大一倍。
這不是危言聳聽(tīng),在傳統(tǒng)系統(tǒng)級(jí)程序設(shè)計(jì)中,序列化與反序列化一直是一個(gè)碼農(nóng)問(wèn)題。
尤其是在機(jī)器學(xué)習(xí)系統(tǒng)中,復(fù)雜多變的數(shù)據(jù)結(jié)構(gòu),給序列化和反序列化帶來(lái)巨大麻煩。
Protocol Buffer在序列化階段,是一個(gè)高效的編碼器,能將數(shù)據(jù)最小體積序列化。
而在反序列化階段,它是一個(gè)強(qiáng)大的解碼器,支持二進(jìn)制/文本兩類(lèi)數(shù)據(jù)的解析與結(jié)構(gòu)反序列化。
其中,從文本反序列化意義頗大,這就形成了Caffe著名的文本配置文件prototxt,用于net和solver。
相對(duì)靈活的配置方式,尤其適合超大規(guī)模神經(jīng)網(wǎng)絡(luò),這點(diǎn)在早期機(jī)器學(xué)習(xí)系統(tǒng)中獨(dú)領(lǐng)風(fēng)騷(很多人認(rèn)為這比圖形界面還要方便)。
宏
據(jù)說(shuō)寫(xiě)庫(kù)狂人都是用宏狂人。
C/C++提供了強(qiáng)大了自定義宏函數(shù)(#define),Caffe通過(guò)宏,大概減少了1000~2000行代碼。
宏函數(shù)大致有如下幾種:
?
① #define DISABLE_COPY_AND_ASSIGN(classname)
俗稱禁止拷貝和賦值宏,如果你熟悉Qt,就會(huì)發(fā)現(xiàn),Qt中大部分?jǐn)?shù)據(jù)結(jié)構(gòu)都用了這個(gè)宏來(lái)保護(hù)。
這個(gè)宏算是最沒(méi)用的宏,用在了所有Caffe大型數(shù)據(jù)結(jié)構(gòu)上(Blob、Layer、Net、Solver)
目的是禁止兩個(gè)大型數(shù)據(jù)結(jié)構(gòu)直接復(fù)制、構(gòu)造、然后賦值。
實(shí)際上,Caffe也沒(méi)有去編寫(xiě)復(fù)制構(gòu)造函數(shù)代碼,所以最終還是會(huì)被編譯器攔下。
前面以及說(shuō)過(guò)了,兩個(gè)大型數(shù)據(jù)結(jié)構(gòu)之間的復(fù)制會(huì)是什么樣的下場(chǎng),這是絕對(duì)應(yīng)該被禁止的。
如果你要使用一個(gè)數(shù)據(jù)結(jié)構(gòu),請(qǐng)用指針或是引用指向它。
如果你有亂賦值的編程陋習(xí),請(qǐng)及時(shí)打上這個(gè)宏,避免自己手賤。反之,可以暫時(shí)無(wú)視它。
當(dāng)然,從庫(kù)的完整性角度,這個(gè)宏是明智的。
Java/Python不需要這個(gè)宏,因?yàn)镴ava對(duì)大型數(shù)據(jù)結(jié)構(gòu),默認(rèn)是淺拷貝,也就是直接引用。
而Python,這個(gè)沒(méi)有數(shù)據(jù)類(lèi)型的奇怪語(yǔ)言,則默認(rèn)全部是淺拷貝。
?
②#define INSTANTIATE_CLASS(classname)
非常非常非常重要的宏,重要的事說(shuō)三遍。
由于Caffe采用分離式模板編程方法(據(jù)說(shuō)也是Google倡導(dǎo)的)
模板未類(lèi)型實(shí)例化的定義空間和實(shí)例化的定義空間是不同的。
實(shí)際上,編譯器并不會(huì)理睬分離在cpp里的未實(shí)例化的定義代碼,而是將它放置在一個(gè)虛擬的空間。
一旦一段明確類(lèi)型的代碼,訪問(wèn)這段虛擬代碼空間,就會(huì)被編譯器攔截。
如果你想要讓模板的聲明和定義分離編寫(xiě),就需要在cpp定義文件里,將定義指定明確的類(lèi)型,實(shí)例化。
這個(gè)宏的作用正是如此。(Google編程習(xí)慣的宏吧)。
更詳細(xì)的用法,將在后續(xù)文章中詳細(xì)介紹。
?
③#define INSTANTIATE_LAYER_GPU_FUNCS(classname)
通樣是實(shí)例化宏,專(zhuān)門(mén)寫(xiě)這個(gè)宏的原因,是因?yàn)镹VCC編譯器相當(dāng)傲嬌。
打在cpp文件里的INSTANTIATE_CLASS宏,NVCC在編譯cu文件時(shí),可不會(huì)知道。
所以,你需要在cu文件里,為這些函數(shù)再次實(shí)例化。
其實(shí)也沒(méi)幾個(gè)函數(shù),也就是forward_gpu和backward_gpu
?
④#define NOT_IMPLEMENTED
俗稱偷懶宏,你要是這段代碼不想寫(xiě)了,打個(gè)NOT_IMPLEMENTED就行了。
就是宣告:“老子就是不想寫(xiě)這段代碼,留空,留空!”
但是注意,宏封裝了LOG(FATAL),這是個(gè)Assert(斷言),會(huì)引起CPU硬件中斷。
一旦代碼空間轉(zhuǎn)到你沒(méi)寫(xiě)的這段,整個(gè)程序就會(huì)被終止。
所以,偷懶有度,還是認(rèn)真寫(xiě)代碼吧。
?
⑤#define REGISTER_LAYER_CLASS(type)?
Layer工廠模式用的宏,也就是將這個(gè)Layer的信息寫(xiě)到工廠的管理數(shù)據(jù)庫(kù)里。
此宏省了不少代碼,在使用工廠之前,記得要為每個(gè)成品(Layer)打上這個(gè)宏。
命名空間
Caffe為了與Boost等庫(kù)接軌,幾乎為所有結(jié)構(gòu)提供了以caffe為關(guān)鍵字的命名空間。
設(shè)置命名空間的主要目的是防止Caffe的函數(shù)、變量與其他庫(kù)產(chǎn)生沖突。
在我們的山寨過(guò)程中,為了代碼的簡(jiǎn)潔,將忽略全部的命名空間。
命名法
Caffe中普遍采用下劃線命名法。
我們對(duì)其作出了部分修改,整體采用兩種命名法:
①針對(duì)變量而言: 采用下劃線命名法
②針對(duì)函數(shù)而言:采用駝峰命名法
腳踏實(shí)地
編程手冊(cè)
Caffe幾乎是C++ Primer 第五版的鮮活例子,如果你需要讀懂它,經(jīng)常翻一翻C++ Primer是一個(gè)不錯(cuò)的主意。
(另:不要閱讀C++ Primer Plus,它的作者僅僅是一個(gè)普通教師,
而C++ Primer作者則包含C++協(xié)發(fā)明者、ISO C++委員會(huì)的人,是權(quán)威圣經(jīng))
耐心閱讀和模仿代碼
注意你接觸的是一個(gè)系統(tǒng)級(jí)程序,Windows還是全球5000位微軟工程師開(kāi)發(fā)的。
系統(tǒng)級(jí)程序相當(dāng)龐大和復(fù)雜,切記不要心浮氣躁,不要以套庫(kù)的心理去學(xué)習(xí)。
更不要認(rèn)為,看看高層代碼就可以了,這簡(jiǎn)直是噩夢(mèng),最后你會(huì)發(fā)現(xiàn)你根本讀不懂。
來(lái)一個(gè)響亮的名字
為自己的工程取個(gè)名字是一件有趣的事,本項(xiàng)目默認(rèn)名為:Dragon。
因?yàn)樯疃壬窠?jīng)網(wǎng)絡(luò)活像一頭蠢龍。
總結(jié)
以上是生活随笔為你收集整理的从零开始山寨Caffe·壹:仰望星空与脚踏实地的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Jmeter 压测基础笔记
- 下一篇: Python | 四种运行其他程序的黑科