深度學習中的程式語言Tensorflow入門

程序員書屋 發佈 2020-05-04T15:59:22+00:00

2.1 預備知識Tensorflow是谷歌開發的一種開源程式語言,旨在讓深度學習程序編程變得更簡單。

本章講述的主要內容包括:預備知識;Tensorflow程序;多層神經網絡;檢查點、Tensordot、TF變量的初始化和TF圖創建的簡化;參考文獻和補充閱讀;習題。

2.1 預備知識

Tensorflow是谷歌開發的一種開源程式語言,旨在讓深度學習程序編程變得更簡單。我們首先從一個程序開始。

import tensorflow as tf
x = tf.constant("Hello World")
sess = tf.Session()
print(sess.run(x)) #will print out "Hello World"

該程序是否看起來像Python代碼呢?它的確就是Python代碼。事實上,Tensorflow(後稱TF)是一組函數集合,可以使用不同的程式語言來調用它。最完整的接口是Python的,這就是我們在上述程序中使用的。

要注意的是,TF函數與其說是執行一個程序,不如說是定義一個只有在調用run命令時才執行的計算,就像上面程序的最後一行一樣。更準確地說,第3行中的TF函數Session創建了一個會話,與該會話相關聯的是定義計算的圖。像constant這樣的命令會將元素添加到計算中。在本例中,元素只是一個常量數據項,其值是Python字符串「Hello World」。第4行代碼顯示TF計算與會話sess相關聯的圖中的x指向的TF變量。最終結果是——列印輸出「Hello World」。

我們可以將上例最後一行替換為print(x),進行對比。替換後輸出

Tensor("Const:0", shape=(), dtype=string)

關鍵是Python變量x並不綁定到字符串,而是綁定到Tensorflow計算圖的一部分。只有當通過執行sess.run(x)來計算圖的這一部分時,我們才能訪問TF常量的值。

圖2.1 TF中的placeholder

所以,在上面的代碼中,x和sess是Python變量,可以根據我們的需要命名。import和print是Python函數,必須這樣拼寫,Python才能理解我們想要執行哪個函數。constant、Session和run是TF命令,拼寫必須準確(包括Session中需要大寫「S」)。此外,需要首先import tensorflow,這是固定的,我們在後文中不再提及。

在圖2.1中的代碼中,x仍是Python變量,其值是TF常量,在本例中是浮點數2.0。然後,z是Python變量,其值是TF placeholder。TF中的placeholder類似於程式語言函數中的變量。假設我們有以下Python代碼。

x = 2.0
def sillyAdd(z):
   return z+x
print(sillyAdd(3))  # Prints out 5.0
print(sillyAdd(16)) # Prints out 18.0

這裡z是sillyAdd參數的名稱,當我們調用sillyAdd(3)中的函數時,z被它的值3所取代。TF程序的工作方式類似,不同之處在於給TF placeholder賦值的方式不同,如圖2.1的第5行所示。

print(sess.run(comp,feed_dict={z:3.0}))

這裡的feed_dict是run的命名參數(因此它的名稱必須拼寫正確)。它接受Python字典這類值。在字典中,計算所需的每個placeholder都必須給定一個值。所以第一次sess.run列印輸出為2.0和3.0的總和,第二次列印輸出18.0。第三次調用sess.run時需要注意的是,如果計算不需要placeholder的值,則不必提供其值。另一方面,正如第4個列印輸出語句後的注釋所指出的,如果計算需要一個值,但沒有提供該值,就會出現錯誤。

Tensorflow的命名源於其基本數據結構是張量型(tensor)多維數組。大約有十五種或更多張量類型。當我們定義上面的placeholder z時,我們給出了它的類型為float32。除了它的類型,張量也有形狀。想像一個2×3的矩陣,它的形狀就是[2, 3]。長度為4的向量形狀為[4],它不同於形狀為[1,4]的1×4矩陣,或者形狀為[4,1]的4×1矩陣。一個3×17×6的數組形狀為[3,17,6]。他們都是張量。標量(即數字)的形狀是null,也屬於張量。此外,請注意張量不像線性代數,它不需要區分行向量和列向量。有些張量的形狀只有一個分量,例如[5]。我們如何在紙上畫出這些張量對數學來說無關緊要。我們對數組張量進行圖示時,總是遵循第零個維度垂直繪製,第一個維度水平繪製的規則。但這是我們為保持一致進行的限制。請注意,張量維數和下標都是從零開始的。

