1. 程式人生 > >卷積神經網路CNN與基於MNIST的Python程式碼示例

卷積神經網路CNN與基於MNIST的Python程式碼示例

卷積神經網路入門學(1)

原文地址http://blog.csdn.net/hjimce/article/details/47323463

作者:hjimce

卷積神經網路演算法是n年前就有的演算法,只是近年來因為深度學習相關演算法為多層網路的訓練提供了新方法,然後現在電腦的計算能力已非當年的那種計算水平,同時現在的訓練資料很多,於是神經網路的相關演算法又重新火了起來因此卷積神經網路就又活了起來。

在開始前,我們需要明確的是網上講的卷積神經網路的相關教程一般指的是神經網路的前向傳導過程,反向傳播都是用梯度下降法進行訓練,大部分深度學習庫,都已經把反向求導的功能給封裝好了,如果想要深入學習反向求導,就需要自己慢慢學了。

因為卷積神經網路的經典模型是:Lenet-5實現,只要理解了這個的前向傳導過程,基本上就OK了,因此我們後面主要講解Lenet-5的實現。

一、理論階段

作為CNN的入門文章,沒有打算囉嗦太多的東西,因為什麼權值共享、區域性感受野什麼的,講那麼多,都是那些生物學的相關理論,看了那些玩意,大部分初學者已經煩了。卷積神經網路的相關博文也是一大堆,但是講的,基本上都是抄過來抄過去,就像我之前不理解從S2層到C3層是怎麼實現的,網上看了一大堆教程,沒有一個解答這個問題的。我的個人感覺整個過程,就只有S2到C3是最難理解的。接著我將用最淺顯易懂的方式進行講解。

1、卷積

卷積的概念這個我想只要學過影象處理的人都懂的概念了,這個不解釋。我們知道對於給定的一幅影象來說,給定一個卷積核,卷積就是根據卷積視窗,進行畫素的加權求和。


卷積神經網路與我們之前所學到的影象的卷積的區別,我的理解是:我們之前學影象處理遇到卷積,一般來說,這個卷積核是已知的,比如各種邊緣檢測運算元、高斯模糊等這些,都是已經知道卷積核,然後再與影象進行卷積運算。然而深度學習中的卷積神經網路卷積核是未知的,我們訓練一個神經網路,就是要訓練得出這些卷積核,而這些卷積核就相當於我們學單層感知器的時候的那些引數W,因此你可以把這些待學習的卷積核看成是神經網路的訓練引數W。

2、池化

剛開始學習CNN的時候,看到這個詞,好像高大上的樣子,於是查了很多資料,理論一大堆,但是實踐、演算法實現卻都沒講到,也不懂池化要怎麼實現?其實所謂的池化,就是圖片下采樣。這個時候,你會發現CNN每一層的構建跟影象高斯金字塔的構建有點類似,因此你如果已經懂得了影象金字塔融合的相關演算法,那麼就變的容易理解了。在高斯金子塔構建中,每一層通過卷積,然後卷積後進行下采樣,而CNN也是同樣的過程。廢話不多說,這裡就講一下,CNN的池化:

CNN的池化(影象下采樣)方法很多:Mean pooling(均值取樣)、Max pooling(最大值取樣)、Overlapping (重疊取樣)、L2 pooling(均方取樣)、Local Contrast Normalization(歸一化取樣)、Stochasticpooling(隨即取樣)、Def-pooling(形變約束取樣)。其中最經典的是最大池化,因此我就解釋一下最大池化的實現:


原圖片

為了簡單起見,我用上面的圖片作為例子,假設上面的圖片大小是4*4的,如上圖所示,然後圖片中每個畫素點的值是上面各個格子中的數值。然後我要對這張4*4的圖片進行池化,池化的大小為(2,2),跨步為2,那麼採用最大池化也就是對上面4*4的圖片進行分塊,每個塊的大小為2*2,然後統計每個塊的最大值,作為下采樣後圖片的畫素值,具體計算如下圖所示:


也就是說我們最後得到下采樣後的圖片為:


這就是所謂的最大池化。當然以後你還會遇到各種池化方法,比如均值池化,也就是對每個塊求取平均值作為下采樣的新畫素值。還有重疊取樣的池化,我上面這個例子是沒有重疊的取樣的,也就是每個塊之間沒有相互重疊的部分,上面我說的跨步為2,就是為了使得分塊都非重疊,等等,這些以後再跟大家解釋池化常用方法。這裡就先記住最大池化就好了,因為這個目前是最常用的。

3、feature maps 

這個單詞國人把它翻譯成特徵圖,挺起來很專業的名詞。那麼什麼叫特徵圖呢?特徵圖其實說白了就是CNN中的每張圖片,都可以稱之為特徵圖張。在CNN中,我們要訓練的卷積核並不是僅僅只有一個,這些卷積核用於提取特徵,卷積核個數越多,提取的特徵越多,理論上來說精度也會更高,然而卷積核一堆,意味著我們要訓練的引數的個數越多。在LeNet-5經典結構中,第一層卷積核選擇了6個,而在AlexNet中,第一層卷積核就選擇了96個,具體多少個合適,還有待學習。

回到特徵圖概念,CNN的每一個卷積層我們都要人為的選取合適的卷積核個數,及卷積核大小。每個卷積核與圖片進行卷積,就可以得到一張特徵圖了,比如LeNet-5經典結構中,第一層卷積核選擇了6個,我們可以得到6個特徵圖,這些特徵圖也就是下一層網路的輸入了。我們也可以把輸入圖片看成一張特徵圖,作為第一層網路的輸入。

4、CNN的經典結構

對於剛入門CNN的人來說,我們首先需要現在的一些經典結構:

(1)LeNet-5。這個是n多年前就有的一個CNN的經典結構,主要是用於手寫字型的識別,也是剛入門需要學習熟悉的一個網路,我的這篇博文主要就是要講這個網路


(2)AlexNet。


在imagenet上的影象分類challenge上大神Alex提出的alexnet網路結構模型贏得了2012屆的冠軍,振奮人心,利用CNN實現了圖片分類,別人用傳統的機器學習演算法調參跳到半死也就那樣,Alex利用CNN精度遠超傳統的網路。

其它的還有什麼《Network In Network》,GoogLeNet、Deconvolution Network,在以後的學習中我們會遇到。比如利用Deconvolution Network反捲積網路實現圖片的去模糊,牛逼哄哄。

    OK,理論階段就囉嗦到這裡就好了,接著就講解 LeNet-5, LeNet-5是用於手寫字型的識別的一個經典CNN:


LeNet-5結構

輸入:32*32的手寫字型圖片,這些手寫字型包含0~9數字,也就是相當於10個類別的圖片

輸出:分類結果,0~9之間的一個數

因此我們可以知道,這是一個多分類問題,總共有十個類,因此神經網路的最後輸出層必然是SoftMax問題,然後神經元的個數是10個。LeNet-5結構:

輸入層:32*32的圖片,也就是相當於1024個神經元

C1層:paper作者,選擇6個特徵卷積核,然後卷積核大小選擇5*5,這樣我們可以得到6個特徵圖,然後每個特徵圖的大小為32-5+1=28,也就是神經元的個數為6*28*28=784。

S2層:這就是下采樣層,也就是使用最大池化進行下采樣,池化的size,選擇(2,2),也就是相當於對C1層28*28的圖片,進行分塊,每個塊的大小為2*2,這樣我們可以得到14*14個塊,然後我們統計每個塊中,最大的值作為下采樣的新畫素,因此我們可以得到S1結果為:14*14大小的圖片,共有6個這樣的圖片。

C3層:卷積層,這一層我們選擇卷積核的大小依舊為5*5,據此我們可以得到新的圖片大小為14-5+1=10,然後我們希望可以得到16張特徵圖。那麼問題來了?這一層是最難理解的,我們知道S2包含:6張14*14大小的圖片,我們希望這一層得到的結果是:16張10*10的圖片。這16張圖片的每一張,是通過S2的6張圖片進行加權組合得到的,具體是怎麼組合的呢?問題如下圖所示:


為了解釋這個問題,我們先從簡單的開始,我現在假設輸入6特徵圖的大小是5*5的,分別用6個5*5的卷積核進行卷積,得到6個卷積結果圖片大小為1*1,如下圖所示:


    為了簡便起見,我這裡先做一些標號的定義:我們假設輸入第i個特徵圖的各個畫素值為x1i,x2i……x25i,因為每個特徵圖有25個畫素。因此第I個特徵圖經過5*5的圖片卷積後,得到的卷積結果圖片的畫素值Pi可以表示成:


這個是卷積公式,不解釋。因此對於上面的P1~P6的計算方法,這個就是直接根據公式。然後我們把P1~P6相加起來,也就是:

P=P1+P2+……P6

把上面的Pi的計算公式,代入上式,那麼我們可以得到:

P=WX

其中X就是輸入的那6張5*5特徵圖片的各個畫素點值,而W就是我們需要學習的引數,也就相當於6個5*5的卷積核,當然它包含著6*(5*5)個引數。因此我們的輸出特徵圖就是:

Out=f(P+b)

這個就是從S2到C3的計算方法,其中b表示偏置項,f為啟用函式。

我們迴歸到原來的問題:有6張輸入14*14的特徵圖片,我們希望用5*5的卷積核,然後最後我們希望得到一張10*10的輸出特徵圖片?

根據上面的過程,也就是其實我們用5*5的卷積核去卷積每一張輸入的特徵圖,當然每張特徵圖的卷積核引數是不一樣的,也就是不共享,因此我們就相當於需要6*(5*5)個引數。對每一張輸入特徵圖進行卷積後,我們得到6張10*10,新圖片,這個時候,我們把這6張圖片相加在一起,然後加一個偏置項b,然後用啟用函式進行對映,就可以得到一張10*10的輸出特徵圖了。

    而我們希望得到16張10*10的輸出特徵圖,因此我們就需要卷積引數個數為16*(6*(5*5))=16*6*(5*5)個引數。總之,C3層每個圖片是通過S2圖片進行卷積後,然後相加,並且加上偏置b,最後在進行啟用函式對映得到的結果。

S4層:下采樣層,比較簡單,也是知己對C3的16張10*10的圖片進行最大池化,池化塊的大小為2*2。因此最後S4層為16張大小為5*5的圖片。至此我們的神經元個數已經減少為:16*5*5=400。

C5層:我們繼續用5*5的卷積核進行卷積,然後我們希望得到120個特徵圖。這樣C5層圖片的大小為5-5+1=1,也就是相當於1個神經元,120個特徵圖,因此最後只剩下120個神經元了。這個時候,神經元的個數已經夠少的了,後面我們就可以直接利用全連線神經網路,進行這120個神經元的後續處理,後面具體要怎麼搞,只要懂多層感知器的都懂了,不解釋。

上面的結構,只是一種參考,在現實使用中,每一層特徵圖需要多少個,卷積核大小選擇,還有池化的時候取樣率要多少,等這些都是變化的,這就是所謂的CNN調參,我們需要學會靈活多變。

比如我們可以把上面的結構改為:C1層卷積核大小為7*7,然後把C3層卷積核大小改為3*3等,然後特徵圖的個數也是自己選,說不定得到手寫字型識別的精度比上面那個還高,這也是有可能的,總之一句話:需要學會靈活多變,需要學會CNN的調參。

二、實戰階段

學習CNN的原始碼實現網站:http://deeplearning.net/tutorial/lenet.html#lenet

1、訓練資料獲取

