轉自:http://blog.csdn.net/cxmscb/article/details/71023576
一、CNN的引入
在人工的全連接神經網絡中,每相鄰兩層之間的每個神經元之間都是有邊相連的。當輸入層的特征維度變得很高時,這時全連接網絡需要訓練的參數就會增大很多,計算速度就會變得很慢,例如一張黑白的 28×28 的手寫數字圖片,輸入層的神經元就有784個,如下圖所示:

若在中間只使用一層隱藏層,參數 w 就有 784×15=11760 多個;若輸入的是28×28 帶有顏色的RGB格式的手寫數字圖片,輸入神經元就有28×28×3=2352 個…… 。這很容易看出使用全連接神經網絡處理圖像中的需要訓練參數過多的問題。
而在卷積神經網絡(Convolutional Neural Network,CNN)中,卷積層的神經元只與前一層的部分神經元節點相連,即它的神經元間的連接是非全連接的,且同一層中某些神經元之間的連接的權重 w 和偏移 b 是共享的(即相同的),這樣大量地減少了需要訓練參數的數量。
卷積神經網絡CNN的結構一般包含這幾個層:
- 輸入層:用於數據的輸入
- 卷積層:使用卷積核進行特征提取和特征映射
- 激勵層:由於卷積也是一種線性運算,因此需要增加非線性映射
- 池化層:進行下采樣,對特征圖稀疏處理,減少數據運算量。
- 全連接層:通常在CNN的尾部進行重新擬合,減少特征信息的損失
- 輸出層:用於輸出結果
當然中間還可以使用一些其他的功能層:
- 歸一化層(Batch Normalization):在CNN中對特征的歸一化
- 切分層:對某些(圖片)數據的進行分區域的單獨學習
- 融合層:對獨立進行特征學習的分支進行融合
二、CNN的層次結構
輸入層:
在CNN的輸入層中,(圖片)數據輸入的格式 與 全連接神經網絡的輸入格式(一維向量)不太一樣。CNN的輸入層的輸入格式保留了圖片本身的結構。
對於黑白的 28×28 的圖片,CNN的輸入是一個 28×28 的的二維神經元,如下圖所示:

而對於RGB格式的28×28圖片,CNN的輸入則是一個 3×28×28 的三維神經元(RGB中的每一個顏色通道都有一個 28×28 的矩陣),如下圖所示:

卷積層:
在卷積層中有幾個重要的概念:
- local receptive fields(感受視野)
- shared weights(共享權值)
假設輸入的是一個 28×28 的的二維神經元,我們定義5×5 的 一個 local receptive fields(感受視野),即 隱藏層的神經元與輸入層的5×5個神經元相連,這個5*5的區域就稱之為Local Receptive Fields,如下圖所示:

可類似看作:隱藏層中的神經元 具有一個固定大小的感受視野去感受上一層的部分特征。在全連接神經網絡中,隱藏層中的神經元的感受視野足夠大乃至可以看到上一層的所有特征。
而在卷積神經網絡中,隱藏層中的神經元的感受視野比較小,只能看到上一次的部分特征,上一層的其他特征可以通過平移感受視野來得到同一層的其他神經元,由同一層其他神經元來看:

設移動的步長為1:從左到右掃描,每次移動 1 格,掃描完之后,再向下移動一格,再次從左到右掃描。
具體過程如動圖所示:


可看出 卷積層的神經元是只與前一層的部分神經元節點相連,每一條相連的線對應一個權重 w 。
一個感受視野帶有一個卷積核,我們將 感受視野 中的權重 w 矩陣稱為 卷積核 ;將感受視野對輸入的掃描間隔稱為步長(stride);當步長比較大時(stride>1),為了掃描到邊緣的一些特征,感受視野可能會“出界”,這時需要對邊界擴充(pad),邊界擴充可以設為 0 或 其他值。步長 和 邊界擴充值的大小由用戶來定義。
卷積核的大小由用戶來定義,即定義的感受視野的大小;卷積核的權重矩陣的值,便是卷積神經網絡的參數,為了有一個偏移項 ,卷積核可附帶一個偏移項 b ,它們的初值可以隨機來生成,可通過訓練進行變化。
因此 感受視野 掃描時可以計算出下一層神經元的值為:
對下一層的所有神經元來說,它們從不同的位置去探測了上一層神經元的特征。
我們將通過 一個帶有卷積核的感受視野 掃描生成的下一層神經元矩陣 稱為 一個feature map (特征映射圖),如下圖的右邊便是一個 feature map:

因此在同一個 feature map 上的神經元使用的卷積核是相同的,因此這些神經元 shared weights,共享卷積核中的權值和附帶的偏移。一個 feature map 對應 一個卷積核,若我們使用 3 個不同的卷積核,可以輸出3個feature map:(感受視野:5×5,布長stride:1)

因此在CNN的卷積層,我們需要訓練的參數大大地減少到了 (5×5+1)×3=78個。
假設輸入的是 28×28 的RGB圖片,即輸入的是一個 3×28×28 的的二維神經元,這時卷積核的大小不只用長和寬來表示,還有深度,感受視野也對應的有了深度,如下圖所示:

由圖可知:感受視野: 3×2×2 ; 卷積核: 3×2×2 ,深度為3;下一層的神經元的值為:b+∑2d=0∑1i=0∑1j=0wdijxdij . 卷積核的深度和感受視野的深度相同,都由輸入數據來決定,長寬可由自己來設定,數目也可以由自己來設定,一個卷積核依然對應一個 feature map 。
注:“stride=1”表示在長和寬上的移動間隔都為1,即 stridewidth=1 且 strideheight=1
激勵層:
激勵層主要對卷積層的輸出進行一個非線性映射,因為卷積層的計算還是一種線性計算。使用的激勵函數一般為ReLu函數:
卷積層和激勵層通常合並在一起稱為“卷積層”。
池化層:
當輸入經過卷積層時,若感受視野比較小,布長stride比較小,得到的feature map (特征圖)還是比較大,可以通過池化層來對每一個 feature map 進行降維操作,輸出的深度還是不變的,依然為 feature map 的個數。
池化層也有一個“池化視野(filter)”來對feature map矩陣進行掃描,對“池化視野”中的矩陣值進行計算,一般有兩種計算方式:
- Max pooling:取“池化視野”矩陣中的最大值
- Average pooling:取“池化視野”矩陣中的平均值
掃描的過程中同樣地會涉及的掃描布長stride,掃描方式同卷積層一樣,先從左到右掃描,結束則向下移動布長大小,再從左到右。如下圖示例所示:

其中“池化視野”filter: 2×2;布長stride:2。(注:“ 池化視野”為個人叫法)
最后可將 3 個 24×24 的 feature map 下采樣得到 3 個 24×24 的特征矩陣:

歸一化層:
1. Batch Normalization
Batch Normalization(批量歸一化)實現了在神經網絡層的中間進行預處理的操作,即在上一層的輸入歸一化處理后再進入網絡的下一層,這樣可有效地防止“梯度彌散”,加速網絡訓練。
Batch Normalization具體的算法如下圖所示:

每次訓練時,取 batch_size 大小的樣本進行訓練,在BN層中,將一個神經元看作一個特征,batch_size 個樣本在某個特征維度會有 batch_size 個值,然后在每個神經元 xi 維度上的進行這些樣本的均值和方差,通過公式得到 xi∧,再通過參數 γ 和 β 進行線性映射得到每個神經元對應的輸出 yi 。在BN層中,可以看出每一個神經元維度上,都會有一個參數 γ 和 β ,它們同權重w一樣可以通過訓練進行優化。
在卷積神經網絡中進行批量歸一化時,一般對 未進行ReLu激活的 feature map進行批量歸一化,輸出后再作為激勵層的輸入,可達到調整激勵函數偏導的作用。
一種做法是將 feature map 中的神經元作為特征維度,參數 γ 和 β 的數量和則等於 2×fmapwidth×fmaplength×fmapnum,這樣做的話參數的數量會變得很多;
另一種做法是把 一個 feature map 看做一個特征維度,一個 feature map 上的神經元共享這個 feature map的 參數 γ 和 β ,參數 γ 和 β 的數量和則等於 2×fmapnum,計算均值和方差則在batch_size個訓練樣本在每一個feature map維度上的均值和方差。
注:fmapnum指的是一個樣本的feature map數量,feature map 跟神經元一樣也有一定的排列順序。
Batch Normalization 算法的訓練過程和測試過程的區別:
在訓練過程中,我們每次都會將 batch_size 數目大小的訓練樣本 放入到CNN網絡中進行訓練,在BN層中自然可以得到計算輸出所需要的 均值 和 方差 ;
而在測試過程中,我們往往只會向CNN網絡中輸入一個測試樣本,這是在BN層計算的均值和方差會均為 0,因為只有一個樣本輸入,因此BN層的輸入也會出現很大的問題,從而導致CNN網絡輸出的錯誤。所以在測試過程中,我們需要借助訓練集中所有樣本在BN層歸一化時每個維度上的均值和方差,當然為了計算方便,我們可以在 batch_num 次訓練過程中,將每一次在BN層歸一化時每個維度上的均值和方差進行相加,最后再進行求一次均值即可。
2. Local Response Normalization
近鄰歸一化(Local Response Normalization)的歸一化方法主要發生在不同的相鄰的卷積核(經過ReLu之后)的輸出之間,即輸入是發生在不同的經過ReLu之后的 feature map 中。
LRN的公式如下:
其中:
a(i,x,y) 表示第i個卷積核的輸出(經過ReLu層)的feature map上的 (x,y) 位置上的值。
b(i,x,y) 表示 a(i,x,y) 經LRN后的輸出。
N 表示卷積核的數量,即輸入的 feature map的個數。
n 表示近鄰的卷積核(或feature map)個數,由自己來決定。
k,α,β是超參數,由用戶自己調整或決定。
與BN的區別:BN依據mini batch的數據,近鄰歸一僅需要自己來決定,BN訓練中有學習參數;BN歸一化主要發生在不同的樣本之間,LRN歸一化主要發生在不同的卷積核的輸出之間。
切分層:
在一些應用中,需要對圖片進行切割,獨立地對某一部分區域進行單獨學習。這樣可以對特定部分進行通過調整 感受視野 進行力度更大的學習。
融合層:
融合層可以對切分層進行融合,也可以對不同大小的卷積核學習到的特征進行融合。
例如在GoogleLeNet 中,使用多種分辨率的卷積核對目標特征進行學習,通過 padding 使得每一個 feature map 的長寬都一致,之后再將多個 feature map 在深度上拼接在一起:

融合的方法有幾種,一種是特征矩陣之間的拼接級聯,另一種是在特征矩陣進行運算 (+,−,x,max,conv)。
全連接層和輸出層
全連接層主要對特征進行重新擬合,減少特征信息的丟失;輸出層主要准備做好最后目標結果的輸出。例如VGG的結構圖,如下圖所示:

典型的卷積神經網絡
LeNet-5模型
第一個成功應用於數字數字識別的卷積神經網絡模型(卷積層自帶激勵函數,下同):

卷積層的卷積核邊長都是5,步長都為1;池化層的窗口邊長都為2,步長都為2。
AlexNet 模型
具體結構圖:

從AlexNet的結構可發現:經典的卷積神經網絡結構通常為:
AlexNet卷積層的卷積核邊長為5或3,池化層的窗口邊長為3。具體參數如圖所示:

VGGNet 模型
VGGNet 模型 和 AlexNet模型 在結構上沒多大變化,在卷積層部位增加了多個卷積層。AlexNet(上) 和 VGGNet (下)的對比如下圖所示:

具體參數如圖所示:其中CONV3-64:表示卷積核的長和寬為3,個數有64個;POOL2:表示池化窗口的長和寬都為2,其他類似。

GoogleNet 模型
使用了多個不同分辨率的卷積核,最后再對它們得到的feature map 按深度融合在一起,結構如圖:

其中,有一些主要的模塊稱為 Inception module,例如:

在 Inception module 中使用到了很多 1×1 的卷積核,使用 1×1 的卷積核,步長為1時,輸入的feature map和輸出的feature map長寬不會發生改變,但可以通過改變 1×1 的卷積核的數目,來達到減小feature map的厚度的效果,從而做到一些訓練參數的減少。
GoogleNet還有一個特點就是它是全卷積結構(FCN)的,網絡的最后沒有使用全連接層,一方面這樣可以減少參數的數目,不容易過擬合,一方面也帶來了一些空間信息的丟失。代替全連接層的是全局平均池化(Global Average Pooling,GAP)的方法,思想是:為每一個類別輸出一個 feature map ,再取每一個 feature map上的平均值,作為最后的softmax層的輸入。
ResNet模型
在前面的CNN模型中,都是將輸入一層一層地傳遞下去(圖左),當層次比較深時,模型不是很好訓練。在ResNet模型中,它將低層學習到的特征和高層的學習到的特征進行一個融合(加法運算,圖右),這樣反向傳遞時,導數傳遞得更快,減少梯度彌散的現象。
注意:F(X)的shape需要等於 X 的shape ,這樣才可以進行相加。

四、Tensorflow代碼
主要的函數說明:
卷積層:
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)
參數說明:
-
data_format:表示輸入的格式,有兩種分別為:“NHWC”和“NCHW”,默認為“NHWC”
-
input:輸入是一個4維格式的(圖像)數據,數據的 shape 由 data_format 決定:當 data_format 為“NHWC”輸入數據的shape表示為[batch, in_height, in_width, in_channels],分別表示訓練時一個batch的圖片數量、圖片高度、 圖片寬度、 圖像通道數。當 data_format 為“NHWC”輸入數據的shape表示為[batch, in_channels, in_height, in_width]
-
filter:卷積核是一個4維格式的數據:shape表示為:[height,width,in_channels, out_channels],分別表示卷積核的高、寬、深度(與輸入的in_channels應相同)、輸出 feature map的個數(即卷積核的個數)。
-
strides:表示步長:一個長度為4的一維列表,每個元素跟data_format互相對應,表示在data_format每一維上的移動步長。當輸入的默認格式為:“NHWC”,則 strides = [batch , in_height , in_width, in_channels]。其中 batch 和 in_channels 要求一定為1,即只能在一個樣本的一個通道上的特征圖上進行移動,in_height , in_width表示卷積核在特征圖的高度和寬度上移動的布長,即 strideheight 和stridewidth 。
-
padding:表示填充方式:“SAME”表示采用填充的方式,簡單地理解為以0填充邊緣,當stride為1時,輸入和輸出的維度相同;“VALID”表示采用不填充的方式,多余地進行丟棄。具體公式:
“SAME”: output_spatial_shape[i]=⌈(input_spatial_shape[i] / strides[i])⌉
“VALID”: output_spatial_shape[i]=⌈((input_spatial_shape[i]−(spatial_filter_shape[i]−1)/strides[i])⌉
池化層:
tf.nn.max_pool( value, ksize,strides,padding,data_format=’NHWC’,name=None)
或者
tf.nn.avg_pool(…)
參數說明:
-
value:表示池化的輸入:一個4維格式的數據,數據的 shape 由 data_format 決定,默認情況下shape 為[batch, height, width, channels]
-
其他參數與 tf.nn.cov2d 類型
-
ksize:表示池化窗口的大小:一個長度為4的一維列表,一般為[1, height, width, 1],因不想在batch和channels上做池化,則將其值設為1。
Batch Nomalization層:
batch_normalization( x,mean,variance,offset,scale, variance_epsilon,name=None)
-
mean 和 variance 通過 tf.nn.moments 來進行計算:
batch_mean, batch_var = tf.nn.moments(x, axes = [0, 1, 2], keep_dims=True),注意axes的輸入。對於以feature map 為維度的全局歸一化,若feature map 的shape 為[batch, height, width, depth],則將axes賦值為[0, 1, 2] -
x 為輸入的feature map 四維數據,offset、scale為一維Tensor數據,shape 等於 feature map 的深度depth。
代碼示例:
通過搭建卷積神經網絡來實現sklearn庫中的手寫數字識別,搭建的卷積神經網絡結構如下圖所示:

1 import tensorflow as tf
1 from sklearn.datasets import load_digits 2 import numpy as np
1 digits = load_digits() 2 X_data = digits.data.astype(np.float32) 3 Y_data = digits.target.astype(np.float32).reshape(-1,1) 4 print X_data.shape 5 print Y_data.shape
1 (1797, 64) 2 (1797, 1)
1 from sklearn.preprocessing import MinMaxScaler
1 scaler = MinMaxScaler()
1 X_data = scaler.fit_transform(X_data)
1 from sklearn.preprocessing import OneHotEncoder
1 Y = OneHotEncoder().fit_transform(Y_data).todense() #one-hot編碼
2 Y
1 matrix([[ 1., 0., 0., ..., 0., 0., 0.], 2 [ 0., 1., 0., ..., 0., 0., 0.], 3 [ 0., 0., 1., ..., 0., 0., 0.], 4 ..., 5 [ 0., 0., 0., ..., 0., 1., 0.], 6 [ 0., 0., 0., ..., 0., 0., 1.], 7 [ 0., 0., 0., ..., 0., 1., 0.]])
1 # 轉換為圖片的格式 (batch,height,width,channels) 2 X = X_data.reshape(-1,8,8,1)
1 batch_size = 8 # 使用MBGD算法,設定batch_size為8
1 def generatebatch(X,Y,n_examples, batch_size): 2 for batch_i in range(n_examples // batch_size): 3 start = batch_i*batch_size 4 end = start + batch_size 5 batch_xs = X[start:end] 6 batch_ys = Y[start:end] 7 yield batch_xs, batch_ys # 生成每一個batch
1 tf.reset_default_graph() 2 # 輸入層 3 tf_X = tf.placeholder(tf.float32,[None,8,8,1]) 4 tf_Y = tf.placeholder(tf.float32,[None,10])
1 # 卷積層+激活層 2 conv_filter_w1 = tf.Variable(tf.random_normal([3, 3, 1, 10])) 3 conv_filter_b1 = tf.Variable(tf.random_normal([10])) 4 relu_feature_maps1 = tf.nn.relu(\ 5 tf.nn.conv2d(tf_X, conv_filter_w1,strides=[1, 1, 1, 1], padding='SAME') + conv_filter_b1)
1 # 池化層 2 max_pool1 = tf.nn.max_pool(relu_feature_maps1,ksize=[1,3,3,1],strides=[1,2,2,1],padding='SAME')
1 print max_pool1
1 Tensor("MaxPool:0", shape=(?, 4, 4, 10), dtype=float32)
1 # 卷積層 2 conv_filter_w2 = tf.Variable(tf.random_normal([3, 3, 10, 5])) 3 conv_filter_b2 = tf.Variable(tf.random_normal([5])) 4 conv_out2 = tf.nn.conv2d(relu_feature_maps1, conv_filter_w2,strides=[1, 2, 2, 1], padding='SAME') + conv_filter_b2 5 print conv_out2
Tensor("add_4:0", shape=(?, 4, 4, 5), dtype=float32)
1 # BN歸一化層+激活層 2 batch_mean, batch_var = tf.nn.moments(conv_out2, [0, 1, 2], keep_dims=True) 3 shift = tf.Variable(tf.zeros([5])) 4 scale = tf.Variable(tf.ones([5])) 5 epsilon = 1e-3 6 BN_out = tf.nn.batch_normalization(conv_out2, batch_mean, batch_var, shift, scale, epsilon) 7 print BN_out 8 relu_BN_maps2 = tf.nn.relu(BN_out)
Tensor("batchnorm/add_1:0", shape=(?, 4, 4, 5), dtype=float32)
# 池化層 max_pool2 = tf.nn.max_pool(relu_BN_maps2,ksize=[1,3,3,1],strides=[1,2,2,1],padding='SAME')
1 print max_pool2
1 Tensor("MaxPool_1:0", shape=(?, 2, 2, 5), dtype=float32)
1 # 將特征圖進行展開 2 max_pool2_flat = tf.reshape(max_pool2, [-1, 2*2*5])
1 # 全連接層 2 fc_w1 = tf.Variable(tf.random_normal([2*2*5,50])) 3 fc_b1 = tf.Variable(tf.random_normal([50])) 4 fc_out1 = tf.nn.relu(tf.matmul(max_pool2_flat, fc_w1) + fc_b1)
1 # 輸出層 2 out_w1 = tf.Variable(tf.random_normal([50,10])) 3 out_b1 = tf.Variable(tf.random_normal([10])) 4 pred = tf.nn.softmax(tf.matmul(fc_out1,out_w1)+out_b1)
1 loss = -tf.reduce_mean(tf_Y*tf.log(tf.clip_by_value(pred,1e-11,1.0)))
1 train_step = tf.train.AdamOptimizer(1e-3).minimize(loss)
1 y_pred = tf.arg_max(pred,1) 2 bool_pred = tf.equal(tf.arg_max(tf_Y,1),y_pred)
1 accuracy = tf.reduce_mean(tf.cast(bool_pred,tf.float32)) # 准確率
1 with tf.Session() as sess: 2 sess.run(tf.global_variables_initializer()) 3 for epoch in range(1000): # 迭代1000個周期 4 for batch_xs,batch_ys in generatebatch(X,Y,Y.shape[0],batch_size): # 每個周期進行MBGD算法 5 sess.run(train_step,feed_dict={tf_X:batch_xs,tf_Y:batch_ys}) 6 if(epoch%100==0): 7 res = sess.run(accuracy,feed_dict={tf_X:X,tf_Y:Y}) 8 print (epoch,res) 9 res_ypred = y_pred.eval(feed_dict={tf_X:X,tf_Y:Y}).flatten() # 只能預測一批樣本,不能預測一個樣本 10 print res_ypred
1 (0, 0.36338341) 2 (100, 0.96828049) 3 (200, 0.99666113) 4 (300, 0.99554813) 5 (400, 0.99888706) 6 (500, 0.99777406) 7 (600, 0.9961046) 8 (700, 0.99666113) 9 (800, 0.99499166) 10 (900, 0.99888706) 11 [0 1 2 ..., 8 9 8]
在第100次個batch size 迭代時,准確率就快速接近收斂了,這得歸功於Batch Normalization 的作用!需要注意的是,這個模型還不能用來預測單個樣本,因為在進行BN層計算時,單個樣本的均值和方差都為0,會得到相反的預測效果,解決方法詳見歸一化層。
1 from sklearn.metrics import accuracy_score
1 print accuracy_score(Y_data,res_ypred.reshape(-1,1))
1 0.998887033945