回到我們對placeholder的討論:大多數placeholder不是前述例子中的簡單標量,而是多維張量。2.2節從一個簡單的用於Mnist數字識別的Tensorflow程序開始。其中將一張圖片輸入TF代碼,並運行神經網絡前向傳遞,以獲得網絡對數字的預測。此外,在訓練階段,運行反向傳遞並修改程序的參數。為了給程序傳入圖片輸入,我們定義了一個placeholder。它是float32型,形狀為[ 28,28],或者是[784],這取決於我們給它的是一個二維Python列表還是一維Python列表。例如,

img=tf.placeholder(tf.float32,shape=[28,28])

請注意,shape是placeholder函數的命名參數。

在深入討論真正的程序之前,我們先看TF數據結構。如前所述,神經網絡模型由它們的參數和程序的結構來定義——如何將參數與輸入值組合以產生答案。通常我們隨機初始化參數(例如,連接輸入圖像和答案logit的權重w),神經網絡會修改參數以在訓練數據上最小化損失。創建TF參數有三個階段。首先,用初始值創建張量,然後將張量轉換為Variable(TF對參數的稱謂),然後初始化變量或者說參數。我們來創建圖1.11中前饋Mnist偽代碼所需的參數。首先是偏置項b,然後是權重W。

bt = tf.random_normal([10], stddev=.1)
b = tf.Variable(bt)
W = tf.Variable(tf.random_normal([784,10],stddev=.1))
sess=tf.Session()
sess.run(tf.global_variables_initializer())
print(sess.run(b))

第1行添加了創建形狀為[10]的張量的指令,張量的十個值是從標準偏差為0.1的正態分布生成的隨機數。正態分布,也稱為高斯分布,是常見的鐘形曲線。從正態分布中選取的數字將以平均值(µ)為中心,它們離平均值的距離由標準偏差(σ)決定。更具體地說,大約68 %的值處在平均值的一個標準偏差範圍內,超出這個範圍的數字出現機率會大大降低。

上面代碼的第2行輸入為bt,並添加了一段TF圖,該圖創建了一個與bt具有相同形狀和值的變量。一旦我們創建了變量,我們就很少需要原始張量,所以通常會同時進行上述兩個事件而不保存張量指針,就像創建參數W的第3行一樣。在使用b或W之前,我們需要在創建的會話中對它們進行初始化,這是第5行的工作。第6行是列印輸出結果(結果如下,每次都會不同)。

[-0.05206999 0.08943175 -0.09178174 -0.13757218 0.15039739
  0.05112269 -0.02723283 -0.02022207 0.12535755 -0.12932496]

如果我們顛倒了最後兩行的順序,當嘗試列印b所指的變量時,就會收到一條錯誤消息。

因此,在TF程序中,我們創建變量來存儲模型參數。最初,參數的值是不含信息的,通常是標準偏差很小的隨機值。根據之前的討論,梯度下降的反向傳遞修改了它們。一旦被修改,sess指向的會話將保留新值,並在下次運行會話時使用它們。

2.2 TF程序

圖2.2是前饋神經網絡Mnist程序的TF版本,它比較完整,應該可以運行。這裡隱藏的關鍵元素是代碼mnist.train.next_batch,它處理Mnist數據中的讀取細節。先大體看一看圖2.2,請注意虛線之前的所有內容都與設置TF計算圖有關;虛線之後首先使用圖來訓練參數,然後運行程序來查看測試數據的準確性。現在我們逐行解讀這個程序。

首先,是import tensorflow和Mnist數據的讀取代碼,然後在第5行和第6行定義了兩組參數,這和剛才討論的TF變量定義有一點小變化。接下來,我們為輸入神經網絡的數據定義placeholder。首先,在第8行,定義圖像數據的placeholder,這是一個形狀為[batchSz,784]的張量。在討論線性代數為什麼是表示神經網絡計算的好方法時(1.5節),我們注意到,同時處理幾個樣本時,我們的計算速度會加快,而且,這與隨機梯度下降中的批處理概念非常吻合。在圖2.2中,我們可以看到這一點在TF中如何實現。也就是說,圖片的placeholder不是一行784個像素,而是100行(這取於batchSz的值)。程序第9行與之類似,我們的程序一次性給出100張圖片的預測。

