CNN實現MNIST手寫數字識別
關鍵詞:CNN
、TensorFlow
、卷積
、池化
、特徵圖
一. 前言
本文用TensorFlow實現了CNN(卷積神經網路)的經典結構LeNet-5, 具體CNN的LeNet-5模型原理見《深度學習(四)卷積神經網路入門學習(1)》,講得還是比較清楚的。資料來源自MNIST, 為了方便起見,已經準備了一個指令碼來自動下載和匯入MNIST資料集。它會自動建立一個’MNIST_data’的目錄來儲存資料。
二. CNN經典結構模型
下圖為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相加起來,也就是:
把上面的Pi的計算公式,代入上式,那麼我們可以得到:
其中X就是輸入的那6張5*5特徵圖片的各個畫素點值,而W就是我們需要學習的引數,也就相當於6個5*5的卷積核,當然它包含著6*(5*5)個引數。因此我們的輸出特徵圖就是:
這個就是從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個神經元的後續處理,後面具體要怎麼搞,只要懂多層感知器的都懂了,不解釋。
三. 原始碼+註釋
# -*- coding: utf-8 -*-
import tensorflow as tf
#匯入input_data用於自動下載和安裝MNIST資料集
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
#建立兩個佔位符,x為輸入網路的影象,y_為輸入網路的影象類別
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])
#權重初始化函式
def weight_variable(shape):
#輸出服從截尾正態分佈的隨機值
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
#偏置初始化函式
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
#建立卷積op
#x 是一個4維張量,shape為[batch,height,width,channels]
#卷積核移動步長為1。填充型別為SAME,可以不丟棄任何畫素點, VALID丟棄邊緣畫素點
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding="SAME")
#建立池化op
#採用最大池化,也就是取視窗中的最大值作為結果
#x 是一個4維張量,shape為[batch,height,width,channels]
#ksize表示pool視窗大小為2x2,也就是高2,寬2
#strides,表示在height和width維度上的步長都為2
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1,2,2,1],
strides=[1,2,2,1], padding="SAME")
#第1層,卷積層
#初始化W為[5,5,1,32]的張量,表示卷積核大小為5*5,1表示影象通道數,6表示卷積核個數即輸出6個特徵圖
W_conv1 = weight_variable([5,5,1,6])
#初始化b為[6],即輸出大小
b_conv1 = bias_variable([6])
#把輸入x(二維張量,shape為[batch, 784])變成4d的x_image,x_image的shape應該是[batch,28,28,1]
#-1表示自動推測這個維度的size
x_image = tf.reshape(x, [-1,28,28,1])
#把x_image和權重進行卷積,加上偏置項,然後應用ReLU啟用函式,最後進行max_pooling
#h_pool1的輸出即為第一層網路輸出,shape為[batch,14,14,6]
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
#第2層,卷積層
#卷積核大小依然是5*5,通道數為6,卷積核個數為16
W_conv2 = weight_variable([5,5,6,16])
b_conv2 = weight_variable([16])
#h_pool2即為第二層網路輸出,shape為[batch,7,7,16]
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
#第3層, 全連線層
#這層是擁有1024個神經元的全連線層
#W的第1維size為7*7*16,7*7是h_pool2輸出的size,16是第2層輸出神經元個數
W_fc1 = weight_variable([7*7*16, 120])
b_fc1 = bias_variable([120])
#計算前需要把第2層的輸出reshape成[batch, 7*7*16]的張量
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*16])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
#Dropout層
#為了減少過擬合,在輸出層前加入dropout
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
#輸出層
#最後,新增一個softmax層
#可以理解為另一個全連線層,只不過輸出時使用softmax將網路輸出值轉換成了概率
W_fc2 = weight_variable([120, 10])
b_fc2 = bias_variable([10])
y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
#預測值和真實值之間的交叉墒
cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
#train op, 使用ADAM優化器來做梯度下降。學習率為0.0001
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
#評估模型,tf.argmax能給出某個tensor物件在某一維上資料最大值的索引。
#因為標籤是由0,1組成了one-hot vector,返回的索引就是數值為1的位置
correct_predict = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
#計算正確預測項的比例,因為tf.equal返回的是布林值,
#使用tf.cast把布林值轉換成浮點數,然後用tf.reduce_mean求平均值
accuracy = tf.reduce_mean(tf.cast(correct_predict, "float"))
saver = tf.train.Saver()
#開始訓練模型,迴圈20000次,每次隨機從訓練集中抓取50幅影象
def cnn_train():
# 建立一個互動式Session
sess = tf.InteractiveSession()
sess.run(tf.initialize_all_variables())
for i in range(20000):
batch = mnist.train.next_batch(50)
if i%100 == 0:
#每100次輸出一次日誌
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))
saver.save(sess, 'model')
train_step.run(feed_dict={x:batch[0], y_:batch[1], keep_prob:0.5})
#預測
def predict():
sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver(tf.global_variables())
saver.restore(sess, 'model')
print( "test accuracy %g" % accuracy.eval(feed_dict={
x:mnist.test.images, y_:mnist.test.labels, keep_prob:1.0}))
cnn_train()
predict()
四. 訓練結果
精度大約98%