這不僅僅是另一個使用TensorFlow來做MNIST數字影象識別的教程

OReillyData發表於2017-01-17

編者注:想學習如何從零開始構建和訓練你的第一個TensorFlow圖,請檢視Aaron Schumacher線上教程裡的《你好,TensorFlow!》!

請耐心看完這篇博文。每一個人學習機器學習幾乎都是從用MNIST資料集來做手寫數字影象識別開始的,但是我希望這個教程和其他的有所不同。

我記得當TensorFlow在2015年11月被髮布的時候,我按照這個《為TensorFlow初學者準備的MNIST教程》裡面的步驟,盲目地複製和貼上其中所有的程式碼到我的命令列終端。然後一些數字按照它們本應該出現的樣子跳了出來。我想“OK,我知道有些神奇的事情發生了。但為什麼我看不到它們?”所以我寫這篇博文的目的就是製作一個既有互動性還能有視覺化的MNIST教程。同時還希望能教給你一兩件其他的教程僅僅假定你知道的事情。

這個教程裡,我會使用TensorFlow的機器學習庫,並基於Ubuntu 14.04版和Python3。如果你想了解如何在你自己的系統裡安裝TensorFlow,可以在這裡檢視我的其他教程。

如果你還沒有安裝numpy和matplotlib庫,你需要安裝他們。請開啟一個Ubuntu命令列終端,並敲入如下的一行命令:

$ sudo apt-get install python-numpy python3-numpy python-matplotlib python3-matplotlib

首先,我們需要在終端裡開啟一個python命令列編輯器,並用如下幾行程式碼匯入MNIST資料集(和其他python的依賴庫):

from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets(‘MNIST_data’, one_hot=True)

import matplotlib.pyplot as plt

import numpy as np

import random as ran

接著讓我們定義幾個函式,用來指定從資料集裡匯入的訓練和測試資料的數量。這些程式碼並不是特別緊要,除非你希望能瞭解它們背後的邏輯。

你需要複製貼上每個函式,並在終端裡敲兩次回車。

def TRAIN_SIZE(num):

print (‘Total Training Images in Dataset = ‘ + str(mnist.train.images.shape))

print (‘————————————————–‘)

x_train = mnist.train.images[:num,:]

print (‘x_train Examples Loaded = ‘ + str(x_train.shape))

y_train = mnist.train.labels[:num,:]

print (‘y_train Examples Loaded = ‘ + str(y_train.shape))

print(”)

return x_train, y_train

def TEST_SIZE(num):

print (‘Total Test Examples in Dataset = ‘ + str(mnist.test.images.shape))

print (‘————————————————–‘)

x_test = mnist.test.images[:num,:]

print (‘x_test Examples Loaded = ‘ + str(x_test.shape))

y_test = mnist.test.labels[:num,:]

print (‘y_test Examples Loaded = ‘ + str(y_test.shape))

return x_test, y_test

我們還要定義兩個簡單的函式來重新排列影象尺寸並顯示這些影象資料:

def display_digit(num):

print(y_train[num])

label = y_train[num].argmax(axis=0)

image = x_train[num].reshape([28,28])

plt.title(‘Example: %d  Label: %d’ % (num, label))

plt.imshow(image, cmap=plt.get_cmap(‘gray_r’))

plt.show()

def display_mult_flat(start, stop):

images = x_train[start].reshape([1,784])

for i in range(start+1,stop):

images = np.concatenate((images, x_train[i].reshape([1,784])))

plt.imshow(images, cmap=plt.get_cmap(‘gray_r’))

plt.show()

現在開始構建和訓練我們的模型的部分。首先我們定義一些變數來指定希望匯入多少訓練和測試樣本。剛開始我會匯入所有的資料,但是後面我會改變這個值來節省一些資源:

x_train, y_train = TRAIN_SIZE(55000)

Total Training Images in Dataset = (55000, 784)

————————————————–

x_train Examples Loaded = (55000, 784)

y_train Examples Loaded = (55000, 10)

那麼這些程式碼到底是什麼意思哪?在我們用的這個資料集裡面,一共有55000個手寫的從0到9的數字樣本。每個樣本都是一個28×28畫素的影象,並被扁平化成一個包含784個值的陣列,其中每個值代表了一個畫素的灰度。之所以要被扁平化,是因為只有這樣TensorFlow才能線性地使用它。上面的程式碼顯示了在變數x_train 裡面,我們匯入了55000個樣本,每個都包含784個畫素。變數x_train 是一個55000行加784列的矩陣。變數y_train 包含了所有x_train 樣本所對應的正確的標識。這裡並沒有用一個整數來存貯標識,而是用一個1×10的二元陣列來儲存。其中某一位數字為1就代表這個數字是它對應那個位置。這種方式也被叫做一熱位編碼(one-hot encoding)。下面的例子給出了代表數字7的陣列。

640?wx_fmt=png

圖1 代表數字7的陣列。來源:Justin Francis

讓我們來隨便找一張圖片,並用我們上面定義的函式來讀取扁平化的資料,重新排列它,再顯示出來,並把它所對應的標識也列印出來(注意:想要繼續編輯python程式碼,你需要關掉matplot開啟的視窗):

display_digit(ran.randint(0, x_train.shape[0]))

640?wx_fmt=png

圖2  [ 0.  0.  0.  0.  0.  0.  0.  1.  0.  0.] 來源:Justin Francis

下面是分類器使用的多個訓練樣本的扁平化的資料形式。當然,我們的分類器看到的是從零到一個代表灰度的數值,而不是看到的畫素點。

display_mult_flat(0,400)

640?wx_fmt=jpeg

圖3 前400個訓練樣本。來源:Justin Francis

到目前為止,我們還根本沒用到TensorFlow。下一步就是匯入TensorFlow,並定義我們的會話(session)。某種意義上說,TensorFlow會建立一個有向無環圖,讓你匯入資料,並在一個會話裡執行這個圖。

import tensorflow as tf

sess = tf.Session()

接著我們定義一個佔位符。顧名思義,佔位符是一個用來匯入資料的變數。唯一需要注意的就是,想要匯入資料到這個變數,資料必須完全匹配佔位符的尺寸(shape)和型別(type)。TensorFlow的官網是這樣解釋的:“一個佔位符存在的意思僅僅是為了作為匯入資料的目標。它並沒有被初始化且不包含資料。”這裡我們定義x佔位符作為一個匯入x_train資料的變數。

x = tf.placeholder(tf.float32, shape=[None, 784])

當我們把None分配給佔位符時,就意味著這個佔位符可以被匯入任意數量的樣本。在我們的例子裡,這個佔位符可以被匯入任意多個有784列的值。

下面我們定義y_,用來匯入y_train的資料。這個變數後面會被用來比較我們的分類預測與標準答案。這裡可以認為我們的標識就是分類的類別。

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

接著我們定義權重 W和偏置量b。這兩個變數是分類器裡的普通工作者:它們是在訓練完的分類器裡,我們用來計算預測所需要的唯一的值。

首先我們把權重和偏置量都設為零,因為隨後TensorFlow會自己優化這些值。注意:我們的權重W是一個784個值對應於10個分類的每一個類的集合。

W = tf.Variable(tf.zeros([784,10]))

b = tf.Variable(tf.zeros([10]))

我喜歡把這些權重看做是對應於10個數字的10張速查表。這和老師們用一個透明的速查表來給多選題目打分很類似。不幸的是對偏置量的解釋有些超出這個教程的範疇,但我還是喜歡把它看成一個和權重相關的特殊關係。它們兩個一起影響我們的最終答案。

現在我們來定義分類函式,即 y。這個特殊的分類器也被叫做多元邏輯迴歸。我們通過把每個扁平化的數字與我們的權重相乘,並加上偏置量來做出預測:

y = tf.nn.softmax(tf.matmul(x,W) + b)

讓我們暫時先忽略softmax,直接看看softmax函式內部是什麼樣。matmul是矩陣相乘的函式。如果你瞭解矩陣相乘,你應該能較好地理解這個計算,並且知道x * W + b會產生一個訓練樣本數量(m)×分類的數量(n)的(m×n)的矩陣。

640?wx_fmt=jpeg

圖4 簡單的矩陣相乘。來源:Quartl on Wikimedia Commons

如果你還對此有疑惑,可以通過執行對y的輸出來確認:

print(y)

Tensor(“Softmax:0”, shape=(?, 10), dtype=float32)

上面的輸出結果告訴我們y在會話裡什麼,但如果我們是想看到y的實際值哪?由於不能通過直接列印一個TensorFlow圖裡面的物件來獲得它的實際值,你必須執行一個給它匯入資料的適當的會話。注意:如果你僅僅只是執行sess.run(y),TensorFlow會提醒你需要給它匯入資料。

x_train, y_train = TRAIN_SIZE(3)

sess.run(tf.global_variables_initializer())

#如果使用TensorFlow 0.12之前的版本,請用下面的命令:

#sess.run(tf.initialize_all_variables())

print(sess.run(y, feed_dict={x: x_train}))

[[ 0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1]

[ 0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1]

[ 0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1]]

現在我們可以看到對前三個訓練樣本的分類預測。到目前為止,我們的分類器什麼也不知道。所以它就給出了一個平均的10%概率,來預測我們的三個樣本在每個類別裡的概率。

但是你可能會問TensorFlow是怎麼知道這個概率的?它是通過計算softmax函式的結果來學習概率的。Softmax函式獲得一系列數值,並設法讓它們的和為1,並以此來獲得每個值的概率。任何softmax值總是會被轉換成大於0小於1。還是沒搞清楚?可以試著執行下面的程式碼(譯者注:在TensorFlow版本0.10執行會報錯,版本0.11上正常),以瞭解softmax是怎麼做數學計算的。

sess.run(tf.nn.softmax(tf.zeros([4])))

sess.run(tf.nn.softmax(tf.constant([0.1, 0.005, 2])))

接著定義我們的cross_entropy函式,也叫做損失或代價函式。它被用來衡量我們分類的結果的好壞。代價越高,則越不準確。它是通過比較預測的每個樣本的y與y_train裡面的真實值來計算準確度的。我們的目標是最小化這個損失。

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))

這個函式對所有的預測值y(值的範圍是從0到1)進行log運算後和真實值y_進行矩陣元素相乘。如果值接近0,log計算後的值就是一個非常大的負值(例如,-np.log(0.01) = 4.6);如果值接近1,log計算後的值就是一個小的負值(例如,-np.log(0.99) = 0.1)。

640?wx_fmt=jpeg

圖5 Y = log (x)函式。來源Lfahlberg on Wikimedia Commons

本質上,如果預測結果是不對的但是置信度很高,我們就用一個很大的值來“懲罰”分類器;而如果結果是對的且置信度也很高,我們只用一個非常小的值。

下面是一個編造的python陣列的例子。顯示了softmax預測的結果是3且置信度很高。

j = [0.03, 0.03, 0.01, 0.9, 0.01, 0.01, 0.0025,0.0025, 0.0025, 0.0025]

讓我用下面這個標識為3的陣列作為基準來比較它和我們的softmax函式。

k = [0,0,0,1,0,0,0,0,0,0]

您能猜出損失函式會返回什麼結果嗎?你能看出下面對“j”進行log計算是如何對一個錯誤答案用一個非常大的負數進行懲罰的嗎?執行下面的例子來試著理解吧。

-np.log(j)

-np.multiply(np.log(j),k)

上面會返回9個0和一個0.1053。加和後,我們就會認為這是一個不錯的預測。下面看看如果我們的預測結果不變,但實際的正確答案是2的時候會怎麼樣。

k = [0,0,1,0,0,0,0,0,0,0]

np.sum(-np.multiply(np.log(j),k))

這次cross_entropy函式返回的是4.6051。意味著對我們錯誤的預測做了一個很嚴重的懲罰。之所以有這樣的嚴重的懲罰,是因為分類器很有信心地認為是3,但真實值應該是2。

下面我們開始訓練分類器。訓練的目的就是找到合適的W和b值來得到儘可能小的損失值。

下面部分裡,你可以自己修改一些變數的值。下面所有的全大寫字母變數的值都是可以被修改和嘗試的。實際上我很鼓勵你這麼做!先用例子裡給出的這些值,然後你可以修改來得到更多或者更少的訓練樣本數量,或者是不同的學習率的值,再看看結果有什麼不同。

如果你設定TRAIN_SIZE為一個很大的數,那麼要有等一會的心理準備。任何時間,你都可以從這裡開始再執行這些程式碼,並嘗試不同的值。