圖2.2 Mnist的前饋神經網絡的Tensorflow代碼

在第9行中還需注意一點。我們用包含10個數字的向量表示一個答案,所有數字值都為零,除了第a個,其中a是該圖像對應的正確數字。例如,第1章中的數字7的圖片(圖1.1),正確答案的對應表示是( 0,0,0,0,0,0,0,1,0,0 )。這種形式的向量被稱為獨熱(one-hot)向量,因為它們具有僅選擇一個值作為激活值的特性。

截至第9行是程序的參數定義和輸入,下面的代碼是完成圖中的計算。其中第11行開始顯示TF用於神經網絡計算的強大能力。它定義了模型的神經網絡前向傳遞,將(一個批大小的)圖片輸入線性單元(由W和b定義),然後對所有結果應用softmax函數以得到一個機率向量。

我們建議在查看類似代碼時,首先檢查所涉及的張量的形狀,以確保它們是合理的。這裡隱藏最深的計算是矩陣乘法matmul,即輸入圖片[100,784]乘以W[784, 10]得到一個形狀為[100,10]的矩陣。接著我們將偏置與矩陣相加,得到一個形狀為[100,10]的矩陣,這是100張圖片的批中的10個logit。然後,將結果通過softmax函數處理,最後會得到圖片對應的[100, 10]大小的標籤機率分配矩陣。

第12行並行計算100個樣本的平均交叉熵損失。我們從裡到外進行講解。tf.log(x)返回一個張量,使得x的每個元素都被它的自然對數代替。圖2.3展示了tf.log如何進行批操作,批大小為3,批中每個向量都包含5個機率分布。

圖2.3 tf.log的批操作

接下來,ans * tf.log(prbs)中的標準乘法符號「*」代表兩個張量的逐元素相乘。圖2.4顯示了在批運算中,每個標籤的獨熱向量與負自然對數矩陣的逐元素相乘如何進行。結果中的每一行,除了正確答案機率對應的負對數之外,所有內容都被清零。

圖2.4 答案乘機率的負對數的計算

此時,為了獲得每張圖片的交叉熵,我們只需要對數組中的所有值求和。求和的第一步操作是

tf.reduce_sum( A, reduction_indices = [ 1 ] )

它將A的各行相加,如圖2.5所示。這裡的一個關鍵部分是

reduction_indices = [ 1 ]

在我們之前對張量的介紹中,提到了張量的維數是從零開始的。reduce_sum可以對列求和,默認情況下,reduction_indices=[0],或者,如本例中,對行求和,reduction_indices=[1]。這將生成一個[100,1]的數組,每行中只有正確機率的對數作為唯一的條目。圖2.5設批大小為3,並假設有5個類,而不是10個。作為交叉熵計算的最後一個部分,圖2.2中第12行reduce_mean對所有列求和(同樣reduction_indices是默認值),並返回平均值(1.1左右)。

圖2.5 根據reduction_indices為[1]進行tf.reduce_sum計算

最後,我們可以轉到圖2.2中的第14行,在此TF真正展示了它的優點,這一行就實現了整個反向傳遞所需的全部內容。

tf.train.GradientDescentOptimizer(0.5).minimize(xEnt)

即,使用梯度下降來計算權重變化,並最小化由第12行和第13行定義的交叉熵損失函數。該行還指定了0.5的學習率。我們不必擔心計算導數或其他元素,因為如果你在TF中定義了前向計算和損失,那麼TF編譯器會知道如何計算必要的導數,並按照正確的順序將它們串在一起對權重進行修改。我們可以通過選擇不同的學習率來修改這個函數調用,或者,如果我們使用不同的損失函數,可以用另一個TF計算的元素替換xEnt。

當然,TF基於前向傳遞導出反向傳遞的能力是有限的。再強調一次,只有當所有前向傳遞計算都用TF函數完成時,它才能做到這一點。對於像我們這樣的初學者來說,這並不是太大的限制,因為TF有各種各樣的內置操作,它知道如何進行區分和連接。

第15行和第16行代碼計算模型的accuracy(精度)。精度是模型計算正確答案的數量除以處理的圖片數量。首先,關註標准數學函數argmax,如

