三, Tensorflow編程模型
在這一章,我們將深度討論Tensorflow的計(jì)算范式。開始,我們會研究Tensorflow的基本架構(gòu)和計(jì)算原理,解釋機(jī)器學(xué)習(xí)算法如何在Tensorflow里面使用數(shù)據(jù)流圖語言表征。接下來,我們研究Tensorflow的運(yùn)行模型,也就是Tensorflow如何把Tensorflow圖轉(zhuǎn)化為運(yùn)行狀態(tài)。然后,我們調(diào)查Tensorflow內(nèi)在的,針對軟硬件的不同算法優(yōu)化。最后,我們列舉一系列算法擴(kuò)展,以支撐用戶在Tensorflow對于計(jì)算模型,邏輯模型訓(xùn)練。
A. 計(jì)算圖架構(gòu)
在Tensorflow里面,機(jī)器學(xué)習(xí)算法被表證為計(jì)算圖。計(jì)算圖或者數(shù)據(jù)流圖是一種有向圖。有向圖的頂點(diǎn)或者節(jié)點(diǎn)代表運(yùn)算過程,有向圖的邊代表運(yùn)算過程之間的數(shù)據(jù)流。如下圖所示,先看左邊,輸出變量z是輸入x和y的二值運(yùn)算的結(jié)果,那么畫兩條分別從x和y向z的有向邊,并且在z上標(biāo)明運(yùn)算加號。一個更加完整和復(fù)雜的數(shù)據(jù)流圖在右邊。下面,對于數(shù)據(jù)流圖里面的元素(operation - 算子,tensor - 張量,variable - 變量和session - 過程)進(jìn)行更為詳盡的討論。

1)Operations(算子):使用圖來表示一個算法的主要好處不僅僅是直觀展示計(jì)算模型之間的依賴和關(guān)聯(lián),而且能夠更普適的定義運(yùn)算節(jié)點(diǎn)。在Tensorflow里,node(節(jié)點(diǎn))代表著算子,也就是一種運(yùn)算,更精確來說,代表了輸入的數(shù)據(jù)在有向圖上,如何流經(jīng)這個節(jié)點(diǎn)【8】。一個算子可以是0到多個輸入,也能產(chǎn)生0到多個輸出。因此,一個算子代表一個數(shù)學(xué)等式,一個變量,一個常量,一個有向控制流,一個文件I/O操作或者甚至是一個網(wǎng)絡(luò)通訊連接端口。在算子可以表達(dá)為一個常量或者變量不是像表達(dá)成一個函數(shù)那樣直觀,但是一個常量可以被當(dāng)成一個沒有輸入,而且輸出恒定的運(yùn)算。相似的情況也適用于表征一個變量,也就是沒有輸入,輸出當(dāng)前狀態(tài)或者當(dāng)前變量的值。
? ? ? ? 任何算子必須要嚴(yán)格的定義和實(shí)現(xiàn)。在論文【8】的研究,任何一種實(shí)現(xiàn)被當(dāng)作一個算子的核函數(shù)。一種特定的核函數(shù)的具體編程都是針對某種硬件,比如CPU或者GPU的運(yùn)算。
2)Tensors(張量):在Tensorflow里面,代表數(shù)據(jù)流從一個算子流向另一個算子的邊稱之為張量。一個張量是一個具有固定類型同類數(shù)據(jù)的多維集合。張量維度也稱為rank(階)。張量的形狀(shape)描述張量大小的元祖(tuple)。比如,對于每一個維度的元素個數(shù)。從數(shù)學(xué)的觀點(diǎn)來看,張量是一個二維矩陣加一個表達(dá)張量階的一維向量或者標(biāo)量的生成物。
? ? ? ? 從計(jì)算圖的觀點(diǎn)來看,張量是表征通向輸出的算子的符號把手(symbolic handle)。在內(nèi)存里,張量本身并不包含和存儲數(shù)據(jù),但是它提供了張量代表的數(shù)據(jù)訪問的接口。在Tensorflow里面創(chuàng)建一個算子的時候,比如x+y,返回一個張量對象。然后,這個張量可能作為其他計(jì)算的輸入,從而張量是連接源算子和目的算子的邊?;谶@樣的認(rèn)知,數(shù)據(jù)在Tensorflow的圖中川流不息。
? ? ? ? 除了通常意義的張量,Tensorflow還支持一種稱之為稀疏張量(SparseTensor)的數(shù)據(jù)結(jié)構(gòu)。這是一種空間有效字典類的數(shù)據(jù)表證,也就是大量零值的稀疏張量。
3)Variables(變量):在通常情況,比如做隨機(jī)梯度下降的單次運(yùn)算的時候,機(jī)器學(xué)習(xí)模型的圖會從開始到結(jié)束反復(fù)運(yùn)算多次。在兩次調(diào)用之間,圖中主要的張量并不會被保存。但是整體上對于圖的求值是需要保存狀態(tài)的,比如神經(jīng)網(wǎng)絡(luò)的權(quán)重和參數(shù)。因此,變量正是為了滿足這一需求而創(chuàng)建的算子,能夠被添加到計(jì)算圖中。
? ? ? ? 變量可以看成是在內(nèi)存中持久不變的張量副本。因此,變量的定義需要形狀和固定數(shù)據(jù)類型兩個特征。Tensorflow提供了一系列賦值函數(shù)完成圖運(yùn)算。
? ? ? ? 在Tensorflow的圖中創(chuàng)建一個變量節(jié)點(diǎn)的時候,需要定義相應(yīng)的張量,這樣在圖運(yùn)行時,變量可以隨之初始化。變量的形狀和數(shù)據(jù)類型也來自于這個初始器。有趣的是,變量自己并不存儲這個初始的張量,相反構(gòu)造一個變量會增加三種不同的節(jié)點(diǎn):
? ? ? ? 1)變量節(jié)點(diǎn),保存持久狀態(tài)。
? ? ? ? 2)保存初始值的算子,通常是一個常量。
? ? ? ? 3)初始器算子,在圖求值的時候把初始值賦值給變量張量。
? ? ? ? 一個例子如下圖所示:三個節(jié)點(diǎn)代表變量定義。第一個變量v是一個變量的張量值在內(nèi)存中的持久副本。第二個變量i是給變量提供初始值(可以是任意張量)的節(jié)點(diǎn)。最后一個賦值節(jié)點(diǎn)把初始值賦給變量,在賦值節(jié)點(diǎn)產(chǎn)生一個新的張量具有初始值的變量v‘。這樣,v'可能作為另外算子的一個輸入。

