來源:http://deeplearning.net/tutorial/lenet.html#lenet
Convolutional Neural Networks (LeNet)
note:這部分假設讀者已經看過(Theano3.3-練習之邏輯回歸)和(Theano3.4-練習之多層感知機)。另外,這里是用新的theano函數和概念: T.tanh, shared variables, basic arithmetic ops, T.grad, floatX,downsample , conv2d, dimshuffle.如果你想要在GPU上跑,記得看看 GPU。為了在GPU上跑這個例子,你需要一個好的GPU,至少需要1GB的顯存。如果你的顯示器也是連接着這個GPU,那么就需要注意一些事情了。因為GPU連接着顯示器的時候,在每個GPU的函數調用的時候都會有幾秒的限制。這是因為在當顯示器需要請求GPU的時候,是無法進行計算的。沒有這個限制的時候,你的屏幕看起來就像是死機一樣,而且還會保持很長時間。(該例子就是在中端GPU下遇到的這個問題,這句話是原作者遇到的)當GPU沒有連接到顯示器的時候,就沒有時間限制了,你可以降低batch size來解決這個時間延遲的問題。
note:這部分的可用代碼可以從這里 here和這里 3wolfmoon image下到。
一、動機
卷積神經網絡(Convolutional Neural Networks ,CNN)是MLPs的生物啟發的變種。從Hubel和Wiesel的早期在貓的視覺皮層上的工作[Hubel68]上來看,我們知道視覺皮層包含着許多復雜排列的細胞。這些細胞對於視覺區域中小的子區域是非常敏感的,叫做感受野。這些子區域可以平鋪從而覆蓋整個視覺區域。這些細胞扮演着基於輸入空間的局部過濾器而且很適合用來探索自然圖像中的空間局部的強相關性。
另外,兩類基本的細胞類型已經被發現:簡單細胞當檢測到它們感受野的類邊緣模式的時候的響應是最大的。復雜的細胞有着更大的感受野,而且對於陌生的提取的位置具有局部不變性。
動物的視覺皮層是現今最好的視覺處理系統,所以我們很自然的去效仿它的原理。因此,能夠在文獻中找到許多神經啟發的模型。比如:NeoCogitron [Fukushima], HMAX [Serre07]and LeNet-5 [LeCun98],本教程關注的是第三個模型。
二、稀疏鏈接
CNN是通過在毗連的層的神經元之間建立局部連接模式來達到空間局部化的相關性。換句話說,第 m 層的隱藏單元的輸入就是來自於第 m -1 層單元的子集,這些單元有着空間連續感受野。我們可以以下圖示意:
想象下第m -1 層是視網膜作為輸入。在上面部分,第 m 層的單元在基於視網膜輸入的基礎上有着width 為3的感受野,所以在視網膜層就只有3個毗連的神經元與上層的一個單元相連。第 m +1 層的單元與下層有着相似的連接,這里關於下層的感受野也同樣是3,不過它們關於視網膜輸入層的感受野是5(比3大)。每個單元對於視網膜上感受野外部區域的響應是無變化的(沒反應也就是)。這個結構因此就能確保學到的“過濾器”能夠對空間局部輸入模式生成最強的響應。
然而,正如上面介紹的,堆疊許多這樣的(非線性)“過濾器”層就能夠增加“全局”性(即,能夠對更大的像素空間進行響應)。例如,第m+1的隱藏層中的單元能夠編碼一個有着width為5(在像素空間中的單位)的非線性特征。
三、共享權重
另外,在CNN中,每個過濾器 在整個視覺區域上是交叉重復的。在一個特征圖中這些重復的單元共享相同的參數(權重向量和偏置):
在上面的圖中,我們展示的是屬於同一個特征圖的3個隱藏單元。具有相同顏色的權重的值是相同的。梯度下降仍然可以用來學習這樣的共享的參數,只是需要稍微改動下原來的算法。共享權重的梯度是簡單的共享的參數的梯度的和。
以這種形式來重復的單元允許檢測到的特征在視覺區域中無視它們所處的位置。另外權重共享通過大量的減少了所需要學習的自由參數的數量而提升了學習的效率。這個模型上的約束條件能夠保證CNN在視覺問題上得到更好的泛化。
四、細節和符號介紹
一個特征圖可以通過重復的將同一個函數交叉的應用在整個圖像的子區域上,換句話說,通過使用一個線性分類器來對輸入圖像進行卷積,並增加一個偏置項,然后使用一個非線性函數來計算。假設我們在給定的層上第 k 個特征圖為,該特征圖上的過濾器是由權重
和偏置
決定的 ,然后這個特征圖
可以如下形式獲得:
note:回顧下1D信號的卷積的定義 。這可以擴展到2D的形式:
。
為了形成數據的更豐富的表征,每個隱藏層都是由多特征圖組成的, 。一個隱藏層的權重
可以被表示成4D張量的形式,其中包含了每個元素都是由目的特征圖、源特征圖、源垂直位置、源水平位置的組合而成的。偏置
可以被表示成一個向量,其中包含着的每個元素都是對應着每個不同的目標特征圖。圖示的形式如下:
圖1:一個卷積層的例子
該圖顯示的是一個CNN的兩層。第m-1層包含着4個特征圖。隱藏層m 包含着2個特征圖( 和
)。在
和
(以藍色和紅色標出的方形區域)中的像素(神經元輸出)是從第m-1層中的像素計算出來的,而第m-1層中的感受野為2×2(有色的矩形框)。注意這里感受野是如何跨越所有的四個輸入特征圖的。
、
的權重
、
所以是3D權重張量,其中第一個維度是用來索引輸入特征圖的,同時其他兩個用來表示像素的坐標。
將它們放在一起,那么, 用來表示在第m 層的第k 個特征圖上每個像素與第m-1層的第 I 個特征圖的(i,j)位置上的像素相連的權重。
五、卷積操作
在Theano中ConvOp是實現一個卷積層的主力。ConvOp通過theano.tensor.singal.conv2d來使用,這里需要兩個符號輸入:
- 一個 4D 張量對應着輸入圖像的一個mini-batch。 張量的原型為: [mini-batch size, 輸入特征圖的個數, 圖像的高度, 圖像的寬度].
- 一個 4D 張量對應着權重矩陣
. 該張量的原型為: [第m層特征圖的個數, 第m-1層特征圖的格式,過濾器的高度,過濾器的寬度]。
import theano from theano import tensor as T from theano.tensor.nnet import conv import numpy rng = numpy.random.RandomState(23455) # instantiate 4D tensor for input input = T.tensor4(name='input') # initialize shared variable for weights. w_shp = (2, 3, 9, 9) w_bound = numpy.sqrt(3 * 9 * 9) W = theano.shared( numpy.asarray( rng.uniform( low=-1.0 / w_bound, high=1.0 / w_bound, size=w_shp), dtype=input.dtype), name ='W') # initialize shared variable for bias (1D tensor) with random values # IMPORTANT: biases are usually initialized to zero. However in this # particular application, we simply apply the convolutional layer to # an image without learning the parameters. We therefore initialize # them to random values to "simulate" learning. b_shp = (2,) b = theano.shared(numpy.asarray( rng.uniform(low=-.5, high=.5, size=b_shp), dtype=input.dtype), name ='b') # build symbolic expression that computes the convolution of input with filters in w conv_out = conv.conv2d(input, W) # build symbolic expression to add bias and apply activation function, i.e. produce neural net layer output # A few words on ``dimshuffle`` : # ``dimshuffle`` is a powerful tool in reshaping a tensor; # what it allows you to do is to shuffle dimension around # but also to insert new ones along which the tensor will be # broadcastable; # dimshuffle('x', 2, 'x', 0, 1) # This will work on 3d tensors with no broadcastable # dimensions. The first dimension will be broadcastable, # then we will have the third dimension of the input tensor as # the second of the resulting tensor, etc. If the tensor has # shape (20, 30, 40), the resulting tensor will have dimensions # (1, 40, 1, 20, 30). (AxBxC tensor is mapped to 1xCx1xAxB tensor) # More examples: # dimshuffle('x') -> make a 0d (scalar) into a 1d vector # dimshuffle(0, 1) -> identity # dimshuffle(1, 0) -> inverts the first and second dimensions # dimshuffle('x', 0) -> make a row out of a 1d vector (N to 1xN) # dimshuffle(0, 'x') -> make a column out of a 1d vector (N to Nx1) # dimshuffle(2, 0, 1) -> AxBxC to CxAxB # dimshuffle(0, 'x', 1) -> AxB to Ax1xB # dimshuffle(1, 'x', 0) -> AxB to Bx1xA output = T.nnet.sigmoid(conv_out + b.dimshuffle('x', 0, 'x', 'x')) # create theano function to compute filtered images f = theano.function([input], output)
用這個來做點有趣的事情:
import numpy import pylab from PIL import Image # open random image of dimensions 639x516 img = Image.open(open('doc/images/3wolfmoon.jpg')) # dimensions are (height, width, channel) img = numpy.asarray(img, dtype='float64') / 256. # put image in 4D tensor of shape (1, 3, height, width) img_ = img.transpose(2, 0, 1).reshape(1, 3, 639, 516) filtered_img = f(img_) # plot original image and first and second components of output pylab.subplot(1, 3, 1); pylab.axis('off'); pylab.imshow(img) pylab.gray(); # recall that the convOp output (filtered image) is actually a "minibatch", # of size 1 here, so we take index 0 in the first dimension: pylab.subplot(1, 3, 2); pylab.axis('off'); pylab.imshow(filtered_img[0, 0, :, :]) pylab.subplot(1, 3, 3); pylab.axis('off'); pylab.imshow(filtered_img[0, 1, :, :]) pylab.show()
應該會生成這樣的輸出:
注意到一個隨機初始化的過濾器就像是一個邊緣檢測器!
注意到我們使用了和MLP中一樣的權重初始化公式。權重是從一個均勻分布 [-1/fan-in, 1/fan-in]中隨機采樣得到的。這里fan-in就是輸入到一個隱藏單元的數量。對於MLPs來說,這是下層的單元的數量。對於CNN來說,需要考慮到輸入特征圖的數量和感受野的size。
六、最大池化
另一個CNN的重要概念就是最大池化,這是非線性下采樣的一種形式。最大池化是將輸入圖像划分成一個非重疊矩陣集合,然后對於每個子區域,輸出他們的最大值。
最大池化在視覺中很有用是基於以下兩個原因:
-
通過消除非最大值, 減少了上層的計算量.
-
提供了一種平移不變性的形式. 想象下一個卷積層級聯着一層最大池化層。對於一個單一的像素來說它有8個方向可以平移,如果是在一個2×2區域上使用最大池化,那么這8個可能的組合中的3個將會在卷積層上生成一樣的輸出,對於基於3×3的窗口上的最大池化來說,它達到了5/8(就是原來是3/8)。因為它提供額外的位置上的魯棒性,最大池化是一種“明智”的方式來減少中間表征的維度。
一個例子勝過千言萬語:
from theano.tensor.signal import downsample input = T.dtensor4('input') maxpool_shape = (2, 2) pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=True) f = theano.function([input],pool_out) invals = numpy.random.RandomState(1).rand(3, 2, 5, 5) print 'With ignore_border set to True:' print 'invals[0, 0, :, :] =\n', invals[0, 0, :, :] print 'output[0, 0, :, :] =\n', f(invals)[0, 0, :, :] pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=False) f = theano.function([input],pool_out) print 'With ignore_border set to False:' print 'invals[1, 0, :, :] =\n ', invals[1, 0, :, :] print 'output[1, 0, :, :] =\n ', f(invals)[1, 0, :, :] This should generate the following output: With ignore_border set to True: invals[0, 0, :, :] = [[ 4.17022005e-01 7.20324493e-01 1.14374817e-04 3.02332573e-01 1.46755891e-01] [ 9.23385948e-02 1.86260211e-01 3.45560727e-01 3.96767474e-01 5.38816734e-01] [ 4.19194514e-01 6.85219500e-01 2.04452250e-01 8.78117436e-01 2.73875932e-02] [ 6.70467510e-01 4.17304802e-01 5.58689828e-01 1.40386939e-01 1.98101489e-01] [ 8.00744569e-01 9.68261576e-01 3.13424178e-01 6.92322616e-01 8.76389152e-01]] output[0, 0, :, :] = [[ 0.72032449 0.39676747] [ 0.6852195 0.87811744]] With ignore_border set to False: invals[1, 0, :, :] = [[ 0.01936696 0.67883553 0.21162812 0.26554666 0.49157316] [ 0.05336255 0.57411761 0.14672857 0.58930554 0.69975836] [ 0.10233443 0.41405599 0.69440016 0.41417927 0.04995346] [ 0.53589641 0.66379465 0.51488911 0.94459476 0.58655504] [ 0.90340192 0.1374747 0.13927635 0.80739129 0.39767684]] output[1, 0, :, :] = [[ 0.67883553 0.58930554 0.69975836] [ 0.66379465 0.94459476 0.58655504] [ 0.90340192 0.80739129 0.39767684]]
注意到和大多數theano代碼相比,max_pool_2d操作有一點特別。他需要一個縮小因子 ds (長度為2的元組,包含圖像寬度和高度的縮小因子)在graph建立的時候需要知道的。這在將來也許會改變(也就是10年的這個theano和今年15年的這個函數有可能不一樣,要注意)。
七、完整的模型:LeNet
稀疏、卷積層和最大池化是LeNet模型家族的核心。不過這些模型的詳細細節還是變化很大的,下圖顯示了一個LeNet模型的示意圖:
低層都是有交替的卷積和最大池化層構成的。高層是全連接層,對應着一個傳統的MLP(隱藏層+邏輯回歸)。輸入到第一個全連接層的是低層的所有特征圖的集合。
從一個實現的角度來看,這意味着低層是在4D張量上操作的,然后平鋪成一個2D矩陣柵格特征圖,用來兼容之前的MLP實現。
八、把上面的合並到一起
我們現在有了所有需要的。先來構建一個LeNetConvPoolLayer 類,用來實現{卷積+最大池化}層:
class LeNetConvPoolLayer(object): """Pool Layer of a convolutional network """ def __init__(self, rng, input, filter_shape, image_shape, poolsize=(2, 2)): """ Allocate a LeNetConvPoolLayer with shared variable internal parameters. :type rng: numpy.random.RandomState :param rng: a random number generator used to initialize weights :type input: theano.tensor.dtensor4 :param input: symbolic image tensor, of shape image_shape :type filter_shape: tuple or list of length 4 :param filter_shape: (number of filters, num input feature maps, filter height, filter width) :type image_shape: tuple or list of length 4 :param image_shape: (batch size, num input feature maps, image height, image width) :type poolsize: tuple or list of length 2 :param poolsize: the downsampling (pooling) factor (#rows, #cols) """ assert image_shape[1] == filter_shape[1] self.input = input # there are "num input feature maps * filter height * filter width" # inputs to each hidden unit fan_in = numpy.prod(filter_shape[1:]) # each unit in the lower layer receives a gradient from: # "num output feature maps * filter height * filter width" / # pooling size fan_out = (filter_shape[0] * numpy.prod(filter_shape[2:]) / numpy.prod(poolsize)) # initialize weights with random weights W_bound = numpy.sqrt(6. / (fan_in + fan_out)) self.W = theano.shared( numpy.asarray( rng.uniform(low=-W_bound, high=W_bound, size=filter_shape), dtype=theano.config.floatX ), borrow=True ) # the bias is a 1D tensor -- one bias per output feature map b_values = numpy.zeros((filter_shape[0],), dtype=theano.config.floatX) self.b = theano.shared(value=b_values, borrow=True) # convolve input feature maps with filters conv_out = conv.conv2d( input=input, filters=self.W, filter_shape=filter_shape, image_shape=image_shape ) # downsample each feature map individually, using maxpooling pooled_out = downsample.max_pool_2d( input=conv_out, ds=poolsize, ignore_border=True ) # add the bias term. Since the bias is a vector (1D array), we first # reshape it to a tensor of shape (1, n_filters, 1, 1). Each bias will # thus be broadcasted across mini-batches and feature map # width & height self.output = T.tanh(pooled_out + self.b.dimshuffle('x', 0, 'x', 'x')) # store parameters of this layer self.params = [self.W, self.b]
注意到當初始化權重值的時候,fan-in是由感受野的size和輸入特征圖的個數決定的。
最后,使用在(Theano3.3-練習之邏輯回歸)中的LogisticRegression 類和(Theano3.4-練習之多層感知機)中定義的HiddenLayer 類,我們可以如下來實例化:
x = T.matrix('x') # the data is presented as rasterized images y = T.ivector('y') # the labels are presented as 1D vector of # [int] labels ###################### # BUILD ACTUAL MODEL # ###################### print '... building the model' # Reshape matrix of rasterized images of shape (batch_size, 28 * 28) # to a 4D tensor, compatible with our LeNetConvPoolLayer # (28, 28) is the size of MNIST images. layer0_input = x.reshape((batch_size, 1, 28, 28)) # Construct the first convolutional pooling layer: # filtering reduces the image size to (28-5+1 , 28-5+1) = (24, 24) # maxpooling reduces this further to (24/2, 24/2) = (12, 12) # 4D output tensor is thus of shape (batch_size, nkerns[0], 12, 12) layer0 = LeNetConvPoolLayer( rng, input=layer0_input, image_shape=(batch_size, 1, 28, 28), filter_shape=(nkerns[0], 1, 5, 5), poolsize=(2, 2) ) # Construct the second convolutional pooling layer # filtering reduces the image size to (12-5+1, 12-5+1) = (8, 8) # maxpooling reduces this further to (8/2, 8/2) = (4, 4) # 4D output tensor is thus of shape (batch_size, nkerns[1], 4, 4) layer1 = LeNetConvPoolLayer( rng, input=layer0.output, image_shape=(batch_size, nkerns[0], 12, 12), filter_shape=(nkerns[1], nkerns[0], 5, 5), poolsize=(2, 2) ) # the HiddenLayer being fully-connected, it operates on 2D matrices of # shape (batch_size, num_pixels) (i.e matrix of rasterized images). # This will generate a matrix of shape (batch_size, nkerns[1] * 4 * 4), # or (500, 50 * 4 * 4) = (500, 800) with the default values. layer2_input = layer1.output.flatten(2) # construct a fully-connected sigmoidal layer layer2 = HiddenLayer( rng, input=layer2_input, n_in=nkerns[1] * 4 * 4, n_out=500, activation=T.tanh ) # classify the values of the fully-connected sigmoidal layer layer3 = LogisticRegression(input=layer2.output, n_in=500, n_out=10) # the cost we minimize during training is the NLL of the model cost = layer3.negative_log_likelihood(y) # create a function to compute the mistakes that are made by the model test_model = theano.function( [index], layer3.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( [index], layer3.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] } ) # create a list of all model parameters to be fit by gradient descent params = layer3.params + layer2.params + layer1.params + layer0.params # create a list of gradients for all model parameters grads = T.grad(cost, params) # train_model is a function that updates the model parameters by # SGD Since this model has many parameters, it would be tedious to # manually create an update rule for each model parameter. We thus # create the updates list by automatically looping over all # (params[i], grads[i]) pairs. updates = [ (param_i, param_i - learning_rate * grad_i) for param_i, grad_i in zip(params, grads) ] train_model = theano.function( [index], 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] } )
這里沒有實際訓練和早期停止的代碼,因為它實際上和MLP的一樣。感興趣的讀者可以訪問DeepLearningTutorials中“code”這個文件夾。
九、運行該代碼
用戶可以如下形式運行該代碼:
接下來的輸出可以在 Core i7-2600K CPU clocked at 3.40GHz上使用默認參數和 flags ‘floatX=float32’:來得到: 使用GeForce GTX 285,得到如下結果: 使用GeForce GTX 480的結果:注意到在驗證的時候和測試時候誤差的差異(迭代的次數),這是因為硬件中舍入機制的不同實現造成的。這可以被忽略掉,不用管。
在win7_64bit+cuda6.5_64bit+anaconda2.1.0_64bit+gtx 780ti,結果:
(不知道為什么時間反而多了,原因待分析)。
十、提示和技巧
選擇超參數
CNN訓練的時候特別需要技巧,因為它們相比一個標准的MLP來說有着更多的超參數。不過通常的學習率和正則化約束的經驗規則還是適用的,接下來就是在優化CNN的時候需要記住的。
過濾器的數量
當選擇每一層的過濾器的個數的時候,記得一個單一的卷積過濾器的激活值的計算比傳統的MLPs代價更高昂。
假設層包含着
個特征圖和
個像素位置(即,位置個數乘以特征圖個數),在形狀為
的層
上有
個過濾器。然后計算一個特征圖(在所有過濾器能夠使用的
個像素位置上使用一個
的過濾器)的代價為
。總的cost是
乘以這個值。如果在同一層的所有的特征不是連接到之前一層的所有特征,那么事情就會變得更加的復雜。
對於一個標准的MLP,cost將會只是 ,這里 在
層上有
個不同的神經元。同樣的,在CNNs中使用的過濾器的個數通常要小於在MLPs中隱藏單元的個數,並且依賴於特征圖的size(它本身的一個輸入圖像的size和過濾器形狀的函數)
因為特征圖的size隨着深度的增加而下降,靠近輸入層的層會有着更少的過濾器而更高層就會有更多的過濾器。事實上,為了在每一層中平衡下計算量,這些特征個數和像素位置個數的乘積通常在層之間是差不多保持穩定的。為了保留有關輸入的信息,將會需要維持激活的總數(特征圖的數量乘以像素位置數量)來使得從這一層到下一層的時候沒有減少(當然,我們希望在做有監督訓練的時候沒有變得更少)。特征圖的個數直接控制着能力(capacity),同樣依賴於可利用的樣本的個數和任務的復雜程度。
過濾器的形狀
一般過濾器形狀在文獻中變化萬千,通常是基於特定的數據集的。在MNIST-sized 圖像(28×28)這樣的上,最好的結果通常是第一層上有着5×5的過濾器大小,同時對於自然圖像數據集(通常在每一維上有着上百個像素)傾向於第一層使用更大的過濾器,例如12×12或者15×15。
所以這里的技巧就是在給定的數據集的基礎上,去找到正確的“粒度”(即,過濾器形狀),從而能夠在合適的尺寸下生成好的抽象表征。
最大池化的形狀
通常來說值為2×2或者沒有最大池化操作。非常大的輸入圖像也許在低層上會有着4×4的池化。不過記得,這會以因子為16來減少信號的維度,同時也許會導致丟失過多的信息。
腳注
[1] 更清晰的說,我們使用“unit”或者“neuron”來表示人工神經元,“cell”來表示生物神經元。
提示
如果你想在一個新的數據集上使用這個模型,這里有一些提示也許有助於你生成更好的結果:
- 對數據進行白化 (e.g. with PCA)
- 在每個epoch上衰減學習率
參考資料:
[1] 官網:http://deeplearning.net/tutorial/lenet.html#lenet
[2] Deep learning with Theano 官方中文教程(翻譯)(四) 卷積神經網絡(CNN):http://www.cnblogs.com/charleshuang/p/3651843.html