1. 程式人生 > >TensorFlow系列專題(六):實戰專案Mnist手寫資料集識別

TensorFlow系列專題(六):實戰專案Mnist手寫資料集識別

歡迎大家關注我們的網站和系列教程:http://panchuang.net/ ,學習更多的機器學習、深度學習的知識!

目錄:

  • 導讀
  • MNIST資料集
  • 資料處理
  • 單層隱藏層神經網路的實現
  • 多層隱藏層神經網路的實現
  • 導讀
就像我們在學習一門程式語言時總喜歡把“Hello World!”作為入門的示例程式碼一樣,MNIST手寫數字識別問題就像是深度學習的“Hello World!”。通過這個例子,我們將瞭解如何將資料轉化為神經網路所需要的資料格式,以及如何使用TensorFlow搭建簡單的單層和多層的神經網路。
  • 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資料集的處理進行了封裝,但是作為第一個程式,我們希望帶著讀者從資料處理開始做,資料處理在整個機器學習專案中是很關鍵的一個環節,因此有必要在第一個專案中就讓讀者體會到它的重要性。

我們將下載的壓縮檔案解壓後會發現數據都是以二進位制檔案的形式儲存的,以訓練集的影象資料為例:

表1 訓練集影象資料的檔案格式

[offset] [type] [value] [description]
0000 32 bit integer 0x00000803(2051) magic number
0004 32 bit integer 60000 number of images
0008 32 bit integer 28 number of rows
0012 32 bit integer 28 number of columns
0016 unsigned byte ?? pixel
017 unsigned byte ?? pixel
……
xxxx unsigned byte ?? pixel
如表3-1所示,解壓後的訓練集影象資料“train-images-idx3-ubyte”檔案,其前16個位元組的內容是檔案的基本資訊,分別是magic number(又稱為幻數,用來標記檔案的格式)、影象樣本的數量(60000)、每張影象的行數以及每張影象的列數。由於每張影象的大小是28*28,所以我們從編號0016的位元組開始,每次讀取28*28=784個位元組,即讀取了一張完整的影象。我們讀取的每一個位元組代表一個畫素,取值範圍是[0,255],畫素值越接近0,顏色越接近白色,畫素值越接近255,顏色越接近黑色。

訓練集類標檔案的格式如下:

表2 訓練集類標資料的檔案格式

[offset] [type] [value] [description]
0000 32 bit integer 0x00000801(2049) magic number(MSB first)
0004 32 bit integer 60000 number of items
0008 unsigned byte ?? label
0009 unsigned byte ?? label
……
xxxx unsigned byte ?? label
如上表所示,訓練集類標資料檔案的前8個位元組記錄了檔案的基本資訊,包括magic number和類標項的數量(60000)。從編號0008的位元組開始,每一個位元組就是一個類標,類標的取值範圍是[0,9],類標直接標明瞭對應的影象樣本的真實數值。如圖1所示,我們將部分資料進行了視覺化。測試集的影象資料和類標資料的檔案格式與訓練集一樣。

圖1 訓練集影象資料視覺化效果

  • 資料處理
在開始實現神經網路之前,我們要先準備好資料。雖然MNIST資料集本身就已經處理過了,但是我們還是需要做一些封裝以及簡單的特徵工程。我們定義一個MnistData類用來管理資料:
1 import numpy as np
2 import struct
3 import random
4
5 class MnistData:
6 def __init__(self, train_image_path, train_label_path,
7 test_image_path, test_label_path):
8 # 訓練集和測試集的檔案路徑
9 self.train_image_path = train_image_path
10 self.train_label_path = train_label_path
11 self.test_image_path = test_image_path
12 self.test_label_path = test_label_path
13
14 # 獲取訓練集和測試集資料
15 # get_data()方法,引數為0獲取訓練集資料,引數為1獲取測試集
16 self.train_images, self.train_labels = self.get_data(0)
17 self.test_images, self.test_labels = self.get_data(1)
18
19 # 定義兩個輔助變數,用來判斷一個回合的訓練是否完成
20 self.num_of_batch = 0
21 self.got_batch = 0
在“__init__”方法中初始化了“MnistData”類相關的一些引數,其中“train_image_path”和“train_label_path”分別是訓練集資料和類標的檔案路徑,“test_image_path”和“test_label_path”分別是測試集資料和類標的檔案路徑。

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

22 def get_data(self, data_type):
23 if data_type == 0: # 獲取訓練集資料
24 image_path = self.train_image_path
25 label_path = self.train_label_path
26 else: # 獲取測試集資料
27 image_path = self.test_image_path
28 label_path = self.test_label_path
29
30 with open(image_path, 'rb') as file1:
31 image_file = file1.read()
32 with open(label_path, 'rb') as file2:
33 label_file = file2.read()
34
35 label_index = 0
36 image_index = 0
37 labels = []
38 images = []
39
40 # 讀取訓練集影象資料檔案的檔案資訊
41 magic, num_of_datasets, rows, columns =\
42 struct.unpack_from('>IIII', image_file, image_index)
43 image_index += struct.calcsize('>IIII')
44
45 for i in range(num_of_datasets):
46 # 讀取784個unsigned byte,即一副影象的所有畫素值
47 temp = struct.unpack_from('>784B', image_file, image_index)
48 # 將讀取的畫素資料轉換成28*28的矩陣
49 temp = np.reshape(temp, (28, 28))
50 # 歸一化處理
51 temp = temp / 255
52 images.append(temp)
53 image_index += struct.calcsize('>784B') # 每次增加784B
54
55 # 跳過描述資訊
56 label_index += struct.calcsize('>II')
57 labels = struct.unpack_from('>' + str(num_of_datasets)
58 + 'B', label_file, label_index)
59
60 # one-hot
61 labels = np.eye(10)[np.array(labels)]
62
63 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手寫數字的識別問題。

四、單層隱藏層神經網路的實現

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

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

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

接下來我們實現具體的程式碼,首先匯入上一小節中我們實現的資料處理的類以及TensorFlow的包:
1 from mnist_data import MnistData
2 import tensorflow as tf
建立一個Session會話,並定義好相關的變數:
3 # 建立Session會話
4 sess = tf.InteractiveSession()
5
6 # 訓練集、測試集的檔案路徑
7 train_image_path = './data/train-images-idx3-ubyte'
8 train_label_path = './data/train-labels-idx1-ubyte'
9 test_image_path = './data/t10k-images-idx3-ubyte'
10 test_label_path = './data/t10k-labels-idx1-ubyte'
11
12 epochs = 10 # 訓練的總輪數
13 batch_size = 100 # 每個batch的大小
14 learning_rate = 0.2 # 學習率
“epochs”是我們想要訓練的總輪數,每一輪都會使用訓練集的所有資料去訓練一遍模型。由於我們使用隨機梯度下降方法更新引數,所以不會一次把所有的資料送進模型去訓練,而是按批次訓練,“batch_size”是我們定義的一個批次的資料量的大小,這裡我們設定了100,那麼每個“batch”就會送100個樣本到模型中去訓練,一輪訓練的“batch”數等於總的訓練集數量除以“batch_size”。“learning_rate”是我們定義的學習率,即模型引數更新的速率。

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

15 # 建立樣本資料的placeholder
16 x = tf.placeholder(tf.float32, [None, 28, 28])
17 # 定義權重矩陣和偏置項
18 W = tf.Variable(tf.zeros([28*28, 10]))
19 b = tf.Variable(tf.zeros([10]))
20
21 # 樣本的真實標籤
22 y_ = tf.placeholder(tf.float32, [None, 10])
23 # 使用softmax函式將單層網路的輸出轉換為預測結果
24 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”。

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

31 # 比較預測結果和真實類標
32 correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
33 # 計算準確率
34 accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
“tf.equal()”方法用於比較兩個矩陣或向量相應位置的元素是否相等,相等為“True”,不等為“False”。“tf.cast”用於將“True”和“False”轉換為“1”和“0”,“tf.reduce_mean”對轉換後的資料求平均值,該值即為模型在測試集上預測結果的準確率。最後,我們實現模型的訓練和預測:
35 # 初始化MnistData類
36 data = MnistData(train_image_path, train_label_path,
37 test_image_path, test_label_path)
38 # 初始化模型引數
39 init = tf.global_variables_initializer().run()
40
41 # 開始訓練
42 for i in range(epochs):
43 for j in range(600):
44 # 獲取一個batch的資料
45 batch_x, batch_y = data.get_batch(batch_size)
46 # 優化引數
47 train_step.run({x: batch_x, y_: batch_y})
48
49 # 對測試集進行預測並計算準確率
50 print(accuracy.eval({x: data.test_images, y_: data.test_labels}))
因為我們的“batch_size”設定為100,Mnist資料集的訓練資料有60000條,因此我們訓練600個“batch”正好是一輪。第50行程式碼中,我們訓練完的模型對測試集資料進行了預測,並輸出了預測的準確率,結果為0.9228。
  • 多層神經網路的實現
多層神經網路的實現也很簡單,我們只需要在上一小節的程式碼基礎上對網路的結構稍作修改即可,我們先來看一下這一小節裡要實現的多層(兩層隱藏層)神經網路的結構:

圖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。

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

歡迎大家關注我們的網站和系列教程:http://panchuang.net/ ,學習更多的機器學習、深度學習的知識!