李理:Theano tutorial和卷積神經網絡的Theano實現 Part1

本系列文章面向深度學習研發者,希望通過Image Caption Generation,一個有意思的具體任務,深入淺出地介紹深度學習的知識。本系列文章涉及到很多深度學習流行的模型,如CNN,RNN/LSTM,Attention等。本文為第8篇。

作者:李理

目前就職于環信,即時通訊云平臺和全媒體智能客服平臺,在環信從事智能客服和智能機器人相關工作,致力于用深度學習來提高智能機器人的性能。

相關文章:

李理:從Image Caption Generation理解深度學習(part I)

李理:從Image Caption Generation理解深度學習(part II)

李理:從Image Caption Generation理解深度學習(part III)

李理:自動梯度求解 反向傳播算法的另外一種視角

李理:自動梯度求解——cs231n的notes

李理:自動梯度求解——使用自動求導實現多層神經網絡

李理:詳解卷積神經網絡

1. Theano的發音

第一次碰到時很自然的發音是 /θi.??.no?/,不過如果看一些視頻可能也有發/te.?a?.no/的。這兩種都有,比較官方的說法可能是這個

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 :)

另外從這里也有說明:

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.

維基百科對此作出的解釋是:

Theano (/θ???no?/; Greek: Θεαν?; fl. 6th-century BC), or Theano of Crotone,[1] is the name given to perhaps two Pythagorean philosophers.

因此用英語的發音是 /θ???no?/。

2. Theano簡介

Theano是一個Python庫,它可以讓你定義,優化以及對數學表達式求值,尤其是多維數組(numpy的ndarray)的表達式的求值。對于解決大量數據的問題,使用Theano可能獲得與手工用C實現差不多的性能。另外通過利用GPU,它能獲得比CPU上的C實現快很多數量級。

Theano把計算機代數系統(CAS)和優化的編譯器結合在一起。 它也可以對許多數學操作生成自定義的c代碼。這種CAS和優化編譯的組合對于有復雜數學表達式重復的被求值并且求值速度很關鍵的問題是非常有用的。對于許多不同的表達式只求值一次的場景,Theano也能最小化編譯/分析的次數,但是仍然可以提供諸如自動差分這樣的符號計算的特性。

Theano的編譯器支持這些符號表達式的不同復雜程度的許多優化方法:

用GPU來計算

常量折疊(constant folding)【編譯時的常量表達式計算,參考這里】

合并相似的子圖,避免重復計算

算術簡化,比如把x*y/y簡化成y,–x【兩次求負】簡化成x

在不同的上下文中插入高效的BLAS函數(比如GEMM)

使用Memory Aliasing【詳細參考這里】來避免重復計算

對于不涉及aliasing的操作盡量使用就地的運算【類似與x*=2 vs y=x*2】

Elementwise的子表達式的循環的合并(loop fusion)【這是一項編譯器優化技巧,簡單的說就是把相同下標的循環合并起來,例子可以參考這里】

提高數值運算的穩定性,比如:

log(1+exp(x))andlog(∑iexp(x[i]))[/i]

?【關于這個我們羅嗦一點,讀者如果讀過的文章,肯定還記得計算softmax時先把向量減去最大的元素,避免exp運算的溢出】[list][*]更多內容請參考優化部分