,它返回讓f(x)最大化的x值。在這裡,我們使用的tf.argmax(prbs, 1)有兩個參數:第一個是張量,我們從中取argmax;第二個是取argmax的張量軸。張量軸的作用與我們用於reduce_sum的命名參數類似——它幫助我們在張量的不同軸上求和。舉例來說,如果張量是( ( 0,2,4 ), ( 4,0,3 ) ),並且使用軸0(默認值),我們會得到( 1,0,0 )。我們先比較0和4,由於4更大,所以返回1。然後我們比較2和0,由於2更大,所以返回0。如果我們使用軸1,我們會返回( 2,0 )。第15行有一個批大小logit的數組。argmax函數返回批大小的最大logit所在位置的數組。接下來,我們應用tf.equal將最大logit與正確答案進行比較。tf.equal返回一個批向量的布爾值(如果它們相等,則為True),tf.cast(tensor,tf.float32)將該向量轉換為浮點數,以便tf.reduce_mean將它們相加,得到正確率的百分比。請注意不要將布爾值轉換成整數,因為取平均值時,它會返回一個整數,在這種情況下,該整數將始終為零。

定義了會話(第18行)並初始化參數值(第19行)之後,我們可以訓練模型(第21行至第23行)。在這三行代碼中,我們使用從TF Mnist庫中獲得的代碼每次提取100張圖片及其答案,然後通過調用sess.run在訓練的計算圖上運行程序。當這個循環結束時,我們共訓練了1,000次,每次疊代有100張圖片,或者說總共訓練了100,000張測試圖片。我的Mac Pro電腦具有四核處理器,完成這輪循環大約需要5秒(第一次將內容放入緩存中會花費較長時間)。提到「四核處理器」是因為TF會查看可用的計算能力,在沒有指導時也能很好地使用電腦的計算能力。

你可能注意到了,第21行到第23行有一點奇怪——我們從來沒有明確提到過要進行前向傳遞,而TF根據計算圖(Computation graph)計算出了這一點。從GradientDescentOptimizer中,TF知道自己需要執行xEnt所需的計算(第12行),這需要計算prbs,而該計算又指向了第11行的前向傳遞計算。

最後,第25行到第29行計算測試數據的正確率(91%或92%)。首先,通過瀏覽計算圖的組織結構可以發現,accuracy計算最終需要的是在前向傳遞中計算prbs,而不是反向傳遞的訓練。因此,為了更好地測試數據,不對權重進行修改。

第1章中提到,在訓練模型時列印輸出錯誤率是良好的調試實踐。一般來說,錯誤率會下降。為此,我們將第23行改為

acc,ignore= sess.run([accuracy,train],
                          feed_dict={img: imgs, ans: anss})

這裡的語法是用於組合計算的普通Python語言。計算的第一個值(accuracy的值)分配給變量acc,計算的第二個值分配給ignore。Python的習慣做法是用下劃線符號( _ )代替ignore,當語法要求變量接受一個值,但我們不需要記住它時,Python會使用下劃線符號。當然,我們還需要添加一個命令來列印輸出acc的值。

我們提到這一點是為了幫助讀者避免一個常見的錯誤——無視第23行,反而自己新增了第23.5行(我和一些剛入門的學生都犯過這個錯誤)。

acc= sess.run(accuracy, feed_dict={img: imgs, ans: anss})

這種做法效率較低,因為TF在這種情況下需要進行兩次前向傳遞,一次是在要進行訓練時,另一次是在求accuracy時。更重要的是,第一次調用會修改權重,從而更有可能為該圖片預測正確的標籤。如果在此之後計算accuracy,程序的性能就會有所誇大。當我們調用一次sess.run,但同時求兩個值時,就不會發生這種情況。

2.3 多層神經網絡

我們設計的程序,如第1章中的偽代碼和第2章的TF代碼,都是單層神經網絡,只有一層線性單元。問題來了,多層線性單元表現會更好嗎?早期神經網絡研究人員認為答案是「否」,下面解釋為什麼。線性單元可以被看作線性代數矩陣,即我們看到一層前饋神經網絡只是計算y = XW。在我們的Mnist模型中,為了將784個像素值轉換成10個logit值,W的形狀設置為[ 784,10 ],並增加額外的權重來替換偏置項。假設我們又添加了一層線性單元U,其形狀為[ 784,784 ],輸出到層V中,層VW形狀一樣,是[ 784,10 ],

(2.1)

(2.2)

其中第2行遵循矩陣乘法的結合律。這裡的重點是,使用兩層神經網絡UV相乘得到的能力,都可以由W= UV的單層神經網絡得到。

有一個簡單的解決方案——在層與層之間添加一些非線性計算。最常用的一種是tf.nn.relu(或ρ),修正線性單元(rectified linear unit,以下簡稱relu),定義為

(2.3)

函數圖像如圖2.6所示。

圖2.6 tf.nn.relu的行為

在深度學習中,置於各層之間的非線性函數稱為激活函數(activation function)。除了常用的relu以外,其他一些激活函數也活躍於程序中,例如sigmoid函數,定義為

(2.4)

函數圖像如圖2.7所示。在所有情況下,激活函數都分別應用於張量參數中的各個實數。例如,ρ([1,17, −3 ] ) = [ 1,17,0]。

圖2.7 sigmoid函數

在發現relu這種有效簡單的非線性函數前,sigmoid函數非常受歡迎。但是sigmoid可以輸出的值範圍非常有限,只限於從0到1,而relu輸出的值可以從0到無窮大。當我們進行反向傳遞計算梯度找出參數如何影響損失時,這一點非常關鍵。反向傳播時,若使用sigmoid函數會使梯度為0——這個過程被稱為梯度消失(vanishing gradient)問題。更簡單的激活函數會極大改善這個問題,鑒於此,tf.nn.lrelu——帶泄露修正線性單元(leaky relu)——使用非常頻繁,因為它比relu可輸出的值範圍更大,如圖2.8所示。

圖2.8 lrelu函數

將多層神經網絡放在一起,得出新模型。

(2.5)

其中σ是softmax函數,UV是第一層和第二層線性單元的權重,bu和bv是它們的偏置。

現在我們在TF中進行實現。我們將圖2.2第5行和第6行中的W和b的定義替換為圖2.9第1行到第4行的層U和V,圖2.2第11行prbs的計算替換為圖2.9的第5行至第7行。這些替換將原代碼轉換成多層神經網絡。此外,考慮到參數數量更多了,我們將學習率降低為

。舊程序在100,000張圖片上訓練後得出的精度穩定在92%左右,新程序在100,000張圖片上的精度會達到94%左右。另外,如果我們增加訓練圖片的數量,測試集的性能會一直提高到大約97%。注意,這個代碼和沒有非線性函數的代碼之間的唯一區別是第6行。如果我們刪除它,精度會下降到大約92%。這足以讓你相信數學的力量!

圖2.9 用於多層神經網絡識別數字的TF圖構造代碼

還有一點需要注意,在具有數組參數W的單層神經網絡中,W的形狀由輸入數量(784)和輸出數量(10)固定。對於兩層線性單元,我們則可以自由地選擇隱藏層大小(hidden size)。所以U是輸入大小×隱藏層大小,V是隱藏層大小×輸出大小。在圖2.9中,我們只是將隱藏層大小設定為784,與輸入大小相同,但是這並不是必須的。通常,加大隱藏層會提高性能,但也會有極限。

2.4 其他方面

在本節中,我們將介紹TF的其他方面——有助於完成本書其餘部分中提出的編程任務的知識(例如,檢查點),或者是在接下來的章節中會用到的知識。

2.4.1 檢查點

在TF計算中添加檢查點(checkpoint)通常很有用——將張量保存下來,以便可以在下一次恢復計算,或者在不同的程序中重新使用該張量。在TF中,我們通過創建和使用saver對象來實現這一點。

saveOb= tf.train.Saver()

如前節所述,saveOb是Python變量,你可以選擇名稱。在使用對象之前,可以在任意時間創建它,但是由於後文提到的原因,在初始化變量(調用global_variable_initialize)之前創建這個對象更為合理。然後在每n輪訓練後,保存所有變量的當前值。

saveOb.save(sess, "mylatest.ckpt")

save函數有兩個參數:要保存的會話以及文件名和位置。在上述語句的情況下,保存的目錄與Python程序所在目錄相同。如果這個參數是tmp/model.checkpt,它就會出現在tmp子目錄中。