4)Session(會話):在Tensorflow里面,算子的運(yùn)算和張量的估值會在一定的上下文中進(jìn)行,我們稱之為Session(會話)。會話的責(zé)任之一就是將分配和管理資源的工作封裝起來,比如緩存變量。更進(jìn)一步來看,Tensorflow庫里面的Session接口提供了一個run函數(shù),作為整個圖計(jì)算的執(zhí)行入口。這個方法將輸入節(jié)點(diǎn)帶入整個圖計(jì)算的過程,并且根據(jù)圖定義返回相應(yīng)的結(jié)果。另外,一個可選的映射,即從任意節(jié)點(diǎn)到相應(yīng)替代值的映射,被稱為feed nodes(反饋節(jié)點(diǎn)),可能也被run調(diào)用【8】。
? ? ? ? 在調(diào)用run的時候,Tensorflow將會出輸出節(jié)點(diǎn)開始反向分析計(jì)算圖的節(jié)點(diǎn)依賴,計(jì)算所有節(jié)點(diǎn)的傳遞閉包。這些節(jié)點(diǎn)可能被分配到一個或者多個物理計(jì)算單元(CPU,GPU等),這些計(jì)算單元可以在一臺或者多臺機(jī)器上。分配的規(guī)則由Tensorflow的placement algorithm(分配算法)定義。這個算法會在本文后面部分談到。此外,因?yàn)榇嬖诠?jié)點(diǎn)評估順序的特定顯式可能性,姑且稱為控制依賴關(guān)系,執(zhí)行算法將確保這些依賴關(guān)系不變。
B. 執(zhí)行模型
? ? ? ? 如剛剛討論的那樣,對于執(zhí)行各種計(jì)算圖元素的組成,TensorFlow劃分其任務(wù)在四個不同的組中實(shí)現(xiàn):客戶端,主控端,一組工作進(jìn)程和一些設(shè)備。 當(dāng)客戶端通過會話的run調(diào)用請求評估一個TensorFlow圖,這個請求被發(fā)送到主控端,主控端將任務(wù)委托給一個或多個工作進(jìn)程處理和協(xié)調(diào)它們的執(zhí)行。 每個工作進(jìn)程隨后負(fù)責(zé)一個或多個設(shè)備真實(shí)的運(yùn)算和處理操作。
? ? ? ? 在這個模型中,有兩個擴(kuò)展度。 第一擴(kuò)展度是關(guān)于執(zhí)行圖運(yùn)算機(jī)器的數(shù)量。 事實(shí)上,第二擴(kuò)展度指的是在每臺機(jī)器上,可能會有更多設(shè)備,例如,五個獨(dú)立的GPU和/或三個CPU。 為此,存在兩個“版本”的TensorFlow,一個用于在單個機(jī)器上本地執(zhí)行(但可能有許多設(shè)備),一個支持分布式在許多機(jī)器和許多設(shè)備上實(shí)現(xiàn)。

? ? ? ? 上圖展示了執(zhí)行模型和響應(yīng)擴(kuò)展度。而且Tensorflow在開始發(fā)布的時候,確實(shí)只公布了單機(jī)版本,而多機(jī)版本是在2016年的4月13日才姍姍來遲【16】。
? ? ? 1)Devices(設(shè)備):設(shè)備是在TensorFlow執(zhí)行模型中最小,最基本的實(shí)體。 上圖中的所有節(jié)點(diǎn),也就是每個算子的內(nèi)核,最終都必須是映射到要執(zhí)行的可用設(shè)備。 在實(shí)際執(zhí)行過程中,設(shè)備通常是CPU或GPU。 然而,TensorFlow能夠支持更多種類的物理執(zhí)行單元。 例如,在2016年5月,谷歌宣布其定制的ASIC(專用集成電路)張量處理單元(TPU),是專門用于快速張量計(jì)算[17]。 因此,Tensorflow是可以容易地集成新出現(xiàn)的設(shè)備類新型硬件。
? ? ? ? 為了整體評估在某個設(shè)備上的節(jié)點(diǎn),主控端生成相應(yīng)的工作進(jìn)程。 作為工作進(jìn)程可以在單個機(jī)器上管理一個或多個設(shè)備,設(shè)備是不僅通過名稱標(biāo)識,還標(biāo)識其所在工作進(jìn)程組的索引。 例如,特定組中的第一CPU可以由字符串“/ cpu:0”標(biāo)識。
? ? ? ? 2)placement algorithm(分配算法):為了決定哪一個節(jié)點(diǎn)分配給那一個設(shè)備,Tensorflow使用了一種特定分配算法。該算法模擬計(jì)算圖的運(yùn)算和遍歷從輸入張量到輸出張量的全部節(jié)點(diǎn)。在遍歷節(jié)點(diǎn)過程中,要決定哪一個可用設(shè)備D={d1,d2,...dn}運(yùn)行給定節(jié)點(diǎn)v,算法使用一個成本模型Cv(d)。這個成本模型考慮四種信息來決定最優(yōu)設(shè)備d = arg mind∈D Cν(d):
? ? ? ? 1)在給定設(shè)備上是否存在該節(jié)點(diǎn)的實(shí)現(xiàn)(核函數(shù))。比如,如果任何一個GPU上都不存在某種特定算子,那么選擇任意GPU的成本都是無限的。
? ? ? ? 2)估計(jì)一個節(jié)點(diǎn)的輸入和輸出丈量的大小(按字節(jié)計(jì)算)。
? ? ? ? 3)給定設(shè)備對于核函數(shù)的期望計(jì)算時間。
? ? ? ? 4)對于輸入丈量到相應(yīng)算子跨設(shè)備或者跨機(jī)器的傳輸成本進(jìn)行啟發(fā)式估算,萬一該節(jié)點(diǎn)已經(jīng)賦值的輸入張量所在設(shè)備和當(dāng)前設(shè)備并不一樣。
? ? 3)cross-device execution(跨設(shè)備運(yùn)行):只要是用戶擁有多個設(shè)備,Tensorflow通常都會把節(jié)點(diǎn)分配到這些設(shè)備上去。這個過程是通過把節(jié)點(diǎn)分類,然后一類分配到一個設(shè)備。這樣分配的話,必須處理跨設(shè)備分配的節(jié)點(diǎn)依賴問題。讓我們考慮A和B這樣兩個設(shè)備,其中節(jié)點(diǎn)v在設(shè)備A上。如果v的輸出張量作為另外兩個算子α, β的輸入,同時α, β在設(shè)備B上。那么就存在從A到B的跨設(shè)備的邊ν → α 和 ν → β。如下圖所示:

? ? ? ? 在實(shí)際運(yùn)行中,需要使用一些傳輸手段完成v的輸出張量從A設(shè)備,比如GPU,到設(shè)備B,比如CPU的過程。如下圖所示,send和recv兩類節(jié)點(diǎn)被生成完成這個傳輸過程。

? ? ? ? 最后,Tensorflow使用“規(guī)范化”來優(yōu)化(send,recv)對。在上圖所示的例子中,我們看到兩個recv節(jié)點(diǎn),分別連接α, β。然而,一種等價的,但是更為有效的辦法就是在設(shè)備B上只運(yùn)行一個recv,如下圖所示。