在theano學習庫中有手寫字型的庫,可以從網上下載到,名為:mnist.pkl.gz的手寫字型庫,裡面包含了三個部分的資料,訓練資料集train_set:50000個訓練樣本,驗證集valid_set,我們可以用如下的程式碼讀取這些資料,然後用plot顯示其中的一張圖片:


    
  1. <span style= "font-size:18px;"> import cPickle
  2. import gzip
  3. import numpy as np
  4. import matplotlib.pyplot as plt
  5. f = gzip.open( 'mnist.pkl.gz', 'rb')
  6. train_set, valid_set, test_set = cPickle.load(f)
  7. f.close()
  8. tx,ty=train_set;
  9. #檢視訓練樣本
  10. print np.shape(tx) #可以看到tx大小為(50000,28*28)的二維矩陣
  11. print np.shape(ty) #可以看到ty大小為(50000,1)的矩陣
  12. #圖片顯示
  13. A=tx[ 8].reshape( 28, 28) #第八個訓練樣本
  14. Y=ty[ 8]
  15. print Y
  16. plt.imshow(A,cmap= 'gray') #顯示手寫字型圖片</span>

在上面的程式碼中我顯示的是第8張圖片,可以看到如下結果:


第八個樣本是數字1。

2、LeNet-5實現

