在Caffe中加Python Layer的方法

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


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

網(wǎng)上的參考資料大多是教你怎么寫一個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. 準(zhǔn)備工作

1.1 系統(tǒng)配置

1.2 編譯Caffe

按照一般的caffe編譯流程(可參考官網(wǎng),也可參考Install caffe in Ubuntu)就好,唯一的區(qū)別就是在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的系統(tǒng)路徑中,步驟是:

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

如果這部分沒有看明白,需要上網(wǎng)補(bǔ)一下如何在Linux環(huán)境中用vim語句修改文檔的知識. 實質(zhì)上就是修改一個在~/路徑下的叫.bashrc的文檔.

2. 修改代碼

首先我們定義一個要實現(xiàn)的目標(biāo):訓(xùn)練過程中,在Softmax層和Loss層之間,加入一個Python Layer,使得這個Layer的輸入等于輸出. 換句話說,這個Layer沒有起到一點作用,正向傳播的時候y=x,反向傳播的時候?qū)?shù)y'=1. 因此訓(xùn)練的結(jié)果應(yīng)該和沒加很相似.

2.1 train_val.prototxt

這個文檔是Caffe訓(xùn)練的時候,定義數(shù)據(jù)和網(wǎng)絡(luò)結(jié)構(gòu)用的,所以如果用添加新的層,需要在這里定義. 第一步是在網(wǎng)絡(luò)結(jié)構(gòu)的定義中找到添加Python Layer的位置,根據(jù)問題的定義,Python Layer應(yīng)該在softmax和loss層之間,不過網(wǎng)上的prototxt大多會把這兩個層合并在一起定義,成為了

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

我們需要把這個層拆開,變成softmax層和loss層,根據(jù)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輸入對接好,就像這樣(請仔細(xì)看注釋和代碼):

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" # 調(diào)用的Python代碼的文件名
    # 也就是1.3中添加的Python路徑中有一個python文件叫my_layer.py
    # Caffe通過這個文件名和Python系統(tǒng)路徑,找到你寫的python代碼文件
    layer: "MyLayer" # my_layer.py中定義的一個類,在下文中會講到
    # MyLayer是類的名字
    param_str: '{ "x1": 1, "x2": 2 }' # 額外傳遞給my_layer.py的值
    # 如果沒有要傳遞的值,可以不定義. 相當(dāng)于給python的全局變量
    # 當(dāng)Python Layer比較復(fù)雜的時候會需要用到.
  }
}
layer {
  name: "loss"
  type: "MultinomialLogisticLoss"
  bottom: "output" # 要和Python Layer輸出保持一致
  bottom: "label" # loss層的另一個輸入
  # 因為要計算output和label間的距離
  top: "loss" # loss層的輸出,即loss值
}

加完以后的參數(shù)傳遞如圖


2.2 my_layer.py

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

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

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

結(jié)構(gòu)如下:

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

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

setup()的定義:

def setup(self, bottom, top):

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

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

    # 功能3: 接受train_val.prototxt中設(shè)置的變量值
    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)
    # 看了很多材料,我感覺這個函數(shù)就是比較雞肋的那種.
    # 這個函數(shù)就像格式一樣,反正寫上就好了...

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

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

def forward(self, bottom, top):

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

了解bottom中數(shù)據(jù)的存儲結(jié)構(gòu)是比較重要的,因為參考文檔不多,我只能通過print out everything來了解bottom里面究竟存著些什么. 回想在2.1的prototxt中,我們有定義輸入Python Layer的都有什么(bottom). bottom可以有多個定義,如果像例子中的只有一個bottom: "output_2",那么bottom[0].data中就存著output_2的值,當(dāng)二分類問題時也就是兩列,一列是Softmax后屬于label 0的概率,一列是Softmax后屬于label 1的概率. 當(dāng)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],想用的時候調(diào)用bottom[n].data就可以了. top[n].data和bottom的情況類似,也是取決于prototxt中的定義.

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

是的.

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

backward()的定義:

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

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

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


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

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

總的來說,要實現(xiàn)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. 結(jié)語

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

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


祝好!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容