1. 程式人生 > >【Tensorflow】 寫給初學者的深度學習教程之 MNIST 數字識別

【Tensorflow】 寫給初學者的深度學習教程之 MNIST 數字識別

一般而言,MNIST 資料集測試就是機器學習和深度學習當中的"Hello World"工程,幾乎是所有的教程都會把它放在最開始的地方.這是因為,這個簡單的工程包含了大致的機器學習流程,通過練習這個工程有助於讀者加深理解機器學習或者是深度學習的大致流程.

但恰恰有那麼一部分同學,由於初入深度學習這個領域,腦海中還沒有清晰的概念,所以即使是 MNIST 數字識別這樣簡單的例子,我覺得也應該有人稍微詳細地講解一下。

本文的目的就是用更耐心的方式去引導初學者理解深度學習的大致流程和操作技巧.

最核心的模型

無論是機器學習還是深度學習,都繞不過模型.深度學習中的模型主要是各種神經網路.

但只有模型是不夠的,前提條件其實是資料,然後,後置的操作是訓練,再之後是測試.
這裡寫圖片描述

模型通過不斷的訓練從資料中學習,然後通過測試去驗證模型的正確性.

MNIST 數字識別工程,也是為了確定一個模型,然後進行訓練,訓練過程中這個模型從大量的數字圖片中學習得到識別手寫數字的能力,最後,需要測試驗證這個模型是否足夠理想和優秀.

MNIST 數字識別專案,模型可以是傳統的機器學習中的模型,也可以使用深度學習中的神經網路.在本文中,我使用的是 CNN,然後用的是 Python 和 Tensorflow.

MNIST 是什麼?

MNIST 是一個小型的手寫數字圖片庫,它總共有 60000 張圖片,其中 50000 張訓練圖片,10000 張測試圖片.每張圖片的畫素都是 28 * 28
這裡寫圖片描述
MNIST 對應上圖資料這一塊,它需要匯入到模型當中.

從資料到模型一般而言是需要轉化的,這一步叫做資料預處理。Tensorflow 接受 Numpy 中的 ndarray ,所以要想辦法進行轉換.

資料庫其實只由 4 個檔案組成.
這裡寫圖片描述

下載下來,然後分別解壓縮,可以發現其實只是 4 個 bin 檔案.
這裡寫圖片描述

train-images.idx3-ubyte 包含了 50000 張訓練圖片.
train-labels.idx1-ubyte 包含了 50000 個標籤.

t10k-images.idx3-ubyte 包含了 10000 張訓練圖片.
t10k-labels.idx1-ubyte 包含了 10000 個標籤.

在這裡有一點,非常重要,MNIST 將需要圖片或者標籤全部寫入到一個 bin 檔案當中去了,如果要讀取某張圖片和對應的標籤值就需要按照一定的方法從 bin 檔案中分割.

不過,MNIST 官網有 bin 檔案的結果說明,所以根據結構很容易編寫程式碼實現.
這裡寫圖片描述

我們先看,訓練用的標籤檔案.

0000 起始位置是一個魔數 數值為 2049
0004 檔案這個地方存放的數值是 6000 代表 6000 個標籤
0008 檔案這個地方開始按順序存放與訓練圖片對應的數字標籤 數值 0~9 我想大家都知道是什麼吧

所以,如果我們要讀取標籤的話,從標籤檔案開始偏移8個ubyte就能讀取所有的標籤數值了.

再看看訓練用的圖片集檔案

0000 位置也是一個魔數
0004 代表了本檔案中的圖片數量
0008 檔案這個位置存放的是一張圖片的高
0012 檔案這個位置存放的是一張圖片的寬
0016 從這裡起,代表的是影象中的每一個畫素點

如果我們想要讀取第一張圖片怎麼辦?

從檔案起始位置偏移16個byte,然後讀取後面的28*28也就是 784 個位元組.
如果要讀取第二張圖片能?
從檔案起始位置偏移16+(2-1)*784個byte,然後讀取後面的28*28個位元組.
讀取第 n 張圖片時
從檔案起始位置偏移 16+(n-1)*784 個byte,然後讀取後面的28*28個位元組.