調用save函數創建了四個文件。最小的文件,名為checkpoint,是一個Ascii文件,指定了在該目錄存儲檢查點的一些高級細節。名稱checkpoint是固定的,如果你將某個文件命名為「checkpoint」,它將被覆蓋。其他三個文件名會根據你提供的字符串來定義。在本例中,它們被命名為

mylatest.ckpt.data-00000-of-00001
mylatest.ckpt.index
mylatest.chpt.meta

第一個文件保存了參數值。另外兩個文件包含TF導入這些值時使用的元信息(稍後將進行描述)。如果你的程序反覆調用save,這些文件每次都會被覆蓋。

接下來如果我們想在已經訓練過的同一個神經網絡模型上做進一步的訓練,最簡單的操作就是修改原來的訓練程序。你保留了saver對象,現在我們想用保存的值初始化所有TF變量。因此,我們通常會移除global_variable_initialize,通過調用saver對象的「restore」方法來替換global_variable_initialize。

saveOb.restore(sess, "mylatest.ckpt")

下次調用訓練程序時,它會恢復訓練,TF變量自動設置為上次訓練中保存的值,其他一切都沒有改變。因此,如果在訓練代碼時,列印輪數及其對應損失,它會從1開始列印輪數,除非你修改了代碼。當然,如果你想調整列印輸出,或者想讓程序更加優雅,你可以修改代碼,但是在這裡編寫更好的Python代碼不是我們要關心的。

2.4.2 tensordot

tensordot函數是TF中矩陣乘法在張量上的推廣。我們對標準矩陣乘法非常熟悉,即前一章中的matmul。當A和B具有相同的維度個數n,A的最後一個維度與B的倒數第二個維度大小相同,並且前n−2個維度相同時,我們可以調用函數tf.matmul( A,B )。因此,如果A的維度是[ 2,3,4 ],B的維度是[ 2,4,6 ],那麼乘積維度是[ 2,3,6 ]。矩陣乘法可以看作重複的點積。例如,矩陣乘法

(2.6)

可以通過將向量( 1,2,3)和( −1, −3, −5 )進行點積,並將答案放在結果矩陣的左上角位置來實現。以這種方式繼續運算,第i行與第j列的點積,即為第i行第j列的結果。因此,設A是式(2.6)的第一個矩陣,B是第二個,這個計算也可以表示為

tf.tensordot(A, B, [[ 1 ], [ 0 ]])

前兩個參數是進行運算的張量,第三個參數是一個雙元素列表:第一個元素是來自第一個參數需要進行點積的維度列表,第二個元素是第二個參數的相應維度列表。這個雙元素列表指導tensordot獲取這兩個維度的點積。當然,如果我們要取它們的點積,指定的維度大小必須相等。由於垂直繪製第0個維度,水平繪製第1個維度,這意味著取A的每一行和B的每一列的點積。tensordot的結果按照從左到右取維度,先取A的剩餘的維度後取B的。也就是說,在本例中,輸入張量維度為[2,3]和[3,2],在點積中指定的兩個維度「消失」了(維度1和維度0),以得到維度為[2,2]的結果。

圖2.10給出的例子更為複雜,導致matmul無法在一條指令中處理它。我們將此圖從第5章拿過來作為例子(第5章中會解釋變量的名字含義),但在本章中,我們只是通過它觀察tensordot在做什麼。不看數字,只看tensordot函數調用中的第三個參數 [ [ 1 ], [ 0 ] ],即取encOut的1維和wAT的0維的點積。因為他們大小都為4,所以這是可行的。也就是說,我們取兩個維度分別為[2,4,4]和[4,3]的張量的點積(斜體數字是進行點積的維度)。由於這些維度在點積之後消失,因此得到的張量具有維度[ 2,4,3 ],當我們在例子最後列印輸出時,該張量維度是正確的。簡單地說一下實際的計算,我們對兩個張量顯示為列的維度取點積,即,第一個點積是對[ 1,1,1,−1 ]和[ 0.6,0.2,0.1,0.1 ]進行計算,得出的0.8作為結果張量中的第一個數值。

圖2.10 tensordot實例

最後,tensordot不限於在每個張量中進行一維的點積。如果A的維度是[ 2,4,4 ],而B的維度是[ 4,4 ],那麼運算tensordot ( A,B,[ [ 1,2 ],[ 0,1 ] ])會得到維度[ 2 ]的張量。

