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