一切都是有套路可以循的.

至於測試圖片集檔案和測試標籤集檔案,跟上面的類似,就不繼續分析了.

我們可以自己按照bin檔案的格式提取圖片和標籤,但考慮到這個沒有技術含量又枯燥無畏,常見的機器學習框架都預置了對MNIST的處理,如scklean和Tensorflow,並不需要我們動手.極大減低了我們的痛苦
人生苦短,我用python大概就是這個意思.

接下來的內容,我們可以看到 Tensorflow 可以很輕鬆地實現對 MNIST 中資料的讀取.

Tensorflow 讀取MNIST圖片資料

前面說過 Tensorflow 能很容易對 MNIST 進行讀取和格式轉換,其實是因為 Tensorflow 示例教程替我們做了這一部分的工作.

from tensorflow.examples.tutorials.mnist import input_data

從mnist這個模組中引入 input_data 這個類.

# MNIST_data 代表當前程式檔案所在的目錄中,用於存放MNIST資料的資料夾,如果沒有則新建,然後下載.
mnist = input_data.read_data_sets("MNIST_data",one_hot=True)

只需要呼叫 input_data 的 read_data_sets() 方法就好了。

如果當前檔案所在目錄中,不存在 MNIST_data 這個目錄的話,程式會自動下載 MNIST 資料到這個位置,如果已經存在了的話,就直接讀取資料檔案。

把所有的圖片讀取出來後,建立一個 mnist,mnist 是一個 dataset 類例項,裡面有許多 numpy 陣列,存放圖片和標籤.

需要注意的是 MNIST 本身資料集分為兩個部分.

訓練集測試集

但在 input_data 中,人為增加了驗證集,預設 5000 張圖片.

validation_images = train_images[:validation_size]
validation_labels = train_labels[:validation_size]
train_images = train_images[validation_size:]
train_labels = train_labels[validation_size:]

所以,最終有3個數據集:
訓練集、測試集、驗證集

通過 mnist 物件可以輕鬆訪問它們,如下面程式碼所示

mnist.train.images
mnist.train.labels

mnist.test.images
mnist.test.labels

mnist.validation.images
mnist.validation.labels

需要注意的是,讀取後的圖片資料,每張圖片就是numpy 陣列中一行的資料.

我們簡單列印一下

print(mnist.train.images.shape)
print(mnist.train.labels.shape)

列印的結果如下:

(55000, 784)
(55000, 10)

可以看到,train.images 陣列行數為55000 列數為 784,代表了 55000 張測試圖片.

好奇的同學也可以將測試圖片視覺化的方式呈現.

#獲取第二張圖片
image = mnist.train.images[1,:]
#將影象資料還原成28*28的解析度
image = image.reshape(28,28)
#列印對應的標籤
print(mnist.train.labels[1])

plt.figure()
plt.imshow(image)
plt.show()

這裡寫圖片描述

可以看到圖片其實是數字3,標籤內容如下.

[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]

這是 one-hot 的形式,它代表標籤值是 3.

Tensorflow 設定 CNN 結構

上面的內容介紹瞭如何在 Tensorflow 中讀取 MNIST 資料集的圖片和標籤,接下來要做的事情就是搞定模組這一環節.
這裡寫圖片描述
模型我選定的是 CNN,也就是卷積神經網路,在這裡我假設大家都明白 CNN 的概念,我要確定一個 CNN 來學習如何識別手寫數字的能力.

為了簡單起見,我確定了一個 4 層的神經網路.
這裡寫圖片描述
從左到右,分別是輸入層、卷積層、全連線層、輸出層.

卷積層我用了 3x3 的卷積核,數量為 32 stride為 1
啟用方法用了 relu
然後用了池化層  2x2 的核  stride 為 2
fc1 層用了 784 個神經元
output 層 10 個神經元,用於預測一張測試圖片中每個數字的概率,其中的概率經 softmax 處理過

本文想測試一下,就是這個再簡單不過的卷積神經網路,它對 MNIST 中數字的識別效果如何.

下面就是程式碼