首先你要知道mnist.pkl.gz這個庫給我們的圖片的大小是28*28的,因此我們可以第一步選擇5*5的卷積核進行卷積得到24*24,同時我們希望C1層得到20張特徵圖,等等,具體的程式碼實現如下;


    
  1. import os
  2. import sys
  3. import timeit
  4. import numpy
  5. import theano
  6. import theano.tensor as T
  7. from theano.tensor.signal import downsample
  8. from theano.tensor.nnet import conv
  9. from logistic_sgd import LogisticRegression, load_data
  10. from mlp import HiddenLayer
  11. #卷積神經網路的一層,包含:卷積+下采樣兩個步驟
  12. #演算法的過程是:卷積-》下采樣-》啟用函式
  13. class LeNetConvPoolLayer(object):
  14. #image_shape是輸入資料的相關引數設定 filter_shape本層的相關引數設定
  15. def __init__(self, rng, input, filter_shape, image_shape, poolsize=(2, 2)):
  16. """
  17. :type rng: numpy.random.RandomState
  18. :param rng: a random number generator used to initialize weights
  19. 3、input: 輸入特徵圖資料,也就是n幅特徵圖片
  20. 4、引數 filter_shape: (number of filters, num input feature maps,
  21. filter height, filter width)
  22. num of filters:是卷積核的個數,有多少個卷積核,那麼本層的out feature maps的個數
  23. 也將生成多少個。num input feature maps:輸入特徵圖的個數。
  24. 然後接著filter height, filter width是卷積核的寬高,比如5*5,9*9……
  25. filter_shape是列表,因此我們可以用filter_shape[0]獲取卷積核個數
  26. 5、引數 image_shape: (batch size, num input feature maps,
  27. image height, image width),
  28. batch size:批量訓練樣本個數 ,num input feature maps:輸入特徵圖的個數
  29. image height, image width分別是輸入的feature map圖片的大小。
  30. image_shape是一個列表型別,所以可以直接用索引,訪問上面的4個引數,索引下標從
  31. 0~3。比如image_shape[2]=image_heigth image_shape[3]=num input feature maps
  32. 6、引數 poolsize: 池化下采樣的的塊大小,一般為(2,2)
  33. """
  34. assert image_shape[ 1] == filter_shape[ 1] #判斷輸入特徵圖的個數是否一致,如果不一致是錯誤的
  35. self.input = input
  36. # fan_in=num input feature maps *filter height*filter width
  37. #numpy.prod(x)函式為計算x各個元素的乘積
  38. #也就是說fan_in就相當於每個即將輸出的feature map所需要連結引數權值的個數
  39. fan_in = numpy.prod(filter_shape[ 1:])
  40. # fan_out=num output feature maps * filter height * filter width
  41. fan_out = (filter_shape[ 0] * numpy.prod(filter_shape[ 2:]) /
  42. numpy.prod(poolsize))
  43. # 把引數初始化到[-a,a]之間的數,其中a=sqrt(6./(fan_in + fan_out)),然後引數採用均勻取樣
  44. #權值需要多少個?卷積核個數*輸入特徵圖個數*卷積核寬*卷積核高?這樣沒有包含取樣層的連結權值個數
  45. W_bound = numpy.sqrt( 6. / (fan_in + fan_out))
  46. self.W = theano.shared(
  47. numpy.asarray(
  48. rng.uniform(low=-W_bound, high=W_bound, size=filter_shape),
  49. dtype=theano.config.floatX
  50. ),
  51. borrow= True
  52. )
  53. # b為偏置,是一維的向量。每個輸出特徵圖i對應一個偏置引數b[i]
  54. #,因此下面初始化b的個數就是特徵圖的個數filter_shape[0]
  55. b_values = numpy.zeros((filter_shape[ 0],), dtype=theano.config.floatX)
  56. self.b = theano.shared(value=b_values, borrow= True)
  57. # 卷積層操作,函式conv.conv2d的第一個引數為輸入的特徵圖,第二個引數為隨機出事化的卷積核引數
  58. #第三個引數為卷積核的相關屬性,輸入特徵圖的相關屬性
  59. conv_out = conv.conv2d(
  60. input=input,
  61. filters=self.W,
  62. filter_shape=filter_shape,
  63. image_shape=image_shape
  64. )
  65. # 池化操作,最大池化
  66. pooled_out = downsample.max_pool_2d(
  67. input=conv_out,
  68. ds=poolsize,
  69. ignore_border= True
  70. )
  71. #激勵函式,也就是說是先經過卷積核再池化後,然後在進行非線性對映
  72. # add the bias term. Since the bias is a vector (1D array), we first
  73. # reshape it to a tensor of shape (1, n_filters, 1, 1). Each bias will
  74. # thus be broadcasted across mini-batches and feature map
  75. # width & height
  76. self.output = T.tanh(pooled_out + self.b.dimshuffle( 'x', 0, 'x', 'x'))
  77. # 儲存引數
  78. self.params = [self.W, self.b]
  79. self.input = input
  80. #測試函式
  81. def evaluate_lenet5(learning_rate=0.1, n_epochs=200,
  82. dataset='mnist.pkl.gz',
  83. nkerns=[20, 50], batch_size=500):
  84. """ Demonstrates lenet on MNIST dataset
  85. :learning_rate: 梯度下降法的學習率
  86. :n_epochs: 最大迭代次數
  87. :type dataset: string
  88. :param dataset: path to the dataset used for training /testing (MNIST here)
  89. :nkerns: 每個卷積層的卷積核個數,第一層卷積核個數為 nkerns[0]=20,第二層卷積核個數
  90. 為50個
  91. """
  92. rng = numpy.random.RandomState( 23455)
  93. datasets = load_data(dataset) #載入訓練資料,訓練資料包含三個部分
  94. train_set_x, train_set_y = datasets[ 0] #訓練資料
  95. valid_set_x, valid_set_y = datasets[ 1] #驗證資料
  96. test_set_x, test_set_y = datasets[ 2] #測試資料
  97. # 計算批量訓練可以分多少批資料進行訓練,這個只要是知道批量訓練的人都知道
  98. n_train_batches = train_set_x.get_value(borrow= True).shape[ 0] #訓練資料個數
  99. n_valid_batches = valid_set_x.get_value(borrow= True).shape[ 0]
  100. n_test_batches = test_set_x.get_value(borrow= True).shape[ 0]
  101. n_train_batches /= batch_size #批數
  102. n_valid_batches /= batch_size
  103. n_test_batches /= batch_size