TensorFlow系列專題(六):實戰項目Mnist手寫數據集識別

1. 導讀

就像我們在學習一門編程語言時總喜歡把"Hello World!"作為入門的示例代碼一樣,MNIST手寫數字識別問題就像是深度學習的"Hello World!"。通過這個例子,我們將瞭解如何將數據轉化為神經網絡所需要的數據格式,以及如何使用TensorFlow搭建簡單的單層和多層的神經網絡。

2. MNIST數據集

MNIST 數據集可以從網站 http://yann.lecun.com/exdb/mnist/ 上下載,需要下載的數據集總共有4個文件,其中"train-images-idx3-ubyte.gz"是訓練集的圖片,總共有60000張,"train-labels-idx1-ubyte.gz"是訓練集圖片對應的類標(0~9)。"t10k-images-idx3-ubyte.gz"是測試集的圖片,總共有10000張,"t10k-labels-idx1-ubyte.gz"是測試集圖片對應的類標(0~9)。TensorFlow的示例代碼中已經對MNIST數據集的處理進行了封裝,但是作為第一個程序,我們希望帶著讀者從數據處理開始做,數據處理在整個機器學習項目中是很關鍵的一個環節,因此有必要在第一個項目中就讓讀者體會到它的重要性。

我們將下載的壓縮文件解壓後會發現數據都是以二進制文件的形式存儲的,以訓練集的圖像數據為例:

TensorFlow系列專題(六):實戰項目Mnist手寫數據集識別

表1 訓練集圖像數據的文件格式

如表1所示,解壓後的訓練集圖像數據"train-images-idx3-ubyte"文件,其前16個字節的內容是文件的基本信息,分別是magic number(又稱為幻數,用來標記文件的格式)、圖像樣本的數量(60000)、每張圖像的行數以及每張圖像的列數。由於每張圖像的大小是 28*28,所以我們從編號0016的字節開始,每次讀取28*28=784個字節,即讀取了一張完整的圖像。我們讀取的每一個字節代表一個像素,取值範圍是[0,255],像素值越接近0,顏色越接近白色,像素值越接近255,顏色越接近黑色。

訓練集類標文件的格式如下:

TensorFlow系列專題(六):實戰項目Mnist手寫數據集識別

表2 訓練集類標數據的文件格式

如上表所示,訓練集類標數據文件的前8個字節記錄了文件的基本信息,包括magic number和類標項的數量(60000)。從編號0008的字節開始,每一個字節就是一個類標,類標的取值範圍是[0,9],類標直接標明瞭對應的圖像樣本的真實數值。如圖1所示,我們將部分數據進行了可視化。測試集的圖像數據和類標數據的文件格式與訓練集一樣。

TensorFlow系列專題(六):實戰項目Mnist手寫數據集識別

圖1 訓練集圖像數據可視化效果

3. 數據處理

在開始實現神經網絡之前,我們要先準備好數據。雖然MNIST數據集本身就已經處理過了,但是我們還是需要做一些封裝以及簡單的特徵工程。我們定義一個MnistData類用來管理數據:

import numpy as np
import struct
import random
class MnistData:
def __init__(self, train_image_path, train_label_path,
test_image_path, test_label_path):
# 訓練集和測試集的文件路徑
self.train_image_path = train_image_path
self.train_label_path = train_label_path
self.test_image_path = test_image_path
self.test_label_path = test_label_path
# 獲取訓練集和測試集數據
# get_data()方法,參數為0獲取訓練集數據,參數為1獲取測試集
self.train_images, self.train_labels = self.get_data(0)
self.test_images, self.test_labels = self.get_data(1)
# 定義兩個輔助變量,用來判斷一個回合的訓練是否完成
self.num_of_batch = 0
self.got_batch = 0

在"__init__"方法中初始化了"MnistData"類相關的一些參數,其中"train_image_path"和"train_label_path"分別是訓練集數據和類標的文件路徑,"test_image_path"和"test_label_path"分別是測試集數據和類標的文件路徑。