x_train, y_train = TRAIN_SIZE(5500)

x_test, y_test = TEST_SIZE(10000)

LEARNING_RATE = 0.1

TRAIN_STEPS = 2500

現在我們可以初始化所有的變數了。初始化之後,TensorFlow的圖才能夠使用它們。

init = tf.global_variables_initializer()

#If using TensorFlow prior to 0.12 use:

#init = tf.initialize_all_variables()

sess.run(init)

我們需要使用梯度下降法來訓練分類器。首先定義我們的訓練方法以及一些測量準確度的變數。變數training將會基於選定的LEARNING_RATE(學習速率)執行梯度下降優化器,以期獲得最小化的損失函式cross_entropy。

training = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(cross_entropy)

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

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

我們定義一個迴圈,重複執行TRAIN_STEPS次。每迴圈一次,就執行一次training,使用feed_dict從x_train和y_train裡輸入訓練樣本。為了計算準確率,程式碼會執行來accuracy對x_test裡沒見過的資料進行分類並比較獲得的y值和y_test的值。我們的測試資料必須是分類器沒見過的,沒有用於訓練,這一點非常重要。如果一個老師給學生們一個小測驗,而最後還用同樣的試題來做期末考試,那麼最終會得到一個對學生們掌握知識的程度的不準確的測量。

for i in range(TRAIN_STEPS+1):

sess.run(training, feed_dict={x: x_train, y_: y_train})

if i%100 == 0:

print(‘Training Step:’ + str(i) + ‘  Accuracy =  ‘ + str(sess.run(accuracy, feed_dict={x: x_test, y_: y_test})) + ‘  Loss = ‘ + str(sess.run(cross_entropy, {x: x_train, y_: y_train})))

為了對梯度下降的原理做視覺化,你可以把損失想象為一個基於y_和y的784維的影象,其中包含不同的x、W和b值。如果你無法視覺化784維,這很正常。我強烈建議你看看Chris Olah的這個博文來了解MNIST所涉及的維度問題。為了能更好更簡單地解釋這個問題,我們使用兩維空間的函式y = x^2。

640?wx_fmt=jpeg

圖6 拋物線y = x^2。來源: Adrignola on Wikimedia Commons

在迴圈的每一步中,依賴於cross_entropy的計算值的大小,分類器會根據LEARNING_RATE移動一步,向它認為會降低cross_entropy的方向前進一點。而這個更低的點則是由TensorFlow通過對cross_entropy求導數計算得出的。這個導數是在當前點的切線的斜率。在向這個新的點移動的時候,相應地W和b都會被改變,而斜率也在降低。在我們舉的這個y = x^2例子裡,這就意味著向X=0方向移動,也即最小化。如果這個學習速率非常小,那麼分類器就會在訓練過程中採用很小的步長;如果這個值太大,分類器的步長就會很大,那麼就很可能“跨越”過真正的最小點。

640?wx_fmt=gif

圖7 實黑線顯示的是在某個點的切線。來源:Tosha on Wikimedia Commons

有沒有注意到上面例子裡訓練迴圈結束前的階段,雖然損失還在降低但是準確率則是在略微地降低?這意味著我們還是可以持續地降低損失值,但是這對於預測未見過的測試資料並提高準確度並沒有什麼幫助。這也被稱為過擬合(不能泛化)。使用程式碼給出的預設設定值,我得到了大概91%的準確率。如果我想通過作弊得到94%的準確率,我可以把測試樣本數量取為100。這顯示瞭如果沒有充足的測試樣本的話,你可能會得到一個有偏頗的準確率。

需要記住的是上面是計算我們的分類器的一個非常低效的方法。但我是故意這樣做的,主要是為了學習和做實驗。理想情況是當用一個非常大的資料集做訓練時,可以採取小批次多次訓練,而不是一次都訓練完。如果你想學習這麼做的話,可以看看TensorFlow網站上的這個教程。

下面到了我最喜歡的部分。我們已經計算出了權重的速查表,那麼就用下面的程式碼把他們畫出來吧。

for i in range(10):

plt.subplot(2, 5, i+1)

weight = sess.run(W)[:,i]

plt.title(i)

plt.imshow(weight.reshape([28,28]), cmap=plt.get_cmap(‘seismic’))

frame1 = plt.gca()

frame1.axes.get_xaxis().set_visible(False)

frame1.axes.get_yaxis().set_visible(False)

再視覺化一下。

plt.show()

640?wx_fmt=png

圖8 來源:Justin Francis

上圖對從0到9的權重值進行了視覺化。這也是我們的分類器最重要的部分。這個機器學習的整個工作就是找出什麼是最優的權重。一旦這些權重被計算出來,就可以用這個權重速查表來很容易地找到答案了。這也就是為什麼神經網路可以很容易地被移植移動裝置上。因為模型一旦訓練好,就不再需要很多儲存和計算資源來做計算了。我們的分類器就是通過比較測試樣例和圖裡面的紅色與藍色區域去做預測的。認為越紅的區域命中的概率越高;白色的區域是中性的,而藍色的區域則是錯誤的。

所以現在讓我們使用這個速查表來對一個樣本進行分類。

x_train, y_train = TRAIN_SIZE(1)

display_digit(0)

看看得出的預測y。

answer = sess.run(y, feed_dict={x: x_train})

print(answer)

這是一個(1×10)的矩陣。每一列包含一個概率值。

[[  2.12480136e-05   1.16469264e-05   8.96317810e-02   1.92015115e-02

8.20863759e-04   1.25168199e-05   3.85381973e-05   8.53746116e-01

6.91888575e-03   2.95970142e-02]]

但這樣的顯示對我們不是很有用。因此我們用argmax函式來獲得最高預測概率值的那個位置。

answer.argmax()

至此,讓我們使用這個速查表來建立一個函式,對這個資料集裡的一個隨機選取的圖片做識別。

def display_compare(num):

# THIS WILL LOAD ONE TRAINING EXAMPLE

x_train = mnist.train.images[num,:].reshape(1,784)

y_train = mnist.train.labels[num,:]

# THIS GETS OUR LABEL AS A INTEGER

label = y_train.argmax()

# THIS GETS OUR PREDICTION AS A INTEGER

prediction = sess.run(y, feed_dict={x: x_train}).argmax()

plt.title(‘Prediction: %d Label: %d’ % (prediction, label))

plt.imshow(x_train.reshape([28,28]), cmap=plt.get_cmap(‘gray_r’))

plt.show()

然後執行這個函式。

display_compare(ran.randint(0, 55000))

你能找到一個預測錯誤的例子嗎?只要執行display_compare(2),你就可以找到分類器做的一個錯誤的數字識別(把9認成4)。你覺得為什麼對這個樣本的分類會出錯?

下面的部分是這個教程有趣的地方。看看下面的動畫。注意當使用1到10個訓練樣本時對權重做的視覺化了嗎?很明顯,訓練資料很少的時候,模型很難有泛化的能力。下面的動畫顯示了權重是如何隨著訓練樣本數量增加而變化的。你能發現是怎麼變化的嗎?

圖9 來源:Justin Francis

(由於此動圖檔案量過大,在微信中無法正常顯示,還請大家點選閱讀原文在官網上檢視原文)

你也可以看出一個線性分類器的侷限性:在某個數量點後,用更多的資料訓練並不能進一步提升準確率。如果我們試圖去識別一個寫在這個方形靠左邊的數字“1”,你覺得會發生什麼?分類器會很難做出識別,因為我們所有的訓練樣本的數字1都是在中間附近的。

希望這篇博文能幫助你更好地理解這個MNIST程式碼背後的故事。要注意的是,這個神經網路僅僅只有兩層。這不是深度學習!想獲得幾近完美的準確率,我們必須開始卷積地深度思考。

如果你想更互動地執行這個會話,這裡是我的GitHub庫,其中包括一個Jupyter Notebook的版本。在寫這個教程的過程中我很開心,也學到了很多。非常感謝你的閱讀。真心希望你在看完這篇博文後,一些新的思路能在你的腦海中形成。

640?wx_fmt=jpeg

Justin Francis

Justin居住在加拿大西海岸的一個小農場。這個農場專注於樸門道德和設計的農藝。在此之前,他是一個非營利性社群合作社自行車商店的創始人和教育者。在過去的兩年中,他住在一艘帆船上,全職探索和體驗加拿大的喬治亞海峽。但現在他的主要精力都放在了學習機器學習上。

640?wx_fmt=png

相關文章