1. 程式人生 > >TensorFlow筆記(3)——利用TensorFlow和MNIST資料集訓練一個最簡單的手寫數字識別模型

TensorFlow筆記(3)——利用TensorFlow和MNIST資料集訓練一個最簡單的手寫數字識別模型

前言

當我們開始學習程式設計的時候,第一件事往往是學習列印"Hello World"。就好比程式設計入門有Hello World,機器學習入門有MNIST。

MNIST是一個入門級的計算機視覺資料集,它包含各種手寫數字圖片:

手寫陣列

它也包含每一張圖片對應的標籤,告訴我們這個是數字幾。比如,上面這四張圖片的標籤分別是5,0,4,1

其實訓練一個簡單的手寫數字識別模型的程式碼很短,我的示例程式碼總共也就50行,除去註釋、空格之類的估計連30行也沒有,但是去理解包含在程式碼中的設計思想是很重要的,因此這篇筆記我會將我對每段程式碼的理解都記錄下來。

參考:

MNIST機器學習入門

機器學習-損失函式

MNIST資料集

MNIST資料集的官網是Yann LeCun's website。 雖然python提供了直接下載這個資料集的程式碼,但是考慮到國內網路的原因,建議點這下載資料集,然後匯入到專案根目錄下就可以了。

資料集
下載下來的資料集被分成兩部分:60000行的訓練資料集(mnist.train)和10000行的測試資料集(mnist.test)。這樣的劃分很重要,在機器學習模型設計時必須有一個單獨的測試資料集不用於訓練而是用來評估這個模型的效能,從而更加容易把設計的模型推廣到其他資料集上(泛化)。

正如前面提到的一樣,每一個MNIST資料單元有兩部分組成:一張包含手寫數字的圖片和一個對應的標籤。我們把這些圖片設為xs

,把這些標籤設為ys。訓練資料集和測試資料集都包含xsys,比如訓練資料集的圖片是 mnist.train.images,訓練資料集的標籤是 mnist.train.labels

每一張圖片包含28畫素X28畫素。我們可以用一個數字陣列來表示這張圖片:

示例

我們把這個陣列展開成一個向量,長度是 28x28 = 784。如何展開這個陣列(數字間的順序)不重要,只要保持各個圖片採用相同的方式展開就可以了。

因此,在MNIST訓練資料集中,mnist.train.images 是一個形狀為 [60000, 784] 的張量,第一個維度數字用來索引圖片,第二個維度數字用來索引每張圖片中的畫素點。在此張量裡的每一個元素,都表示某張圖片裡的某個畫素的強度值,值介於0和1之間。

mnist.train.images

相對應的MNIST資料集的標籤是介於0到9的數字,用來描述給定圖片裡表示的數字。為了用於這個教程,我們使標籤資料是"one-hot vectors"。 一個one-hot向量除了某一位的數字是1以外其餘各維度數字都是0。所以在此教程中,數字n將表示成一個只有在第n維度(從0開始)數字為1的10維向量。比如,標籤0將表示成([1,0,0,0,0,0,0,0,0,0,0])。因此, mnist.train.labels 是一個 [60000, 10] 的數字矩陣。

mnist.train.labels

現在,我們準備好可以開始構建我們的模型啦!

Softmax迴歸介紹

(因為這段很枯燥,而且我也解釋不太好,所以乾脆直接從Tensorflow的網站上覆制貼上來了,如果不想看的可以直接跳過到模型實現,最後寫程式碼的時候只要知道用softmax函式就可以了)

我們知道MNIST的每一張圖片都表示一個數字,從0到9。我們希望得到給定圖片代表每個數字的概率。比如說,我們的模型可能推測一張包含9的圖片代表數字9的概率是80%但是判斷它是8的概率是5%(因為8和9都有上半部分的小圓),然後給予它代表其他數字的概率更小的值。

這是一個使用softmax迴歸(softmax regression)模型的經典案例。softmax模型可以用來給不同的物件分配概率。即使在之後,我們訓練更加精細的模型時,最後一步也需要用softmax來分配概率。

softmax迴歸(softmax regression)分兩步:第一步

為了得到一張給定圖片屬於某個特定數字類的證據(evidence),我們對圖片畫素值進行加權求和。如果這個畫素具有很強的證據說明這張圖片不屬於該類,那麼相應的權值為負數,相反如果這個畫素擁有有利的證據支援這張圖片屬於這個類,那麼權值是正數。

下面的圖片顯示了一個模型學習到的圖片上每個畫素對於特定數字類的權值。紅色代表負數權值,藍色代表正數權值。

我們也需要加入一個額外的偏置量(bias),因為輸入往往會帶有一些無關的干擾量。因此對於給定的輸入圖片 x 它代表的是數字 i 的證據可以表示為

其中 W_i 代表權重,b_i代表數字 i 類的偏置量,j 代表給定圖片 x 的畫素索引用於畫素求和。然後用softmax函式可以把這些證據轉換成概率 y

這裡的softmax可以看成是一個激勵(activation)函式或者連結(link)函式,把我們定義的線性函式的輸出轉換成我們想要的格式,也就是關於10個數字類的概率分佈。因此,給定一張圖片,它對於每一個數字的吻合度可以被softmax函式轉換成為一個概率值。softmax函式可以定義為:

展開等式右邊的子式,可以得到:

但是更多的時候把softmax模型函式定義為前一種形式:把輸入值當成冪指數求值,再正則化這些結果值。這個冪運算表示,更大的證據對應更大的假設模型(hypothesis)裡面的乘數權重值。反之,擁有更少的證據意味著在假設模型裡面擁有更小的乘數係數。假設模型裡的權值不可以是0值或者負值。Softmax然後會正則化這些權重值,使它們的總和等於1,以此構造一個有效的概率分佈。(更多的關於Softmax函式的資訊,可以參考Michael Nieslen的書裡面的這個部分,其中有關於softmax的可互動式的視覺化解釋。)

對於softmax迴歸模型可以用下面的圖解釋,對於輸入的xs加權求和,再分別加上一個偏置量,最後再輸入到softmax函式中:

如果把它寫成一個等式,我們可以得到:

我們也可以用向量表示這個計算過程:用矩陣乘法和向量相加。這有助於提高計算效率。(也是一種更有效的思考方式)

更進一步,可以寫成更加緊湊的方式:

實現模型

在使用TensorFlow之前,首先匯入它:

import tensorflow as tf
複製程式碼

然後匯入資料集並載入資料

from tensorflow.examples.tutorials.mnist import input_data

# 載入資料
mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
複製程式碼

我們通過操作符號變數來描述這些可互動的操作單元,可以用下面的方式建立一個:

x = tf.placeholder(tf.float32, [None, 784])
複製程式碼

x不是一個特定的值,而是一個佔位符placeholder,我們在TensorFlow執行計算時輸入這個值。我們希望能夠輸入任意數量的MNIST影象,每一張圖展平成784維的向量。我們用2維的浮點數張量來表示這些圖,這個張量的形狀是[None,784 ]。(這裡的None表示此張量的第一個維度可以是任何長度的。)

我們的模型也需要權重和偏量,當然我們可以把它們當做是另外的輸入(使用佔位符),但TensorFlow有一個更好的方法來表示它們:Variable 。 一個Variable代表一個可修改的張量,存在在TensorFlow的用於描述互動性操作的圖中。它們可以用於計算輸入值,也可以在計算中被修改。對於各種機器學習應用,一般都會有模型引數,可以用Variable表示。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
複製程式碼

我們賦予tf.Variable不同的初值來建立不同的Variable:在這裡,我們都用全為零的張量來初始化Wb。因為我們要學習Wb的值,它們的初值可以隨意設定。

注意,W的維度是[784,10],因為我們想要用784維的圖片向量乘以它以得到一個10維的證據值向量,每一位對應不同數字類。b的形狀是[10],所以我們可以直接把它加到輸出上面。

現在,我們可以實現我們的模型啦。只需要一行程式碼!

prediction = tf.nn.softmax(tf.matmul(x, W)+b)
複製程式碼

首先,我們用tf.matmul(X,W)表示x乘以W,對應之前等式裡面的Wx,這裡x是一個2維張量擁有多個輸入。然後再加上b,把和輸入到tf.nn.softmax函式裡面。

訓練模型

為了訓練我們的模型,我們首先需要定義一個指標來評估這個模型是好的。其實,在機器學習,我們通常定義指標來表示一個模型是壞的,這個指標稱為成本(cost)或損失(loss),然後儘量最小化這個指標。但是,這兩種方式是相同的。損失函式有很多種,在這裡我們採用平方損失函式,通常我們會用均方差(MSE)作為衡量指標,公式如下:MSE=\frac{1}{n}\sum_{i=1}^{n}\ (y-prediction)^2

為了計算損失函式,我們首先需要新增一個新的佔位符用於輸入正確值:

y = tf.placeholder(tf.float32, [None, 10])
複製程式碼

然後定義損失函式(loss):

# 二次代價函式
loss = tf.reduce_mean(tf.square(y-prediction))
複製程式碼

這段程式碼的含義我在上一篇筆記中已經介紹過了,不清楚的推薦閱讀TensorFlow筆記(2)——利用TensorFlow訓練一個最簡單的一元線性模型

然後使用優化演算法來不斷的修改變數來降低損失值:

# 使用梯度下降法
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)
複製程式碼

在這裡,我們要求TensorFlow用梯度下降演算法(gradient descent algorithm)以0.01的學習速率最小化交叉熵。梯度下降演算法(gradient descent algorithm)是一個簡單的學習過程,TensorFlow只需將每個變數一點點地往使成本不斷降低的方向移動。當然TensorFlow也提供了其他許多優化演算法:只要簡單地調整一行程式碼就可以使用其他的演算法。

TensorFlow在這裡實際上所做的是,它會在後臺給描述你的計算的那張圖裡面增加一系列新的計算操作單元用於實現反向傳播演算法和梯度下降演算法。然後,它返回給你的只是一個單一的操作,當執行這個操作時,它用梯度下降演算法訓練你的模型,微調你的變數,不斷減少成本。

現在,我們已經設定好了我們的模型。在執行計算之前,我們需要新增一個操作來初始化我們建立的變數:

# 初始化變數
init = tf.global_variables_initializer()
複製程式碼

接下來我們就可以定義一個會話了,並在該會話中執行初始化變數的操作:

with tf.Session() as sess:
    sess.run(init)
複製程式碼

然後開始訓練模型,我們需要先定義一個批次batch_size,因為我們在訓練的時候不可能每次都只放一張圖片進入神經網路(因為這樣太慢了),批次為100在這表示的就是我們一次放入100張圖片進入神經網路,然後我們需要計算一共會有多少個批次:

# 每個批次的大小
batch_size = 100
# 計算一共有多少個批次
n_batch = mnist.train.num_examples // batch_size
複製程式碼

然後我們讓模型迴圈訓練30次:

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(30):
        for batch in range(n_batch):
            batch_xs, batch_ys = mnist.train.next_batch(batch_size)
            sess.run(train_step, feed_dict={x: batch_xs, y: batch_ys})
複製程式碼

該迴圈的每個步驟中,我們都會隨機抓取訓練資料中的n_batch個批處理資料點,然後我們用這些資料點作為引數替換之前的佔位符來執行train_step

使用一小部分的隨機資料來進行訓練被稱為隨機訓練(stochastic training)- 在這裡更確切的說是隨機梯度下降訓練。在理想情況下,我們希望用我們所有的資料來進行每一步的訓練,因為這能給我們更好的訓練結果,但顯然這需要很大的計算開銷。所以,每一次訓練我們可以使用不同的資料子集,這樣做既可以減少計算開銷,又可以最大化地學習到資料集的總體特性。

評估我們的模型

那麼我們的模型效能如何呢?

首先讓我們找出那些預測正確的標籤。tf.argmax 是一個非常有用的函式,它能給出某個tensor物件在某一維上的其資料最大值所在的索引值。由於標籤向量是由0,1組成,因此最大值1所在的索引位置就是類別標籤,比如tf.argmax(y,1)返回的是模型對於任一輸入x預測到的標籤值,而 tf.argmax(prediction,1) 代表正確的標籤,我們可以用 tf.equal 來檢測我們的預測是否真實標籤匹配(索引位置一樣表示匹配)。

# 結果存放在一個布林型列表中
# argmax返回一維張量中最大的值所在的位置
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))
複製程式碼

這行程式碼會給我們一組布林值。為了確定正確預測項的比例,我們可以把布林值轉換成浮點數,然後取平均值。例如,[True, False, True, True] 會變成 [1,0,1,1] ,取平均值後得到 0.75.

# 求準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
複製程式碼

最後,我們計算所學習到的模型在測試資料集上面的正確率。

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(30):
        for batch in range(n_batch):
            batch_xs, batch_ys = mnist.train.next_batch(batch_size)
            sess.run(train_step, feed_dict={x: batch_xs, y: batch_ys})
        acc = sess.run(accuracy, feed_dict={
                       x: mnist.test.images, y: mnist.test.labels})
        print("Iter "+str(epoch)+",Testing Accuracy "+str(acc))
複製程式碼

最終結果如下圖所示,精確度大約在90%

image-20181204162301761

完整程式碼

我加了datetime這個包,目的是為了計算程式碼的執行時間,不影響閱讀。

import datetime

# 3.2 MNIST資料集分類簡單版本
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

start = datetime.datetime.now()

# 載入資料
mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
# 每個批次的大小
batch_size = 100
# 計算一共有多少個批次
n_batch = mnist.train.num_examples // batch_size

# 定義兩個placeholder
x = tf.placeholder(tf.float32, [None, 784])
y = tf.placeholder(tf.float32, [None, 10])

# 建立一個簡單的神經網路
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
prediction = tf.nn.softmax(tf.matmul(x, W)+b)

# 二次代價函式
loss = tf.reduce_mean(tf.square(y-prediction))
# 使用梯度下降法
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

# 初始化變數
init = tf.global_variables_initializer()

# 結果存放在一個布林型列表中
# argmax返回一維張量中最大的值所在的位置
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))
# 求準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(30):
        for batch in range(n_batch):
            batch_xs, batch_ys = mnist.train.next_batch(batch_size)
            sess.run(train_step, feed_dict={x: batch_xs, y: batch_ys})
        acc = sess.run(accuracy, feed_dict={
                       x: mnist.test.images, y: mnist.test.labels})
        print("Iter "+str(epoch)+",Testing Accuracy "+str(acc))

end = datetime.datetime.now()
print((end-start).seconds)
複製程式碼