[/*]

[/list]

3. Theano安裝

請參考這里,這里就不贅述了。

4. 官方Tutorial

4.1 Baby Steps - Algebra

內容來自這里

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)

我們這段代碼首先定義了符號變量x和y,它們的類型是double。使用theano.tensor.dscalar(‘x’)定義了一個名字叫x的類型為double的標量(scalar)。

注意符號變量的名字是theano看到的,而我們把theano創建的dscalar賦給x是在python里的。在使用theano是我們需要區分普通的python變量和theano的符號變量。theano用符號變量創建出一個computing graph,然后在這個graph上執行各種運算。

定義了x和y之后,我們通過操作(op)+定義了符號變量z。

接下來我們定義了一個函數(function) f,這個函數的輸入是符號變量x和y,輸出是符號變量z

接下來我們可以”執行“這個函數 f(2,3)

運行 f = function([x, y], z)會花費比較長的時間,theano會將函數構建成計算圖,并且做一些優化。

>>> type(x)

>>> x.type

TensorType(float64, scalar)

>>> T.dscalar

TensorType(float64, scalar)

>>> x.type is T.dscalar

True

dscalar(‘x’) 返回的對象的類型是theano.tensor.var.TensorVariable,也就是一種符號變量。這種對象有一個type屬性,x.type是TensorType。對于dscalar,它的TensorType是64位的浮點數的一個標量。

除了變量,我們也可以定義向量(vector)和矩陣matrix。

然后用在前面增加’b’,’w’,’i’,’l’,’f’,’d’,’c’分別表示8位,16位,32位,64位的整數,float,double以及負數。比如imatrix就是32位整數類型的矩陣,dvector就是單精度浮點數的向量。

4.2 More Examples

參考這里

這部分會介紹更多的theano的概念,最后包含一個Logistic Regression的例子,包括怎么用theano自動求梯度。

4.2.1 Logistic Function

函數定義為:

s(x)=11+e?x

函數圖像為:

這個函數的特點是它的值域是(0,1),當x趨近 ?∞ 時值趨近于0,當x趨近 ∞ 時值趨近于1。

我們經常需要對一個向量或者矩陣的每一個元素都應用一個函數,我們把這種操作叫做elementwise的操作(numpy里就叫universal function, ufunc)

比如下面的代碼對一個矩陣計算logistic函數:

>>> 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的原因是:定義這個符號變量的所有操作——除法,加法,指數取反都是elementwise的操作。

另外logistic函數和tanh函數有如下關系:

s(x)=11+e?x=1+tanh(x/2)2

我們可以使用下面的代碼來驗證這個式子:

>>> 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)

一個函數可以有內部的狀態。比如我們可以實現一個累加器,在開始的時候,它的值被初始化成零。然后每一次調用,這個狀態會加上函數的參數。

首先我們定義這個累加器函數,它把參數加到這個內部狀態變量,同時返回這個狀態變量老的值【調用前的值】

>>> from theano import shared>>> state = shared(0)>>> inc = T.iscalar('inc')>>> accumulator = function([inc], state, updates=[(state, state+inc)])

這里有不少新的概念。shared函數會返回共享變量。這種變量的值在多個函數直接可以共享。可以用符號變量的地方都可以用共享變量。但不同的是,共享變量有一個內部狀態的值,這個值可以被多個函數共享。我們可以使用get_value和set_value方法來讀取或者修改共享變量的值。

另外一個新的概念是函數的updates參數。updates參數是一個list,其中每個元素是一個tuple,這個tuple的第一個元素是一個共享變量,第二個元素是一個新的表達式。updates也可以是一個dict,key是共享變量,值是一個新的表達式。不管用哪種方法,它的意思是:當函數運行完成后,把新的表達式的值賦給這個共享變量。上面的accumulator函數的updates是把state+inc賦給state,也就是每次調用accumulator函數后state增加inc。

讓我們來試一試!

>>> print(state.get_value())

0

>>> accumulator(1)

array(0)

>>> print(state.get_value())

1

>>> accumulator(300)

array(1)

>>> print(state.get_value())

301

開始時state的值是0。然后調用一次accumulator(1),這個函數返回state原來的值,也就是0。然后把state更新為1。

然后再調用accumulator(300),這一次返回1,同時把state更新為301。

我們有可以重新設置state的值。只需要調用set_value方法就行:

>>> state.set_value(-1)

>>> accumulator(3)

array(-1)

>>> print(state.get_value())

2

我們首先把state設置成-1,然后調用accumulator(3),返回-1,同時吧state更新成了2。

我們前面提到過,多個函數可以“共享”一個共享變量,因此我們可以定義如下的函數:

>>> decrementor = function([inc], state, updates=[(state, state-inc)])>>> decrementor(2)array(2)>>> print(state.get_value())0

我們定義了decrementor函數,它每次返回之前的state的值,同時把state減去輸入參數inc后賦給state。

調用decrementor(2),返回state的之前的值2,同時把state更新成0。

你可能會奇怪為什么需要updates機制。你也可以讓這個函數返回這個新的表達式【當然原來的返回值仍然返回,多返回一個就行】,然后用在numpy更新state。首先updates機制是一種語法糖,寫起來更簡便。但更重要的是為了效率。共享變量的共享又是可以使用就地(in-place)的算法【符號變量包括共享變量的內存是由Theano來管理的,把它從Theano復制到numpy,然后修改,然后在復制到Theano很多時候是沒有必要的,更多Theano的內存管理請參考這里】。另外,共享變量的內存是由Theano來分配和管理,因此Theano可以根據需要來把它放到GPU的顯存里,這樣用GPU計算時可以避免CPU到GPU的數據拷貝,從而獲得更好的性能。

有些時候,你可以通過共享變量來定義了一個公式(函數),但是你不想用它的值。這種情況下,你可以用givens這個參數。

>>> 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

首先我們定義了一個符號變量fn_of_state,它用到了共享變量state。

然后我們定義skip_shared,他的輸入參數是inc和foo,輸出是fn_of_state。注意:fn_of_state依賴state和inc兩個符號變量,如果參數inc直接給定了。另外一個參數foo取代(而不是賦值給)了inc,因此實際 fn_of_state = foo * 2 + inc。我們調用skip_shared(1,3)會得到7,而state依然是0(而不是3)。如果把這個計算圖畫出來的話,實際是用foo替代了state。

givens參數可以取代任何符號變量,而不只是共享變量【從計算圖的角度就非常容易理解了,后面我們會講到Theano的計算圖】。你也可以用這個參數來替代常量和表達式。不過需要小心的是替代的時候不要引入循環的依賴。【比如a=b+c,你顯然不能把c又givens成a,這樣循環展開就不是有向無環圖了】

有了上面的基礎,我們可以用Theano來實現Logistic Regression算法了。不過這里沒有介紹grad,我們先簡單的介紹一下,內容來自這里。

使用Theano的好處就是auto diff,在前面也介紹過來,幾乎所有的深度學習框架/工具都是提供類似的auto diff的功能,只不過定義graph的“語言/語法”和“粒度”不一樣。另外除了求梯度,大部分工具還把訓練算法都封裝好了。而Theano就比較“原始”,它除了自動求梯度,并不會幫你實現sgd或者Adam算法,也不會幫你做dropout,不會幫你做weight decay和normalization,所有這些都得你自己完成。這可能會讓那些希望把深度學習當成一個“黑盒”的用戶有些失望,對于這樣的用戶最好用Keras,caffe這樣的工具。但是對于想理解更多細節和自己“創造”一種新的網絡結構的用戶,Theano是個非常好的工具,它提供常見的op,也可以自定義op(python或者c),對于rnn也有非常好的支持。

我們下面用Theano來實現對函數

f(x)=x2

的導數。

>>> 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

首先我們定義符號變量x,然后用x定義y,然后使用grad函數求y對x的(偏)導數gy【grad函數返回的仍然只是一個符號變量,可以認為用y和x定義了一個新的符號變量gy】,然后定義函數f,它的輸入是x,輸出是gy。注意:y是x的函數,gy是x和y的函數,所以最終gy只是x的函數,所以f的輸入只有x。

f編譯好了之后,給定x,我們就可以求

?y?x

在這個點上的值了。4.2.3 一個實際的例子:Logistic Regression

Logistic Regression(LR)簡介

LR模型用來進行二分類,它對輸入進行仿射變換,然后用logistic函數把它壓縮到0和1之間,訓練模型就是調整參數,對于類別0,讓模型輸出接近0的數,對于類別1,讓模型輸出接近1的數。預測的時候如果大于0.5就輸出1,反之輸出0。

因此我們可以把模型的輸出當成概率:

P(y=1|x)=hw(x)=11+exp(?wTx)

P(y=0|x)=1?P(y=1|x)=1?hw(x)

對于兩個概念分布,cross-entroy是最常見的一種度量方式。【詳細介紹參考這里】

loss=?ylogP(y=1|x)?(1?y)logP(y=0|x)

=?yloghw(x)?(1?y)log(1?hw(x))

如果真實值y=1,那么第二項就是0,

loss=?loghw(x)

,如果

hw(x)

趨近1,那么loss就趨近0;反之如果

hw(x)

趨近0,那么loss就趨近無窮大。如果真實值y=0,那么第一項就是0,

loss=?log(1?hw(x))

,如果

hw(x)

趨近0,

1?hw(x)

趨近1,loss趨近0;反之loss趨近無窮大。因此從上面的分析我們發現,這個loss函數是符合直覺的,模型輸出

hw(x)

越接近真實值,loss越小。有了loss,我們就可以用梯度下降求(局部)最優參數了。【這個loss函數是一個凸函數,所以局部最優就是全局最優,有興趣的讀者可以參考這里,不過對于工程師來說沒有必要了解這些細節。我們常見的神經網絡是非常復雜的非線性函數,因此loss通常也是非凸的,因此(隨機)梯度下降只能得到局部最優解,但是深度神經網絡通常能找到比較好的局部最優解,有也一些學者在做研究,有興趣的讀者請參考這里以及這里】

接下來是求梯度?有了Theano,我們只需要寫出loss就可以啦,剩下的梯度交給Theano就行了。

代碼分析

接下來我們來分析用Theano實現LR算法的代碼。每行代碼前面都會加上相應的注釋,請讀者閱讀仔細閱讀每行代碼和注釋。

import numpyimport theanoimport theano.tensor as Trng = numpy.randomN = 400? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # 訓練數據的數量 400feats = 784? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # 特征數 784# 生成訓練數據: D = ((N, feates), N個隨機數值) ,隨機數是0或者1D = (rng.randn(N, feats), rng.randint(size=N, low=0, high=2))training_steps = 10000# 定義兩個符號變量,x和y,其中x是一個double的matrix,y是一個double的vectorx = T.dmatrix("x")y = T.dvector("y")# 隨機初始化參數w,它的大小是feats## 我們把w定義為共享變量,這樣可以在多次迭代中共享。w = theano.shared(rng.randn(feats), name="w")# b也是共享變量,我們不需要隨機初始化,一般bias出初始化為0就行了。b = theano.shared(0., name="b")print("Initial model:")print(w.get_value())print(b.get_value())# 構造Theano表達式圖p_1 = 1 / (1 + T.exp(-T.dot(x, w) - b))? # 模型輸出1的概率,一次輸出的是N個樣本prediction = p_1 > 0.5? ? ? ? ? ? ? ? ? ? # 基于p_1預測分類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函數,前面xent是一個向量,所以求mean,然后使用L2 正則化,w越大就懲罰越大gw, gb = T.grad(cost,[w, b])? ? ? ? ? ? # 計算cost對w和b的梯度# train是一個函數,它的輸入是x和y,輸出是分類預測prediction和xent,注意updates參數,每次調用train函數之后都會更新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是一個函數,輸入x,輸出predictionpredict = theano.function(inputs=[x], outputs=prediction)# 訓練,就是用訓練數據x=D[0], y=D[1]進行訓練。# 也就算調用train函數,train函數會使用當前的w和b“前向”計算出prediction和xent,同時也計算出cost對w和b的梯度。然后再根據updates參數更新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]))

注意:我們為了提高效率,一次計算N個訓練數據,p_1 = 1 / (1 + T.exp(-T.dot(x, w) - b)),這里x是N feats,w是feats 1,-T.dot(x,w)是N 1,而-b是一個1 1的數,所以會broadcasting,N個數都加上-b。然后exp,然后得到p_1,因此p_1是N*1的向量,代表了N個訓練數據的輸出1的概率。

我們可以看到,在Theano里,我們實現一個模型非常簡單,我們之需要如下步驟:

只需要把輸入和輸出定義成符號變量【有時為了加速我們可能也會把它定義成共享變量從而讓它放到gpu的顯存里,后面的例子會介紹到】

把模型的參數定義成共享變量

然后寫出loss函數的公式,同時定義loss對參數的梯度

定義一個訓練函數train,輸入是模型的輸入變量,輸出是loss【或者prediction,這樣我們可以在訓練的時候打印出預測的準確率】,updates用來更新模型參數

寫有一個for循環不停的調用train

當然這是全量的梯度下降,如果是batch的隨機梯度下降,只需要每次循環傳入一個batch的輸入和輸出就行。

5. 計算圖

5.1 圖的結構

內容來自這里

如果不了解原理而想在Theano里調試和profiling代碼不是件簡單的事情。這部分介紹給你關于Theano你必須要了解的一些實現細節。

寫Theano代碼的第一步是使用符號變量寫出所有的數學變量。然后用+,-,*,sum(), tanh()等操作寫出各種表達式。所有這些在theano內部都表示成op。一個op表示一種特定的運算,它有一些輸入,然后計算出一些輸出。你可以把op類比成編程語言中的函數。

Theano用圖來表示符號數學運算。這些圖的點包括:Apply(實在想不出怎么翻譯),變量和op,同時圖也包括這些點的連接(有向的邊)。Apply代表了op對某些變量的計算【op類比成函數的定義,apply類比成函數的實際調用,變量就是函數的參數】。區分通過op定義的計算和把這個計算apply到某個實際的值是非常重要的。【我們在編程時里定義 x和y,然后定義z=x+y,我們就得到了z的值,但是我們在Theano里定義符號變量x和y,然后定義z=x+y,因為x和y只是一個符號,所以z也只是一個符號,我們需要再定義一個函數,它的輸入是x和y輸出z。然后”調用“這個函數,傳入x和y的實際值,才能得到z的值】。符號變量的類型是通過Type這個類來表示的。下面是一段Theano的代碼以及對應的圖。

代碼:

import theano.tensor as T

x = T.dmatrix('x')

y = T.dmatrix('y')

z = x + y

圖:

圖中的箭頭代表了Python對象的引用。藍色的框是Apply節點,紅色的是變量,綠色的是Op,紫色的是Type。

當我們常見符號變量并且用Apply Op來產生更多變量的時候,我們創建了一個二分的有向無環圖。如果變量的owner有指向Apply的邊,那么說明這個變量是由Apply對應的Op產生的。此外Apply節點的input field和output field分別指向這個Op的輸入和輸出變量。

x和y的owner是None,因為它不是由其它Op產生的,而是直接定義的。z的owner是非None的,這個Apply節點的輸入是x和y,輸出是z,Op是+,Apply的output指向了z,z.owner指向Apply,因此它們 是互相引用的。

5.2 自動求導

有了這個圖的結構,自動計算導數就很容易了。tensor.grad()唯一需要做的就是從outputs逆向遍歷到輸入節點【如果您閱讀過之前的自動求導部分,就會明白每個Op就是當時我們說的一個Gate,它是可以根據forward階段的輸入值計算出對應的local gradient,然后把所有的路徑加起來就得到梯度了】。對于每個Op,它都定義了怎么根據輸入計算出偏導數。使用鏈式法則就可以計算出梯度了。

5.3 優化

當編譯一個Theano函數的時候,你給theano.function的其實是一個圖(從輸出變量遍歷到輸入遍歷)。你通過這個圖結構來告訴theano怎么從input算出output,同時這也讓theano有機會來優化這個計算圖【你可以把theano想像成一個編譯器,你通過它定義的符號計算語法來定義函數,然后調用函數。而theano會想方設法優化你的函數(當然前提是保證結果是正確的)】。Theano的優化包括發現圖里的一些模式(pattern)然后把他替換新的模式,這些新的模式計算的結果和原來是一樣的,但是心模式可能更快更穩定。它也會檢測圖里的重復子圖避免重復計算,還有就是把某些子圖的計算生成等價的GPU版本放到GPU里執行。

比如,一個簡單的優化可能是把

xyy

優化成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是函數,輸入a,輸出b。下面是沒有優化的圖:

沒有優化的圖有兩個Op,power和add【還有一個DimShuffle,這個是Theano自己增加的一個Op,對于常量10,theano會創建一個TensorConstant。它是0維的tensor,也就是一個scalar。但是a我們定義的是一個vector,power是一個elementwise的操作,底數是一個vector,那么指數也要是同樣大小的vector。dimshuffle(‘x’)就是給0維tensor增加一個維度變成1維的tensor(也就是vector),這樣維數就對上了,但是x的shape可能是(100,)的,而常量是(1,),大小不一樣怎么辦呢?這就是broadcasting作的事情了,它會把dimshuffle(‘x’, 10)擴展成(100,)的向量,每一個值都是10【實際numpy不會那么笨的復制100個10,不過我們可以這么理解就好了】。之前我們也學過numpy的broadcasting,theano和numpy的broadcasting使用一些區別的,有興趣的讀者可以參考這里。這里就不過多介紹了,如果后面有用到我們再說。

下面是優化過的圖:

優化后變成了一個ElementWise的操作,其實就是把

b=a+a10

優化成了

b=a+((a2)2)2+a2

關于Theano的簡單介紹就先到這里,后面講到RNN/LSTM會更多的介紹theano的scan函數以及怎么用Theano實現RNN/LSTM。下面我們講兩個實際的例子:用Theano來實現LR和MLP。

6. Classifying MNIST digits using Logistic Regression

參考鏈接

注意這里說的LR和前面的LR是不同的,很多文獻說的Logistic Regression是兩類的分類器,這里的LR推廣到了多類,有些領域把它叫做最大熵(Max Entropy)模型,有的叫多類LR(multi-class logistic regression)。這里的LR是多類(10)的分類器,前面我們說的是標準的LR,是一個兩類的分類器。

6.1 模型定義

Logistic Regression可以認為是一個1層的神經網絡,首先是一個仿射變換(沒有激活函數),然后接一個softmax。

logistic regression的公式如下:

輸出Y是有限的分類。比如對于MNIST數據,Y的取值是0,1,…,9。我們訓練的時候如果圖片是數字3,那么Y就是one-hot的表示的十維的向量[0,0,0,1,0,0,0,0,0,0]

預測的時候給定一個x,我們會計算出一個十維的向量,比如[0.1, 0.8 , 0.0125, 0.0125,…0.0125]。那么我們會認為這是數字1,因為模型認為輸出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變量,我們一般把模型的參數定義為shared變量,我們可以用numpy的ndarray來定義它的shape并且給這些變量賦初始化的值。

self.W = theano.shared(

value=numpy.zeros(

(n_in, n_out),

dtype=theano.config.floatX

),

name='W',

borrow=True

)

(1) shared函數的value參數

上面我們定義了shared變量self.W,用numpy.zeros((n_in, n_out), dtype=theano.config.floatX)來定義了它是二維的數組(也就是矩陣),并且shape是(n_in, n_out),數據類型是theano.config.floatX,這是theano的一個配置項,我們可以在環境變量THEANO_FLAGS或者在$HOME/.theanorc文件里配置。所有的配置選項請參考這里。

config.floatX用來配置使用多少位的浮點數。我們定義shared變量時引用theano.config.floatX,這樣就不用在代碼里寫死到底是用32位還是64位的浮點數,而是可以在環境變量或者配置文件里制定了。

比如我們在允許python是加上 THEANO_FLAGS=’floatX=float32’ python xxx.py,那么W就是32位的浮點數。

(2) shared函數的name參數

shared變量另外一個參數就是name,給變量命名便于調試。

(3) shared函數的borrow參數

使用theano時要區分兩部分內存,一部分是我們的代碼(包括numpy)的內存,另外就是theano自己管理的內存,這包括shared變量和apply函數時的一些臨時內存。所有的theano的函數只能處理它自己管理的內存。那么函數的input呢?默認情況下我們傳給theano函數的是python的對象或者numpy的對象,會復制到theano管理的臨時變量里。因此為了優化速度,我們有時會把訓練數據定義成shared變量,避免重復的內存拷貝。

borrow=True(默認是False)讓theano shallow copy numpy的ndarray,從而不節省空間。borrow是True的缺點是復用ndarray的內存空間,如果用同一個ndarray給多個shared變量使用,那么它們是共享這個內存,任何一個人改了,別人都能看得到。我們一般不會用一個ndarray構造多個shared 變量,所以一般設置成True。

更多theano的內存管理請參考這里。

【self.b的定義類似】

接下來我們定義p_y_given_x,首先是仿射變換 T.dot(input, selft.W) + selft.b。然后加一個softmax。

接下來是y_pred:

self.y_pred = T.argmax(self.p_y_given_x, axis=1)

我們使用argmax函數來選擇概率最大的那個下標。注意axis=1,如果讀者follow之前的代碼,應該能明白代碼的含義,這和numpy里的argmax的axis完全是一樣的,原因是因為我們一次求了batch個輸入的y。如果不太理解,請讀者參考之前的文章。

6.2 定義loss function

前面的文章已經講過很多次cross entropy的損失函數了。也就是真實分類作為下標去取p_y_given_x 對應的值,然后-log就是這一個訓練樣本的loss,但是我們需要去一個batch的loss,所以要用兩個下標,一個是[0,1, …, batchSize-1],另一個就是樣本的真實分類y(每個y都是0-9)。

具體的代碼如下:

return -T.mean(T.log(self.p_y_given_x)[T.arange(y.shape[0]), y])

這里先對所有的p_y_given_x求log,然后在切片出想要的值,其實也可以先切片在求log:

return -T.mean(T.log(self.p_y_given_x[T.arange(y.shape[0]), y]))

我自己測試了一下,后者確實快(30s vs 20s),這么一個小小的修改速度就快了很多。

6.3 定義類LogisticRegression

我們可以把上面的所有代碼封裝成一個LogisticRegression類,以便重復使用。請讀者仔細閱讀每行代碼和注釋。

class LogisticRegression(object):? ? """多類 Logistic Regression 分類器? ? lr模型由weight矩陣W和biase向量b確定。通過把數據投影到一系列(分類數量個)超平面上,到朝平面的距離就被認為是預測為這個分類的概率? ? """? ? def __init__(self, input, n_in, n_out):? ? ? ? """ 初始化參數? ? ? ? :參數類型 input: theano.tensor.TensorType? ? ? ? :參數說明 input: 符號變量代表輸入的一個mini-batch? ? ? ? :參數類型 n_in: int? ? ? ? :參數說明 n_in: 輸入神經元的個數,mnist是28*28=784? ? ? ? :參數類型 n_out: int? ? ? ? :參數說明 n_out: 輸出的個數,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的概率,前面解釋過了? ? ? ? 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? ? ? ? # 把模型的參數都保存起來,后面updates會用到? ? ? ? self.params =[self.W, self.b]? ? ? ? # 記下input? 為什么要保存到self里?因為我們在預測的時候一般會重新load這個LogisticRegression類,因為模型的參數是LogisticRegression的成員變量(self.W, self.b),使用pickle.load的時候會恢復這些參數,同時也會重新調用__init__方法,所以整個計算圖就恢復了。我們預測的時候需要定義predict的函數(還有一張方法就是在LogisticRegression里定義predict函數),這個時候就還需要輸入input,所以保存input,具體預測的代碼:? ? ? #### 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):? ? ? ? """返回預測值在給定真實分布下的負對數似然(也就是cross entropy loss)? ? ? ? 參數類型 type y: theano.tensor.TensorType? ? ? ? 參數說明 param y: 每個訓練數據對應的正確的標簽(分類)組成的vecotr(因為我們一次計算一個minibatch)? ? 注意:我們這里使用了平均值而不是求和因為這樣的話learning rate就和batch大小無關了【我們調batch的時候可以不影響learning rate】? ? ? ? """? ? ? ? #前面已經說過了,這里不再解釋? ? ? ? return -T.mean(T.log(self.p_y_given_x)[T.arange(y.shape[0]), y])? ? def errors(self, y):? ? ? ? """返回一個float代表這個minibatch的錯誤率? ? ? ? :參數類型 type y: theano.tensor.TensorType? ? ? ? :參數說明 param y: 同上面negative_log_likelihood的參數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類型的數據? ? ? ? if y.dtype.startswith('int'):? ? ? ? ? ? # the T.neq op 返回0和1,如果預測值y_pred和y不同就返回1? ? ? ? ? ? # T.neq是一個elementwise的操作,所以用T.mean求評價的錯誤率? ? ? ? ? ? return T.mean(T.neq(self.y_pred, y))? ? ? ? else:? ? ? ? ? ? raise NotImplementedError()

我們使用這個類的方法:

# 生成輸入的符號變量 (x and y 代表了一個minibatch的數據)

x = T.matrix('x')? # 數據

y = T.ivector('y')? # labels

# 構造LogisticRegression對象

# MNIST的圖片是28*28的,我們把它展開成784的向量

classifier = LogisticRegression(input=x, n_in=28 * 28, n_out=10)

有了這個類的對象,接下來就可以定義lost function:

cost = classifier.negative_log_likelihood(y)

6.4 模型訓練

在大部分編程語言里,我們都需要手工求loss對參數的梯度:

??/?W

??/?b

。對于復雜的模型,這非常容易弄錯。另外還有很多細節比如數值計算的穩定性(stability)。如果使用Theano,問題就很簡單了,因為它會自動求導并且會做一些數學變換來提供數值計算的穩定性。

To get the gradients \partial{\ell}/\partial{W} and \partial{\ell}/\partial{b} 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是符號變量,也是計算圖的一部分。函數train_model,沒調用一次進行一個minibatch的梯度下降,可以如下定義:

# 參數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]? ? ? ? }? ? )

注意:這個train_model函數的參數是minibatch的下標。為了提高訓練速度,我們使用Theano時通常會把所有的訓練數據也定義為共享變量,以便把它們放到GPU的顯存里,從而避免在cpu和gpu直接來回的復制數據【如果訓練數據太大不能放到顯存里呢?比較容易想到的就是把訓練數據(隨機)的切分成能放到內存的一個個window,然后把這個window的數據加載到顯存訓練,然后再訓練下一個window】。而我們每次訓練時通過index來從train_set_x里選取這個minibatch的數據:

givens={? ? ? ? ? ? x: train_set_x[index * batch_size: (index + 1) * batch_size],? ? ? ? ? ? y: train_set_y[index * batch_size: (index + 1) * batch_size]? ? ? ? }

givens之前我們解釋過了,就是通過參數index來確定當前的訓練數據。為什么要用givens來制定x和y?因為我們沒有辦法直接把x和y作為參數傳給train_model【否則就需要在cpu和gpu復制數據了】我們通過把train_set_x和train_set_y定義為共享變量,然后通過givens和index來制定當前這個minibatch的x和y的值。

每次調用train_model,Theano會根據當前的W和b計算loss和梯度g_W和g_b,然后執行updates更新W和b。

6.5 測試模型

要測試模型,首先需要定義錯誤率:

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是否匹配,因為Theano的Tensor在編譯時是沒有shape信息的。另外y是運行是傳入的,我們也要檢查一下它的Type是否int。

關鍵的一行代碼是:

return T.mean(T.neq(self.y_pred, y))

T.neq是個elementwise的函數,如果兩個值相等就返回0,不相等返回1,然后調用mean函數就得到錯誤率。

接下來我們需要定義一個函數來計算錯誤率,這個函數和訓練非常類似,不過用的數據是測試數據和validation數據而已。validation可以幫助我們進行early-stop。我們保留的最佳模型是在validation上表現最好的模型。

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()

大部分代碼都已經解釋過來,不過還有兩個函數shared_dataset和sgd_optimization_mnist需要再稍微解釋一下。

前面說過,為了提高訓練速度,我們需要把訓練數據定義成共享變量。不過GPU里只能存儲浮點數【這不是GPU的限制,而是Theano的限制,具體參考這里】,但是我們需要把y當成下標用,所以需要轉成int32:

return shared_x, T.cast(shared_y, 'int32')

不過即使這樣,cast操作(op)還是會把y復制到cpu上進行運算的。所有涉及到y的計算是會放到cpu上的,也就是計算圖的loss會在cpu上運行。這是Theano的一個缺陷,不知道為什么會是這樣的設計。不過那個stackoverflow的帖子回復里Daniel Renshaw說如果只是把int用作下標,不知會不會能在GPU上。但是計算error肯定是在CPU上了,不過error函數不是在訓練階段,調用的次數也不會太多。

sgd_optimization_mnist實現sgd訓練。

其實就是不停的調用train_model函數,每經過一次epoch,就在validation數據上進行一次validation,如果錯誤率比當前的最佳模型好,就把它保存為最佳模型【用的是pickle】。不過這里使用了一個early-stop的技巧【參考這里】。

除了一個最大的epoch的限制,如果迭代次數iter大于patience,那么就early-stop。patience的初始值是5000,也就是說至少要進行5000次迭代。如果這一次的錯誤率 < 上一次的錯誤率乘以improvement_threshold(0.995),那么就認為是比較大的一個提高,patience = max(patience, iter * patience_increase)。patience_increase=2。 大概的idea就是,如果有比較大的提高,那么就多一些”耐心“,多迭代幾次。反之如果沒有太多提高,咱就沒”耐心“了,就early-stop了。

6.7 使用訓練好的模型來預測

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)

前面都解釋過了,首先pickle恢復模型的參數和計算圖,然后定義predict_model函數,然后進行預測就行了。

7. 使用Theano實現CNN

更新中...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容