卷積神經網絡(Convolutional Neural Network, CNN)是一種前饋神經網絡, 在計算機視覺等領域被普遍應用. 本文將簡單介紹其原理並分析Tensorflow官方提供的示例.html
關於神經網絡與偏差反向傳播的原理能夠參考做者的另外一篇博文BP神經網絡與Python實現.python
卷積是圖像處理中一種基本方法. 卷積核是一個f*f
的矩陣. 一般n取奇數,使得卷積核有中心點.git
對圖像中每一個點取以其爲中心的f
階方陣, 將該方陣中各值與卷積核中對應位置的值相乘, 並用它們的和做爲結果矩陣中對應點的值.github
1*1 + 1*0 + 1*1 + 0*0 + 1*1 + 1*0 + 0*1 + 0*0 + 1*1 = 4
算法
卷積核每次向右移動1列, 遇行末向下移動1列直到完成全部計算. 咱們把每次移動的距離稱爲步幅s.緩存
上述操做處理圖像獲得新圖像的操做稱爲卷積, 在圖像處理中卷積核也被稱爲過濾器(filter).網絡
卷積獲得的結果矩陣一般用於表示原圖的某種特徵(如邊緣), 所以卷積結果被稱爲特徵圖(Feature Map).app
每一個卷積核能夠包含一個偏置參數b, 即對卷積結果的每個元素都加b做爲輸出的特徵圖.ide
邊緣檢測是卷積的一種典型應用, 人眼所見的邊緣是圖像中不一樣區域的分界線. 分界線兩側的色彩或灰度一般有着較大的不一樣.函數
下面咱們使用一個很是簡單的示例來展現邊緣檢測過程. 第一個6*6
的矩陣是灰度圖, 顯然圖像左側較亮右側較暗, 中間造成了一條明顯的垂直邊緣.
在特徵圖中央有一條垂直亮線(第2,3列), 即原圖的垂直邊緣. 相似的能夠檢測縱向邊緣:
卷積核的中心沒法對準原圖像中邊緣的像素點(與邊緣距離小於卷積核半徑), 若要對邊緣的點進行計算必須填充(padding)外部缺乏的點使卷積核的中心能夠對準它們. 經常使用的填充策略有:
此外還有0值填充, 均值填充等方法. 一般用p來描述填充的寬度.
SAME填充效果, 4*4
矩陣被填充爲6*6
矩陣, 填充寬度p=1
:
對於n*n
的矩陣, 使用f*f
的核進行卷積, 填充寬度爲p
, 若縱向步幅爲s1
, 橫向步幅爲s2
則特徵圖的行列數爲:
\[ [ \frac{n + 2*p - f }{s1} + 1 ] \times [ \frac{n + 2*p - f }{s2} + 1 ] \]
灰度圖所能描述的信息的極爲有限, 咱們更多地處理RGB圖像. RGB圖像須要3個矩陣才能描述圖片, 咱們稱爲3個通道(channel).
如下圖6*6
的RGB圖爲例, 3個矩陣分別與黃色卷積核進行卷積獲得3個4*4
特徵圖, 將3個特徵圖同位置的值疊加獲得最終的卷積結果.
在邊緣檢測中咱們注意到, 一個卷積核一般只能提取圖像一種特徵如水平邊緣或垂直邊緣. 爲了提取圖像的多個特徵, 咱們一般使用多個卷積核.
咱們使用高維矩陣來描述這一過程, RGB圖像爲6*6*3
矩陣, 兩個卷積核疊加爲3*3*2
矩陣, 兩個特徵圖疊加爲4*4*2
矩陣. 輸入, 輸出和卷積核均使用三維矩陣來表示, 這樣咱們能夠方便的級聯多個卷積層.
在上一節中咱們已經介紹了一個卷積層如何工做的, 如今咱們來探討爲何使用卷積提取圖像特徵.
首先分析卷積層的輸入輸出, 每一個卷積層輸入了一個w1 * h1 * c1
的三維矩陣, 輸出w2 * h2 *c2
的三維矩陣.
若使用全鏈接層須要(w1 * h1 * c1) * (w2 * h2 *c2)
個參數, 卷積層只須要訓練c2
個二維卷積核中的f1 * f1 * c2
個參數和c2
個偏置值, 可見卷積層極大地減小了參數的數量.
更少的參數對於訓練數據和計算資源都有限的任務而言, 一般意味着更高的精度和更好的訓練效率.
更重要的是, 卷積針對小塊區域而不是單個像素進行處理, 更好地從空間分佈中提取特徵, 這與人類視覺是相似的. 而全鏈接層嚴重忽略了空間分佈所包含的信息.
特徵圖中一個像素只與輸入矩陣中f * f
個像素有關, 這種性質被稱爲局部感知. 一個卷積核用於生成特徵圖中全部像素, 該特性被稱爲權值共享.
經過卷積學習到的圖像特徵仍然數量巨大, 不便直接進行分類. 池化層便用於減小特徵數量.
池化操做很是簡單, 好比咱們使用一個卷積覈對一張圖片進行過濾獲得一個8x8的方陣, 咱們能夠將方陣劃分爲16個2x2方陣, 每一個小方陣稱爲鄰域.
用16個小方陣的均值組成一個4x4方陣即是均值池化, 相似地還有最大值池化等操做. 均值池化對保留背景等特徵較好, 最大值池化對紋理提取更好.
隨機池化則是根據像素點數值大小賦予機率(權值), 而後按其加權求和.
池化操做用於減小圖的寬度和高度, 但不能減小通道數.
用1*1*c2
的核進行卷積可使w1 * h1 * c1
的輸入矩陣映射到w1 * h1 * c2
的輸出矩陣. 即對各通道輸出加權求和實現減小通道數的效果.
TensorFlow的文檔Deep MNIST for Experts介紹了使用CNN在MNIST數據集上識別手寫數字的方法., 該示例採用了LeNet5模型.
完整代碼能夠在GitHub上找到, 本文將對其進行簡單分析. 源碼來自tensorflow-1.3.0版本示例.
主要有3條引入:
import tempfile from tensorflow.examples.tutorials.mnist import input_data import tensorflow as tf
main(_)
函數負責網絡的構建:
def main(_): # 導入MNIST數據集 # FLAGS.data_dir是本地數據的路徑, 能夠用空字符串代替以自動下載數據集 mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True) # x是輸入層, 每一個28x28的圖像被展開爲784階向量 x = tf.placeholder(tf.float32, [None, 784]) # y_是訓練集預標註好的結果, 採用one-hot的方法表示10種分類 y_ = tf.placeholder(tf.float32, [None, 10]) # deepnn方法構建了一個cnn, y_conv是cnn的預測輸出 # keep_prob是dropout層的參數, 下文再講 y_conv, keep_prob = deepnn(x) # 計算預測y_conv和標籤y_的交叉熵做爲損失函數 with tf.name_scope('loss'): cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv) cross_entropy = tf.reduce_mean(cross_entropy) # 使用Adam優化算法, 以最小化損失函數爲目標 with tf.name_scope('adam_optimizer'): train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy) # 計算精確度(正確分類的樣本數佔測試樣本數的比例), 用於評估模型效果 with tf.name_scope('accuracy'): correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1)) correct_prediction = tf.cast(correct_prediction, tf.float32) accuracy = tf.reduce_mean(correct_prediction)
main
函數與其它tensorflow神經網絡並沒有二致, 關鍵分析deepnn
方法如何構建cnn:
def deepnn(x): # x的結構爲[n, 784], 將其展開爲[n, 28, 28] # 灰度圖只有一個通道, x_image第四維爲1 # x_image的四維分別是[n_sample, width, height, channel] with tf.name_scope('reshape'): x_image = tf.reshape(x, [-1, 28, 28, 1]) # 第一個卷積層, 將28x28*1灰度圖使用5*5*32核進行卷積 with tf.name_scope('conv1'): # 初始化鏈接權值, 爲了不梯度消失權值使用正則分佈進行初始化 W_conv1 = weight_variable([5, 5, 1, 32]) # 初始化偏置值, 這裏使用的是0.1 b_conv1 = bias_variable([32]) # strides是卷積核移動的步幅. 採用SAME策略填充, 即便用相同值填充 # def conv2d(x, W): # tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') # h_conv1的結構爲[n, 28, 28, 32] h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) # 第一個池化層, 2*2最大值池化, 獲得14*14矩陣 with tf.name_scope('pool1'): h_pool1 = max_pool_2x2(h_conv1) # 第二個卷積層, 將28*28*32特徵圖使用5*5*64核進行卷積 with tf.name_scope('conv2'): W_conv2 = weight_variable([5, 5, 32, 64]) b_conv2 = bias_variable([64]) # h_conv2的結構爲[n, 14, 14, 64] h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) # 第二個池化層, 2*2最大值池化, 獲得7*7矩陣 with tf.name_scope('pool2'): # h_pool2的結構爲[n, 7, 7, 64] h_pool2 = max_pool_2x2(h_conv2) # 第一個全鏈接層, 將7*7*64特徵矩陣用全鏈接層映射到1024個特徵 with tf.name_scope('fc1'): W_fc1 = weight_variable([7 * 7 * 64, 1024]) b_fc1 = bias_variable([1024]) h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64]) h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1) # 使用dropout層避免過擬合 # 即在訓練過程當中的一次迭代中, 隨機選擇必定比例的神經元不參與這次迭代 # 參與迭代的機率值由keep_prob指定, keep_prob=1.0爲使用整個網絡 with tf.name_scope('dropout'): keep_prob = tf.placeholder(tf.float32) h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob) # 第二個全鏈接層, 將1024個特徵映射到10個特徵, 即10個分類的one-hot編碼 # one-hot編碼是指用 `100`代替1, `010`代替2, `001`代替3... 的編碼方式 with tf.name_scope('fc2'): W_fc2 = weight_variable([1024, 10]) b_fc2 = bias_variable([10]) y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2 return y_conv, keep_prob
整個網絡暴露的接口有3個:
x[n, 784]
y_conv[n, 10]
keep_prob[1]
如今能夠繼續關注main
方法了, 完成網絡構建以後main
先將網絡結構緩存到硬盤:
graph_location = tempfile.mkdtemp() print('Saving graph to: %s' % graph_location) train_writer = tf.summary.FileWriter(graph_location) train_writer.add_graph(tf.get_default_graph())
接下來初始化tf.Session()
進行訓練:
with tf.Session() as sess: # 初始化全局變量 sess.run(tf.global_variables_initializer()) for i in range(10000): # 每次取訓練數據集中50個樣本, 分10000次取出 # batch[0]爲特徵集, 結構爲[50, 784]即50組784階向量 # batch[1]爲標籤集, 結構爲[50, 10]即50個採用one-hot編碼的標籤 batch = mnist.train.next_batch(50) # 每進行100次迭代評估一次精度 if i % 100 == 0: train_accuracy = accuracy.eval(feed_dict={ x: batch[0], y_: batch[1], keep_prob: 1.0}) print('step %d, training accuracy %g' % (i, train_accuracy)) # 進行訓練, dropout keep prob設爲0.5 train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5}) # 評估最終精度, dropout keep prob設爲1.0即便用所有網絡 print('test accuracy %g' % accuracy.eval(feed_dict={ x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
啓動代碼會處理命令行參數和選項:
if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--data_dir', type=str, default='/tmp/tensorflow/mnist/input_data', help='Directory for storing input data') FLAGS, unparsed = parser.parse_known_args() tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)