下面是一些參考鏈接
[2]Using Python Layers in your Caffe models with DIGITS
[3]What is a “Python” layer in caffe?
[5]Building custom Caffe layer in python
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這個(gè)功能的虚缎。然后編譯成功后,在Terminator中輸入:
$ caffe$ python>>> import caffe
像這樣,沒有給你報(bào)錯(cuò),說明caffe和python_layer都編譯成功啦.
1.3 添加Python路徑
1.3.1 在~/.bashrc文件中加入路徑
寫自己的python layer勢必需要.py文件,為了讓caffe運(yùn)行的時(shí)候可以找到你的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語句修改文檔的知識. 實(shí)質(zhì)上就是修改一個(gè)在~/路徑下的叫.bashrc的文檔.
1.3.2 在要訓(xùn)練的.sh腳本文件中加入路徑
首先將PythonLayer放入任意位置(要知道其所在路徑),然后在訓(xùn)練網(wǎng)絡(luò)的.sh腳本文件中加入PythonLayer腳本路徑
export PYTHONPATH=/home/用戶名/pythonlayer文件夾名
2. 修改代碼
首先我們定義一個(gè)要實(shí)現(xiàn)的目標(biāo):訓(xùn)練過程中刘急,在Softmax層和Loss層之間棚菊,加入一個(gè)Python Layer,使得這個(gè)Layer的輸入等于輸出. 換句話說叔汁,這個(gè)Layer沒有起到一點(diǎn)作用统求,正向傳播的時(shí)候y=x,反向傳播的時(shí)候?qū)?shù)y'=1. 因此訓(xùn)練的結(jié)果應(yīng)該和沒加很相似.
2.1 train_val.prototxt
這個(gè)文檔是Caffe訓(xùn)練的時(shí)候据块,定義數(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大多會把這兩個(gè)層合并在一起定義边篮,成為了
layer{name:"loss"type:"SoftmaxWithLoss"bottom:"fc8_2"bottom:"label"top:"loss"}
我們需要把這個(gè)層拆開己莺,變成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加到它們中間就好了凌受,注意這個(gè)層的輸出和輸出阵子,輸入是bottom,輸出是top胜蛉,這兩個(gè)值需要和上一層的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路徑中有一個(gè)python文件叫my_layer.py# Caffe通過這個(gè)文件名和Python系統(tǒng)路徑誊册,找到你寫的python代碼文件layer:"MyLayer"# my_layer.py中定義的一個(gè)類领突,在下文中會講到# MyLayer是類的名字param_str:'{ "x1": 1, "x2": 2 }'# 額外傳遞給my_layer.py的值# 如果沒有要傳遞的值,可以不定義. 相當(dāng)于給python的全局變量# 當(dāng)Python Layer比較復(fù)雜的時(shí)候會需要用到.}}layer {? name:"loss"type:"MultinomialLogisticLoss"bottom:"output"# 要和Python Layer輸出保持一致bottom:"label"# loss層的另一個(gè)輸入# 因?yàn)橐?jì)算output和label間的距離top:"loss"# loss層的輸出解虱,即loss值}
加完以后的參數(shù)傳遞如圖
2.2 my_layer.py
重頭戲其實(shí)就是這一部分攘须,以上說的都是相對固定的修改漆撞,不存在什么算法層面的改動殴泰,但是python里面不一樣,可以實(shí)現(xiàn)很多調(diào)整和試驗(yàn)性的試驗(yàn). 最最基本的就是加入一個(gè)上面定義的那個(gè)"可有可無"的Python Layer.
在Python的文件中浮驳,需要定義類悍汛,類的里面包括幾個(gè)部分:
setup( ): 用于檢查輸入的參數(shù)是否存在異常,初始化的功能.
reshape( ): 也是初始化至会,設(shè)定一下參數(shù)的size
forward( ): 前向傳播
backward( ): 反向傳播
結(jié)構(gòu)如下:
importcaffeclassMyLayer(caffe.Layer):defsetup(self, bottom, top):passdefreshape(self, bottom, top):passdefforward(self, bottom, top):passdefbackward(self, top, propagate_down, bottom):pass
根據(jù)需要慢慢地填充這幾個(gè)函數(shù)离咐,關(guān)于這方面的知識,我很推薦閱讀"Guide to Convolutional Neural Networks: A Practical Application to Traffic-Sign Detection and Classification." 中的這個(gè)章節(jié) [6].
setup()的定義:
def setup(self, bottom, top):# 功能1: 檢查輸入輸出是否有異常iflen(bottom) !=1:? ? ? ? raiseException("異常:輸入應(yīng)該就一個(gè)值奉件!")iflen(top) !=1:? ? ? ? raiseException("異常:輸出應(yīng)該就一個(gè)值宵蛀!")# 功能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()的定義:
defreshape(self, bottom, top):# 功能1: 修改變量的sizetop[0].reshape(*bottom[0].data.shape)# 看了很多材料县貌,我感覺這個(gè)函數(shù)就是比較雞肋的那種.# 這個(gè)函數(shù)就像格式一樣术陶,反正寫上就好了...# 不知道還有其他什么功能了,歡迎補(bǔ)充煤痕!
forward()的定義:
這個(gè)函數(shù)可以變的花樣就多了梧宫,如果是要定義不同的loss function,可以參考[1]摆碉,稍微高級一點(diǎn)的可以參考[2]塘匣,這里就實(shí)現(xiàn)一個(gè)y=x的簡單功能.
defforward(self, bottom, top):# 目標(biāo):y = x# bottom相當(dāng)于輸入x# top相當(dāng)于輸出ytop[0].data[...] = bottom[0].data[:]# 哈哈哈哈,是不是感覺被騙了巷帝,一行代碼就完事兒了:-)
了解bottom中數(shù)據(jù)的存儲結(jié)構(gòu)是比較重要的忌卤,因?yàn)閰⒖嘉臋n不多,我只能通過print out everything來了解bottom里面究竟存著些什么. 回想在2.1的prototxt中楞泼,我們有定義輸入Python Layer的都有什么(bottom). bottom可以有多個(gè)定義驰徊,如果像例子中的只有一個(gè)bottom: "output_2"团甲,那么bottom[0].data中就存著output_2的值,當(dāng)二分類問題時(shí)也就是兩列叮阅,一列是Softmax后屬于label 0的概率璧函,一列是Softmax后屬于label 1的概率. 當(dāng)bottom定義了多個(gè)輸入的時(shí)候,即
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]捐韩,想用的時(shí)候調(diào)用bottom[n].data就可以了. top[n].data和bottom的情況類似,也是取決于prototxt中的定義.
想象一下鹃锈,既然你可以掌控了output_2和label和其他你需要的任何值(通過bottom或者param_str定義)荤胁,是不是可以在這個(gè)forward()函數(shù)里面大展身手了?
是的.
但是同時(shí)屎债,也要負(fù)責(zé)計(jì)算這個(gè)前饋所帶來的梯度仅政,可以自己定義變量存起來,網(wǎng)上修改loss函數(shù)的例子就是拿self.diff來存梯度的盆驹,不過在這個(gè)例子中圆丹,因?yàn)樘荻仁?,所以我沒有管它.
backward()的定義:
defbackward(self, top, propagate_down, bottom):# 由于是反向傳播躯喇,top和bottom的意義就和前向傳播反一反# top:從loss層傳回來的值# bottom:反向?qū)拥妮敵鰂oriinrange(len(propagate_down)):ifnotpropagate_down[i]:continuebottom[i].diff[...] = top[i].diff[:]# 其實(shí)還要乘以這個(gè)層的導(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這個(gè)東西劣欢,我認(rèn)為是定義是否在這個(gè)層做反向傳播(權(quán)值更新)的,也就是在遷移學(xué)習(xí)中裁良,如果要固定不更新某一層的參數(shù)凿将,就是用propagate_down來控制的. 不用管它,反正用默認(rèn)的代碼就好了.
總的來說价脾,要實(shí)現(xiàn)y=x這么一個(gè)層牧抵,需要寫的python代碼就是:
import caffeclassMyLayer(caffe.Layer):defsetup(self, bottom, top):? ? ? ? passdefreshape(self, bottom, top):? ? ? ? top[0].reshape(*bottom[0].data.shape)defforward(self, bottom, top):? ? ? ? top[0].data[...] = bottom[0].data[:]defbackward(self,self, top, propagate_down, bottom):foriinrange(len(propagate_down)):ifnotpropagate_down[i]:? ? ? ? ? ? ? ? continue? ? ? ? ? ? bottom[i].diff[...] = top[i].diff[:]
3. 結(jié)語
我認(rèn)為要在Caffe中寫好一個(gè)Python Layer,最重要的是抓住兩點(diǎn)
1)處理好prototxt到python文件的參數(shù)傳遞
2)不能忘了在forward()中計(jì)算反向傳播梯度
接下來就是一些代碼理解和學(xué)術(shù)創(chuàng)新的事情了,懂得如何寫Python Layer犀变,在運(yùn)動Caffe的過程中就多開了一扇窗妹孙,從此不再只是調(diào)整solver.prototxt,還有在train_val.prototxt中組合卷積層/池化層/全連接/Residual Unit/Dense Unit這些低級的修改.
更多的細(xì)節(jié)可以參考最前面的幾個(gè)參考鏈接還有自己的理解實(shí)踐. 在實(shí)踐過程中获枝,超級建議print所有你不了解的數(shù)據(jù)結(jié)構(gòu)蠢正,例如forward()中的bottom,top; backward()中的bottom省店,top嚣崭,即便Caffe用GPU加速,它也會給你打印出來你想要看的數(shù)據(jù)懦傍,一步一步的摸索數(shù)據(jù)的傳遞和存儲雹舀,這也是我花最多時(shí)間去弄明白的地方.
該文章主要引用于 :引用文章