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