接下來我們要實現"MnistData"類的另一個方法"get_data",該方法實現了Mnist數據集的讀取以及數據的預處理。

def get_data(self, data_type):
if data_type == 0: # 獲取訓練集數據
image_path = self.train_image_path
label_path = self.train_label_path
else: # 獲取測試集數據
image_path = self.test_image_path
label_path = self.test_label_path
with open(image_path, 'rb') as file1:
image_file = file1.read()
with open(label_path, 'rb') as file2:
label_file = file2.read()
label_index = 0
image_index = 0
labels = []
images = []
# 讀取訓練集圖像數據文件的文件信息
magic, num_of_datasets, rows, columns =\
struct.unpack_from('>IIII', image_file, image_index)
image_index += struct.calcsize('>IIII')
for i in range(num_of_datasets):
# 讀取784個unsigned byte,即一副圖像的所有像素值
temp = struct.unpack_from('>784B', image_file, image_index)
# 將讀取的像素數據轉換成28*28的矩陣
temp = np.reshape(temp, (28, 28))
# 歸一化處理
temp = temp / 255
images.append(temp)
image_index += struct.calcsize('>784B') # 每次增加784B
# 跳過描述信息
label_index += struct.calcsize('>II')
labels = struct.unpack_from('>' + str(num_of_datasets)
+ 'B', label_file, label_index)
# one-hot
labels = np.eye(10)[np.array(labels)]
return images, labels

由於Mnist數據是以二進制文件的形式存儲,所以我們需要用到struct模塊來處理文件,uppack_from函數用來解包二進制文件,第42行代碼中,參數">IIII"指定讀取16個字節的內容,這正好是文件的基本信息部分。其中">"代表二進制文件是以大端法存儲,"IIII"代表四個int類型的長度,這裡一個int類型佔4個字節。參數"image_file"是堯都區的文件,"image_index"是偏移量。如果要連續的讀取文件內容,每讀取一部分數據後就要增加相應的偏移量。

第51行代碼中,我們對數據進行了歸一化處理,關於歸一化我們在第一章中有介紹。在後面兩節實現神經網絡模型的時候,讀者可以嘗試註釋掉歸一化的這行代碼,比較一下做了歸一化和不做歸一化,模型的效果有什麼差別。

最後,我們要實現一個"get_batch"方法。在訓練模型的時候,我們通常會用訓練集數據訓練多個回合(epoch),每個回合都會用且只用一次訓練集中的每一條數據。因為我們使用隨機梯度下降的方式來更新參數,所以每個回合中,我們會把訓練集數據分為多個批次(batch)送進模型中去訓練,每次送進模型的數據量的大小為"batch_size"。因此,我們需要將數據按"batch_size"進行劃分。

def get_batch(self, batch_size):
# 剛開始訓練或當一輪訓練結束之後,打亂數據集數據的順序

if self.got_batch == self.num_of_batch:
train_list = list(zip(self.train_images, self.train_labels))
random.shuffle(train_list)
self.train_images, self.train_labels = zip(*train_list)

# 重置兩個輔助變量
self.num_of_batch = 60000 / batch_size
self.got_batch = 0

# 獲取一個batch size的訓練數據
train_images = self.train_images[
self.got_batch*batch_size:(self.got_batch+1)*batch_size]
train_labels = self.train_labels[
self.got_batch*batch_size:(self.got_batch+1)*batch_size]
self.got_batch += 1

return train_images, train_labels

在第68行代碼中,我們使用了"random"模塊的"shuffle"方法對數據進行了"洗牌",即打亂了數據原來的順序,"shuffle"操作的目的是為了讓各類樣本數據儘可能混合在一起,從而在模型訓練的過程中,各類樣本都可以對模型的參數變化產生影響。不過需要記住的是,"shuffle"操作並不總是必須的,而且是否可以使用"shuffle"操作也要看具體的數據來定。

到這裡我們已經實現了Mnist數據的讀取和預處理,在後面兩小節的內容裡,我們會分別實現一個單層的神經網絡和一個多層的前饋神經網絡模型,實現Mnist手寫數字的識別問題。