2.4.3 TF變量的初始化

在1.4節中,我們說過,隨機初始化神經網絡參數(即TF變量)且保證其接近於0是個很好的實踐。在第一個TF程序(圖2.9)中,我們使用如下命令實現這一操作。

b = tf.Variable(tf.random normal([10], stddev=.1))

其中,我們假設0.1的標準偏差足夠「接近0」。

然而,關於標準差的選擇自有一套理論和實踐體系。這裡我們給出了一個名為「Xavier初始化」的規則,它通常用於在隨機初始化變量時設置標準差。設ni為層的輸入數,no為層的輸出數,對於圖2.9中的變量W,ni= 784,即像素的數量;no = 10,即備選分類的數量。針對Xavier初始化,設置標準差σ為

(2.7)

例如,對於W將值784和10代入,標準差σ約為0.0502,四捨五入為0.1。通常,推薦將標準差設在0.3(10×10層)和0.03(1,000×1,000層)之間。輸入和輸出值越多,標準差越低。

Xavier初始化最初是為了與sigmoid激活函數一起使用而提出的(見圖2.7)。如前所述,當x遠低於−2或高於+2時,σ(x)對x幾乎毫無反應。也就是說,如果輸入sigmoid函數的值太高或太低,它們的變化可能對損失幾乎沒有影響。進行反向傳遞時,如果損失的變化被sigmoid函數抵消,那麼它不會影響輸入sigmoid函數的參數。實際上,我們希望一層的輸入和輸出之間的比率方差(variance)大約為1。這裡我們使用技術意義上的方差:數值隨機變量 值和其均值之間平方差的期望值。此外,隨機變量X的期望值(expected value)(用E[X]表示)是其可能取值的機率平均值。

(2.8)

以六面骰子為例,滾動一個六面骰子的期望值計算如下:

(2.9)

因此,我們希望將輸入方差與輸出方差之比保持在1左右,該層不會由於sigmoid函數而對信號過度衰減。這限制了我們初始化的方式。我們傳達了一個原始事實(你可以查看推導過程),對於一個權重矩陣為W的線性單元,前向傳遞的方差(Vf)和反向傳遞的方差(Vb)分別為

(2.10)

(2.11)

其中σ是W權重的標準偏差。(單個高斯的方差是(σ2),所以這說得通。)如果我們把Vf和Vb都設為零,然後求解σ可得

(2.12)

(2.13)

除非輸入的基數與輸出的基數相同,否則這沒有解。由於通常情況並非如此,所以我們在這兩個值之間取一個「平均值」,得出Xavier規則。

(2.14)

對於其他激活函數,也有等價的方程。隨著relu和其他激活函數的出現,而這些激活函數不像sigmoid那樣容易飽和,因此這個問題不再像以前那麼重要了。儘管如此,Xavier規則確實提供了很好地設置標準偏差的方法,它的TF程序版本和其他語言相關版本都十分常用。

2.4.4 TF圖創建的簡化

回顧圖2.9,可以看到需要7行代碼來描述兩層前饋網絡。可以想想看,如果在沒有TF的情況下,我們用Python編程描述這樣少的內容會需要多少代碼。如果我們用圖2.9的方式創建一個8層網絡——在本書結尾你需要完成這個任務——將需要大約24行代碼。

TF有一組方便的函數,即layers模塊,可以更緊湊地對常見的分層情形進行編碼。在這裡我們介紹

tf.contrib.layers.fully_connected.

如果一層的所有單元都連接到下一層的所有單元,則該層稱為完全連接。我們在前兩章中使用的層都是完全連接的,因此之前沒有將它們和其他網絡進行區分。定義這樣一個層,我們會做以下工作:(a)創建權重W;(b)創建偏置b;(c)進行矩陣乘法並加和偏置;(d)應用激活函數。假設我們已經執行了import tensorflow.contrib.layers as layers,可以用下面的一行代碼來完成定義工作。

layerOut=layers.fully_connected(layerIn,outSz,activeFn)

上述調用創建了一個用Xavier方法初始化的矩陣和一個以零初始化的偏置向量。它返回layerIn乘矩陣再加上偏置的結果,並將activeFn指定的激活函數應用於該結果。如果你沒有指定激活函數,它會使用relu;如果你指定None作為激活函數,則不使用激活函數。

使用fully_connected,我們可以將圖2.9中的7行代碼寫為

L1Output=layers.fully_connected(img,756)
prbs=layers.fully_connected(L1Output,10,tf.nn.softmax)

請注意,我們指定tf.nn.softmax作為第二層的激活函數,以作用於第二層的輸出。

當然,如果我們有一個100層的神經網絡,寫出100個fully_connected的調用是非常冗長乏味的。幸運的是,我們可以使用Python或者TF API來定義我們的網絡。舉一個想像中的例子,假設我們想要創建100個隱藏層,每一層比前一層小1,其中第一層的大小是一個系統參數。我們可以寫出

outpt = input
for i in range(100):
    outpt = layers.fully_connected(outpt, sysParam - i)}

這個例子很傻,但反映了很重要的一點:TF圖的部分可以像列表或字典一樣在Python中傳遞和操作。

2.5 參考文獻和補充閱讀

Tensorflow起源於谷歌內部項目——谷歌大腦,這個項目由兩名谷歌的研究人員Jeff Dean和Greg Corrado以及史丹福大學教授Andrew Ng發起。開始時,該項目被稱為「Distbelief」,當它的應用超越了初始項目時,谷歌正式接管了進一步的開發,並聘請了多倫多大學的Geoffrey Hinton,我們在第1章中提到了他對深度學習的開創性貢獻。

Xavier初始化來源於Xavier Glorot的名字。他以第一作者的身份撰寫了介紹Xavier初始化的文章[GB10]。

如今,Tensorflow只是深度學習的程式語言之一(參見文獻[Var17])。就用戶數量而言,Tensorflow是迄今為止最受歡迎的語言。第二位是Keras,一種建立在Tensorflow之上的高級語言。第三位是Caffe,最早是由加州大學伯克利分校開發的。Facebook現在支持Caffe的開源版本Caffe2。Pytorch是Torch的Python接口,它在深度學習自然語言處理社群十分受歡迎。

2.6 習題

練習2.1 如果在圖2.5中,我們計算tf.reduce_sum(A),其中A是圖左側的數組,結果會是怎樣的?

練習2.2 從圖2.2中取出第14行並將其插入第22行和第23行之間(循環如下),會產生什麼問題?

for i in range(1000):
   imgs, anss = mnist.train.next_batch(batchSz)
   train = tf.train.GradientDescentOptimizer(0.5).minimize(xEnt)
   sess.run(train, feed_dict={img: imgs, ans: anss})

練習2.3 下面是圖2.2中第21行到第23行代碼的另一個變體,它有沒有問題?如果有問題,是什麼問題?

for i in range(1000):
   img, anss= mnist.test.next_batch(batchSz)
   sumAcc+=sess.run(accuracy, feed_dict={img:img, ans:anss})

練習2.4 在圖2.10中,以下操作輸出的張量形狀是什麼?

   tensordot(wAT, encOut, [[0],[1]])

並給出解釋。

練習2.5 展開計算過程,確認圖2.10的例子最後列印輸出的張量中第一個數字( 0.8 )是正確的(精確到三位小數)。

練習2.6 假設input的形狀為[50,10],以下代碼創建了多少TF變量?

   O1 = layers.fully connected(input, 20, tf.sigmoid)

創建的矩陣中變量的標準偏差是多少?

本文摘自最新上架的《深度學習導論》

  • 人工智慧深度學習經典入門書
  • 基於TensorFlow和Python,美國常青藤名校經典教材
  • 理論與實戰結合的良好典範,附帶習題和答案

編輯推薦:

1.國內知識圖譜界領軍人物、文因互聯CEO鮑捷作序。國內外產業界和學術界大咖鼎力推薦

2.本書編寫簡明扼要,是美國常青藤名校布朗大學的教材。本書的每一章都包括了一個編程項目和一些書面練習,並附上了參考資料,可供讀者進一步閱讀。

3.人工智慧經典入門書,基於Tensorflow編寫,以項目為導向,通過一系列的編程任務,向讀者介紹了熱門的人工智慧應用,包括計算機視覺、自然語言處理和強化學習等。

4.做中學。作者在前言中寫道:「對我而言,學習計算機科學的最好方法,就是坐下來寫程序。」本書正是採用了這種方法。

關鍵字: