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%。
http://www.infoq.com/cn/articles/introduction-of-tensorflow-part02