1. 程式人生 > >CNN實現MNIST手寫數字識別

CNN實現MNIST手寫數字識別

關鍵詞:CNNTensorFlow卷積池化特徵圖

一. 前言

本文用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可以表示成:

Pi=W1ix1i+W2ix2i+......+W25ix25i

這個是卷積公式,不解釋。因此對於上面的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個神經元的後續處理,後面具體要怎麼搞,只要懂多層感知器的都懂了,不解釋。

三. 原始碼+註釋

# -*- 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%
image.png

五. 引用