## 卷積神經網絡Lenet-5實現
> 原文地址:[http://blog.csdn.net/hjimce/article/details/47323463](http://blog.csdn.net/hjimce/article/details/47323463)
> 作者:hjimce
卷積神經網絡算法是n年前就有的算法,只是近年來因為深度學習相關算法為多層網絡的訓練提供了新方法,然后現在電腦的計算能力已非當年的那種計算水平,同時現在的訓練數據很多,于是神經網絡的相關算法又重新火了起來,因此卷積神經網絡就又活了起來,再開始前,我們需要明確的是網上講的卷積神經網絡的相關教程一般指的是神經網絡的前向傳導過程,反向傳播都是用梯度下降法進行訓練。
###一、理論階段
講解這個算法,沒有打算啰嗦太多的東西,因為什么權值共享、局部感受野什么的,講那么多,都是那些生物學的相關理論。卷積神經網絡的相關博文也是一大堆,但是講的,基本上都是抄過來抄過去,就像我之前不理解從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中,我們要訓練的卷積核并不是僅僅只有一個,這些卷積核用于提取特征,卷積核個數越多,提取的特征越多,理論上來說精度也會更高,然而卷積核一堆,意味著我們要訓練的參數的個數越多。在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,也就是神經元的個數由1024減小到了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的調參。
###二、實戰階段
**1、訓練數據獲取**
在theano學習庫中有手寫字體的庫,可以從網上下載到,名為:mnist.pkl.gz的手寫字體庫,里面包含了三個部分的數據,訓練數據集train_set:50000個訓練樣本,驗證集valid_set,我們可以用如下的代碼讀取這些數據,然后用plot顯示其中的一張圖片:
~~~
<span style="font-size:18px;">import cPickle
import gzip
import numpy as np
import matplotlib.pyplot as plt
f = gzip.open('mnist.pkl.gz', 'rb')
train_set, valid_set, test_set = cPickle.load(f)
f.close()
tx,ty=train_set;
#查看訓練樣本
print np.shape(tx)#可以看到tx大小為(50000,28*28)的二維矩陣
print np.shape(ty)#可以看到ty大小為(50000,1)的矩陣
#圖片顯示
A=tx[8].reshape(28,28)#第八個訓練樣本
Y=ty[8]
print Y
plt.imshow(A,cmap='gray')#顯示手寫字體圖片</span>
~~~
在上面的代碼中我顯示的是第8張圖片,可以看到如下結果:

第八個樣本是數字1。
**2、LeNet-5實現**
首先你要知道mnist.pkl.gz這個庫給我們的圖片的大小是28*28的,因此我們可以第一步選擇5*5的卷積核進行卷積得到24*24,同時我們希望C1層得到20張特征圖,等等,具體的代碼實現如下;
~~~
import os
import sys
import timeit
import numpy
import theano
import theano.tensor as T
from theano.tensor.signal import downsample
from theano.tensor.nnet import conv
from logistic_sgd import LogisticRegression, load_data
from mlp import HiddenLayer
#卷積神經網絡的一層,包含:卷積+下采樣兩個步驟
#算法的過程是:卷積-》下采樣-》激活函數
class LeNetConvPoolLayer(object):
#image_shape是輸入數據的相關參數設置 filter_shape本層的相關參數設置
def __init__(self, rng, input, filter_shape, image_shape, poolsize=(2, 2)):
"""
:type rng: numpy.random.RandomState
:param rng: a random number generator used to initialize weights
3、input: 輸入特征圖數據,也就是n幅特征圖片
4、參數 filter_shape: (number of filters, num input feature maps,
filter height, filter width)
num of filters:是卷積核的個數,有多少個卷積核,那么本層的out feature maps的個數
也將生成多少個。num input feature maps:輸入特征圖的個數。
然后接著filter height, filter width是卷積核的寬高,比如5*5,9*9……
filter_shape是列表,因此我們可以用filter_shape[0]獲取卷積核個數
5、參數 image_shape: (batch size, num input feature maps,
image height, image width),
batch size:批量訓練樣本個數 ,num input feature maps:輸入特征圖的個數
image height, image width分別是輸入的feature map圖片的大小。
image_shape是一個列表類型,所以可以直接用索引,訪問上面的4個參數,索引下標從
0~3。比如image_shape[2]=image_heigth image_shape[3]=num input feature maps
6、參數 poolsize: 池化下采樣的的塊大小,一般為(2,2)
"""
assert image_shape[1] == filter_shape[1]#判斷輸入特征圖的個數是否一致,如果不一致是錯誤的
self.input = input
# fan_in=num input feature maps *filter height*filter width
#numpy.prod(x)函數為計算x各個元素的乘積
#也就是說fan_in就相當于每個即將輸出的feature map所需要鏈接參數權值的個數
fan_in = numpy.prod(filter_shape[1:])
# fan_out=num output feature maps * filter height * filter width
fan_out = (filter_shape[0] * numpy.prod(filter_shape[2:]) /
numpy.prod(poolsize))
# 把參數初始化到[-a,a]之間的數,其中a=sqrt(6./(fan_in + fan_out)),然后參數采用均勻采樣
#權值需要多少個?卷積核個數*輸入特征圖個數*卷積核寬*卷積核高?這樣沒有包含采樣層的鏈接權值個數
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
)
# b為偏置,是一維的向量。每個輸出特征圖i對應一個偏置參數b[i]
#,因此下面初始化b的個數就是特征圖的個數filter_shape[0]
b_values = numpy.zeros((filter_shape[0],), dtype=theano.config.floatX)
self.b = theano.shared(value=b_values, borrow=True)
# 卷積層操作,函數conv.conv2d的第一個參數為輸入的特征圖,第二個參數為隨機出事化的卷積核參數
#第三個參數為卷積核的相關屬性,輸入特征圖的相關屬性
conv_out = conv.conv2d(
input=input,
filters=self.W,
filter_shape=filter_shape,
image_shape=image_shape
)
# 池化操作,最大池化
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'))
# 保存參數
self.params = [self.W, self.b]
self.input = input
#測試函數
def evaluate_lenet5(learning_rate=0.1, n_epochs=200,
dataset='mnist.pkl.gz',
nkerns=[20, 50], batch_size=500):
""" Demonstrates lenet on MNIST dataset
:learning_rate: 梯度下降法的學習率
:n_epochs: 最大迭代次數
:type dataset: string
:param dataset: path to the dataset used for training /testing (MNIST here)
:nkerns: 每個卷積層的卷積核個數,第一層卷積核個數為 nkerns[0]=20,第二層卷積核個數
為50個
"""
rng = numpy.random.RandomState(23455)
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]#測試數據
# 計算批量訓練可以分多少批數據進行訓練,這個只要是知道批量訓練的人都知道
n_train_batches = train_set_x.get_value(borrow=True).shape[0]#訓練數據個數
n_valid_batches = valid_set_x.get_value(borrow=True).shape[0]
n_test_batches = test_set_x.get_value(borrow=True).shape[0]
n_train_batches /= batch_size#批數
n_valid_batches /= batch_size
n_test_batches /= batch_size
# allocate symbolic variables for the data
index = T.lscalar() # index to a [mini]batch
# start-snippet-1
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
# 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))
'''''構建第一層網絡:
image_shape:輸入大小為28*28的特征圖,batch_size個訓練數據,每個訓練數據有1個特征圖
filter_shape:卷積核個數為nkernes[0]=20,因此本層每個訓練樣本即將生成20個特征圖
經過卷積操作,圖片大小變為(28-5+1 , 28-5+1) = (24, 24)
經過池化操作,圖片大小變為 (24/2, 24/2) = (12, 12)
最后生成的本層image_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)
)
'''''構建第二層網絡:輸入batch_size個訓練圖片,經過第一層的卷積后,每個訓練圖片有nkernes[0]個特征圖,每個特征圖
大小為12*12
經過卷積后,圖片大小變為(12-5+1, 12-5+1) = (8, 8)
經過池化后,圖片大小變為(8/2, 8/2) = (4, 4)
最后生成的本層的image_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)
'''''全鏈接:輸入layer2_input是一個二維的矩陣,第一維表示樣本,第二維表示上面經過卷積下采樣后
每個樣本所得到的神經元,也就是每個樣本的特征,HiddenLayer類是一個單層網絡結構
下面的layer2把神經元個數由800個壓縮映射為500個'''
layer2 = HiddenLayer(
rng,
input=layer2_input,
n_in=nkerns[1] * 4 * 4,
n_out=500,
activation=T.tanh
)
# 最后一層:邏輯回歸層分類判別,把500個神經元,壓縮映射成10個神經元,分別對應于手寫字體的0~9
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]
}
)
#把所有的參數放在同一個列表里,可直接使用列表相加
params = layer3.params + layer2.params + layer1.params + layer0.params
#梯度求導
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]
}
)
# end-snippet-1
###############
# TRAIN MODEL #
###############
print '... training'
# early-stopping parameters
patience = 10000 # 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
best_iter = 0
test_score = 0.
start_time = timeit.default_timer()
epoch = 0
done_looping = False
while (epoch < n_epochs) and (not done_looping):
epoch = epoch + 1
for minibatch_index in xrange(n_train_batches):#每一批訓練數據
cost_ij = train_model(minibatch_index)
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 xrange(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)
# save best validation score and iteration number
best_validation_loss = this_validation_loss
best_iter = iter
# test it on the test set
test_losses = [
test_model(i)
for i in xrange(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.))
if patience <= iter:
done_looping = True
break
end_time = timeit.default_timer()
print('Optimization complete.')
print('Best validation score of %f %% obtained at iteration %i, '
'with test performance %f %%' %
(best_validation_loss * 100., best_iter + 1, test_score * 100.))
print >> sys.stderr, ('The code for file ' +
os.path.split(__file__)[1] +
' ran for %.2fm' % ((end_time - start_time) / 60.))
if __name__ == '__main__':
evaluate_lenet5()
def experiment(state, channel):
evaluate_lenet5(state.learning_rate, dataset=state.dataset)
~~~
訓練結果:

**參考文獻:**
1、http://blog.csdn.net/zouxy09/article/details/8775360/
2、http://www.deeplearning.net/tutorial/lenet.html#lenet
作者:hjimce ? 時間:2015.8.6 ?聯系QQ:1393852684 ? 地址:[http://blog.csdn.net/hjimce](http://blog.csdn.net/hjimce)?轉載請保留本行信息