1. 程式人生 > >深入淺出TensorFlow(二):TensorFlow解決MNIST問題入門

深入淺出TensorFlow(二):TensorFlow解決MNIST問題入門

http://www.infoq.com/cn/articles/introduction-of-tensorflow-part02

2017年2月16日,Google正式對外發布Google TensorFlow 1.0版本,並保證本次的釋出版本API介面完全滿足生產環境穩定性要求。這是TensorFlow的一個重要里程碑,標誌著它可以正式在生產環境放心使用。在國內,從InfoQ的判斷來看,TensorFlow仍處於創新傳播曲線的創新者使用階段,大部分人對於TensorFlow還缺乏瞭解,社群也缺少幫助落地和使用的中文資料。InfoQ期望通過深入淺出TensorFlow系列文章能夠推動Tensorflow在國內的發展。歡迎加入QQ群(群號:183248479)深入討論和交流。

本文是整個系列的第二篇文章,將會簡單介紹TensorFlow安裝方法、TensorFlow基本概念、神經網路基本模型,並在MNIST資料集上使用TensorFlow實現一個簡單的神經網路。

TensorFlow安裝

Docker是新一代的虛擬化技術,它可以將TensorFlow以及TensorFlow的所有依賴關係統一封裝到Docker映象當中,從而大大簡化了安裝過程。Docker是可移植性最強的一種安裝方式,它支援大部分的作業系統(比如Windows,Linux和Mac OS)。對於TensorFlow釋出的每一個版本,谷歌都提供了官方映象。在官方映象的基礎上,才雲科技提供的映象進一步整合了其他機器學習工具包以及TensorFlow視覺化工具TensorBoard,使用起來可以更加方便。目前才雲科技提供的映象有:

cargo.caicloud.io/tensorflow/tensorflow:0.12.0 cargo.caicloud.io/tensorflow/tensorflow:0.12.0-gpu cargo.caicloud.io/tensorflow/tensorflow:0.12.1 cargo.caicloud.io/tensorflow/tensorflow:0.12.1-gpu cargo.caicloud.io/tensorflow/tensorflow:1.0.0
cargo.caicloud.io/tensorflow/tensorflow:1.0.0-gpu

當Docker安裝完成之後(Docker安裝可以參考 https://docs.docker.com/engine/installation/),可以通過以下命令來啟動一個TensorFlow容器。在第一次執行的時候,Docker會自動下載映象。

$ docker run -p 8888:8888 –p 6006:6006 \
    cargo.caicloud.io/tensorflow/tensorflow:1.0.0

在這個命令中,-p 8888:8888 將容器內執行的Jupyter服務對映到本地機器,這樣在瀏覽器中開啟localhost:8888就能看到Jupyter介面。在此映象中執行的Jupyter是一個網頁版的程式碼編輯器,它支援建立、上傳、修改和執行Python程式。

-p 6006:6006將容器內執行的TensorFlow視覺化工具TensorBoard對映到本地機器,通過在瀏覽器中開啟localhost:6006就可以將TensorFlow在訓練時的狀態、圖片資料以及神經網路結構等資訊全部展示出來。此映象會將所有輸出到/log目錄底下的日誌全部視覺化。

-it將提供一個Ubuntu 14.04的bash環境,在此環境中已經將TensorFlow和一些常用的機器學習相關的工具包(比如Scikit)安裝完畢。注意這裡無論本地機器作業系統是什麼,這個bash環境都是基於Ubuntu 14.04的。這是由編譯Docker映象的方式決定的,和本地的作業系統沒有關係。

雖然有支援GPU的Docker映象,但是要執行這些映象需要安裝最新的NVidia驅動以及nvidia-docker。在安裝完成nvidia-docker之後,可以通過以下的命令執行支援GPU的TensorFlow映象。在映象啟動之後可以通過和上面類似的方式使用TensorFlow。

$ nvidia-docker run -it -p 8888:8888 –p 6006:6006 \
cargo.caicloud.io/tensorflow/tensorflow:1.0.0-gpu

除了Docker安裝,在本地使用最方便的TensorFlow安裝方式是pip。通過以下命令可以在Linux環境下使用pip安裝TensorFlow 1.0.0。

$ sudo apt-get install python-pip python-dev  # 安裝pip和Python 2.7
$ sudo pip install tensorflow                    # 安裝只支援CPU的TensorFlow
$ sudo pip install tensorflow-gpu               # 安裝支援GPU的TensorFlow

目前只有在安裝了CUDA toolkit 8.0和CuDNN v5.1的64位Ubuntu下可以通過pip安裝支援GPU的TensorFlow,對於其他系統或者其他CUDA/CuDNN版本的使用者則需要從原始碼進行安裝來支援GPU使用。從原始碼安裝TensorFlow可以參考https://www.tensorflow.org/install/。

TensorFlow樣例

TensorFlow對Python語言的支援是最全面的,所以本文中將使用Python來編寫TensorFlow程式。下面的程式給出一個簡單的TensorFlow樣例程式來實現兩個向量求和。

import tensorflow as tf
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b
print result        # 輸出“Tensor("add:0", shape=(2,), dtype=float32) ”

sess = tf.Session()
print sess.run(result)    # 輸出“[ 3.  5.]”
sess.close()

TensorFlow基本概念

TensorFlow的名字中已經說明了它最重要的兩個概念——Tensor和Flow。Tensor就是張量。在TensorFlow中,所有的資料都通過張量的形式來表示。從功能的角度上看,張量可以被簡單理解為多維陣列。但張量在TensorFlow中的實現並不是直接採用陣列的形式,它只是對TensorFlow中運算結果的引用。在張量中並沒有真正儲存數字,它儲存的是如何得到這些數字的計算過程。在上面給出的測試樣例程式中,第一個print輸出的只是一個引用而不是計算結果。

一個張量中主要儲存了三個屬性:名字(name)、維度(shape)和型別(type)。張量的第一個屬性名字不僅是一個張量的唯一識別符號,它同樣也給出了這個張量是如何計算出來的。張量的命名是通過“node:src_output”的形式來給出。其中node為計算節點的名稱,src_output表示當前張量來自節點的第幾個輸出。

比如張量“add:0”就說明了result這個張量是計算節點“add”輸出的第一個結果(編號從0開始)。張量的第二個屬性是張量的維度(shape)。這個屬性描述了一個張量的維度資訊。比如“shape=(2,) ”說明了張量result是一個一維陣列,這個陣列的長度為2。張量的第三個屬性是型別(type),每一個張量會有一個唯一的型別。TensorFlow會對參與運算的所有張量進行型別的檢查,當發現型別不匹配時會報錯。

如果說TensorFlow的第一個詞Tensor表明了它的資料結構,那麼Flow則體現了它的計算模型。Flow翻譯成中文就是“流”,它直觀地表達了張量之間通過計算相互轉化的過程。

TensorFlow是一個通過計算圖的形式來表述計算的程式設計系統。TensorFlow中的每一個計算都是計算圖上的一個節點,而節點之間的邊描述了計算之間的依賴關係。圖1展示了通過TensorBoard畫出來的測試樣例的計算圖。

圖1 通過TensorBoard視覺化測試樣例的計算圖

圖1中的每一個節點都是一個運算,而每一條邊代表了計算之間的依賴關係。如果一個運算的輸入依賴於另一個運算的輸出,那麼這兩個運算有依賴關係。在圖1中,a和b這兩個常量不依賴任何其他計算。而add計算則依賴讀取兩個常量的取值。於是在圖1中可以看到有一條從a到add的邊和一條從b到add的邊。在圖1中,沒有任何計算依賴add的結果,於是代表加法的節點add沒有任何指向其他節點的邊。所有TensorFlow的程式都可以通過類似圖1所示的計算圖的形式來表示,這就是TensorFlow的基本計算模型。

TensorFlow計算圖定義完成後,我們需要通過會話(Session)來執行定義好的運算。會話擁有並管理TensorFlow程式執行時的所有資源。當所有計算完成之後需要關閉會話來幫助系統回收資源,否則就可能出現資源洩漏的問題。TensorFlow可以通過Python的上下文管理器來使用會話。以下程式碼展示瞭如何使用這種模式。

# 建立一個會話,並通過Python中的上下文管理器來管理這個會話。
with tf.Session() as sess
# 使用這建立好的會話來計算關心的結果。
sess.run(...)
# 不需要再呼叫“Session.close()”函式來關閉會話,
# 當上下文退出時會話關閉和資源釋放也自動完成了。

通過Python上下文管理器的機制,只要將所有的計算放在“with”的內部就可以。當上下文管理器退出時候會自動釋放所有資源。這樣既解決了因為異常退出時資源釋放的問題,同時也解決了忘記呼叫Session.close函式而產生的資源洩。

TensorFlow實現前向傳播

為了介紹神經網路的前向傳播演算法,需要先了解神經元的結構。神經元是構成一個神經網路的最小單元,圖2顯示了一個神經元的結構。

圖2  神經元結構示意圖

從圖2可以看出,一個神經元有多個輸入和一個輸出。每個神經元的輸入既可以是其他神經元的輸出,也可以是整個神經網路的輸入。所謂神經網路的結構就是指的不同神經元之間的連線結構。如圖2所示,神經元結構的輸出是所有輸入的加權和加上偏置項再經過一個啟用函式。圖3給出了一個簡單的三層全連線神經網路。之所以稱之為全連線神經網路是因為相鄰兩層之間任意兩個節點之間都有連線。這也是為了將這樣的網路結構和後面文章中將要介紹的卷積層、LSTM結構區分。圖3中除了輸入層之外的所有節點都代表了一個神經元的結構。本小節將通過這個樣例來解釋前向傳播的整個過程。

圖3  三層全連線神經網路結構圖

計算神經網路的前向傳播結果需要三部分資訊。第一個部分是神經網路的輸入,這個輸入就是從實體中提取的特徵向量。第二個部分為神經網路的連線結構。神經網路是由神經元構成的,神經網路的結構給出不同神經元之間輸入輸出的連線關係。神經網路中的神經元也可以稱之為節點。在圖3中,a11節點有兩個輸入,他們分別是x1和x2的輸出。而a11的輸出則是節點Y的輸入。最後一個部分是每個神經元中的引數。圖3用w來表示神經元中的權重,b表示偏置項。W的上標表明瞭神經網路的層數,比如W(1)表示第一層節點的引數,而W(2)表示第二層節點的引數。W的下標表明瞭連線節點編號,比如W1,2(1)表示連線x1和a12節點的邊上的權重。給定神經網路的輸入、神經網路的結構以及邊上權重,就可以通過前向傳播演算法來計算出神經網路的輸出。下面公式給出了在ReLU啟用函式下圖3神經網路前向傳播的過程。

a11=f(W1,1(1)x1+W2,1(1)x2+b1(1))=f(0.7×0.2+0.9×0.3+(-0.5))=f(-0.09)=0
a12=f(W1,2(1)x1+W2,2(1)x2+b2(1))=f(0.7×0.1+0.9×(-0.5)+0.1)=f(-0.28)=0
a13=f(W1,3(1)x1+W2,3(1)x2+b3(1))=f(0.7×0.4+0.9×0.2+(-0.1))=f(0.36)=0.36
Y=f(W1,1(2)a11+W1,2(2)a12+W1,3(2)a13+b1(2))=f(0.054+0.028+(-0.072)+0.1)=f(0.11)=0.11

在TensorFlow中可以通過矩陣乘法的方法實現神經網路的前向傳播過程。

a = tf.nn.relu(tf.matmul(x, w1)+b1)
y = tf.nn.relu(tf.matmul(a, w2)+b2)

在上面的程式碼中並沒有定義w1、w2、b1、b2,TensorFlow可以通過變數(tf.Variable)來儲存和更新神經網路中的引數。比如通過下面語句可以定義w1:

weights = tf.Variable(tf.random_normal([2, 3], stddev=2))

這段程式碼呼叫了TensorFlow變數的宣告函式tf.Variable。在變數宣告函式中給出了初始化這個變數的方法。TensorFlow中變數的初始值可以設定成隨機數、常數或者是通過其他變數的初始值計算得到。在上面的樣例中,tf.random_normal([2, 3], stddev=2)會產生一個2×3的矩陣,矩陣中的元素是均值為0,標準差為2的隨機數。tf.random_normal函式可以通過引數mean來指定平均值,在沒有指定時預設為0。通過滿足正太分佈的隨機數來初始化神經網路中的引數是一個非常常用的方法。下面的樣例介紹瞭如何通過變數實現神經網路的引數並實現前向傳播的過程。

import tensorflow as tf

# 宣告變數。
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
b1 = tf.Variable(tf.constant(0.0, shape=[3]))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
b2 = tf.Variable(tf.constant(0.0, shape=[1]))

# 暫時將輸入的特徵向量定義為一個常量。注意這裡x是一個1*2的矩陣。
x = tf.constant([[0.7, 0.9]])  

# 實現神經網路的前向傳播過程,並計算神經網路的輸出。
a = tf.nn.relu(tf.matmul(x, w1)+b1)
y = tf.nn.relu(tf.matmul(a, w2)+b2)

sess = tf.Session()
# 執行變數初始化過程。
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 輸出[[3.95757794]]
print(sess.run(y))  
sess.close()

TensorFlow實現反向傳播

在前向傳播的樣例程式中,所有變數的取值都是隨機的。在使用神經網路解決實際的分類或者回歸問題時需要更好地設定引數取值。使用監督學習的方式設定神經網路引數需要有一個標註好的訓練資料集。以判斷零件是否合格為例,這個標註好的訓練資料集就是收集的一批合格零件和一批不合格零件。監督學習最重要的思想就是,在已知答案的標註資料集上,模型給出的預測結果要儘量接近真實的答案。通過調整神經網路中的引數對訓練資料進行擬合,可以使得模型對未知的樣本提供預測的能力。

在神經網路優化演算法中,最常用的方法是反向傳播演算法(backpropagation)。圖4展示了使用反向傳播演算法訓練神經網路的流程圖。本文將不過多講解反向傳播的數學公式,而是重點介紹如何通過TensorFlow實現反向傳播的過程。

圖4  使用反向傳播優化神經網路的流程圖

從圖4中可以看出,通過反向傳播演算法優化神經網路是一個迭代的過程。在每次迭代的開始,首先需要選取一小部分訓練資料,這一小部分資料叫做一個batch。然後,這個batch的樣例會通過前向傳播演算法得到神經網路模型的預測結果。因為訓練資料都是有正確答案標註的,所以可以計算出當前神經網路模型的預測答案與正確答案之間的差距。最後,基於這預測值和真實值之間的差距,反向傳播演算法會相應更新神經網路引數的取值,使得在這個batch上神經網路模型的預測結果和真實答案更加接近。通過TensorFlow實現反向傳播演算法的第一步是使用TensorFlow表達一個batch的資料。在上面的樣例中使用了常量來表達過一個樣例:

x = tf.constant([[0.7, 0.9]])  

但如果每輪迭代中選取的資料都要通過常量來表示,那麼TensorFlow的計算圖將會太大。因為每生成一個常量,TensorFlow都會在計算圖中增加一個節點。一般來說,一個神經網路的訓練過程會需要經過幾百萬輪甚至幾億輪的迭代,這樣計算圖就會非常大,而且利用率很低。為了避免這個問題,TensorFlow提供了placeholder機制用於提供輸入資料。placeholder相當於定義了一個位置,這個位置中的資料在程式執行時再指定。這樣在程式中就不需要生成大量常量來提供輸入資料,而只需要將資料通過placeholder傳入TensorFlow計算圖。在placeholder定義時,這個位置上的資料型別是需要指定的。和其他張量一樣,placeholder的型別也是不可以改變的。placeholder中資料的維度資訊可以根據提供的資料推導得出,所以不一定要給出。下面給出了通過placeholder實現前向傳播演算法的程式碼。

x = tf.placeholder(tf.float32, shape=(1, 2), name="input")
# 其他部分定義和上面的樣例一樣。
print(sess.run(y, feed_dict={x: [[0.7,0.9]]}))  

在呼叫sess.run時,我們需要使用feed_dict來設定x的取值。在得到一個batch的前向傳播結果之後,需要定義一個損失函式來刻畫當前的預測值和真實答案之間的差距。然後通過反向傳播演算法來調整神經網路引數的取值使得差距可以被縮小。損失函式將在後面的文章中更加詳細地介紹。以下程式碼定義了一個簡單的損失函式,並通過TensorFlow定義了反向傳播的演算法。

# 定義損失函式來刻畫預測值與真實值得差距。
cross_entropy = -tf.reduce_mean(
    y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0))) 
# 定義學習率。
learning_rate = 0.001
# 定義反向傳播演算法來優化神經網路中的引數。
train_step = 
    tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

在上面的程式碼中,cross_entropy定義了真實值和預測值之間的交叉熵(cross entropy),這是分類問題中一個常用的損失函式。第二行train_step定義了反向傳播的優化方法。目前TensorFlow支援10種不同的優化器,讀者可以根據具體的應用選擇不同的優化演算法。比較常用的優化方法有三種:tf.train.GradientDescentOptimizer、class tf.train.AdamOptimizer和tf.train.MomentumOptimizer。

TensorFlow解決MNIST問題

MNIST是一個非常有名的手寫體數字識別資料集,在很多資料中,這個資料集都會被用作深度學習的入門樣例。MNIST資料集是NIST資料集的一個子集,它包含了60000張圖片作為訓練資料,10000張圖片作為測試資料。在MNIST資料集中的每一張圖片都代表了0-9中的一個數字。圖片的大小都為28×28,且數字都會出現在圖片的正中間。圖5展示了一張數字圖片及和它對應的畫素矩陣:

圖5. MNIST數字圖片及其畫素矩陣。

在圖5的左側顯示了一張數字1的圖片,而右側顯示了這個圖片所對應的畫素矩陣。MNIST資料集中圖片的畫素矩陣大小為28×28,但為了更清楚的展示,圖5右側顯示的為14×14的矩陣。在Yann LeCun教授的網站中(http://yann.lecun.com/exdb/mnist)對MNIST資料集做出了詳細的介紹。TensorFlow對MNIST資料集做了更高層的封裝,使得使用起來更加方便。下面給出了樣例TensorFlow程式碼來解決MNIST數字手寫體分類問題。

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

# MNIST資料集相關的常數。
INPUT_NODE = 784      # 輸入層的節點數。對於MNIST資料集,這個就等於圖片的畫素。   
OUTPUT_NODE = 10     # 輸出層的節點數。這個等於類別的數目。因為在MNIST資料集中
                         # 需要區分的是0~9這10個數字,所以這裡輸出層的節點數為10。

# 配置神經網路的引數。
LAYER1_NODE = 500   # 隱藏層節點數。這裡使用只有一個隱藏層的網路結構作為樣例。
                        # 這個隱藏層有500個節點。
BATCH_SIZE = 100    # 一個訓練batch中的訓練資料個數。數字越小時,訓練過程越接近
                        # 隨機梯度下降;數字越大時,訓練越接近梯度下降。
LEARNING_RATE = 0.01           # 學習率。
TRAINING_STEPS = 10000              # 訓練輪數。

# 訓練模型的過程。
def train(mnist):
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')
    
    # 定義神經網路引數。
weights1 = tf.Variable(
    tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
bias1 = tf.Variable(tf.constant(0.0, shape=[LAYER1_NODE]))
weights2 = tf.Variable(
    tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
bias2 = tf.Variable(tf.constant(0.0, shape=[OUTPUT_NODE]))

# 計算在當前引數下神經網路前向傳播的結果。
    layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + bias1)
    y = tf.matmul(layer1, weights2) + bias2

# 定義儲存訓練輪數的變數。 
    global_step = tf.Variable(0, trainable=False)
    
# 計算交叉熵作為刻畫預測值和真實值之間差距的損失函式。
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
    labels=y_, logits=y)
    loss = tf.reduce_mean(cross_entropy)
           
    # 使用tf.train.GradientDescentOptimizer優化演算法來優化損失函式。注意這裡損失
    # 函式包含了交叉熵損失和L2正則化損失。
    train_op=tf.train.GradientDescentOptimizer(LEARNING_RATE)\
                 .minimize(loss, global_step=global_step)

    # 檢驗神經網路的正確率。
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_,1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
  
    # 初始化會話並開始訓練過程。
    with tf.Session() as sess:
    tf.initialize_all_variables().run()
    # 準備驗證資料。一般在神經網路的訓練過程中會通過驗證資料來大致判斷停止的
    # 條件和評判訓練的效果。
        validate_feed = {x: mnist.validation.images, 
                             y_: mnist.validation.labels}

    # 準備測試資料。在真實的應用中,這部分資料在訓練時是不可見的,這個資料只是作為  
    # 模型優劣的最後評價標準。
        test_feed = {x: mnist.test.images, y_: mnist.test.labels}     

        # 迭代地訓練神經網路。
        for i in range(TRAINING_STEPS):
            # 每1000輪輸出一次在驗證資料集上的測試結果。
            if i % 1000 == 0:
    validate_acc = sess.run(accuracy, feed_dict=validate_feed)
                  print("After %d training step(s), validation accuracy "
                         "using average model is %g " % (i, validate_acc))
            
            # 產生這一輪使用的一個batch的訓練資料,並執行訓練過程。
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            sess.run(train_op, feed_dict={x: xs, y_: ys})

        # 在訓練結束之後,在測試資料上檢測神經網路模型的最終正確率。
        test_acc = sess.run(accuracy, feed_dict=test_feed)
    print("After %d training step(s), test accuracy using average "
           "model is %g" % (TRAINING_STEPS, test_acc))
 
# 主程式入口
def main(argv=None): 
    # 宣告處理MNIST資料集的類,這個類在初始化時會自動下載資料。
    mnist = input_data.read_data_sets("/tmp/data", one_hot=True)
    train(mnist)

# TensorFlow提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式。
if __name__ == '__main__':
tf.app.run()

執行上面程式碼可以得到結果:

After 0 training step(s), validation accuracy using average model is 0.103 
After 1000 training step(s), validation accuracy using average model is 0.9044 
After 2000 training step(s), validation accuracy using average model is 0.9174 
After 3000 training step(s), validation accuracy using average model is 0.9258 
After 4000 training step(s), validation accuracy using average model is 0.93 
After 5000 training step(s), validation accuracy using average model is 0.9346 
After 6000 training step(s), validation accuracy using average model is 0.94 
After 7000 training step(s), validation accuracy using average model is 0.9422 
After 8000 training step(s), validation accuracy using average model is 0.9472 
After 9000 training step(s), validation accuracy using average model is 0.9498 
After 10000 training step(s), test accuracy using average model is 0.9475

通過該程式可以將MNIST資料集的準確率達到~95%。