天天看點

《面向機器智能的TensorFlow實踐》一3.2 在TensorFlow中定義資料流圖

本節書摘來自華章出版社《面向機器智能的tensorflow實踐》一書中的第3章,第3.2節,作者 山姆·亞伯拉罕(sam abrahams)丹尼亞爾·哈夫納(danijar hafner)[美] 埃裡克·厄威特(erik erwitt)阿裡爾·斯卡爾皮内裡(ariel scarpinelli),更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

3.2 在tensorflow中定義資料流圖

在本書中,你将接觸到多樣化的以及相當複雜的機器學習模型。然而,不同的模型在tensorflow中的定義過程卻遵循着相似的模式。當掌握了各種數學概念,并學會如何實作它們時,對tensorflow核心工作模式的了解将有助于你腳踏實地開展工作。幸運的是,這個工作流非常容易記憶,它隻包含兩個步驟:

1)定義資料流圖。

2)運作資料流圖(在資料上)。

這裡有一個顯而易見的道理,如果資料流圖不存在,那麼肯定無法運作它。頭腦中有這種概念是很有必要的,因為當你編寫代碼時會發現tensorflow功能是如此豐富。每次隻需關注上述工作流的一部分,有助于更周密地組織自己的代碼,并有助于明确接下來的工作方向。

本節将專注于講述在tensorflow中定義資料流圖的基礎知識,下一節将介紹當資料流圖建立完畢後如何運作。最後,我們會将這兩個步驟進行銜接,并展示如何建立在多次運作中狀态不斷發生變化并接收不同資料的資料流圖。

3.2.1 建構第一個tensorflow資料流圖

通過上一節的介紹,我們已對如下資料流圖頗為熟悉。

用于表示該資料流圖的tensorflow代碼如下所示:

下面來逐行解析這段代碼。首先,你會注意到下列導入語句:

毫不意外,這條語句的作用是導入tensorflow庫,并賦予它一個别名—tf。按照慣例,人們通常都是以這種形式導入tensorflow的,因為在使用該庫中的各種函數時,鍵入“tf”要比鍵入完整的“tensorflow”容易得多。

接下來研究前兩行變量指派語句:

這裡定義了“input”節點a和b。語句第一次引用了tensorflow operation:tf.constant()。在tensorflow中,資料流圖中的每個節點都被稱為一個operation(簡記為op)。各op可接收0個或多個tensor對象作為輸入,并輸出0個或多個tensor對象。要建立一個op,可調用與其關聯的python構造方法,在本例中,tf.constant()建立了一個“常量”op,它接收單個張量值,然後将同樣的值輸出給與其直接連接配接的節點。為友善起見,該函數自動将标量值6和3轉換為tensor對象。此外,我們還為這個構造方法傳入了一個可選的字元串參數name,用于對所建立的節點進行辨別。

如果暫時還無法充分了解什麼是operation,什麼是tensor對象,請不必擔心,本章稍後還會對這些概念進行詳細介紹。

這兩個語句定義了資料流圖中的另外兩個節點,而且它們都使用了之前定義的節點a和b。節點c使用了tf.mul op,它接收兩個輸入,然後将它們的乘積輸出。類似地,節點d使用了tf.add,該op可将它的兩個輸入之和輸出。對于這些op,我們均傳入了name參數(今後還将有大量此類用法)。請注意,無需專門對資料流圖中的邊進行定義,因為在tensorflow中建立節點時已包含了相應的op完成計算所需的全部輸入,tensorflow會自動繪制必要的連接配接。

最後的這行代碼定義了資料流圖的終點e,它使用tf.add的方式與節點d是一緻的。差別隻在于它的輸入來自節點c和節點d,這與資料流圖中的描述完全一緻。

通過上述代碼,便完成了第一個小規模資料流圖的完整定義。如果在一個python腳本或shell中執行上述代碼,它雖然可以運作,但實際上卻不會有任何實質性的結果輸出。請注意,這隻是整個流程的資料流圖定義部分,要想體驗一個資料流圖的運作效果,還需在上述代碼之後添加兩行語句,以将資料流圖終點的結果輸出。

如果在某個互動環境中運作這些代碼,如python shell或jupyter/ipython notebook,則可看到正确的輸出:

下面通過一個練習來實踐上述内容。

練習:在tensorflow中建構一個基本的資料流圖

動手實踐的時間已到!在這個練習中,你将編碼實作第一個tensorflow資料流圖,運作它的各個部件,并初步了解極為有用的工具—tensorboard。完成該練習後,你将能夠非常自如地建構基本的tensorflow資料流圖。

下面讓我們在tensorflow中實際定義一個資料流圖吧!請確定已成功安裝tensorflow,并啟動python依賴環境(如果使用的話),如virtualenv、conda、docker等。此外,如果是從源碼安裝tensorflow,請確定控制台的目前工作路徑不同于tensorflow的源檔案夾,否則在導入該庫時,python将會無所适從。現在,啟動一個互動式python會話(既可通過shell指令jupyter notebook使用jupyter notebook,也可通過指令python啟動簡易的python shell)。如果有其他偏好的方式互動式地編寫python代碼,也可放心地使用!

可将代碼寫入一個python檔案,然後以非互動方式運作,但運作資料流圖所産生的輸出在預設情況下是不會顯示出來的。為了使所定義的資料流圖的運作結果可見,同時獲得python解釋器對輸入的句法的即時回報(如果使用的是jupyter notebook),并能夠線上修正錯誤和修改代碼,強烈建議在互動式環境中完成這些例子。此外,你還會發現使用互動式tensorflow樂趣無窮!

首先需要加載tensorflow庫。可按照下列方式編寫導入語句:

導入過程需要持續幾秒鐘,待導入完成後,互動式環境便會等待下一行代碼的到來。如果安裝了有gpu支援的tensorflow,你可能還會看到一些輸出資訊,提示cuda庫已被導入。如果得到一條類似下面的錯誤提示:

請確定互動環境不是從tensorflow的源檔案夾啟動的。而如果得到一條類似下面的錯誤提示:

請複查tensorflow是否被正确安裝。如果使用的是virtualenv或conda,請確定啟動互動式python軟體時,tensorflow環境處于活動狀态。請注意,如果運作了多個終端,則将隻有一個終端擁有活動狀态的tensorflow環境。

假設上述導入語句在執行時沒有遇到任何問題,則可進入下一部分代碼:

這與在上面看到的代碼完全相同,可随意更改這些常量的數值或name參數。在本書中,為了保持前後一緻性,筆者會始終使用相同的數值。

這樣,代碼中便有了兩個實際執行某個數學函數的op。如果對使用tf.mul和tf.add感到厭倦,不妨将其替換為tf.sub、tf.div或tf.mod,這些函數分别執行的是減法、除法和取模運算。

[tf.div](https://www.tensorflow.org/versions/master/api_docs/python/math_ops.html#div)或者執行整數除法,或執行浮點數除法,具體取決于所提供的輸入類型。如果希望確定使用浮點數除法,請使用tf.truediv。

接下來定義資料流圖的終點:

你可能已經注意到,在調用上述op時,沒有顯示任何輸出,這是因為這些語句隻是在背景将一些op添加到資料流圖中,并無任何計算發生。為運作該資料流圖,需要建立一個tensorflow session對象:

session對象在運作時負責對資料流圖進行監督,并且是運作資料流圖的主要接口。在本練習之後,我們還将對session對象進行更為深入的探讨,但現在隻需了解在tensorflow中,如果希望運作自己的代碼,必須定義一個session對象。上述代碼将session對象賦給了變量sess,以便後期能夠對其進行通路。

關于interactivesession

tf.session有一個與之十分相近的變體—tf.interactivesession。它是專為互動式python軟體設計的(例如那些可能正在使用的環境),而且它采取了一些方法使運作代碼的過程更加簡便。不利的方面是在python檔案中編寫tensorflow代碼時用處不大,而且它會将一些作為tensorflow新手應當了解的資訊進行抽象。此外,它不能省去很多的按鍵次數。本書将始終使用标準的tf.session類。

至此,我們終于可以看到運作結果了。執行完上述語句後,你應當能夠看到所定義的資料流圖的輸出。對于本練習中的資料流圖,輸出為23。如果使用了不同的函數和輸入,則最終結果也可能不同。然而,這并非我們能做的全部,還可嘗試着将資料流圖中的其他節點傳入sess.run()函數,如:

通過這個調用,應該能夠看到中間節點c的輸出(在本例中為15)。tensorflow不會對你所建立的資料流圖做任何假設,程式并不會關心節點c是否是你希望得到的輸出!實際上,可對資料流圖中的任意op使用run()函數。當将某個op傳入sess.run()時,本質上是在通知tensorflow“這裡有一個節點,我希望得到它的輸出,請執行所有必要的運算來求取這個節點的輸出”。可反複嘗試該函數的使用,将資料流圖中其他節點的結果輸出。

還可将運作資料流圖所得到的結果儲存下來。下面将節點e的輸出儲存到一個名為output的python變量中:

棒極了!既然我們已經擁有了一個活動狀态的session對象,且資料流圖已定義完畢,下面來對它進行可視化,以确認其結構與之前所繪制的資料流圖完全一緻。為此可使用tensorboard,它是随tensorflow一起安裝的。為利用tensorboard,需要在代碼中添加下列語句:

下面分析這行代碼的作用。我們建立了一個tensorflow的summarywriter對象,并将它賦給變量writer。雖然在本練習中不準備用summarywriter對象完成其他操作,但今後會利用它儲存來自資料流圖的資料和概括統計量,是以我們習慣于将它賦給一個變量。為對summarywriter對象進行初始化,我們傳入了兩個參數。第一個參數是一個字元串輸出目錄,即資料流圖的描述在磁盤中的存放路徑。在本例中,所建立的檔案将被存放在一個名為my_graph的檔案夾中,而該檔案夾位于運作python代碼的那個路徑下。我們傳遞給summarywriter構造方法的第二個輸入是session對象的graph屬性。作為在tensorflow中定義的資料流圖管理器,tf.session對象擁有一個graph屬性,該屬性引用了它們所要追蹤的資料流圖。通過将該屬性傳入summarywriter構造方法,所構造的summarwriter對象便會将對該資料流圖的描述輸出到“my_graph”路徑下。summarywriter對象初始化完成之後便會立即寫入這些資料,是以一旦執行完這行代碼,便可啟動tensorboard。

回到終端,并鍵入下列指令,確定目前工作路徑與運作python代碼的路徑一緻(應該能看到列出的“my_graph”路徑):

從控制台中,應該能夠看到一些日志資訊列印出來,然後是消息“starting tensor-board on port 6066”。剛才所做的是啟動一個使用來自“my_graph”目錄下的資料的tensorboard伺服器。預設情況下,tensorboard伺服器啟動後會自動監聽端口6006—要通路tensorboard,可打開浏覽器并在位址欄輸入http://localhost:6006,然後将看到一個橙白主題的歡迎頁面:

請不要為警告消息“no scalar data was found”緊張,這僅僅表示我們尚未為tensor-board儲存任何概括統計量,進而使其無法正常顯示。通常,這個頁面會顯示利用summarywriter對象要求tensorflow所儲存的資訊。由于尚未儲存任何其他統計量,是以無内容可供顯示。盡管如此,這并不妨礙我們欣賞自己定義的美麗的資料流圖。單擊頁面頂部的“graph”連結,将看到類似下圖的頁面:

這才說得過去!如果資料流圖過小,則可通過在tensorboard上向上滾動滑鼠滾輪将其放大。可以看到,圖中的每個節點都用傳給每個op的name參數進行了辨別。如果單擊這些節點,還會得到一些關于它們的資訊,如它們依賴于哪些節點。還會發現,輸入節點a和b貌似重複出現了,但如果單擊或将滑鼠懸停在标簽為“input_a”的任何一個節點,會發現兩個節點同時高亮。這裡的資料流圖在外觀上與之前所繪制的并不完全相同,但它們本質上是一樣的,因為“input”節點不過是顯示了兩次而已,效果還是相當驚豔的!

就是這樣!現在已經正式地編寫并運作了第一個tensorflow資料流圖,而且還在tensorboard中對其進行了檢查!隻用這樣少的幾行代碼就完成如此多的任務真是棒極了!

要想更多地實踐,可嘗試在資料流圖中添加更多節點,并試驗一些之前介紹過的不同數學運算,然後添加少量tf.constant節點,運作所添加的不同節點,確定真正了解了資料在資料流圖中的流動方式。

完成資料流圖的構造之後,需要将session對象和summarwriter對象關閉,以釋放資源并執行一些清理工作:

從技術角度講,當程式運作結束後(若使用的是互動式環境,當關閉或重新開機python核心時),session對象會自動關閉。盡管如此,筆者仍然建議顯式關閉session對象,以避免任何詭異的邊界用例的出現。

下面給出本練習對應的完整python代碼:

3.2.2 張量思維

在學習資料流圖的基礎知識時,使用簡單的标量值是很好的選擇。既然我們已經掌握了“資料流”,下面不妨熟悉一下張量的概念。

如前所述,所謂張量,即n維矩陣的抽象。是以,1d張量等價于向量,2d張量等價于矩陣,對于更高維數的張量,可稱“n維張量”或“n階張量”。有了這一概念,便可對之前的示例資料流圖進行修改,使其可使用張量。

現在不再使用兩個獨立的輸入節點,而是換成了一個可接收向量(或1階張量)的節點。與之前的版本相比,這個新的流圖有如下優點:

1)客戶隻需将輸入送給單個節點,簡化了流圖的使用。

2)那些直接依賴于輸入的節點現在隻需追蹤一個依賴節點,而非兩個。

3)這個版本的流圖可接收任意長度的向量,進而使其靈活性大大增強。我們還可對這個流圖施加一條嚴格的限制,如要求輸入的長度必須為2(或任何我們希望的長度)。

按下列方式修改之前的代碼,便可在tensorflow中實作這種變動:

除了調整變量名稱外,主要改動還有以下兩處:

1)将原先分離的節點a和b替換為一個統一的輸入節點(不止包含之前的節點a)。傳入一組數值後,它們會由tf.constant函數轉化為一個1階張量。

2)之前隻能接收标量值的乘法和加法op,現在用tf.reduce_prod()和tf.reduce_sum()函數重新定義。當給定某個張量作為輸入時,這些函數會接收其所有分量,然後分别将它們相乘或相加。

在tensorflow中,所有在節點之間傳遞的資料都為tensor對象。我們已經看到,tensorflow op可接收标準python資料類型,如整數或字元串,并将它們自動轉化為張量。手工建立tensor對象有多種方式(即無需從外部資料源讀取),下面對其中一部分進行介紹。

注意:本書在讨論代碼時,會不加區分地使用“張量”或“tensor對象”。

1. python原生類型

tensorflow可接收python數值、布爾值、字元串或由它們構成的清單。單個數值将被轉化為0階張量(或标量),數值清單将被轉化為1階張量(向量),由清單構成的清單将被轉化為2階張量(矩陣),以此類推。下面給出一些例子。

tensorflow資料類型

到目前為止,我們尚未見到布爾值或字元串,但可将張量視為一種以結構化格式儲存任意資料的方式。顯然,數學函數無法對字元串進行處理,而字元串解析函數也無法對數值型資料進行處理,但有必要了解tensorflow所能處理的資料類型并不局限于數值型資料!下面給出tensorflow中可用資料類型的完整清單。

資料類型(dtype)  描  述

tf.float32     32位浮點型

tf.float64     64位浮點型

tf.int8   8位有符号整數

tf.int16  16位有符号整數

tf.int32  32位有符号整數

tf.int64  64位有符号整數

tf.uint8  8位無符号整數

tf.string 字元串(作為非unicode編碼的位元組數組)

tf.bool  布爾型

tf.complex64     複數,實部和虛部分别為32位浮點型

tf.qint8  8位有符号整數(用于量化op)

tf.qint32      32位有符号整數(用于量化op)

tf.quint8      8位無符号整數(用于量化op)

利用python類型指定tensor對象既容易又快捷,且對為一些想法提供原型非常有用。然而,很不幸,這種方式也會帶來無法忽視的不利方面。tensorflow有數量極為龐大的資料類型可供使用,但基本的python類型缺乏對你希望使用的資料類型的種類進行明确聲明的能力。是以,tensorflow不得不去推斷你期望的是何種資料類型。對于某些類型,如字元串,推斷過程是非常簡單的,但對于其他類型,則可能完全無法做出推斷。例如,在python中,所有整數都具有相同的類型,但tensorflow卻有8位、16位、32位和64位整數類型之分。當将資料傳入tensorflow時,雖有一些方法可将資料轉化為恰當的類型,但某些資料類型仍然可能難以正确地聲明,例如複數類型。是以,更常見的做法是借助numpy數組手工定義tensor對象。

2. numpy數組

tensorflow與專為操作n維數組而設計的科學計算軟體包numpy是緊密內建在一起的。如果之前沒有使用過numpy,筆者強烈推薦你從大量可用的入門材料和文檔中選擇一些進行學習,因為它已成為資料科學的通用語言。tensorflow的資料類型是基于numpy的資料類型的。實際上,語句np.int32==tf.int32的結果為true。任何numpy數組都可傳遞給tensorflow

op,而且其美妙之處在于可以用最小的代價輕易地指定所需的資料類型。

字元串資料類型

對于字元串資料類型,有一個“特别之處”需要注意。對于數值類型和布爾類型,tenosrflow和numpy dtype屬性是完全一緻的。然而,在numpy中并無與tf.string精确對應的類型,這是由numpy處理字元串的方式決定的。也就是說,tensorflow可以從numpy中完美地導入字元串數組,隻是不要在numpy中顯式指定dtype屬性。

有一個好處是,在運作資料流圖之前或之後,都可以利用numpy庫的功能,因為從session.run方法所傳回的張量均為numpy數組。下面模仿之前的例子,給出一段用于示範建立numpy數組的示例代碼:

雖然tensorflow是為了解numpy原生資料類型而設計的,但反之不然。請不要嘗試用tf.int32去初始化一個numpy數組!

手工指定tensor對象時,使用numpy是推薦的方式。

3.2.3 張量的形狀

在整個tensorflow庫中,會經常看到一些引用了某個張量對象的“shape”屬性的函數和op。這裡的“形狀”是tensorflow的專有術語,它同時刻畫了張量的維(階)數以及每一維的長度。張量的形狀可以是包含有序整數集的清單(list)或元組(tuple):清單中元素的數量與維數一緻,且每個元素描述了相應次元上的長度。例如,清單[2, 3]描述了一個2階張量的形狀,其第1個維上的長度為2,第2個維上的長度為3。注意,無論元組(用一對小括号包裹),還是清單(用一對方括号包裹),都可用于定義張量的形狀。下面通過更多的例子來說明這一點:

除了能夠将張量的每一維指定為固定長度,也可将none作為某一維的值,使該張量具有可變長度。此外,将形狀指定為none(而非使用包含none的清單或元組)将通知tensorflow允許一個張量為任意形狀,即張量可擁有任意維數,且每一維都可具有任意長度。

如果需要在資料流圖的中間擷取某個張量的形狀,可以使用tf.shape op。它的輸入為希望擷取其形狀的tensor對象,輸出為一個int32類型的向量:

請記住,與其他op一樣,tf.shape隻能通過session對象得到執行。

再次提醒:張量隻是矩陣的一個超集!

3.2.4 tensorflow的operation

上文曾經介紹過,tensorflow operation也稱op,是一些對(或利用)tensor對象執行運算的節點。計算完畢後,它們會傳回0個或多個張量,可在以後為資料流圖中的其他op所使用。為建立op,需要在python中調用其構造方法。調用時,需要傳入計算所需的所有tensor參數(稱為輸入)以及為正确建立op的任何附加資訊(稱為屬性)。python構造方法将傳回一個指向所建立op的輸出(0個或多個tensor對象)的句柄。能夠傳遞給其他op或session.run的輸出如下:

無輸入、無輸出的運算

是的,這意味着從技術角度講,有些 op既無任何輸入,也無任何輸出。op的功能并不隻限于資料運算,它還可用于如狀态初始化這樣的任務。本章中,我們将回顧一些這樣的非數學op,但請記住,并非所有節點都需要與其他節點連接配接。

除了輸入和屬性外,每個op構造方法都可接收一個字元串參數—name,作為其輸入。在上面的練習中我們已經了解到,通過提供name參數,可用描述性字元串來指代某個特定op:

在這個例子中,我們為加法op賦予了名稱“my_add_op”,這樣便可在使用如tensor-board等工具時引用該op。

如果希望在一個資料流圖中對不同op複用相同的name參數,則無需為每個name參數手工添加字首或字尾,隻需利用name_scope以程式設計的方式将這些運算組織在一起便可。在本章最後的練習中,将簡要介紹名稱作用域(name scope)的基礎知識。

運算符重載

tensorflow還對常見數學運算符進行了重載,以使乘法、加法、減法及其他常見運算更加簡潔。如果運算符有一個或多個參數(操作對象)為tensor對象,則會有一個tensorflow op被調用,并被添加到資料流圖中。例如,可按照下列方式輕松地實作兩個張量的加法:

# 假設a和b均為tensor對象,且形狀比對

下面給出可用于張量的重載運算符的完整清單。

一進制運算符

運算符  相關tensorflow運算     描  述

-x   tf.neg() 傳回x中每個元素的相反數

~x  tf.logical_not()   傳回x中每個元素的邏輯非。隻适用于dtype為tf.bool的tensor對象

abs(x)   tf.abs() 傳回x中每個元素的絕對值

二進制運算符

x+y tf.add() 将x和y逐元素相加

x-y  tf.sub() 将x和y逐元素相減

x*y tf.mul() 将x和y逐元素相乘

x/y (python 2.x) tf.div()  給定整數張量時,執行逐元素的整數除法;給定浮點型張量時,将執行浮點數(“真正的”)除法

x/y (python 3.x) tf.truediv()  逐元素的浮點數除法(包括分子分母為整數的情形)

x//y (python

3.x)     tf.floordiv() 逐元素的向下取整除法,不傳回餘數

x%y     tf.mod()     逐元素取模

x**y     tf.pow()      逐一計算x中的每個元素為底數,y中相應元素為指數時的幂

x<y tf.less() 逐元素地計算x<y的真值表

x<=y    tf.less_equal()   逐元素地計算x≤y的真值表

x>y tf.greater()  逐元素地計算x>y的真值表

x>=y    tf.greater_equal()     逐元素地計算x≥y的真值表

x&y tf.logical_and()  逐元素地計算x & y的真值表,每個元素的dtype屬性必須為tf.bool

x|y tf.logical_or()    逐元素地計算x|y的真值表,每個元素的dtype屬性必須為tf.bool

x^y tf.logical_xor()   逐元素地計算x^y的真值表,每個元素的dtype屬性必須為tf.bool

利用這些重載運算符可快速地對代碼進行整合,但卻無法為這些op指定name值。如果需要為op指定name值,請直接調用tensorflow

op。

從技術角度講,==運算符也被重載了,但它不會傳回一個布爾型的tensor對象。它所判斷的是兩個tensor對象名是否引用了同一個對象,若是,則傳回true,否則,傳回false。這個功能主要是在tensorflow内部使用。如果希望檢查張量值是否相同,請使用tf.equal()和tf.not_equal()。

3.2.5 tensorflow的graph對象

到目前為止,我們對資料流圖的了解僅限于在tensorflow中無處不在的某種抽象概念,而且對于開始編碼時op如何自動依附于某個資料流圖并不清楚。既然已經接觸了一些例子,下面來研究tensorflow的graph對象,學習如何建立更多的資料流圖,以及如何讓多個流圖協同工作。

建立graph對象的方法非常簡單,它的構造方法不需要接收任何參數:

graph對象初始化完成後,便可利用graph.as_default()方法通路其上下文管理器,為其添加op。結合with語句,可利用上下文管理器通知tensorflow我們需要将一些op添加到某個特定graph對象中:

你可能會好奇,為什麼在上面的例子中不需要指定我們希望将op添加到哪個graph對象?原因是這樣的:為友善起見,當tensorflow庫被加載時,它會自動建立一個graph對象,并将其作為預設的資料流圖。是以,在graph.as_default()上下文管理器之外定義的任何op、tensor對象都會自動放置在預設的資料流圖中:

如果希望獲得預設資料流圖的句柄,可使用tf.get_default_graph()函數:

在大多數tensorflow程式中,隻使用預設資料流圖就足夠了。然而,如果需要定義多個互相之間不存在依賴關系的模型,則建立多個graph對象十分有用。當需要在單個檔案中定義多個資料流圖時,最佳實踐是不使用預設資料流圖,或為其立即配置設定句柄。這樣可以保證各節點按照一緻的方式添加到每個資料流圖中。

1.正确的實踐—建立新的資料流圖,将預設資料流圖忽略

2.正确的實踐—擷取預設資料流圖的句柄

3.錯誤的實踐—将預設資料流圖和使用者建立的資料流圖混合使用

此外,從其他tensorflow腳本中加載之前定義的模型,并利用graph.as_graph_def()和tf.import_graph_def()函數将其賦給graph對象也是可行的。這樣,使用者便可在同一個python檔案中計算和使用若幹獨立的模型的輸出。本書後續章節将介紹資料流圖的導入和導出。

3.2.6 tensorflow session

在之前的練習中,我們曾經介紹過,session類負責資料流圖的執行。構造方法tf.session()接收3個可選參數:

target指定了所要使用的執行引擎。對于大多數應用,該參數取為預設的空字元串。在分布式設定中使用session對象時,該參數用于連接配接不同的tf.train.server執行個體(本書後續章節将對此進行介紹)。

graph參數指定了将要在session對象中加載的graph對象,其預設值為none,表示将使用目前預設資料流圖。當使用多個資料流圖時,最好的方式是顯式傳入你希望運作的graph對象(而非在一個with語句塊内建立session對象)。

config參數允許使用者指定配置session對象所需的選項,如限制cpu或gpu的使用數目,為資料流圖設定優化參數及日志選項等。

在典型的tensorflow程式中,建立session對象時無需改變任何預設構造參數。

請注意,下列兩種調用方式是等價的:

一旦建立完session對象,便可利用其主要的方法run()來計算所期望的tensor對象的輸出:

session.run()方法接收一個參數fetches,以及其他三個可選參數:feed_dict、options和run_metadata。本書不打算對options和run_metadata進行介紹,因為它們尚處在實驗階段(是以以後很可能會有變動),且目前用途非常有限,但了解feed_dict非常重要,下文将對其進行講解。

1. fetches參數

fetches參數接收任意的資料流圖元素(op或tensor對象),後者指定了使用者希望執行的對象。如果請求對象為tensor對象,則run()的輸出将為一numpy數組;如果請求對象為一個op,則輸出将為none。

在上面的例子中,我們将fetches參數取為張量b(tf.mul op的輸出)。tensorflow便會得到通知,session對象應當找到為計算b的值所需的全部節點,順序執行這些節點,然後将b的值輸出。我們還可傳入一個資料流圖元素的清單:

當fetches為一個清單時,run()的輸出将為一個與所請求的元素對應的值的清單。在本例中,請求計算a和b的值,并保持這種次序。由于a和b均為張量,是以會接收到作為輸出的它們的值。

除了利用fetches擷取tensor對象輸出外,還将看到這樣的例子:有時也會賦予fetches一個指向某個op的句柄,這是在運作中的一種有價值的用法。tf.initialize_all_variables()便是一個這樣的例子,它會準備将要使用的所有tensorflow variable對象(本章稍後将介紹variable對象)。我們仍然将該op傳給fetches參數,但session.run()的結果将為none:

2. feed_dict參數

參數feed_dict用于覆寫資料流圖中的tensor對象值,它需要python字典對象作為輸入。字典中的“鍵”為指向應當被覆寫的tensor對象的句柄,而字典的“值”可以是數字、字元串、清單或numpy數組(之前介紹過)。這些“值”的類型必須與tensor的“鍵”相同,或能夠轉換為相同的類型。下面通過一些代碼來展示如何利用feed_dict重寫之前的資料流圖中a的值:

請注意,即便a的計算結果通常為7,我們傳給feed_dict的字典也會将它替換為15。在相當多的場合中,feed_dict都極為有用。由于張量的值是預先提供的,資料流圖不再需要對該張量的任何普通依賴節點進行計算。這意味着如果有一個規模較大的資料流圖,并希望用一些虛構的值對某些部分進行測試,tensorflow将不會在不必要的計算上浪費時間。對于指定輸入值,feed_dict也十分有用,在稍後的占位符一節中我們将對此進行介紹。

session對象使用完畢後,需要調用其close()方法,将那些不再需要的資源釋放:

或者,也可以将session對象作為上下文管理器加以使用,這樣當代碼離開其作用域後,該session對象将自動關閉:

也可利用session類的as_default()方法将session對象作為上下文管理器加以使用。類似于graph對象被某些op隐式使用的方式,可将一個session對象設定為可被某些函數自動使用。這些函數中最常見的有operation.run()和tensor.eval(),調用這些函數相當于将它們直接傳入session.run()函數。

關于interactivesession的進一步讨論

在本書之前的章節中,我們提到interactivesession是另外一種類型的tensorflow會話,但我們不打算使用它。interactivesession對象所做的全部内容是在運作時将其作為預設會話,這在使用互動式python shell的場合是非常友善的,因為可使用a.eval()或a.run(),而無須顯式鍵入sess.run([a])。然而,如果需要同時使用多個會話,則事情會變得有些棘手。筆者發現,在運作資料流圖時,如果能夠保持一緻的方式,将會使調試變得更容易,是以我們堅持使用正常的session對象。

既然已對運作資料流圖有了切實的了解,下面來探讨如何恰當地指定輸入節點,并結合它們來使用feed_dict。

3.2.7 利用占位節點添加輸入

之前定義的資料流圖并未使用真正的“輸入”,它總是使用相同的數值5和3。我們真正希望做的是從客戶那裡接收輸入值,這樣便可對資料流圖中所描述的變換以各種不同類型的數值進行複用,借助“占位符”可達到這個目的。正如其名稱所預示的那樣,占位符的行為與tensor對象一緻,但在建立時無須為它們指定具體的數值。它們的作用是為運作時即将到來的某個tensor對象預留位置,是以實際上變成了“輸入”節點。利用tf.placeholder op可建立占位符:

調用tf.placeholder()時,dtype參數是必須指定的,而shape參數可選:

dtype指定了将傳給該占位符的值的資料類型。該參數是必須指定的,因為需要確定不出現類型不比對的錯誤。

shape指定了所要傳入的tensor對象的形狀。請參考前文中對tensor形狀的讨論。shape參數的預設值為none,表示可接收任意形狀的tensor對象。

與任何op一樣,也可在tf.placeholder中指定一個name辨別符。

為了給占位符傳入一個實際的值,需要使用session.run()中的feed_dict參數。我們将指向占位符輸出的句柄作為字典(在上述代碼中,對應變量a)的“鍵”,而将希望傳入的tensor對象作為字典的“值”:

必須在feed_dict中為待計算的節點的每個依賴占位符包含一個鍵值對。在上面的代碼中,需要計算d的輸出,而它依賴于a的輸出。如果還定義了一些d不依賴的其他占位符,則無需将它們包含在feed_dict中。

placeholder的值是無法計算的—如果試圖将其傳入session.run(),将引發一個異常。

3.2.8 variable對象

1.建立variable對象

tensor對象和op對象都是不可變的(immutable),但機器學習任務的本質決定了需要一種機制儲存随時間變化的值。借助tensorflow中的variable對象,便可達到這個目的。variable對象包含了在對session.run()多次調用中可持久化的可變張量值。variable對象的建立可通過variable類的構造方法tf.variable()完成:

variable對象可用于任何可能會使用tensor對象的tensorflow函數或op中,其目前值将傳給使用它的op:

variables對象的初值通常是全0、全1或用随機數填充的階數較高的張量。為使建立具有這些常見類型初值的張量更加容易,tensorflow提供了大量輔助op,如tf.zeros()、tf.ones()、tf.random_normal()和tf.random_uniform(),每個op都接收一個shape參數,以指定所建立的tensor對象的形狀:

除了tf.random_normal()外,經常還會看到人們使用tf.truncated_normal(),因為它不會建立任何偏離均值超過2倍标準差的值,進而可以防止有一個或兩個元素與該張量中的其他元素顯著不同的情況出現:

可像手工初始化張量那樣将這些op作為variable對象的初值傳入:

2. variable對象的初始化

variable對象與大多數其他tensorflow對象在graph中存在的方式都比較類似,但它們的狀态實際上是由session對象管理的。是以,為使用variable對象,需要采取一些額外的步驟—必須在一個session對象内對variable對象進行初始化。這樣會使session對象開始追蹤這個variable對象的值的變化。variable對象的初始化通常是通過将tf.initialize_all_variables() op傳給session.run()完成的:

如果隻需要對資料流圖中定義的一個variable對象子集初始化,可使用tf.initialize_variables()。該函數可接收一個要進行初始化的variable對象清單:

3. variable對象的修改

要修改variable對象的值,可使用variable.assign()方法。該方法的作用是為variable對象賦予新值。請注意,variable.assign()是一個op,要使其生效必須在一個session對象中運作:

對于variable對象的簡單自增和自減,tensorflow提供了variable.assign_add()方法和variable.assign_sub()方法:

由于不同session對象會各自獨立地維護variable對象的值,是以每個session對象都擁有自己的、在graph對象中定義的variable對象的目前值:

如果希望将所有variable對象的值重置為初始值,則隻需再次調用tf.initialize_all_variables()(如果隻希望對部分variable對象重新初始化,可調用tf.initialize_variables()):

4. trainable參數

在本書的後續章節将介紹各種能夠自動訓練機器學習模型的optimizer類,這意味着這些類将自動修改variable對象的值,而無須顯式做出請求。在大多數情況下,這與讀者的期望一緻,但如果要求graph對象中的一些variable對象隻可手工修改,而不允許使用optimizer類時,可在建立這些variable對象時将其trainable參數設為false:

對于疊代計數器或其他任何不涉及機器學習模型計算的variable對象,通常都需要這樣設定。