在Caffe中加Python Layer的方法

Author: Zongwei Zhou | 周縱葦
Weibo: @MrGiovanni
Email: zongweiz@asu.edu
Acknowledgement: Md Rahman Siddiquee (mrahmans@asu.edu)


Caffe的參考文檔非常少,自己改代碼需要查閱網上好多好多對的錯的討論. 這篇文章主要講怎么自己編寫python layer,從而逼格很高地在caffe中實現自己的想法. 所謂的python layer,其實就是一個自己編寫的層,用python來實現. 因為近來深度學習方向要發頂會和頂刊,光是用用Caffe,Tensorflow在數據集里頭跑個網絡已經基本不可能啦,需要具備修改底層代碼的能力.

網上的參考資料大多是教你怎么寫一個python layer來修改loss function(部分鏈接需要翻墻~):
[1] Caffe Python Layer
[2] Using Python Layers in your Caffe models with DIGITS
[3] What is a “Python” layer in caffe?
[4] caffe python layer
[5] Building custom Caffe layer in python
[6] Aghdam, Hamed Habibi, and Elnaz Jahani Heravi. Guide to Convolutional Neural Networks: A Practical Application to Traffic-Sign Detection and Classification. Springer, 2017.
[7] Softmax with Loss Layer

1. 準備工作

1.1 系統配置

1.2 編譯Caffe

按照一般的caffe編譯流程(可參考官網,也可參考Install caffe in Ubuntu)就好,唯一的區別就是在Makefile.config中,把這一行修改一下:
# WITH_PYTHON_LAYER := 1
改成
WITH_PYTHON_LAYER := 1
說明我們是要使用python_layer這個功能的。然后編譯成功后,在Terminator中輸入:

$ caffe
$ python
>>> import caffe

像這樣,沒有給你報錯,說明caffe和python_layer都編譯成功啦.

1.3 添加Python路徑

寫自己的python layer勢必需要.py文件,為了讓caffe運行的時候可以找到你的py文件,接下來需要把py文件的路徑加到python的系統路徑中,步驟是:

  1. 打開Terminator
  2. 輸入vi ~/.bashrc
  3. 輸入i,進入編輯模式
  4. 在打開的文件的末尾添加
    export PYTHONPATH=/path/to/my_python_layer:$PYTHONPATH
  5. 鍵入esc:wq,回車,即可保存退出

如果這部分沒有看明白,需要上網補一下如何在Linux環境中用vim語句修改文檔的知識. 實質上就是修改一個在~/路徑下的叫.bashrc的文檔.

2. 修改代碼

首先我們定義一個要實現的目標:訓練過程中,在Softmax層和Loss層之間,加入一個Python Layer,使得這個Layer的輸入等于輸出. 換句話說,這個Layer沒有起到一點作用,正向傳播的時候y=x,反向傳播的時候導數y'=1. 因此訓練的結果應該和沒加很相似.

2.1 train_val.prototxt

這個文檔是Caffe訓練的時候,定義數據和網絡結構用的,所以如果用添加新的層,需要在這里定義. 第一步是在網絡結構的定義中找到添加Python Layer的位置,根據問題的定義,Python Layer應該在softmax和loss層之間,不過網上的prototxt大多會把這兩個層合并在一起定義,成為了

layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "fc8_2"
  bottom: "label"
  top: "loss"
}

我們需要把這個層拆開,變成softmax層和loss層,根據Caffe提供的官方文檔,我們知道SoftmaxWithLoss是softmax層和MultinomialLogisticLoss的合并.

The softmax loss layer computes the multinomial logistic loss of the softmax of its inputs. [7]

那拆開后的代碼就是

layer {
  name: "output_2"
  type: "Softmax"
  bottom: "fc8_2"
  top: "output_2"
}
layer {
  name: "loss"
  type: "MultinomialLogisticLoss"
  bottom: "output_2"
  bottom: "label"
  top: "loss"
}

拆完了以后就只需要把你定義的Python Layer加到它們中間就好了,注意這個層的輸出和輸出,輸入是bottom,輸出是top,這兩個值需要和上一層的softmax輸出和下一層的loss輸入對接好,就像這樣(請仔細看注釋和代碼):

