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)配置
- Ubuntu 16.04 LTS
- Caffe: https://github.com/BVLC/caffe
- Python 2.7.14
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)路徑中,步驟是:
- 打開Terminator
- 輸入
vi ~/.bashrc - 輸入
i,進(jìn)入編輯模式 - 在打開的文件的末尾添加
export PYTHONPATH=/path/to/my_python_layer:$PYTHONPATH - 鍵入
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的文件中,需要定義類,類的里面包括幾個部分:
- setup( ): 用于檢查輸入的參數(shù)是否存在異常,初始化的功能.
- reshape( ): 也是初始化,設(shè)定一下參數(shù)的size
- forward( ): 前向傳播
- 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ù)的傳遞和存儲,這也是我花最多時間去弄明白的地方.
祝好!