# None 代表圖片數量未知
input = tf.placeholder(tf.float32,[None,784])
# 將input 重新調整結構,適用於CNN的特徵提取
input_image = tf.reshape(input,[-1,28,28,1])

# y是最終預測的結果
y = tf.placeholder(tf.float32,[None,10])

因為 Tensorflow 一次可以訓練多張圖片,所以要用一個佔位符 placeholder 這樣具體數值可以在後面訓練時動態分配.

# input 代表輸入,filter 代表卷積核
def conv2d(input,filter):
    return tf.nn.conv2d(input,filter,strides=[1,1,1,1],padding='SAME')
# 池化層
def max_pool(input):
    return tf.nn.max_pool(input,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')

# 初始化卷積核或者是權重陣列的值
def weight_variable(shape):
    initial = tf.truncated_normal(shape,stddev=0.1)
    return tf.Variable(initial)

# 初始化bias的值
def bias_variable(shape):
    return tf.Variable(tf.zeros(shape))

上面4個方法都是工具方法,為了幫助我們創造神經網路的.

conv2d() 是創造卷積層的方法.
max_pool() 是池化層.
然後剩下的兩個方法都是為了初始化超引數的.

#[filter_height, filter_width, in_channels, out_channels]
#定義了卷積核
filter = [3,3,1,32]

filter_conv1 = weight_variable(filter)
b_conv1 = bias_variable([32])
# 建立卷積層,進行卷積操作,並通過Relu啟用,然後池化
h_conv1 = tf.nn.relu(conv2d(input_image,filter_conv1)+b_conv1)
h_pool1 = max_pool(h_conv1)

定義了卷積層的結構.

h_flat = tf.reshape(h_pool1,[-1,14*14*32])

W_fc1 = weight_variable([14*14*32,784])
b_fc1 = bias_variable([784])
h_fc1 = tf.matmul(h_flat,W_fc1) + b_fc1

W_fc2 = weight_variable([784,10])
b_fc2 = bias_variable([10])

y_hat = tf.matmul(h_fc1,W_fc2) + b_fc2

h_flat 是將 pool 後的卷積核全部拉平成一行資料,便於和後面的全連線層進行資料運算.

y_hat 是整個神經網路的輸出層,包含 10 個結點.

cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=y_hat ))

代價函式採用了 cross_entropy,顯然,整個模型輸出的值經過了 softmax 處理,將輸出的值換算成每個類別的概率.

到這裡,神經網路結構我們就確定了,下面要做的就是訓練神經網路和測試神經網路了.

訓練神經網路

這裡寫圖片描述

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

在這裡,定義了一個梯度下降的訓練器,學習率是0.01.
train_step 其實就是一個黑盒子,它隱去了很多的技術細節,但同時也極大方便了我們的開發.
我們只需要知道,train_step在每一次訓練後都會調整神經網路中引數的值,以便 cross_entropy 這個代價函式的值最低,也就是為了神經網路的表現越來越好.