layer { # softmax層
  name: "output_2"
  type: "Softmax"
  bottom: "fc8_2" # 是上一層Fully Connected Layer的輸出
  top: "output_2" # 是Softmax的輸出,Python Layer的輸入
}
layer {
  type: "Python"
  name: "output"
  bottom: "output_2" # 要和Softmax輸出保持一致
  top: "output" # Python Layer的輸出
  python_param {
    module: "my_layer" # 調用的Python代碼的文件名
    # 也就是1.3中添加的Python路徑中有一個python文件叫my_layer.py
    # Caffe通過這個文件名和Python系統路徑,找到你寫的python代碼文件
    layer: "MyLayer" # my_layer.py中定義的一個類,在下文中會講到
    # MyLayer是類的名字
    param_str: '{ "x1": 1, "x2": 2 }' # 額外傳遞給my_layer.py的值
    # 如果沒有要傳遞的值,可以不定義. 相當于給python的全局變量
    # 當Python Layer比較復雜的時候會需要用到.
  }
}
layer {
  name: "loss"
  type: "MultinomialLogisticLoss"
  bottom: "output" # 要和Python Layer輸出保持一致
  bottom: "label" # loss層的另一個輸入
  # 因為要計算output和label間的距離
  top: "loss" # loss層的輸出,即loss值
}

加完以后的參數傳遞如圖


2.2 my_layer.py

重頭戲其實就是這一部分,以上說的都是相對固定的修改,不存在什么算法層面的改動,但是python里面不一樣,可以實現很多調整和試驗性的試驗. 最最基本的就是加入一個上面定義的那個"可有可無"的Python Layer.

在Python的文件中,需要定義類,類的里面包括幾個部分:

  1. setup( ): 用于檢查輸入的參數是否存在異常,初始化的功能.
  2. reshape( ): 也是初始化,設定一下參數的size
  3. forward( ): 前向傳播
  4. backward( ): 反向傳播

結構如下:

import caffe
class MyLayer(caffe.Layer):
  def setup(self, bottom, top):
    pass

  def reshape(self, bottom, top):
    pass

  def forward(self, bottom, top):
    pass

  def backward(self, top, propagate_down, bottom):
    pass

根據需要慢慢地填充這幾個函數,關于這方面的知識,我很推薦閱讀"Guide to Convolutional Neural Networks: A Practical Application to Traffic-Sign Detection and Classification." 中的這個章節 [6].

setup()的定義:

def setup(self, bottom, top):

    # 功能1: 檢查輸入輸出是否有異常
    if len(bottom) != 1:
        raise Exception("異常:輸入應該就一個值!")
    if len(top) != 1:
        raise Exception("異常:輸出應該就一個值!")

    # 功能2: 初始化一些變量,后續可以使用
    self.delta = np.zeros_like(bottom[0].data, dtype=np.float32)

    # 功能3: 接受train_val.prototxt中設置的變量值
    params = eval(self.param_str)
    self.x1 = int(params["x1"])
    self.x2 = int(params["x2"])

reshape()的定義:

def reshape(self, bottom, top):

    # 功能1: 修改變量的size
    top[0].reshape(*bottom[0].data.shape)
    # 看了很多材料,我感覺這個函數就是比較雞肋的那種.
    # 這個函數就像格式一樣,反正寫上就好了...

    # 不知道還有其他什么功能了,歡迎補充!

forward()的定義:
這個函數可以變的花樣就多了,如果是要定義不同的loss function,可以參考[1],稍微高級一點的可以參考[2],這里就實現一個y=x的簡單功能.

def forward(self, bottom, top):

    # 目標:y = x
    # bottom相當于輸入x
    # top相當于輸出y
    top[0].data[...] = bottom[0].data[:]
    # 哈哈哈哈,是不是感覺被騙了,一行代碼就完事兒了:-)