4. 單層隱藏層神經網絡的實現

介紹完MNIST數據集之後,我們現在可以開始動手實現一個神經網絡來解決手寫數字識別的問題了,我們先從一個簡單的兩層(一層隱藏層)神經網絡開始。

本小節所實現的單層神經網絡結構如圖3-16所示。每張圖片的大小為,我們將其轉為長度為784的向量作為網絡的輸入。隱藏層有10個神經元,在這個簡單的神經網絡中我們沒有在隱藏層中使用激活函數。在隱藏層後面我們加了一個Softmax層,用來將隱藏層的輸出直接轉化為模型的預測結果。

TensorFlow系列專題(六):實戰項目Mnist手寫數據集識別

圖2 實現Mnist手寫數字識別的兩層神經網絡結構

接下來我們實現具體的代碼,首先導入上一小節中我們實現的數據處理的類以及TensorFlow的包:

from mnist_data import MnistData
import tensorflow as tf

創建一個Session會話,並定義好相關的變量:

# 創建Session會話
sess = tf.InteractiveSession()
# 訓練集、測試集的文件路徑
train_image_path = './data/train-images-idx3-ubyte'
train_label_path = './data/train-labels-idx1-ubyte'
test_image_path = './data/t10k-images-idx3-ubyte'
test_label_path = './data/t10k-labels-idx1-ubyte'
epochs = 10 # 訓練的總輪數
batch_size = 100 # 每個batch的大小
learning_rate = 0.2 # 學習率

"epochs"是我們想要訓練的總輪數,每一輪都會使用訓練集的所有數據去訓練一遍模型。由於我們使用隨機梯度下降方法更新參數,所以不會一次把所有的數據送進模型去訓練,而是按批次訓練,"batch_size"是我們定義的一個批次的數據量的大小,這裡我們設定了100,那麼每個"batch"就會送100個樣本到模型中去訓練,一輪訓練的"batch"數等於總的訓練集數量除以"batch_size"。"learning_rate"是我們定義的學習率,即模型參數更新的速率。

接下來我們定義模型的參數:

# 創建樣本數據的placeholder
x = tf.placeholder(tf.float32, [None, 28, 28])
# 定義權重矩陣和偏置項
W = tf.Variable(tf.zeros([28*28, 10]))
b = tf.Variable(tf.zeros([10]))
# 樣本的真實標籤
y_ = tf.placeholder(tf.float32, [None, 10])
# 使用softmax函數將單層網絡的輸出轉換為預測結果
y = tf.nn.softmax(tf.matmul(tf.reshape(x, [-1, 28*28]), W) + b)

第16行代碼定義了輸入樣本的placeholder,第18和第19行代碼定義了該單層神經網絡隱藏層的權重矩陣和偏置項。根據圖3-16所示的網絡結構,輸入向量長度為784,隱藏層有10個神經元,因此我們定義權重矩陣的大小為784行10列,偏置項的向量長度為10。在第24行代碼中,我們先將輸入的樣本數據轉換為一維的向量,然後進行的運算,計算的結果再經由Softmax計算得到最終的預測結果。

定義完網絡的參數後我們還需要定義損失函數和優化器:

# 損失函數和優化器
# -tf.reduce_sum(y_ * tf.log(y) 計算這個batch中每個樣本的交叉熵
# reduce_mean方法對一個batch的樣本的交叉熵求平均值,作為最終的loss
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), axis=1))

train_step = \
tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)

第28行我們定義了交叉熵損失函數,關於交叉熵損失函數在本章第三小節中我們已經做了介紹,""計算的是一個"batch"的訓練樣本數據的交叉熵,每個樣本數據都有一個值,TensorFlow的"reduce_mean"方法將這個"batch"的數據的交叉熵求了平均值,作為這個"batch"最終的交叉熵損失值。

第29和30行代碼中,我們定義了一個梯度下降優化器"GradientDescentOptimizer",並設定了學習率為"learning_rate"以及優化目標為"cross_entropy"。