correct_prediction = tf.equal(tf.argmax(y_hat,1),tf.argmax(y,1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

上面程式碼的目的是定義準確率,我們會在後面的程式碼中週期性地列印準確率,訓練測試後,我們還要列印測試集下面神經網路的準確率.

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for i in range(10000):

        batch_x,batch_y = mnist.train.next_batch(50)

        if i % 100 == 0:
            train_accuracy = accuracy.eval(feed_dict={input:batch_x,y:batch_y})
            print("step %d,train accuracy %g " %(i,train_accuracy))

        train_step.run(feed_dict={input:batch_x,y:batch_y})

    print("test accuracy %g " % accuracy.eval(feed_dict={input:mnist.test.images,y:mnist.test.labels}))

我們的 epoch 是 10000 次,也就是說需要訓練10000個週期.每個週期訓練都是小批量訓練 50 張,然後每隔 100 個訓練週期列印階段性的準確率.

訓練完成後,還需要驗證測試集下的準確度

step 9600,train accuracy 0.98 
step 9700,train accuracy 0.96 
step 9800,train accuracy 1 
step 9900,train accuracy 1 

test accuracy 0.9766 

最終的測試成績,準確率 97.66%

那麼準確率為 97.66 % 算不算高呢?

其實,非常不錯了.我們文章採取的模型是我自己設定的最簡單的模型.但即使這樣,相比於傳統的機器學習方法,它的確不錯了.大家可以去官網看看不同的模型,在 MNIST 測試時的表現.

下面是完整程式碼,我是 Python3.5 + Tensorflow1.7
mnist_conv.py

# coding:utf-8

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf

mnist = input_data.read_data_sets("MNIST_data",one_hot=True)


input = tf.placeholder(tf.float32,[None,784])
input_image = tf.reshape(input,[-1,28,28,1])

y = tf.placeholder(tf.float32,[None,10])

# input 代表輸入,filter 代表卷積核
def conv2d(input,filter):
    return tf.nn.conv2d(input,filter,strides=[1,1,1,1],padding='SAME')
# 池化層
def max_pool(input):
    return tf.nn.max_pool(input,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')

# 初始化卷積核或者是權重陣列的值
def weight_variable(shape):
    initial = tf.truncated_normal(shape,stddev=0.1)
    return tf.Variable(initial)

# 初始化bias的值
def bias_variable(shape):
    return tf.Variable(tf.zeros(shape))

#[filter_height, filter_width, in_channels, out_channels]
#定義了卷積核
filter = [3,3,1,32]

filter_conv1 = weight_variable(filter)
b_conv1 = bias_variable([32])
# 建立卷積層,進行卷積操作,並通過Relu啟用,然後池化
h_conv1 = tf.nn.relu(conv2d(input_image,filter_conv1)+b_conv1)
h_pool1 = max_pool(h_conv1)

h_flat = tf.reshape(h_pool1,[-1,14*14*32])

W_fc1 = weight_variable([14*14*32,768])
b_fc1 = bias_variable([768])
h_fc1 = tf.matmul(h_flat,W_fc1) + b_fc1

W_fc2 = weight_variable([768,10])
b_fc2 = bias_variable([10])

y_hat = tf.matmul(h_fc1,W_fc2) + b_fc2



cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=y_hat ))

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
#train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

correct_prediction = tf.equal(tf.argmax(y_hat,1),tf.argmax(y,1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for i in range(10000):

        batch_x,batch_y = mnist.train.next_batch(50)

        if i % 100 == 0:
            train_accuracy = accuracy.eval(feed_dict={input:batch_x,y:batch_y})
            print("step %d,train accuracy %g " %(i,train_accuracy))

        train_step.run(feed_dict={input:batch_x,y:batch_y})

        # sess.run(train_step,feed_dict={x:batch_x,y:batch_y})

    print("test accuracy %g " % accuracy.eval(feed_dict={input:mnist.test.images,y:mnist.test.labels}))

擴充套件

本文中的神經網路,麻雀雖小,但五臟俱全.

不過,同學們可以持續優化它,畢竟有的神經網路能夠達到 99.67% 的準確率.

  1. 設計更深的層次的神經網路,本文只有4層,並且這4層還包括輸入輸出層,同學們可以擴充套件更多的層,變現效果肯定更好.
  2. 使用其它的優化器,比如 AdamOptimizer
  3. 使用 dropout 優化手段
  4. 使用資料增強技術,讓 MNIST 可供訓練的圖片更多,這樣神經網路學習也更充分
  5. 用 Tensorboard 記錄訓練過程的準確率或者 cross_entropy 的數值,最後生成視覺化的報表

最終,還是要建議同學們自己動手敲一遍程式碼,敲完然後思考一下,為什麼要這樣寫,等你能夠比較流利敲出程式碼時,你就通過 MNIST 基本掌握了深度學習的一些套路,這會提高你在後續學習中的興致.如果你不親手敲程式碼的化,那麼深度學習的很多概念,你沒有辦法讓它直觀起來,並且你會把它們忘掉.

最後,如果應對了 MNIST 之後,我們就可以將目光放到更復雜的資料集上去。比如 CIFAR10,比如自動駕駛中的行人識別。

光看書是不行的,真的要親手實踐。

這裡寫圖片描述