日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

从零开始山寨Caffe·壹:仰望星空与脚踏实地

發布時間:2025/3/17 72 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从零开始山寨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·壹:仰望星空与脚踏实地的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。