接下來我們還需要實現模型的評估:

# 比較預測結果和真實類標
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
# 計算準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

"tf.equal()"方法用於比較兩個矩陣或向量相應位置的元素是否相等,相等為"True",不等為"False"。"tf.cast"用於將"True"和"False"轉換為"1"和"0","tf.reduce_mean"對轉換後的數據求平均值,該值即為模型在測試集上預測結果的準確率。最後,我們實現模型的訓練和預測:

# 初始化MnistData類
data = MnistData(train_image_path, train_label_path,
test_image_path, test_label_path)
# 初始化模型參數
init = tf.global_variables_initializer().run()

# 開始訓練
for i in range(epochs):
for j in range(600):
# 獲取一個batch的數據
batch_x, batch_y = data.get_batch(batch_size)
# 優化參數
train_step.run({x: batch_x, y_: batch_y})
# 對測試集進行預測並計算準確率
print(accuracy.eval({x: data.test_images, y_: data.test_labels}))

因為我們的"batch_size"設置為100,Mnist數據集的訓練數據有60000條,因此我們訓練600個"batch"正好是一輪。第50行代碼中,我們訓練完的模型對測試集數據進行了預測,並輸出了預測的準確率,結果為0.9228。

5. 多層神經網絡的實現

多層神經網絡的實現也很簡單,我們只需要在上一小節的代碼基礎上對網絡的結構稍作修改即可,我們先來看一下這一小節裡要實現的多層(兩層隱藏層)神經網絡的結構:

TensorFlow系列專題(六):實戰項目Mnist手寫數據集識別

圖3 實現Mnist手寫數字識別的多層神經網絡結構

如上圖所示,這裡我們增加了一層隱藏層,實現的是一個三層神經網絡。與上一小節的兩層神經網絡不同的是,除了增加了一層隱藏層,在第一層隱藏層中我們還是用了"Sigmoid"激活函數。

實現三層神經網絡我們只需要在上一小節的代碼基礎上對網絡的參數做一些修改:

# 定義權重矩陣和偏置項
w_1 = tf.Variable(tf.truncated_normal([28*28, 200], stddev=0.1))
b_1 = tf.Variable(tf.zeros([200]))
w_2 = tf.Variable(tf.truncated_normal([200, 10], stddev=0.1))
b_2 = tf.Variable(tf.zeros([10]))

因為網絡中有兩層隱藏層,所以我們要為每一層隱藏層都定義一個權重矩陣和偏置項,我們設置第一層隱藏層的神經元數量為200,第二次隱藏層的神經元數量為10。這裡我們初始化權重矩陣的時候沒有像之前那樣直接賦值為0,而是使用"tf.truncated_normal"函數為其賦初值,當然全都賦值為0也可以,不過需要訓練較多輪,模型的參數才會慢慢接近較優的值。為參數初始化一個非零值,在網絡層數較深,模型較複雜的時候,可以加快參數收斂的速度。

定義好模型參數之後,就可以實現網絡的具體結構了:

# 定義一個兩層神經網絡模型
y_1 = tf.nn.sigmoid(tf.matmul(tf.reshape(x, [-1, 28*28]), w_1) + b_1)
y = tf.nn.softmax(tf.matmul(y_1, w_2) + b_2)

這裡具體的計算和上一節內容一樣,不過因為有兩層隱藏層,因此我們需要將第一層隱藏層的輸出再作為第二層隱藏層的輸入,並且第一層隱藏層使用了"Sigmoid"激活函數。第二層隱藏層的輸出經過"Softmax"層計算後,直接輸出預測的結果。最終在測試集上的準確率為0.9664。

到這裡我們已經介紹完基本的前饋神經網絡的內容了,這一章的內容是深度神經網絡的基礎,理解本章的內容對於後續內容的學習會很有幫助。從下一章開始,我們要正式開始深度神經網絡的學習了。


對深度學習感興趣,熱愛Tensorflow的小夥伴,歡迎關注我們的網站http://www.panchuang.net 我們的公眾號:磐創AI。


分享到:


相關文章: