本系列文章面向深度學(xué)習(xí)研發(fā)者栗竖,希望通過(guò)Image Caption Generation,一個(gè)有意思的具體任務(wù),深入淺出地介紹深度學(xué)習(xí)的知識(shí)。本系列文章涉及到很多深度學(xué)習(xí)流行的模型锐朴,如CNN,RNN/LSTM,Attention等。本文為第8篇歌径。
作者:李理
目前就職于環(huán)信,即時(shí)通訊云平臺(tái)和全媒體智能客服平臺(tái)亲茅,在環(huán)信從事智能客服和智能機(jī)器人相關(guān)工作回铛,致力于用深度學(xué)習(xí)來(lái)提高智能機(jī)器人的性能。
相關(guān)文章:
李理:從Image Caption Generation理解深度學(xué)習(xí)(part I)
李理:從Image Caption Generation理解深度學(xué)習(xí)(part II)
李理:從Image Caption Generation理解深度學(xué)習(xí)(part III)
李理:自動(dòng)梯度求解——使用自動(dòng)求導(dǎo)實(shí)現(xiàn)多層神經(jīng)網(wǎng)絡(luò)
1. Theano的發(fā)音
第一次碰到時(shí)很自然的發(fā)音是 /θi.??.no?/克锣,不過(guò)如果看一些視頻可能也有發(fā)/te.?a?.no/的茵肃。這兩種都有,比較官方的說(shuō)法可能是這個(gè):
I think I say roughly /θi.??.no?/ (using the international phonetic alphabet), or /te.?a?.no/ when speaking Dutch, which is my native language. I guess the latter is actually closer to the original Greek pronunciation :)
另外從這里也有說(shuō)明:
Theano was written at the LISA lab to support rapid development of efficient machine learning algorithms. Theano is named after the Greek mathematician, who may have been Pythagoras’ wife.
維基百科對(duì)此作出的解釋是:
Theano (/θ???no?/; Greek: Θεαν?; fl. 6th-century BC), or Theano of Crotone,[1] is the name given to perhaps two Pythagorean philosophers.
因此用英語(yǔ)的發(fā)音是 /θ???no?/袭祟。
2. Theano簡(jiǎn)介
Theano是一個(gè)Python庫(kù)免姿,它可以讓你定義,優(yōu)化以及對(duì)數(shù)學(xué)表達(dá)式求值榕酒,尤其是多維數(shù)組(numpy的ndarray)的表達(dá)式的求值。對(duì)于解決大量數(shù)據(jù)的問(wèn)題故俐,使用Theano可能獲得與手工用C實(shí)現(xiàn)差不多的性能想鹰。另外通過(guò)利用GPU,它能獲得比CPU上的C實(shí)現(xiàn)快很多數(shù)量級(jí)药版。
Theano把計(jì)算機(jī)代數(shù)系統(tǒng)(CAS)和優(yōu)化的編譯器結(jié)合在一起辑舷。 它也可以對(duì)許多數(shù)學(xué)操作生成自定義的c代碼。這種CAS和優(yōu)化編譯的組合對(duì)于有復(fù)雜數(shù)學(xué)表達(dá)式重復(fù)的被求值并且求值速度很關(guān)鍵的問(wèn)題是非常有用的槽片。對(duì)于許多不同的表達(dá)式只求值一次的場(chǎng)景何缓,Theano也能最小化編譯/分析的次數(shù),但是仍然可以提供諸如自動(dòng)差分這樣的符號(hào)計(jì)算的特性还栓。
Theano的編譯器支持這些符號(hào)表達(dá)式的不同復(fù)雜程度的許多優(yōu)化方法:
用GPU來(lái)計(jì)算
常量折疊(constant folding)【編譯時(shí)的常量表達(dá)式計(jì)算碌廓,參考這里】
合并相似的子圖,避免重復(fù)計(jì)算
算術(shù)簡(jiǎn)化剩盒,比如把x*y/y簡(jiǎn)化成y谷婆,–x【兩次求負(fù)】簡(jiǎn)化成x
在不同的上下文中插入高效的BLAS函數(shù)(比如GEMM)
使用Memory Aliasing【詳細(xì)參考這里】來(lái)避免重復(fù)計(jì)算
對(duì)于不涉及aliasing的操作盡量使用就地的運(yùn)算【類似與x*=2 vs y=x*2】
Elementwise的子表達(dá)式的循環(huán)的合并(loop fusion)【這是一項(xiàng)編譯器優(yōu)化技巧,簡(jiǎn)單的說(shuō)就是把相同下標(biāo)的循環(huán)合并起來(lái),例子可以參考這里】
提高數(shù)值運(yùn)算的穩(wěn)定性,比如:
log(1+exp(x))andlog(∑iexp(x[i]))[/i]
?【關(guān)于這個(gè)我們羅嗦一點(diǎn)纪挎,讀者如果讀過(guò)的文章期贫,肯定還記得計(jì)算softmax時(shí)先把向量減去最大的元素,避免exp運(yùn)算的溢出】[list][*]更多內(nèi)容請(qǐng)參考優(yōu)化部分
[/*]
[/list]
3. Theano安裝
請(qǐng)參考這里异袄,這里就不贅述了通砍。
4. 官方Tutorial
4.1 Baby Steps - Algebra
內(nèi)容來(lái)自這里。
4.1.1 Adding two Scalars
>>> import numpy>>> import theano.tensor as T>>> from theano import function>>> x = T.dscalar('x')>>> y = T.dscalar('y')>>> z = x + y>>> f = function([x, y], z)>>> f(2, 3)array(5.0)
我們這段代碼首先定義了符號(hào)變量x和y烤蜕,它們的類型是double封孙。使用theano.tensor.dscalar(‘x’)定義了一個(gè)名字叫x的類型為double的標(biāo)量(scalar)。
注意符號(hào)變量的名字是theano看到的玖绿,而我們把theano創(chuàng)建的dscalar賦給x是在python里的敛瓷。在使用theano是我們需要區(qū)分普通的python變量和theano的符號(hào)變量。theano用符號(hào)變量創(chuàng)建出一個(gè)computing graph斑匪,然后在這個(gè)graph上執(zhí)行各種運(yùn)算呐籽。
定義了x和y之后,我們通過(guò)操作(op)+定義了符號(hào)變量z蚀瘸。
接下來(lái)我們定義了一個(gè)函數(shù)(function) f狡蝶,這個(gè)函數(shù)的輸入是符號(hào)變量x和y,輸出是符號(hào)變量z
接下來(lái)我們可以”執(zhí)行“這個(gè)函數(shù) f(2,3)
運(yùn)行 f = function([x, y], z)會(huì)花費(fèi)比較長(zhǎng)的時(shí)間贮勃,theano會(huì)將函數(shù)構(gòu)建成計(jì)算圖贪惹,并且做一些優(yōu)化。
>>> type(x)
>>> x.type
TensorType(float64, scalar)
>>> T.dscalar
TensorType(float64, scalar)
>>> x.type is T.dscalar
True
dscalar(‘x’) 返回的對(duì)象的類型是theano.tensor.var.TensorVariable寂嘉,也就是一種符號(hào)變量奏瞬。這種對(duì)象有一個(gè)type屬性,x.type是TensorType泉孩。對(duì)于dscalar硼端,它的TensorType是64位的浮點(diǎn)數(shù)的一個(gè)標(biāo)量。
除了變量寓搬,我們也可以定義向量(vector)和矩陣matrix珍昨。
然后用在前面增加’b’,’w’,’i’,’l’,’f’,’d’,’c’分別表示8位,16位句喷,32位镣典,64位的整數(shù),float唾琼,double以及負(fù)數(shù)兄春。比如imatrix就是32位整數(shù)類型的矩陣,dvector就是單精度浮點(diǎn)數(shù)的向量锡溯。
4.2 More Examples
參考這里神郊。
這部分會(huì)介紹更多的theano的概念肴裙,最后包含一個(gè)Logistic Regression的例子,包括怎么用theano自動(dòng)求梯度涌乳。
4.2.1 Logistic Function
函數(shù)定義為:
s(x)=11+e?x
函數(shù)圖像為:
這個(gè)函數(shù)的特點(diǎn)是它的值域是(0,1)蜻懦,當(dāng)x趨近 ?∞ 時(shí)值趨近于0,當(dāng)x趨近 ∞ 時(shí)值趨近于1夕晓。
我們經(jīng)常需要對(duì)一個(gè)向量或者矩陣的每一個(gè)元素都應(yīng)用一個(gè)函數(shù)宛乃,我們把這種操作叫做elementwise的操作(numpy里就叫universal function, ufunc)
比如下面的代碼對(duì)一個(gè)矩陣計(jì)算logistic函數(shù):
>>> import theano>>> import theano.tensor as T>>> x = T.dmatrix('x')>>> s = 1 / (1 + T.exp(-x))>>> logistic = theano.function([x], s)>>> logistic([[0, 1],[-1, -2]])array([[0.5? ? ? ,? 0.73105858],[0.26894142,? 0.11920292]])
logistic是elementwise的原因是:定義這個(gè)符號(hào)變量的所有操作——除法,加法蒸辆,指數(shù)取反都是elementwise的操作征炼。
另外logistic函數(shù)和tanh函數(shù)有如下關(guān)系:
s(x)=11+e?x=1+tanh(x/2)2
我們可以使用下面的代碼來(lái)驗(yàn)證這個(gè)式子:
>>> s2 = (1 + T.tanh(x / 2)) / 2>>> logistic2 = theano.function([x], s2)>>> logistic2([[0, 1],[-1, -2]])array([[0.5? ? ? ,? 0.73105858],[0.26894142,? 0.11920292]])
4.2.2 使用共享變量(shared variable)
一個(gè)函數(shù)可以有內(nèi)部的狀態(tài)。比如我們可以實(shí)現(xiàn)一個(gè)累加器躬贡,在開(kāi)始的時(shí)候谆奥,它的值被初始化成零。然后每一次調(diào)用拂玻,這個(gè)狀態(tài)會(huì)加上函數(shù)的參數(shù)酸些。
首先我們定義這個(gè)累加器函數(shù),它把參數(shù)加到這個(gè)內(nèi)部狀態(tài)變量檐蚜,同時(shí)返回這個(gè)狀態(tài)變量老的值【調(diào)用前的值】
>>> from theano import shared>>> state = shared(0)>>> inc = T.iscalar('inc')>>> accumulator = function([inc], state, updates=[(state, state+inc)])
這里有不少新的概念魄懂。shared函數(shù)會(huì)返回共享變量。這種變量的值在多個(gè)函數(shù)直接可以共享闯第∈欣酰可以用符號(hào)變量的地方都可以用共享變量。但不同的是咳短,共享變量有一個(gè)內(nèi)部狀態(tài)的值填帽,這個(gè)值可以被多個(gè)函數(shù)共享。我們可以使用get_value和set_value方法來(lái)讀取或者修改共享變量的值咙好。
另外一個(gè)新的概念是函數(shù)的updates參數(shù)篡腌。updates參數(shù)是一個(gè)list,其中每個(gè)元素是一個(gè)tuple敷扫,這個(gè)tuple的第一個(gè)元素是一個(gè)共享變量,第二個(gè)元素是一個(gè)新的表達(dá)式诚卸。updates也可以是一個(gè)dict葵第,key是共享變量,值是一個(gè)新的表達(dá)式合溺。不管用哪種方法卒密,它的意思是:當(dāng)函數(shù)運(yùn)行完成后,把新的表達(dá)式的值賦給這個(gè)共享變量棠赛。上面的accumulator函數(shù)的updates是把state+inc賦給state哮奇,也就是每次調(diào)用accumulator函數(shù)后state增加inc膛腐。
讓我們來(lái)試一試!
>>> print(state.get_value())
0
>>> accumulator(1)
array(0)
>>> print(state.get_value())
1
>>> accumulator(300)
array(1)
>>> print(state.get_value())
301
開(kāi)始時(shí)state的值是0鼎俘。然后調(diào)用一次accumulator(1)哲身,這個(gè)函數(shù)返回state原來(lái)的值,也就是0贸伐。然后把state更新為1勘天。
然后再調(diào)用accumulator(300),這一次返回1捉邢,同時(shí)把state更新為301脯丝。
我們有可以重新設(shè)置state的值。只需要調(diào)用set_value方法就行:
>>> state.set_value(-1)
>>> accumulator(3)
array(-1)
>>> print(state.get_value())
2
我們首先把state設(shè)置成-1伏伐,然后調(diào)用accumulator(3)宠进,返回-1,同時(shí)吧state更新成了2藐翎。
我們前面提到過(guò)材蹬,多個(gè)函數(shù)可以“共享”一個(gè)共享變量,因此我們可以定義如下的函數(shù):
>>> decrementor = function([inc], state, updates=[(state, state-inc)])>>> decrementor(2)array(2)>>> print(state.get_value())0
我們定義了decrementor函數(shù)阱高,它每次返回之前的state的值赚导,同時(shí)把state減去輸入?yún)?shù)inc后賦給state。
調(diào)用decrementor(2)赤惊,返回state的之前的值2吼旧,同時(shí)把state更新成0。
你可能會(huì)奇怪為什么需要updates機(jī)制未舟。你也可以讓這個(gè)函數(shù)返回這個(gè)新的表達(dá)式【當(dāng)然原來(lái)的返回值仍然返回圈暗,多返回一個(gè)就行】,然后用在numpy更新state裕膀。首先updates機(jī)制是一種語(yǔ)法糖员串,寫(xiě)起來(lái)更簡(jiǎn)便。但更重要的是為了效率昼扛。共享變量的共享又是可以使用就地(in-place)的算法【符號(hào)變量包括共享變量的內(nèi)存是由Theano來(lái)管理的寸齐,把它從Theano復(fù)制到numpy,然后修改抄谐,然后在復(fù)制到Theano很多時(shí)候是沒(méi)有必要的渺鹦,更多Theano的內(nèi)存管理請(qǐng)參考這里】。另外蛹含,共享變量的內(nèi)存是由Theano來(lái)分配和管理毅厚,因此Theano可以根據(jù)需要來(lái)把它放到GPU的顯存里,這樣用GPU計(jì)算時(shí)可以避免CPU到GPU的數(shù)據(jù)拷貝浦箱,從而獲得更好的性能吸耿。
有些時(shí)候祠锣,你可以通過(guò)共享變量來(lái)定義了一個(gè)公式(函數(shù)),但是你不想用它的值咽安。這種情況下伴网,你可以用givens這個(gè)參數(shù)。
>>> fn_of_state = state * 2 + inc>>> # The type of foo must match the shared variable we are replacing>>> # with the ``givens``>>> foo = T.scalar(dtype=state.dtype)>>> skip_shared = function([inc, foo], fn_of_state, givens=[(state, foo)])>>> skip_shared(1, 3)? # we're using 3 for the state, not state.valuearray(7)>>> print(state.get_value())? # old state still there, but we didn't use it0
首先我們定義了一個(gè)符號(hào)變量fn_of_state板乙,它用到了共享變量state是偷。
然后我們定義skip_shared,他的輸入?yún)?shù)是inc和foo募逞,輸出是fn_of_state蛋铆。注意:fn_of_state依賴state和inc兩個(gè)符號(hào)變量,如果參數(shù)inc直接給定了放接。另外一個(gè)參數(shù)foo取代(而不是賦值給)了inc刺啦,因此實(shí)際 fn_of_state = foo * 2 + inc。我們調(diào)用skip_shared(1,3)會(huì)得到7纠脾,而state依然是0(而不是3)玛瘸。如果把這個(gè)計(jì)算圖畫(huà)出來(lái)的話,實(shí)際是用foo替代了state苟蹈。
givens參數(shù)可以取代任何符號(hào)變量糊渊,而不只是共享變量【從計(jì)算圖的角度就非常容易理解了,后面我們會(huì)講到Theano的計(jì)算圖】慧脱。你也可以用這個(gè)參數(shù)來(lái)替代常量和表達(dá)式渺绒。不過(guò)需要小心的是替代的時(shí)候不要引入循環(huán)的依賴×馀福【比如a=b+c宗兼,你顯然不能把c又givens成a,這樣循環(huán)展開(kāi)就不是有向無(wú)環(huán)圖了】
有了上面的基礎(chǔ)氮采,我們可以用Theano來(lái)實(shí)現(xiàn)Logistic Regression算法了殷绍。不過(guò)這里沒(méi)有介紹grad,我們先簡(jiǎn)單的介紹一下鹊漠,內(nèi)容來(lái)自這里主到。
使用Theano的好處就是auto diff,在前面也介紹過(guò)來(lái)躯概,幾乎所有的深度學(xué)習(xí)框架/工具都是提供類似的auto diff的功能登钥,只不過(guò)定義graph的“語(yǔ)言/語(yǔ)法”和“粒度”不一樣。另外除了求梯度楞陷,大部分工具還把訓(xùn)練算法都封裝好了怔鳖。而Theano就比較“原始”茉唉,它除了自動(dòng)求梯度固蛾,并不會(huì)幫你實(shí)現(xiàn)sgd或者Adam算法结执,也不會(huì)幫你做dropout,不會(huì)幫你做weight decay和normalization艾凯,所有這些都得你自己完成献幔。這可能會(huì)讓那些希望把深度學(xué)習(xí)當(dāng)成一個(gè)“黑盒”的用戶有些失望,對(duì)于這樣的用戶最好用Keras趾诗,caffe這樣的工具蜡感。但是對(duì)于想理解更多細(xì)節(jié)和自己“創(chuàng)造”一種新的網(wǎng)絡(luò)結(jié)構(gòu)的用戶,Theano是個(gè)非常好的工具恃泪,它提供常見(jiàn)的op郑兴,也可以自定義op(python或者c),對(duì)于rnn也有非常好的支持贝乎。
我們下面用Theano來(lái)實(shí)現(xiàn)對(duì)函數(shù)
f(x)=x2
的導(dǎo)數(shù)情连。
>>> import numpy>>> import theano>>> import theano.tensor as T>>> from theano import pp>>> x = T.dscalar('x')>>> y = x ** 2>>> gy = T.grad(y, x)>>> pp(gy)? # print out the gradient prior to optimization'((fill((x ** TensorConstant{2}), TensorConstant{1.0}) * TensorConstant{2}) * (x ** (TensorConstant{2} - TensorConstant{1})))'>>> f = theano.function([x], gy)>>> f(4)array(8.0)>>> numpy.allclose(f(94.2), 188.4)True
首先我們定義符號(hào)變量x,然后用x定義y览效,然后使用grad函數(shù)求y對(duì)x的(偏)導(dǎo)數(shù)gy【grad函數(shù)返回的仍然只是一個(gè)符號(hào)變量却舀,可以認(rèn)為用y和x定義了一個(gè)新的符號(hào)變量gy】,然后定義函數(shù)f锤灿,它的輸入是x挽拔,輸出是gy。注意:y是x的函數(shù)但校,gy是x和y的函數(shù)螃诅,所以最終gy只是x的函數(shù),所以f的輸入只有x始腾。
f編譯好了之后州刽,給定x,我們就可以求
?y?x
在這個(gè)點(diǎn)上的值了浪箭。4.2.3 一個(gè)實(shí)際的例子:Logistic Regression
Logistic Regression(LR)簡(jiǎn)介
LR模型用來(lái)進(jìn)行二分類穗椅,它對(duì)輸入進(jìn)行仿射變換,然后用logistic函數(shù)把它壓縮到0和1之間奶栖,訓(xùn)練模型就是調(diào)整參數(shù)匹表,對(duì)于類別0,讓模型輸出接近0的數(shù)宣鄙,對(duì)于類別1,讓模型輸出接近1的數(shù)袍镀。預(yù)測(cè)的時(shí)候如果大于0.5就輸出1,反之輸出0冻晤。
因此我們可以把模型的輸出當(dāng)成概率:
P(y=1|x)=hw(x)=11+exp(?wTx)
P(y=0|x)=1?P(y=1|x)=1?hw(x)
對(duì)于兩個(gè)概念分布苇羡,cross-entroy是最常見(jiàn)的一種度量方式”腔。【詳細(xì)介紹參考這里】
loss=?ylogP(y=1|x)?(1?y)logP(y=0|x)
=?yloghw(x)?(1?y)log(1?hw(x))
如果真實(shí)值y=1设江,那么第二項(xiàng)就是0锦茁,
loss=?loghw(x)
,如果
hw(x)
趨近1叉存,那么loss就趨近0码俩;反之如果
hw(x)
趨近0,那么loss就趨近無(wú)窮大歼捏。如果真實(shí)值y=0稿存,那么第一項(xiàng)就是0,
loss=?log(1?hw(x))
瞳秽,如果
hw(x)
趨近0瓣履,
1?hw(x)
趨近1,loss趨近0练俐;反之loss趨近無(wú)窮大拂苹。因此從上面的分析我們發(fā)現(xiàn),這個(gè)loss函數(shù)是符合直覺(jué)的痰洒,模型輸出
hw(x)
越接近真實(shí)值瓢棒,loss越小。有了loss丘喻,我們就可以用梯度下降求(局部)最優(yōu)參數(shù)了脯宿。【這個(gè)loss函數(shù)是一個(gè)凸函數(shù)泉粉,所以局部最優(yōu)就是全局最優(yōu)连霉,有興趣的讀者可以參考這里,不過(guò)對(duì)于工程師來(lái)說(shuō)沒(méi)有必要了解這些細(xì)節(jié)嗡靡。我們常見(jiàn)的神經(jīng)網(wǎng)絡(luò)是非常復(fù)雜的非線性函數(shù)跺撼,因此loss通常也是非凸的,因此(隨機(jī))梯度下降只能得到局部最優(yōu)解讨彼,但是深度神經(jīng)網(wǎng)絡(luò)通常能找到比較好的局部最優(yōu)解歉井,有也一些學(xué)者在做研究,有興趣的讀者請(qǐng)參考這里以及這里】
接下來(lái)是求梯度哈误?有了Theano哩至,我們只需要寫(xiě)出loss就可以啦,剩下的梯度交給Theano就行了蜜自。
代碼分析
接下來(lái)我們來(lái)分析用Theano實(shí)現(xiàn)LR算法的代碼菩貌。每行代碼前面都會(huì)加上相應(yīng)的注釋,請(qǐng)讀者閱讀仔細(xì)閱讀每行代碼和注釋重荠。
import numpyimport theanoimport theano.tensor as Trng = numpy.randomN = 400? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # 訓(xùn)練數(shù)據(jù)的數(shù)量 400feats = 784? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # 特征數(shù) 784# 生成訓(xùn)練數(shù)據(jù): D = ((N, feates), N個(gè)隨機(jī)數(shù)值) 箭阶,隨機(jī)數(shù)是0或者1D = (rng.randn(N, feats), rng.randint(size=N, low=0, high=2))training_steps = 10000# 定義兩個(gè)符號(hào)變量,x和y,其中x是一個(gè)double的matrix仇参,y是一個(gè)double的vectorx = T.dmatrix("x")y = T.dvector("y")# 隨機(jī)初始化參數(shù)w媳危,它的大小是feats## 我們把w定義為共享變量,這樣可以在多次迭代中共享冈敛。w = theano.shared(rng.randn(feats), name="w")# b也是共享變量,我們不需要隨機(jī)初始化鸣皂,一般bias出初始化為0就行了抓谴。b = theano.shared(0., name="b")print("Initial model:")print(w.get_value())print(b.get_value())# 構(gòu)造Theano表達(dá)式圖p_1 = 1 / (1 + T.exp(-T.dot(x, w) - b))? # 模型輸出1的概率,一次輸出的是N個(gè)樣本prediction = p_1 > 0.5? ? ? ? ? ? ? ? ? ? # 基于p_1預(yù)測(cè)分類xent = -y * T.log(p_1) - (1-y) * T.log(1-p_1) # Cross-entropy loss functioncost = xent.mean() + 0.01 * (w ** 2).sum()# loss函數(shù)寞缝,前面xent是一個(gè)向量癌压,所以求mean,然后使用L2 正則化荆陆,w越大就懲罰越大gw, gb = T.grad(cost,[w, b])? ? ? ? ? ? # 計(jì)算cost對(duì)w和b的梯度# train是一個(gè)函數(shù)滩届,它的輸入是x和y,輸出是分類預(yù)測(cè)prediction和xent被啼,注意updates參數(shù)帜消,每次調(diào)用train函數(shù)之后都會(huì)更新w<-w-0.1*gw, b<-b-0.1*gbtrain = theano.function(? ? ? ? ? inputs=[x,y],? ? ? ? ? outputs=[prediction, xent],? ? ? ? ? updates=((w, w - 0.1 * gw), (b, b - 0.1 * gb)))# pridict是一個(gè)函數(shù),輸入x浓体,輸出predictionpredict = theano.function(inputs=[x], outputs=prediction)# 訓(xùn)練泡挺,就是用訓(xùn)練數(shù)據(jù)x=D[0], y=D[1]進(jìn)行訓(xùn)練。# 也就算調(diào)用train函數(shù)命浴,train函數(shù)會(huì)使用當(dāng)前的w和b“前向”計(jì)算出prediction和xent娄猫,同時(shí)也計(jì)算出cost對(duì)w和b的梯度。然后再根據(jù)updates參數(shù)更新w和bfor i in range(training_steps):? ? pred, err = train(D[0], D[1])print("Final model:")print(w.get_value())print(b.get_value())print("target values for D:")print(D[1])print("prediction on D:")print(predict(D[0]))
注意:我們?yōu)榱颂岣咝噬校淮斡?jì)算N個(gè)訓(xùn)練數(shù)據(jù)媳溺,p_1 = 1 / (1 + T.exp(-T.dot(x, w) - b)),這里x是N feats碍讯,w是feats 1悬蔽,-T.dot(x,w)是N 1,而-b是一個(gè)1 1的數(shù)捉兴,所以會(huì)broadcasting屯阀,N個(gè)數(shù)都加上-b。然后exp轴术,然后得到p_1难衰,因此p_1是N*1的向量,代表了N個(gè)訓(xùn)練數(shù)據(jù)的輸出1的概率逗栽。
我們可以看到盖袭,在Theano里,我們實(shí)現(xiàn)一個(gè)模型非常簡(jiǎn)單,我們之需要如下步驟:
只需要把輸入和輸出定義成符號(hào)變量【有時(shí)為了加速我們可能也會(huì)把它定義成共享變量從而讓它放到gpu的顯存里鳄虱,后面的例子會(huì)介紹到】
把模型的參數(shù)定義成共享變量
然后寫(xiě)出loss函數(shù)的公式弟塞,同時(shí)定義loss對(duì)參數(shù)的梯度
定義一個(gè)訓(xùn)練函數(shù)train,輸入是模型的輸入變量拙已,輸出是loss【或者prediction决记,這樣我們可以在訓(xùn)練的時(shí)候打印出預(yù)測(cè)的準(zhǔn)確率】,updates用來(lái)更新模型參數(shù)
寫(xiě)有一個(gè)for循環(huán)不停的調(diào)用train
當(dāng)然這是全量的梯度下降倍踪,如果是batch的隨機(jī)梯度下降系宫,只需要每次循環(huán)傳入一個(gè)batch的輸入和輸出就行。
5. 計(jì)算圖
5.1 圖的結(jié)構(gòu)
內(nèi)容來(lái)自這里建车。
如果不了解原理而想在Theano里調(diào)試和profiling代碼不是件簡(jiǎn)單的事情扩借。這部分介紹給你關(guān)于Theano你必須要了解的一些實(shí)現(xiàn)細(xì)節(jié)。
寫(xiě)Theano代碼的第一步是使用符號(hào)變量寫(xiě)出所有的數(shù)學(xué)變量缤至。然后用+,-,*,sum(), tanh()等操作寫(xiě)出各種表達(dá)式潮罪。所有這些在theano內(nèi)部都表示成op。一個(gè)op表示一種特定的運(yùn)算领斥,它有一些輸入嫉到,然后計(jì)算出一些輸出。你可以把op類比成編程語(yǔ)言中的函數(shù)月洛。
Theano用圖來(lái)表示符號(hào)數(shù)學(xué)運(yùn)算屯碴。這些圖的點(diǎn)包括:Apply(實(shí)在想不出怎么翻譯),變量和op膊存,同時(shí)圖也包括這些點(diǎn)的連接(有向的邊)导而。Apply代表了op對(duì)某些變量的計(jì)算【op類比成函數(shù)的定義,apply類比成函數(shù)的實(shí)際調(diào)用隔崎,變量就是函數(shù)的參數(shù)】今艺。區(qū)分通過(guò)op定義的計(jì)算和把這個(gè)計(jì)算apply到某個(gè)實(shí)際的值是非常重要的【糇洌【我們?cè)诰幊虝r(shí)里定義 x和y虚缎,然后定義z=x+y,我們就得到了z的值钓株,但是我們?cè)赥heano里定義符號(hào)變量x和y实牡,然后定義z=x+y,因?yàn)閤和y只是一個(gè)符號(hào)轴合,所以z也只是一個(gè)符號(hào)创坞,我們需要再定義一個(gè)函數(shù),它的輸入是x和y輸出z受葛。然后”調(diào)用“這個(gè)函數(shù)题涨,傳入x和y的實(shí)際值偎谁,才能得到z的值】。符號(hào)變量的類型是通過(guò)Type這個(gè)類來(lái)表示的纲堵。下面是一段Theano的代碼以及對(duì)應(yīng)的圖巡雨。
代碼:
import theano.tensor as T
x = T.dmatrix('x')
y = T.dmatrix('y')
z = x + y
圖:
圖中的箭頭代表了Python對(duì)象的引用。藍(lán)色的框是Apply節(jié)點(diǎn)席函,紅色的是變量铐望,綠色的是Op,紫色的是Type茂附。
當(dāng)我們常見(jiàn)符號(hào)變量并且用Apply Op來(lái)產(chǎn)生更多變量的時(shí)候正蛙,我們創(chuàng)建了一個(gè)二分的有向無(wú)環(huán)圖。如果變量的owner有指向Apply的邊何之,那么說(shuō)明這個(gè)變量是由Apply對(duì)應(yīng)的Op產(chǎn)生的。此外Apply節(jié)點(diǎn)的input field和output field分別指向這個(gè)Op的輸入和輸出變量咽筋。
x和y的owner是None溶推,因?yàn)樗皇怯善渌麿p產(chǎn)生的,而是直接定義的奸攻。z的owner是非None的蒜危,這個(gè)Apply節(jié)點(diǎn)的輸入是x和y,輸出是z睹耐,Op是+辐赞,Apply的output指向了z,z.owner指向Apply硝训,因此它們 是互相引用的响委。
5.2 自動(dòng)求導(dǎo)
有了這個(gè)圖的結(jié)構(gòu),自動(dòng)計(jì)算導(dǎo)數(shù)就很容易了窖梁。tensor.grad()唯一需要做的就是從outputs逆向遍歷到輸入節(jié)點(diǎn)【如果您閱讀過(guò)之前的自動(dòng)求導(dǎo)部分赘风,就會(huì)明白每個(gè)Op就是當(dāng)時(shí)我們說(shuō)的一個(gè)Gate,它是可以根據(jù)forward階段的輸入值計(jì)算出對(duì)應(yīng)的local gradient纵刘,然后把所有的路徑加起來(lái)就得到梯度了】邀窃。對(duì)于每個(gè)Op,它都定義了怎么根據(jù)輸入計(jì)算出偏導(dǎo)數(shù)假哎。使用鏈?zhǔn)椒▌t就可以計(jì)算出梯度了瞬捕。
5.3 優(yōu)化
當(dāng)編譯一個(gè)Theano函數(shù)的時(shí)候,你給theano.function的其實(shí)是一個(gè)圖(從輸出變量遍歷到輸入遍歷)舵抹。你通過(guò)這個(gè)圖結(jié)構(gòu)來(lái)告訴theano怎么從input算出output肪虎,同時(shí)這也讓theano有機(jī)會(huì)來(lái)優(yōu)化這個(gè)計(jì)算圖【你可以把theano想像成一個(gè)編譯器,你通過(guò)它定義的符號(hào)計(jì)算語(yǔ)法來(lái)定義函數(shù)惧蛹,然后調(diào)用函數(shù)笋轨。而theano會(huì)想方設(shè)法優(yōu)化你的函數(shù)(當(dāng)然前提是保證結(jié)果是正確的)】秆剪。Theano的優(yōu)化包括發(fā)現(xiàn)圖里的一些模式(pattern)然后把他替換新的模式,這些新的模式計(jì)算的結(jié)果和原來(lái)是一樣的,但是心模式可能更快更穩(wěn)定薇芝。它也會(huì)檢測(cè)圖里的重復(fù)子圖避免重復(fù)計(jì)算焕阿,還有就是把某些子圖的計(jì)算生成等價(jià)的GPU版本放到GPU里執(zhí)行。
比如洁灵,一個(gè)簡(jiǎn)單的優(yōu)化可能是把
xyy
優(yōu)化成x。
例子
>>> import theano>>> a = theano.tensor.vector("a")? ? ? # declare symbolic variable>>> b = a + a ** 10? ? ? ? ? ? ? ? ? ? # build symbolic expression>>> f = theano.function([a], b)? ? ? ? # compile function>>> print(f([0, 1, 2]))? ? ? ? ? ? ? ? # prints `array([0,2,1026])`[0.? ? 2.? 1026.]>>> theano.printing.pydotprint(b, outfile="./pics/symbolic_graph_unopt.png", var_with_name_simple=True)? The output file is available at ./pics/symbolic_graph_unopt.png>>> theano.printing.pydotprint(f, outfile="./pics/symbolic_graph_opt.png", var_with_name_simple=True)? The output file is available at ./pics/symbolic_graph_opt.png
我們定義
b=a+a10
掺出,f是函數(shù)徽千,輸入a,輸出b汤锨。下面是沒(méi)有優(yōu)化的圖:
沒(méi)有優(yōu)化的圖有兩個(gè)Op双抽,power和add【還有一個(gè)DimShuffle,這個(gè)是Theano自己增加的一個(gè)Op闲礼,對(duì)于常量10牍汹,theano會(huì)創(chuàng)建一個(gè)TensorConstant。它是0維的tensor柬泽,也就是一個(gè)scalar慎菲。但是a我們定義的是一個(gè)vector,power是一個(gè)elementwise的操作锨并,底數(shù)是一個(gè)vector露该,那么指數(shù)也要是同樣大小的vector。dimshuffle(‘x’)就是給0維tensor增加一個(gè)維度變成1維的tensor(也就是vector)第煮,這樣維數(shù)就對(duì)上了解幼,但是x的shape可能是(100,)的,而常量是(1,)包警,大小不一樣怎么辦呢书幕?這就是broadcasting作的事情了,它會(huì)把dimshuffle(‘x’, 10)擴(kuò)展成(100,)的向量揽趾,每一個(gè)值都是10【實(shí)際numpy不會(huì)那么笨的復(fù)制100個(gè)10台汇,不過(guò)我們可以這么理解就好了】。之前我們也學(xué)過(guò)numpy的broadcasting篱瞎,theano和numpy的broadcasting使用一些區(qū)別的苟呐,有興趣的讀者可以參考這里。這里就不過(guò)多介紹了俐筋,如果后面有用到我們?cè)僬f(shuō)牵素。
下面是優(yōu)化過(guò)的圖:
優(yōu)化后變成了一個(gè)ElementWise的操作,其實(shí)就是把
b=a+a10
優(yōu)化成了
b=a+((a2)2)2+a2
關(guān)于Theano的簡(jiǎn)單介紹就先到這里澄者,后面講到RNN/LSTM會(huì)更多的介紹theano的scan函數(shù)以及怎么用Theano實(shí)現(xiàn)RNN/LSTM笆呆。下面我們講兩個(gè)實(shí)際的例子:用Theano來(lái)實(shí)現(xiàn)LR和MLP请琳。
6. Classifying MNIST digits using Logistic Regression
注意這里說(shuō)的LR和前面的LR是不同的,很多文獻(xiàn)說(shuō)的Logistic Regression是兩類的分類器赠幕,這里的LR推廣到了多類俄精,有些領(lǐng)域把它叫做最大熵(Max Entropy)模型,有的叫多類LR(multi-class logistic regression)榕堰。這里的LR是多類(10)的分類器竖慧,前面我們說(shuō)的是標(biāo)準(zhǔn)的LR,是一個(gè)兩類的分類器逆屡。
6.1 模型定義
Logistic Regression可以認(rèn)為是一個(gè)1層的神經(jīng)網(wǎng)絡(luò)圾旨,首先是一個(gè)仿射變換(沒(méi)有激活函數(shù)),然后接一個(gè)softmax魏蔗。
logistic regression的公式如下:
輸出Y是有限的分類砍的。比如對(duì)于MNIST數(shù)據(jù),Y的取值是0,1,…,9莺治。我們訓(xùn)練的時(shí)候如果圖片是數(shù)字3廓鞠,那么Y就是one-hot的表示的十維的向量[0,0,0,1,0,0,0,0,0,0]
預(yù)測(cè)的時(shí)候給定一個(gè)x,我們會(huì)計(jì)算出一個(gè)十維的向量产雹,比如[0.1, 0.8 , 0.0125, 0.0125,…0.0125]诫惭。那么我們會(huì)認(rèn)為這是數(shù)字1翁锡,因?yàn)槟P驼J(rèn)為輸出1的概率是0.8蔓挖。
模型定義的代碼如下所示:
# initialize with 0 the weights W as a matrix of shape (n_in, n_out)
self.W = theano.shared(
value=numpy.zeros(
(n_in, n_out),
dtype=theano.config.floatX
),
name='W',
borrow=True
)
# initialize the biases b as a vector of n_out 0s
self.b = theano.shared(
value=numpy.zeros(
(n_out,),
dtype=theano.config.floatX
),
name='b',
borrow=True
)
# symbolic expression for computing the matrix of class-membership
# probabilities
# Where:
# W is a matrix where column-k represent the separation hyperplane for
# class-k
# x is a matrix where row-j? represents input training sample-j
# b is a vector where element-k represent the free parameter of
# hyperplane-k
self.p_y_given_x = T.nnet.softmax(T.dot(input, self.W) + self.b)
# symbolic description of how to compute prediction as class whose
# probability is maximal
self.y_pred = T.argmax(self.p_y_given_x, axis=1)
theano里最重要的就是shared變量,我們一般把模型的參數(shù)定義為shared變量馆衔,我們可以用numpy的ndarray來(lái)定義它的shape并且給這些變量賦初始化的值瘟判。
self.W = theano.shared(
value=numpy.zeros(
(n_in, n_out),
dtype=theano.config.floatX
),
name='W',
borrow=True
)
(1) shared函數(shù)的value參數(shù)
上面我們定義了shared變量self.W,用numpy.zeros((n_in, n_out), dtype=theano.config.floatX)來(lái)定義了它是二維的數(shù)組(也就是矩陣)角溃,并且shape是(n_in, n_out)拷获,數(shù)據(jù)類型是theano.config.floatX,這是theano的一個(gè)配置項(xiàng)减细,我們可以在環(huán)境變量THEANO_FLAGS或者在$HOME/.theanorc文件里配置匆瓜。所有的配置選項(xiàng)請(qǐng)參考這里。
config.floatX用來(lái)配置使用多少位的浮點(diǎn)數(shù)未蝌。我們定義shared變量時(shí)引用theano.config.floatX驮吱,這樣就不用在代碼里寫(xiě)死到底是用32位還是64位的浮點(diǎn)數(shù),而是可以在環(huán)境變量或者配置文件里制定了萧吠。
比如我們?cè)谠试Spython是加上 THEANO_FLAGS=’floatX=float32’ python xxx.py左冬,那么W就是32位的浮點(diǎn)數(shù)。
(2) shared函數(shù)的name參數(shù)
shared變量另外一個(gè)參數(shù)就是name纸型,給變量命名便于調(diào)試拇砰。
(3) shared函數(shù)的borrow參數(shù)
使用theano時(shí)要區(qū)分兩部分內(nèi)存梅忌,一部分是我們的代碼(包括numpy)的內(nèi)存,另外就是theano自己管理的內(nèi)存除破,這包括shared變量和apply函數(shù)時(shí)的一些臨時(shí)內(nèi)存牧氮。所有的theano的函數(shù)只能處理它自己管理的內(nèi)存。那么函數(shù)的input呢皂岔?默認(rèn)情況下我們傳給theano函數(shù)的是python的對(duì)象或者numpy的對(duì)象蹋笼,會(huì)復(fù)制到theano管理的臨時(shí)變量里。因此為了優(yōu)化速度躁垛,我們有時(shí)會(huì)把訓(xùn)練數(shù)據(jù)定義成shared變量剖毯,避免重復(fù)的內(nèi)存拷貝。
borrow=True(默認(rèn)是False)讓theano shallow copy numpy的ndarray教馆,從而不節(jié)省空間逊谋。borrow是True的缺點(diǎn)是復(fù)用ndarray的內(nèi)存空間,如果用同一個(gè)ndarray給多個(gè)shared變量使用土铺,那么它們是共享這個(gè)內(nèi)存胶滋,任何一個(gè)人改了,別人都能看得到悲敷。我們一般不會(huì)用一個(gè)ndarray構(gòu)造多個(gè)shared 變量究恤,所以一般設(shè)置成True。
更多theano的內(nèi)存管理請(qǐng)參考這里后德。
【self.b的定義類似】
接下來(lái)我們定義p_y_given_x部宿,首先是仿射變換 T.dot(input, selft.W) + selft.b。然后加一個(gè)softmax瓢湃。
接下來(lái)是y_pred:
self.y_pred = T.argmax(self.p_y_given_x, axis=1)
我們使用argmax函數(shù)來(lái)選擇概率最大的那個(gè)下標(biāo)理张。注意axis=1,如果讀者follow之前的代碼绵患,應(yīng)該能明白代碼的含義雾叭,這和numpy里的argmax的axis完全是一樣的,原因是因?yàn)槲覀円淮吻罅薭atch個(gè)輸入的y落蝙。如果不太理解织狐,請(qǐng)讀者參考之前的文章。
6.2 定義loss function
前面的文章已經(jīng)講過(guò)很多次cross entropy的損失函數(shù)了筏勒。也就是真實(shí)分類作為下標(biāo)去取p_y_given_x 對(duì)應(yīng)的值移迫,然后-log就是這一個(gè)訓(xùn)練樣本的loss,但是我們需要去一個(gè)batch的loss奏寨,所以要用兩個(gè)下標(biāo)起意,一個(gè)是[0,1, …, batchSize-1],另一個(gè)就是樣本的真實(shí)分類y(每個(gè)y都是0-9)病瞳。
具體的代碼如下:
return -T.mean(T.log(self.p_y_given_x)[T.arange(y.shape[0]), y])
這里先對(duì)所有的p_y_given_x求log揽咕,然后在切片出想要的值悲酷,其實(shí)也可以先切片在求log:
return -T.mean(T.log(self.p_y_given_x[T.arange(y.shape[0]), y]))
我自己測(cè)試了一下,后者確實(shí)快(30s vs 20s)亲善,這么一個(gè)小小的修改速度就快了很多设易。
6.3 定義類LogisticRegression
我們可以把上面的所有代碼封裝成一個(gè)LogisticRegression類,以便重復(fù)使用蛹头。請(qǐng)讀者仔細(xì)閱讀每行代碼和注釋顿肺。
class LogisticRegression(object):? ? """多類 Logistic Regression 分類器? ? lr模型由weight矩陣W和biase向量b確定。通過(guò)把數(shù)據(jù)投影到一系列(分類數(shù)量個(gè))超平面上渣蜗,到朝平面的距離就被認(rèn)為是預(yù)測(cè)為這個(gè)分類的概率? ? """? ? def __init__(self, input, n_in, n_out):? ? ? ? """ 初始化參數(shù)? ? ? ? :參數(shù)類型 input: theano.tensor.TensorType? ? ? ? :參數(shù)說(shuō)明 input: 符號(hào)變量代表輸入的一個(gè)mini-batch? ? ? ? :參數(shù)類型 n_in: int? ? ? ? :參數(shù)說(shuō)明 n_in: 輸入神經(jīng)元的個(gè)數(shù)屠尊,mnist是28*28=784? ? ? ? :參數(shù)類型 n_out: int? ? ? ? :參數(shù)說(shuō)明 n_out: 輸出的個(gè)數(shù),mnist是10? ? ? ? """? ? ? ? # start-snippet-1? ? ? ? # 把weight W初始化成0耕拷,shape是(n_in, n_out)? ? ? ? self.W = theano.shared(? ? ? ? ? ? value=numpy.zeros(? ? ? ? ? ? ? ? (n_in, n_out),? ? ? ? ? ? ? ? dtype=theano.config.floatX? ? ? ? ? ? ),? ? ? ? ? ? name='W',? ? ? ? ? ? borrow=True? ? ? ? )? ? ? ? # 把biase初始化成0讼昆,shape是(n_out,)? ? ? ? self.b = theano.shared(? ? ? ? ? ? value=numpy.zeros(? ? ? ? ? ? ? ? (n_out,),? ? ? ? ? ? ? ? dtype=theano.config.floatX? ? ? ? ? ? ),? ? ? ? ? ? name='b',? ? ? ? ? ? borrow=True? ? ? ? )? ? ? ? # 給定x,y輸出0-9的概率骚烧,前面解釋過(guò)了? ? ? ? self.p_y_given_x = T.nnet.softmax(T.dot(input, self.W) + self.b)? ? ? ? # 預(yù)測(cè)? ? ? ? self.y_pred = T.argmax(self.p_y_given_x, axis=1)? ? ? ? # end-snippet-1? ? ? ? # 把模型的參數(shù)都保存起來(lái)浸赫,后面updates會(huì)用到? ? ? ? self.params =[self.W, self.b]? ? ? ? # 記下input? 為什么要保存到self里?因?yàn)槲覀冊(cè)陬A(yù)測(cè)的時(shí)候一般會(huì)重新load這個(gè)LogisticRegression類赃绊,因?yàn)槟P偷膮?shù)是LogisticRegression的成員變量(self.W, self.b)既峡,使用pickle.load的時(shí)候會(huì)恢復(fù)這些參數(shù),同時(shí)也會(huì)重新調(diào)用__init__方法碧查,所以整個(gè)計(jì)算圖就恢復(fù)了运敢。我們預(yù)測(cè)的時(shí)候需要定義predict的函數(shù)(還有一張方法就是在LogisticRegression里定義predict函數(shù)),這個(gè)時(shí)候就還需要輸入input么夫,所以保存input者冤,具體預(yù)測(cè)的代碼:? ? ? #### load the saved model? ? ? #### classifier = pickle.load(open('best_model.pkl'))? ? ? #### compile a predictor function? ? ? #### predict_model = theano.function(? ? ? ####? ? ? ? inputs=[classifier.input],? ? ? ####? ? ? ? outputs=classifier.y_pred)? ? ? self.input = input? ? def negative_log_likelihood(self, y):? ? ? ? """返回預(yù)測(cè)值在給定真實(shí)分布下的負(fù)對(duì)數(shù)似然(也就是cross entropy loss)? ? ? ? 參數(shù)類型 type y: theano.tensor.TensorType? ? ? ? 參數(shù)說(shuō)明 param y: 每個(gè)訓(xùn)練數(shù)據(jù)對(duì)應(yīng)的正確的標(biāo)簽(分類)組成的vecotr(因?yàn)槲覀円淮斡?jì)算一個(gè)minibatch)? ? 注意:我們這里使用了平均值而不是求和因?yàn)檫@樣的話learning rate就和batch大小無(wú)關(guān)了【我們調(diào)batch的時(shí)候可以不影響learning rate】? ? ? ? """? ? ? ? #前面已經(jīng)說(shuō)過(guò)了肤视,這里不再解釋? ? ? ? return -T.mean(T.log(self.p_y_given_x)[T.arange(y.shape[0]), y])? ? def errors(self, y):? ? ? ? """返回一個(gè)float代表這個(gè)minibatch的錯(cuò)誤率? ? ? ? :參數(shù)類型 type y: theano.tensor.TensorType? ? ? ? :參數(shù)說(shuō)明 param y: 同上面negative_log_likelihood的參數(shù)y? ? ? ? """? ? ? ? # 檢查維度是否匹配? ? ? ? if y.ndim != self.y_pred.ndim:? ? ? ? ? ? raise TypeError(? ? ? ? ? ? ? ? 'y should have the same shape as self.y_pred',? ? ? ? ? ? ? ? ('y', y.type, 'y_pred', self.y_pred.type)? ? ? ? ? ? )? ? ? ? # y必須是int類型的數(shù)據(jù)? ? ? ? if y.dtype.startswith('int'):? ? ? ? ? ? # the T.neq op 返回0和1,如果預(yù)測(cè)值y_pred和y不同就返回1? ? ? ? ? ? # T.neq是一個(gè)elementwise的操作档痪,所以用T.mean求評(píng)價(jià)的錯(cuò)誤率? ? ? ? ? ? return T.mean(T.neq(self.y_pred, y))? ? ? ? else:? ? ? ? ? ? raise NotImplementedError()
我們使用這個(gè)類的方法:
# 生成輸入的符號(hào)變量 (x and y 代表了一個(gè)minibatch的數(shù)據(jù))
x = T.matrix('x')? # 數(shù)據(jù)
y = T.ivector('y')? # labels
# 構(gòu)造LogisticRegression對(duì)象
# MNIST的圖片是28*28的,我們把它展開(kāi)成784的向量
classifier = LogisticRegression(input=x, n_in=28 * 28, n_out=10)
有了這個(gè)類的對(duì)象邢滑,接下來(lái)就可以定義lost function:
cost = classifier.negative_log_likelihood(y)
6.4 模型訓(xùn)練
在大部分編程語(yǔ)言里腐螟,我們都需要手工求loss對(duì)參數(shù)的梯度:
??/?W
??/?b
。對(duì)于復(fù)雜的模型困后,這非常容易弄錯(cuò)乐纸。另外還有很多細(xì)節(jié)比如數(shù)值計(jì)算的穩(wěn)定性(stability)。如果使用Theano摇予,問(wèn)題就很簡(jiǎn)單了汽绢,因?yàn)樗鼤?huì)自動(dòng)求導(dǎo)并且會(huì)做一些數(shù)學(xué)變換來(lái)提供數(shù)值計(jì)算的穩(wěn)定性。
To get the gradients \partial{\ell}/\partial{W} and \partial{\ell}/\partial侧戴 in Theano, simply do the following:
在Theano中求
??/?W
和
??/?b
宁昭,只需要如下兩行代碼:
g_W = T.grad(cost=cost, wrt=classifier.W)
g_b = T.grad(cost=cost, wrt=classifier.b)
g_W and g_b are symbolic variables, which can be used as part of a computation graph. The function train_model, which performs one step of gradient descent, can then be defined as follows:
g_W和g_b是符號(hào)變量跌宛,也是計(jì)算圖的一部分。函數(shù)train_model积仗,沒(méi)調(diào)用一次進(jìn)行一個(gè)minibatch的梯度下降疆拘,可以如下定義:
# 參數(shù)W和b的更新? ? updates =[(classifier.W, classifier.W - learning_rate * g_W),? ? ? ? ? ? ? (classifier.b, classifier.b - learning_rate * g_b)]? ? train_model = theano.function(? ? ? ? inputs=[index],? ? ? ? outputs=cost,? ? ? ? updates=updates,? ? ? ? givens={? ? ? ? ? ? x: train_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: train_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }? ? )
注意:這個(gè)train_model函數(shù)的參數(shù)是minibatch的下標(biāo)。為了提高訓(xùn)練速度寂曹,我們使用Theano時(shí)通常會(huì)把所有的訓(xùn)練數(shù)據(jù)也定義為共享變量哎迄,以便把它們放到GPU的顯存里,從而避免在cpu和gpu直接來(lái)回的復(fù)制數(shù)據(jù)【如果訓(xùn)練數(shù)據(jù)太大不能放到顯存里呢隆圆?比較容易想到的就是把訓(xùn)練數(shù)據(jù)(隨機(jī))的切分成能放到內(nèi)存的一個(gè)個(gè)window漱挚,然后把這個(gè)window的數(shù)據(jù)加載到顯存訓(xùn)練,然后再訓(xùn)練下一個(gè)window】渺氧。而我們每次訓(xùn)練時(shí)通過(guò)index來(lái)從train_set_x里選取這個(gè)minibatch的數(shù)據(jù):
givens={? ? ? ? ? ? x: train_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: train_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }
givens之前我們解釋過(guò)了棱烂,就是通過(guò)參數(shù)index來(lái)確定當(dāng)前的訓(xùn)練數(shù)據(jù)。為什么要用givens來(lái)制定x和y阶女?因?yàn)槲覀儧](méi)有辦法直接把x和y作為參數(shù)傳給train_model【否則就需要在cpu和gpu復(fù)制數(shù)據(jù)了】我們通過(guò)把train_set_x和train_set_y定義為共享變量颊糜,然后通過(guò)givens和index來(lái)制定當(dāng)前這個(gè)minibatch的x和y的值。
每次調(diào)用train_model秃踩,Theano會(huì)根據(jù)當(dāng)前的W和b計(jì)算loss和梯度g_W和g_b衬鱼,然后執(zhí)行updates更新W和b。
6.5 測(cè)試模型
要測(cè)試模型憔杨,首先需要定義錯(cuò)誤率:
def errors(self, y):
if y.ndim != self.y_pred.ndim:
raise TypeError(
'y should have the same shape as self.y_pred',
('y', y.type, 'y_pred', self.y_pred.type)
)
# check if y is of the correct datatype
if y.dtype.startswith('int'):
return T.mean(T.neq(self.y_pred, y))
else:
raise NotImplementedError()
前面是檢查y和y_pred的shape是否匹配鸟赫,因?yàn)門(mén)heano的Tensor在編譯時(shí)是沒(méi)有shape信息的。另外y是運(yùn)行是傳入的消别,我們也要檢查一下它的Type是否int抛蚤。
關(guān)鍵的一行代碼是:
return T.mean(T.neq(self.y_pred, y))
T.neq是個(gè)elementwise的函數(shù),如果兩個(gè)值相等就返回0寻狂,不相等返回1岁经,然后調(diào)用mean函數(shù)就得到錯(cuò)誤率。
接下來(lái)我們需要定義一個(gè)函數(shù)來(lái)計(jì)算錯(cuò)誤率蛇券,這個(gè)函數(shù)和訓(xùn)練非常類似缀壤,不過(guò)用的數(shù)據(jù)是測(cè)試數(shù)據(jù)和validation數(shù)據(jù)而已。validation可以幫助我們進(jìn)行early-stop纠亚。我們保留的最佳模型是在validation上表現(xiàn)最好的模型塘慕。
test_model = theano.function(? ? ? ? inputs=[index],? ? ? ? outputs=classifier.errors(y),? ? ? ? givens={? ? ? ? ? ? x: test_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: test_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }? ? )? ? validate_model = theano.function(? ? ? ? inputs=[index],? ? ? ? outputs=classifier.errors(y),? ? ? ? givens={? ? ? ? ? ? x: valid_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: valid_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }? ? )
6.6 完整的代碼
from __future__ import print_function__docformat__ = 'restructedtext en'import six.moves.cPickle as pickleimport gzipimport osimport sysimport timeitimport numpyimport theanoimport theano.tensor as Tclass LogisticRegression(object):? ? def __init__(self, input, n_in, n_out):? ? ? ? # start-snippet-1? ? ? ? # initialize with 0 the weights W as a matrix of shape (n_in, n_out)? ? ? ? self.W = theano.shared(? ? ? ? ? ? value=numpy.zeros(? ? ? ? ? ? ? ? (n_in, n_out),? ? ? ? ? ? ? ? dtype=theano.config.floatX? ? ? ? ? ? ),? ? ? ? ? ? name='W',? ? ? ? ? ? borrow=True? ? ? ? )? ? ? ? # initialize the biases b as a vector of n_out 0s? ? ? ? self.b = theano.shared(? ? ? ? ? ? value=numpy.zeros(? ? ? ? ? ? ? ? (n_out,),? ? ? ? ? ? ? ? dtype=theano.config.floatX? ? ? ? ? ? ),? ? ? ? ? ? name='b',? ? ? ? ? ? borrow=True? ? ? ? )? ? ? ? self.p_y_given_x = T.nnet.softmax(T.dot(input, self.W) + self.b)? ? ? ? self.y_pred = T.argmax(self.p_y_given_x, axis=1)? ? ? ? # end-snippet-1? ? ? ? # parameters of the model? ? ? ? self.params =[self.W, self.b]? ? ? ? # keep track of model input? ? ? ? self.input = input? ? def negative_log_likelihood(self, y):? ? ? ? # start-snippet-2? ? ? ? return -T.mean(T.log(self.p_y_given_x)[T.arange(y.shape[0]), y])? ? ? ? # end-snippet-2? ? def errors(self, y):? ? ? ? # check if y has same dimension of y_pred? ? ? ? if y.ndim != self.y_pred.ndim:? ? ? ? ? ? raise TypeError(? ? ? ? ? ? ? ? 'y should have the same shape as self.y_pred',? ? ? ? ? ? ? ? ('y', y.type, 'y_pred', self.y_pred.type)? ? ? ? ? ? )? ? ? ? # check if y is of the correct datatype? ? ? ? if y.dtype.startswith('int'):? ? ? ? ? ? # the T.neq operator returns a vector of 0s and 1s, where 1? ? ? ? ? ? # represents a mistake in prediction? ? ? ? ? ? return T.mean(T.neq(self.y_pred, y))? ? ? ? else:? ? ? ? ? ? raise NotImplementedError()def load_data(dataset):? ? ''' Loads the dataset? ? :type dataset: string? ? :param dataset: the path to the dataset (here MNIST)? ? '''? ? #############? ? # LOAD DATA #? ? #############? ? # Download the MNIST dataset if it is not present? ? data_dir, data_file = os.path.split(dataset)? ? if data_dir == "" and not os.path.isfile(dataset):? ? ? ? # Check if dataset is in the data directory.? ? ? ? new_path = os.path.join(? ? ? ? ? ? os.path.split(__file__)[0],? ? ? ? ? ? "..",? ? ? ? ? ? "data",? ? ? ? ? ? dataset? ? ? ? )? ? ? ? if os.path.isfile(new_path) or data_file == 'mnist.pkl.gz':? ? ? ? ? ? dataset = new_path? ? if (not os.path.isfile(dataset)) and data_file == 'mnist.pkl.gz':? ? ? ? from six.moves import urllib? ? ? ? origin = (? ? ? ? ? ? 'http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz'? ? ? ? )? ? ? ? print('Downloading data from %s' % origin)? ? ? ? urllib.request.urlretrieve(origin, dataset)? ? print('... loading data')? ? # Load the dataset? ? with gzip.open(dataset, 'rb') as f:? ? ? ? try:? ? ? ? ? ? train_set, valid_set, test_set = pickle.load(f, encoding='latin1')? ? ? ? except:? ? ? ? ? ? train_set, valid_set, test_set = pickle.load(f)? ? # train_set, valid_set, test_set format: tuple(input, target)? ? # input is a numpy.ndarray of 2 dimensions (a matrix)? ? # where each row corresponds to an example. target is a? ? # numpy.ndarray of 1 dimension (vector) that has the same length as? ? # the number of rows in the input. It should give the target? ? # to the example with the same index in the input.? ? def shared_dataset(data_xy, borrow=True):? ? ? ? data_x, data_y = data_xy? ? ? ? shared_x = theano.shared(numpy.asarray(data_x,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dtype=theano.config.floatX),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? borrow=borrow)? ? ? ? shared_y = theano.shared(numpy.asarray(data_y,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dtype=theano.config.floatX),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? borrow=borrow)? ? ? ? return shared_x, T.cast(shared_y, 'int32')? ? test_set_x, test_set_y = shared_dataset(test_set)? ? valid_set_x, valid_set_y = shared_dataset(valid_set)? ? train_set_x, train_set_y = shared_dataset(train_set)? ? rval =[(train_set_x, train_set_y), (valid_set_x, valid_set_y),? ? ? ? ? ? (test_set_x, test_set_y)]? ? return rvaldef sgd_optimization_mnist(learning_rate=0.13, n_epochs=1000,? ? ? ? ? ? ? ? ? ? ? ? ? dataset='mnist.pkl.gz',? ? ? ? ? ? ? ? ? ? ? ? ? batch_size=600):? ? datasets = load_data(dataset)? ? train_set_x, train_set_y = datasets[0]? ? valid_set_x, valid_set_y = datasets[1]? ? test_set_x, test_set_y = datasets[2]? ? # compute number of minibatches for training, validation and testing? ? n_train_batches = train_set_x.get_value(borrow=True).shape[0] // batch_size? ? n_valid_batches = valid_set_x.get_value(borrow=True).shape[0] // batch_size? ? n_test_batches = test_set_x.get_value(borrow=True).shape[0] // batch_size? ? ######################? ? # BUILD ACTUAL MODEL #? ? ######################? ? print('... building the model')? ? # allocate symbolic variables for the data? ? index = T.lscalar()? # index to a[mini]batch? ? # generate symbolic variables for input (x and y represent a? ? # minibatch)? ? x = T.matrix('x')? # data, presented as rasterized images? ? y = T.ivector('y')? # labels, presented as 1D vector of[int] labels? ? # construct the logistic regression class? ? # Each MNIST image has size 28*28? ? classifier = LogisticRegression(input=x, n_in=28 * 28, n_out=10)? ? # the cost we minimize during training is the negative log likelihood of? ? # the model in symbolic format? ? cost = classifier.negative_log_likelihood(y)? ? # compiling a Theano function that computes the mistakes that are made by? ? # the model on a minibatch? ? test_model = theano.function(? ? ? ? inputs=[index],? ? ? ? outputs=classifier.errors(y),? ? ? ? givens={? ? ? ? ? ? x: test_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: test_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }? ? )? ? validate_model = theano.function(? ? ? ? inputs=[index],? ? ? ? outputs=classifier.errors(y),? ? ? ? givens={? ? ? ? ? ? x: valid_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: valid_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }? ? )? ? # compute the gradient of cost with respect to theta = (W,b)? ? g_W = T.grad(cost=cost, wrt=classifier.W)? ? g_b = T.grad(cost=cost, wrt=classifier.b)? ? # start-snippet-3? ? # specify how to update the parameters of the model as a list of? ? # (variable, update expression) pairs.? ? updates =[(classifier.W, classifier.W - learning_rate * g_W),? ? ? ? ? ? ? (classifier.b, classifier.b - learning_rate * g_b)]? ? # compiling a Theano function `train_model` that returns the cost, but in? ? # the same time updates the parameter of the model based on the rules? ? # defined in `updates`? ? train_model = theano.function(? ? ? ? inputs=[index],? ? ? ? outputs=cost,? ? ? ? updates=updates,? ? ? ? givens={? ? ? ? ? ? x: train_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: train_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }? ? )? ? # end-snippet-3? ? ###############? ? # TRAIN MODEL #? ? ###############? ? print('... training the model')? ? # early-stopping parameters? ? patience = 5000? # look as this many examples regardless? ? patience_increase = 2? # wait this much longer when a new best is? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # found? ? improvement_threshold = 0.995? # a relative improvement of this much is? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # considered significant? ? validation_frequency = min(n_train_batches, patience // 2)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # go through this many? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # minibatche before checking the network? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # on the validation set; in this case we? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # check every epoch? ? best_validation_loss = numpy.inf? ? test_score = 0.? ? start_time = timeit.default_timer()? ? done_looping = False? ? epoch = 0? ? while (epoch < n_epochs) and (not done_looping):? ? ? ? epoch = epoch + 1? ? ? ? for minibatch_index in range(n_train_batches):? ? ? ? ? ? minibatch_avg_cost = train_model(minibatch_index)? ? ? ? ? ? # iteration number? ? ? ? ? ? iter = (epoch - 1) * n_train_batches + minibatch_index? ? ? ? ? ? if (iter + 1) % validation_frequency == 0:? ? ? ? ? ? ? ? # compute zero-one loss on validation set? ? ? ? ? ? ? ? validation_losses =[validate_model(i)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? for i in range(n_valid_batches)]? ? ? ? ? ? ? ? this_validation_loss = numpy.mean(validation_losses)? ? ? ? ? ? ? ? print(? ? ? ? ? ? ? ? ? ? 'epoch %i, minibatch %i/%i, validation error %f %%' %? ? ? ? ? ? ? ? ? ? (? ? ? ? ? ? ? ? ? ? ? ? epoch,? ? ? ? ? ? ? ? ? ? ? ? minibatch_index + 1,? ? ? ? ? ? ? ? ? ? ? ? n_train_batches,? ? ? ? ? ? ? ? ? ? ? ? this_validation_loss * 100.? ? ? ? ? ? ? ? ? ? )? ? ? ? ? ? ? ? )? ? ? ? ? ? ? ? # if we got the best validation score until now? ? ? ? ? ? ? ? if this_validation_loss < best_validation_loss:? ? ? ? ? ? ? ? ? ? #improve patience if loss improvement is good enough? ? ? ? ? ? ? ? ? ? if this_validation_loss < best_validation_loss *? \? ? ? ? ? ? ? ? ? ? ? improvement_threshold:? ? ? ? ? ? ? ? ? ? ? ? patience = max(patience, iter * patience_increase)? ? ? ? ? ? ? ? ? ? best_validation_loss = this_validation_loss? ? ? ? ? ? ? ? ? ? # test it on the test set? ? ? ? ? ? ? ? ? ? test_losses =[test_model(i)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? for i in range(n_test_batches)]? ? ? ? ? ? ? ? ? ? test_score = numpy.mean(test_losses)? ? ? ? ? ? ? ? ? ? print(? ? ? ? ? ? ? ? ? ? ? ? (? ? ? ? ? ? ? ? ? ? ? ? ? ? '? ? epoch %i, minibatch %i/%i, test error of'? ? ? ? ? ? ? ? ? ? ? ? ? ? ' best model %f %%'? ? ? ? ? ? ? ? ? ? ? ? ) %? ? ? ? ? ? ? ? ? ? ? ? (? ? ? ? ? ? ? ? ? ? ? ? ? ? epoch,? ? ? ? ? ? ? ? ? ? ? ? ? ? minibatch_index + 1,? ? ? ? ? ? ? ? ? ? ? ? ? ? n_train_batches,? ? ? ? ? ? ? ? ? ? ? ? ? ? test_score * 100.? ? ? ? ? ? ? ? ? ? ? ? )? ? ? ? ? ? ? ? ? ? )? ? ? ? ? ? ? ? ? ? # save the best model? ? ? ? ? ? ? ? ? ? with open('best_model.pkl', 'wb') as f:? ? ? ? ? ? ? ? ? ? ? ? pickle.dump(classifier, f)? ? ? ? ? ? if patience <= iter:? ? ? ? ? ? ? ? done_looping = True? ? ? ? ? ? ? ? break? ? end_time = timeit.default_timer()? ? print(? ? ? ? (? ? ? ? ? ? 'Optimization complete with best validation score of %f %%,'? ? ? ? ? ? 'with test performance %f %%'? ? ? ? )? ? ? ? % (best_validation_loss * 100., test_score * 100.)? ? )? ? print('The code run for %d epochs, with %f epochs/sec' % (? ? ? ? epoch, 1. * epoch / (end_time - start_time)))? ? print(('The code for file ' +? ? ? ? ? os.path.split(__file__)[1] +? ? ? ? ? ' ran for %.1fs' % ((end_time - start_time))), file=sys.stderr)def predict():? ? """? ? An example of how to load a trained model and use it? ? to predict labels.? ? """? ? # load the saved model? ? classifier = pickle.load(open('best_model.pkl'))? ? # compile a predictor function? ? predict_model = theano.function(? ? ? ? inputs=[classifier.input],? ? ? ? outputs=classifier.y_pred)? ? # We can test it on some examples from test test? ? dataset='mnist.pkl.gz'? ? datasets = load_data(dataset)? ? test_set_x, test_set_y = datasets[2]? ? test_set_x = test_set_x.get_value()? ? predicted_values = predict_model(test_set_x[:10])? ? print("Predicted values for the first 10 examples in test set:")? ? print(predicted_values)if __name__ == '__main__':? ? sgd_optimization_mnist()
大部分代碼都已經(jīng)解釋過(guò)來(lái),不過(guò)還有兩個(gè)函數(shù)shared_dataset和sgd_optimization_mnist需要再稍微解釋一下蒂胞。
前面說(shuō)過(guò)图呢,為了提高訓(xùn)練速度,我們需要把訓(xùn)練數(shù)據(jù)定義成共享變量。不過(guò)GPU里只能存儲(chǔ)浮點(diǎn)數(shù)【這不是GPU的限制蛤织,而是Theano的限制拥娄,具體參考這里】,但是我們需要把y當(dāng)成下標(biāo)用瞳筏,所以需要轉(zhuǎn)成int32:
return shared_x, T.cast(shared_y, 'int32')
不過(guò)即使這樣稚瘾,cast操作(op)還是會(huì)把y復(fù)制到cpu上進(jìn)行運(yùn)算的。所有涉及到y(tǒng)的計(jì)算是會(huì)放到cpu上的姚炕,也就是計(jì)算圖的loss會(huì)在cpu上運(yùn)行摊欠。這是Theano的一個(gè)缺陷,不知道為什么會(huì)是這樣的設(shè)計(jì)柱宦。不過(guò)那個(gè)stackoverflow的帖子回復(fù)里Daniel Renshaw說(shuō)如果只是把int用作下標(biāo)些椒,不知會(huì)不會(huì)能在GPU上。但是計(jì)算error肯定是在CPU上了掸刊,不過(guò)error函數(shù)不是在訓(xùn)練階段免糕,調(diào)用的次數(shù)也不會(huì)太多。
sgd_optimization_mnist實(shí)現(xiàn)sgd訓(xùn)練忧侧。
其實(shí)就是不停的調(diào)用train_model函數(shù)石窑,每經(jīng)過(guò)一次epoch,就在validation數(shù)據(jù)上進(jìn)行一次validation蚓炬,如果錯(cuò)誤率比當(dāng)前的最佳模型好松逊,就把它保存為最佳模型【用的是pickle】。不過(guò)這里使用了一個(gè)early-stop的技巧【參考這里】肯夏。
除了一個(gè)最大的epoch的限制经宏,如果迭代次數(shù)iter大于patience,那么就early-stop驯击。patience的初始值是5000烁兰,也就是說(shuō)至少要進(jìn)行5000次迭代。如果這一次的錯(cuò)誤率 < 上一次的錯(cuò)誤率乘以improvement_threshold(0.995)徊都,那么就認(rèn)為是比較大的一個(gè)提高沪斟,patience = max(patience, iter * patience_increase)。patience_increase=2碟贾。 大概的idea就是币喧,如果有比較大的提高轨域,那么就多一些”耐心“袱耽,多迭代幾次。反之如果沒(méi)有太多提高干发,咱就沒(méi)”耐心“了朱巨,就early-stop了。
6.7 使用訓(xùn)練好的模型來(lái)預(yù)測(cè)
def predict():? ? """? ? An example of how to load a trained model and use it? ? to predict labels.? ? """? ? # load the saved model? ? classifier = pickle.load(open('best_model.pkl'))? ? # compile a predictor function? ? predict_model = theano.function(? ? ? ? inputs=[classifier.input],? ? ? ? outputs=classifier.y_pred)? ? # We can test it on some examples from test test? ? dataset='mnist.pkl.gz'? ? datasets = load_data(dataset)? ? test_set_x, test_set_y = datasets[2]? ? test_set_x = test_set_x.get_value()? ? predicted_values = predict_model(test_set_x[:10])? ? print("Predicted values for the first 10 examples in test set:")? ? print(predicted_values)
前面都解釋過(guò)了枉长,首先pickle恢復(fù)模型的參數(shù)和計(jì)算圖冀续,然后定義predict_model函數(shù)琼讽,然后進(jìn)行預(yù)測(cè)就行了。
7. 使用Theano實(shí)現(xiàn)CNN
更新中...