構建了最簡單的網絡之后,是時候再加上卷積和池化了。這篇,雖然我還沒開始構思,但我知道,一定是很長的文章。
卷積神經網絡(Convolutional Neural Layer, CNN),除了全連接層以外(有時候也不含全連接層,因為出現了Global average pooling),還包含了卷積層和池化層。卷積層用來提取特征,而池化層可以減少參數數量。
卷積層
先談一下卷積層的工作原理。
我們是使用卷積核來提取特征的,卷積核可以說是一個矩陣。假如我們設置一個卷積核為3*3的矩陣,而我們圖片為一個分辨率5*5的圖片。那么卷積核的任務就如下所示:
從左上角開始,卷積核就對應着數據的3*3的矩陣范圍,然后相乘再相加得出一個值。按照這種順序,每隔一個像素就操作一次,我們就可以得出9個值。這九個值形成的矩陣被我們稱作激活映射(Activation map)。這就是我們的卷積層工作原理。也可以參考下面一個gif:
其中,卷積核為
其實我們平時舉例的卷積核已經被翻轉180度一次了,主要是因為計算過程的原因。詳細不用了解,但原理都一樣。
但其實我們輸入的圖像一般為三維,即含有R、G、B三個通道。但其實經過一個卷積核之后,三維會變成一維。它在一整個屏幕滑動的時候,其實會把三個通道的值都累加起來,最終只是輸出一個一維矩陣。而多個卷積核(一個卷積層的卷積核數目是自己確定的)滑動之后形成的Activation Map堆疊起來,再經過一個激活函數就是一個卷積層的輸出了。
卷積層還有另外兩個很重要的參數:步長和padding。
所謂的步長就是控制卷積核移動的距離。在上面的例子看到,卷積核都是隔着一個像素進行映射的,那么我們也可以讓它隔着兩個、三個,而這個距離被我們稱作步長。
而padding就是我們對數據做的操作。一般有兩種,一種是不進行操作,一種是補0使得卷積后的激活映射尺寸不變。上面我們可以看到5*5*3的數據被3*3的卷積核卷積后的映射圖,形狀為3*3,即形狀與一開始的數據不同。有時候為了規避這個變化,我們使用“補0”的方法——即在數據的外層補上0。
下面是示意圖:
了解卷積發展史的人都應該知道,卷積神經網絡應用最開始出現是LeCun(名字真的很像中國人)在識別手寫數字創建的LeNet-5。

后面爆發是因為AlexNet在ImageNet比賽中拔得頭籌,硬生生把誤差變成去年的一半。從此卷積網絡就成了AI的大熱點,一大堆論文和網絡不斷地發揮它的潛能,而它的黑盒性也不斷被人解釋。
能否對卷積神經網絡工作原理做一個直觀的解釋? - Owl of Minerva的回答 - 知乎里面通過我們對圖像進行平滑的操作進而解釋了卷積核如何讀取特征的。
我們需要先明確一點,實驗告訴我們人類視覺是先對圖像邊緣開始敏感的。在我的理解中,它就是說我們對現有事物的印象是我們先通過提取邊界的特征,然后逐漸的完善再進行組裝而成的。而我們的卷積層很好的做到了這一點。
這是兩個不同的卷積核滑動整個圖像后出來的效果,可以看出,經過卷積之后圖像的邊界變得更加直觀。我們也可以來看下VGG-16網絡第一層卷積提取到的特征:

由此來看,我們也知道為什么我們不能只要一個卷積核。在我的理解下,假使我們只有一個卷積核,那我們或許只能提取到一個邊界。但假如我們有許多的卷積核檢測不同的邊界,不同的邊界又構成不同的物體,這就是我們怎么從視覺圖像檢測物體的憑據了。所以,深度學習的“深”不僅僅是代表網絡,也代表我們能檢測的物體的深度。即越深,提取的特征也就越多。
Google提出了一個項目叫Deepdream,里面通過梯度上升、反卷積形象的告訴我們一個網絡究竟想要識別什么。之前權重更新我們講過梯度下降,而梯度上升便是計算卷積核對輸入的噪聲的梯度,然后沿着上升的方向調整我們的輸入。詳細的以后再講,但得出的圖像能夠使得這個卷積核被激活,也就是說得到一個較好的值。所以這個圖像也就是我們卷積核所認為的最規范的圖像(有點嚇人):

其實這鵝看着還不錯,有點像孔雀。
池化層 (pooling layer)
前面說到池化層是降低參數,而降低參數的方法當然也只有刪除參數了。
一般我們有最大池化和平均池化,而最大池化就我認識來說是相對多的。需要注意的是,池化層一般放在卷積層后面。所以池化層池化的是卷積層的輸出!
掃描的順序跟卷積一樣,都是從左上角開始然后根據你設置的步長逐步掃描全局。有些人會很好奇最大池化的時候你怎么知道哪個是最大值,emmm,其實我也考慮過這個問題。CS2131n里面我記得是說會提前記錄最大值保存在一個矩陣中,然后根據那個矩陣來提取最大值。
至於要深入到計算過程與否,應該是沒有必要的。所以我也沒去查證過程。而且給的都是示例圖,其實具體的計算過程應該也是不同的,但效果我們可以知道就好了。
至於為什么選擇最大池化,應該是為了提取最明顯的特征,所以選用的最大池化。平均池化呢,就是顧及每一個像素,所以選擇將所有的像素值都相加然后再平均。
池化層也有padding的選項。但都是跟卷積層一樣的,在外圍補0,然后再池化。
代碼解析
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
summary_dir = './summary'
#批次
batch_size = 100
n_batch = mnist.train.num_examples // batch_size
x = tf.placeholder(tf.float32, [None, 784], name='input')
y = tf.placeholder(tf.float32, [None, 10], name='label')
def net(input_tensor):
conv_weights = tf.get_variable('weight', [3, 3, 1, 32],
initializer=tf.truncated_normal_initializer(stddev=0.1))
conv_biases = tf.get_variable('biase', [32], initializer=tf.constant_initializer(0.0))
conv = tf.nn.conv2d(input_tensor, conv_weights, strides=[1, 1, 1, 1], padding='SAME')
relu = tf.nn.relu(tf.nn.bias_add(conv, conv_biases))
pool = tf.nn.max_pool(relu, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
pool_shape = pool.get_shape().as_list()
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
pool_reshaped = tf.reshape(pool, [-1, nodes])
W = tf.Variable(tf.zeros([nodes, 10]), name='weight')
b = tf.Variable(tf.zeros([10]), name='bias')
fc = tf.nn.softmax(tf.matmul(pool_reshaped, W) + b)
return fc
reshaped = tf.reshape(x, (-1, 28, 28, 1))
prediction = net(reshaped)
loss_ = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.argmax(y, 1), logits=prediction, name='loss')
loss = tf.reduce_mean(loss_)
train_step = tf.train.GradientDescentOptimizer(0.2).minimize(loss)
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name='accuracy')
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for epoch in range(31):
for batch in range(n_batch):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)
sess.run(train_step, feed_dict={x: batch_xs, y: batch_ys})
acc = sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels})
print('Iter' + str(epoch) + ",Testing Accuracy" + str(acc))
這相對於我第一個只用全連接的網絡只多了一個net函數,還有因為卷積層的關系進來的數據x需要改變形狀。只講這兩部分:
reshaped = tf.reshape(x, (-1, 28, 28, 1))
prediction = net(reshaped)
由於我們feedict上面是,feed_dict={x: mnist.test.images, y: mnist.test.labels}
,而這樣子調用tensorflow的句子我們得到的x
是固定的形狀。因此我們應用tf.reshape(x_need_reshaped,object_shape)
來得到需要的形狀。
其中的−1−1 表示拉平,不能用None,是固定的。
conv_weights = tf.get_variable('weight', [3, 3, 1, 32],
initializer=tf.truncated_normal_initializer(stddev=0.1))
conv_biases = tf.get_variable('biase', [32], initializer=tf.constant_initializer(0.0))
conv = tf.nn.conv2d(input_tensor, conv_weights, strides=[1, 1, 1, 1], padding='SAME')
relu = tf.nn.relu(tf.nn.bias_add(conv, conv_biases))
pool = tf.nn.max_pool(relu, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
大部分都是應用內置的函數,來初始化weight(就是卷積核)和biases(偏置項)。偏置項我們沒有提到,但其實就是多了一個參數來調控,因此我們講卷積層的時候也沒怎么講。按照代碼就是出來Activation Map之后再分別加上bias。池化也是用到了最大池化。
注意一下relu。它也是一個激活函數,作用可以說跟之前講的softmax一樣,不過它在卷積層用的比較多,而且也是公認的比較好的激活函數。它的變體有很多。有興趣大家可以自己去查閱資料。以后才會寫有關這方面的文章。
pool_shape = pool.get_shape().as_list()
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
pool_reshaped = tf.reshape(pool, [-1, nodes])
W = tf.Variable(tf.zeros([nodes, 10]), name='weight')
池化層的輸出我們並不知道它是如何的形狀(當然,你也可以動手算)。因此就算把池化層拉成一維的矩陣,我們也不知道W需要如何的形狀。因此,我們查看pool(即池化層的輸出)的形狀,我暗地里print了一下為[None, 14, 14, 32],因此pool拉平后,就是[None, 14*14*32, 10]。為了接下來進行全連接層的計算,我們的W的形狀也應該為[14*14*32, 10]。這段代碼的原理就是如此。
准確率也一樣取后15次:

emmm, 不用跟之前比了,明顯比以前好很多了。下一章決定總結一下,優化的方法好了。
參考
https://mlnotebook.github.io/post/CNN1/(可惜是全英)
能否對卷積神經網絡工作原理做一個直觀的解釋? - Owl of Minerva的回答 - 知乎
CS231n