了解bottom中數據的存儲結構是比較重要的,因為參考文檔不多,我只能通過print out everything來了解bottom里面究竟存著些什么. 回想在2.1的prototxt中,我們有定義輸入Python Layer的都有什么(bottom). bottom可以有多個定義,如果像例子中的只有一個bottom: "output_2",那么bottom[0].data中就存著output_2的值,當二分類問題時也就是兩列,一列是Softmax后屬于label 0的概率,一列是Softmax后屬于label 1的概率. 當bottom定義了多個輸入的時候,即

layer {
  type: "Python"
  name: "output"
  bottom: "output_2" 
  bottom: "label"
  top: "output"
  python_param {
    ...
  }
}

那么按照順序,bottom[0].data中依舊存著output_2,bottom[1].data中存著label值,以此類推,可以定義到bottom[n],想用的時候調用bottom[n].data就可以了. top[n].data和bottom的情況類似,也是取決于prototxt中的定義.

想象一下,既然你可以掌控了output_2和label和其他你需要的任何值(通過bottom或者param_str定義),是不是可以在這個forward()函數里面大展身手了?

是的.

但是同時,也要負責計算這個前饋所帶來的梯度,可以自己定義變量存起來,網上修改loss函數的例子就是拿self.diff來存梯度的,不過在這個例子中,因為梯度是1,所以我沒有管它.

backward()的定義:

def backward(self, top, propagate_down, bottom):

    # 由于是反向傳播,top和bottom的意義就和前向傳播反一反
    # top:從loss層傳回來的值
    # bottom:反向層的輸出
    for i in range(len(propagate_down)):
        if not propagate_down[i]:
            continue
        bottom[i].diff[...] = top[i].diff[:]
        # 其實還要乘以這個層的導數,但是由于y=x的導數是1.
        # 所以無所謂了,直接把loss值一動不動的傳遞下來就好.

對于top和bottom在forward()和backward()函數中不同的意義,不要懵...


top[i].diff是從loss層返回來的梯度,以二分類為例,它的結構是兩列,一列是label 0的梯度,一列是label 1的梯度. 因此在backward()正常情況是需要把top[i].diff乘以self.diff的,也就是在forward()中算好的Python Layer自身的梯度. 然后賦值給bottom[i].diff,反向傳播到上一層.

關于propagate_down這個東西,我認為是定義是否在這個層做反向傳播(權值更新)的,也就是在遷移學習中,如果要固定不更新某一層的參數,就是用propagate_down來控制的. 不用管它,反正用默認的代碼就好了.

總的來說,要實現y=x這么一個層,需要寫的python代碼就是:

import caffe

class MyLayer(caffe.Layer):
    
    def setup(self, bottom, top):
        pass
    
    def reshape(self, bottom, top):
        top[0].reshape(*bottom[0].data.shape)

    def forward(self, bottom, top):
        top[0].data[...] = bottom[0].data[:]

    def backward(self, self, top, propagate_down, bottom):
        for i in range(len(propagate_down)):
            if not propagate_down[i]:
                continue
            bottom[i].diff[...] = top[i].diff[:]

3. 結語

我認為要在Caffe中寫好一個Python Layer,最重要的是抓住兩點
1)處理好prototxt到python文件的參數傳遞
2)不能忘了在forward()中計算反向傳播梯度
接下來就是一些代碼理解和學術創新的事情了,懂得如何寫Python Layer,在運動Caffe的過程中就多開了一扇窗,從此不再只是調整solver.prototxt,還有在train_val.prototxt中組合卷積層/池化層/全連接/Residual Unit/Dense Unit這些低級的修改.

更多的細節可以參考最前面的幾個參考鏈接還有自己的理解實踐. 在實踐過程中,超級建議print所有你不了解的數據結構,例如forward()中的bottom,top; backward()中的bottom,top,即便Caffe用GPU加速,它也會給你打印出來你想要看的數據,一步一步的摸索數據的傳遞和存儲,這也是我花最多時間去弄明白的地方.


祝好!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 第六章 9. 今日的工作餐是咸鲞燒肉,鲞多肉少,以至于打出的飽嗝都是一股子咸腥味。響河支著頜,裝模作樣地在紙上記了...
    金容與閱讀 553評論 5 6
  • 洗墨人閱讀 305評論 0 0