《麵向機器智能的TensorFlow實踐》TensorFlow與機器學習基礎
TensorFlow基礎
3.1 數據流圖簡介
本節將脫離TensorFlow的語境,介紹一些數據流圖的基礎知識,內容包括節點、邊和節點依賴關係的定義。此外,為對一些關鍵原理進行解釋,本章還提供了若幹實例。如果你對數據流圖已有一定使用經驗或已運用自如,可直接跳過本節。
3.1.1 數據流圖基礎
借助TensorFlow API用代碼描述的數據流圖是每個TensorFlow程序的核心。毫不意外,數據流圖這種特殊類型的有向圖正是用於定義計算結構的。在TensorFlow中,數據流圖本質上是一組鏈接在一起的函數,每個函數都會將其輸出傳遞給0個、1個或更多位於這個級聯鏈上的其他函數。按照這種方式,用戶可利用一些很小的、為人們所充分理解的數學函數構造數據的複雜變換。下麵來看一個比較簡單的例子。
上圖展示了可完成基本加法運算的數據流圖。在該圖中,加法運算是用圓圈表示的,它可接收兩個輸入(以指向該函數的箭頭表示),並將1和2之和3輸出(對應從該函數引出的箭頭)。該函數的運算結果可傳遞給其他函數,也可直接返回給客戶。
該數據流圖可用如下簡單公式表示:
f (1, 2)=1+2=3
上麵的例子解釋了在構建數據流圖時,兩個基礎構件—節點和邊是如何使用的。下麵回顧節點和邊的基本性質:
節點(node):在數據流圖的語境中,節點通常以圓圈、橢圓和方框表示,代表了對數據所做的運算或某種操作。在上例中,“add” 對應於一個孤立節點。
邊(edge):對應於向Operation傳入和從Operation傳出的實際數值,通常以箭頭表示。在“add”這個例子中,輸入1和2均為指向運算節點的邊,而輸出3則為從運算節點引出的邊。可從概念上將邊視為不同Operation之間的連接,因為它們將信息從一個節點傳輸到另一個節點。
下麵來看一個更有趣的例子。
相比之前的例子,上圖所示的數據流圖略複雜。由於數據是從左側流向右側的(如箭頭方向所示),因此可從最左端開始對這個數據流圖進行分析:
1)最開始時,可看到兩個值5和3流入該數據流圖。它們可能來自另一個數據流圖,也可能讀取自某個文件,或是由客戶直接輸入。
2)這些初始值被分別傳入兩個明確的“input”節點(圖中分別以a、b標識)。這些“input”節點的作用僅僅是傳遞它們的輸入值—節點a接收到輸入值5後,將同樣的數值輸出給節點c和節點d,節點b對其輸入值3也完成同樣的動作。
3)節點c代表乘法運算。它分別從節點a和b接收輸入值5和3,並將運算結果15輸出到節點e。與此同時,節點d對相同的兩個輸入執行加法運算,並將計算結果8傳遞給節點e。
4)最後,該數據流圖的終點—節點e是另一個“add”節點。它接收輸入值15和8,將兩者相加,然後輸出該數據流圖的最終結果23。
下麵說明為何上述圖形表示看起來像是一組公式:
a=input1; b=input2
c=a·b; d=a+b
e=c+d
當a=5、b=3時,若要求解e,隻需依次代入上述公式。
a=5; b=3
e=a·b+(a+b)
e=5·3+(5+3)=23
經過上述步驟,便完成了計算,這裏有一些概念值得重點說明:
上述使用“input”節點的模式十分有用,因為這使得我們能夠將單個輸入值傳遞給大量後繼節點。如果不這樣做,客戶(或傳入這些初值的其他數據源)便不得不將輸入值顯式傳遞給數據流圖中的多個節點。按照這種模式,客戶隻需保證一次性傳入恰當的輸入值,而如何對這些輸入重複使用的細節便被隱藏起來。稍後,我們將對數據流圖的抽象做更深入的探討。
突擊小測驗。哪一個節點將首先執行運算?是乘法節點c還是加法節點d?答案是:無從知曉。僅憑上述數據流圖,無法推知c和d中的哪一個節點將率先執行。有的讀者可能會按照從左到右、自上而下的順序閱讀該數據流圖,從而做出節點c先運行的假設。但我們需要指出,在該數據流圖中,將節點d繪製在c的上方也未嚐不可。也可能有些讀者認為這些節點會並發執行,但考慮到各種實現細節或硬件的限製,實際情況往往並非總是如此。實際上,最好的方式是將它們的執行視為相互獨立。由於節點c並不依賴於來自節點d的任何信息,所以節點c在完成自身的運算時無需關心節點d的狀態如何。反之亦然,節點d也不需要任何來自節點c的信息。在本章稍後,還將對節點依賴關係進行更深入的介紹。
接下來,對上述數據流圖稍做修改。
主要的變化有兩點:
1)來自節點b的“input”值3現在也傳遞給了節點e。
2)節點e中的函數“add”被替換為“sum”,表明它可完成兩個以上的數的加法運算。
你已經注意到,上圖在看起來被其他節點“隔離”的兩個節點之間添加了一條邊。一般而言,任何節點都可將其輸出傳遞給數據流圖中的任意後繼節點,而無論這兩者之間發生了多少計算。數據流圖甚至可以擁有下圖所示的結構,它仍然是完全合法的。
通過這兩個數據流圖,想必你已能夠初步感受到對數據流圖的輸入進行抽象所帶來的好處。我們能夠對數據流圖中內部運算的精確細節進行操控,但客戶隻需了解將何種信息傳遞給那兩個輸入節點則可。我們甚至可以進一步抽象,將上述數據流圖表示為如下的黑箱。
這樣,我們便可將整個節點序列視為擁有一組輸入和輸出的離散構件。這種抽象方式使得對級聯在一起的若幹個運算組進行可視化更加容易,而無需關心每個部件的具體細節。
3.1.2 節點的依賴關係
在數據流圖中,節點之間的某些類型的連接是不被允許的,最常見的一種是將造成循環依賴(circular dependency)的連接。為理解“循環依賴”這個概念,需要先理解何為“依賴關係”。再次觀察下麵的數據流圖。
循環依賴這個概念其實非常簡單:對於任意節點A,如果其輸出對於某個後繼節點B的計算是必需的,則稱節點A為節點B的依賴節點。如果某個節點A和節點B彼此不需要來自對方的任何信息,則稱兩者是獨立的。為對此進行可視化,首先觀察當乘法節點c出於某種原因無法完成計算時會出現何種情況。
可以預見,由於節點e需要來自節點c的輸出,因此其運算無法執行,隻能無限等待節點c的數據的到來。容易看出,節點c和節點d均為節點e的依賴節點,因為它們均將信息直接傳遞到最後的加法函數。然而,稍加思索便可看出節點a和節點b也是節點e的依賴節點。如果輸入節點中有一個未能將其輸入傳遞給數據流圖中的下一個函數,情形會怎樣?
可以看出,若將輸入中的某一個移除,會導致數據流圖中的大部分運算中斷,從而表明依賴關係具有傳遞性。即,若A依賴於B,而B依賴於C,則A依賴於C。在本例中,最終節點e依賴於節點c和節點d,而節點c和節點d均依賴於輸入節點b。因此,最終節點e也依賴於輸入節點b。同理可知節點e也依賴於輸入節點a。此外,還可對節點e的不同依賴節點進行區分:
1)稱節點e直接依賴於節點c和節點d。即為使節點e的運算得到執行,必須有直接來自節點c和節點d的數據。
2)稱節點e間接依賴於節點a和節點b。這表示節點a和節點b的輸出並未直接傳遞到節點e,而是傳遞到某個(或某些)中間節點,而這些中間節點可能是節點e的直接依賴節點,也可能是間接依賴節點。這意味著一個節點可以是被許多層的中間節點相隔的另一個節點的間接依賴節點(且這些中間節點中的每一個也是後者的依賴節點)。
最後來觀察將數據流圖的輸出傳遞給其自身的某個位於前端的節點時會出現何種情況。
不幸的是,上麵的數據流圖看起來無法工作。我們試圖將節點e的輸出送回節點b,並希望該數據流圖的計算能夠循環進行。這裏的問題在於節點e現在變為節點b的直接依賴節點;而與此同時,節點e仍然依賴於節點b(前文已說明過)。其結果是節點b和節點e都無法得到執行,因為它們都在等待對方計算的完成。
也許你非常聰明,決定將傳遞給節點b或節點e的值設置為某個初始狀態值。畢竟,這個數據流圖是受我們控製的。不妨假設節點e的輸出的初始狀態值為1,使其先工作起來。
上圖給出了經過幾輪循環各數據流圖中各節點的狀態。新引入的依賴關係製造了一個無窮反饋環,且該數據流圖中的大部分邊都趨向於無窮大。然而,出於多種原因,對於像TensorFlow這樣的軟件,這種類型的無限循環是非常不利的。
1)由於數據流圖中存在無限循環,因此程序無法以優雅的方式終止。
2)依賴節點的數量變為無窮大,因為每輪迭代都依賴於之前的所有輪次的迭代。不幸的是,在統計依賴關係時,每個節點都不會隻被統計一次,每當其輸出發生變化時,它便會被再次記為依賴節點。這就使得追蹤依賴信息變得不可能,而出於多種原因(詳見本節的最後一部分),這種需求是至關重要的。
3)你經常會遇到這樣的情況:被傳遞的值要麼在正方向變得非常大(從而導致上溢),要麼在負方向變得非常大(導致下溢),或者非常接近於0(使得每輪迭代在加法上失去意義)。
基於上述考慮,在TensorFlow中,真正的循環依賴關係是無法表示的,這並非壞事。在實際使用中,完全可通過對數據流圖進行有限次的複製,然後將它們並排放置,並將代表相鄰迭代輪次的副本的輸出與輸入串接。該過程通常被稱為數據流圖的“展開”(unrolling)。第6章還將對此進行更為詳細的介紹。為了以圖形化的方式展示數據流圖的展開效果,下麵給出一個將循環依賴展開5次後的數據流圖。
對這個數據流圖進行分析,便會發現這個由各節點和邊構成的序列等價於將之前的數據流圖遍曆5次。請注意原始輸入值(以數據流圖頂部和底部的跳躍箭頭表示)是傳遞給數據流圖的每個副本的,因為代表每輪迭代的數據流圖的每個副本都需要它們。按照這種方式將數據流圖展開,可在保持確定性計算的同時模擬有用的循環依賴。
既然我們已理解了節點的依賴關係,接下來便可分析為什麼追蹤這種依賴關係十分有用。不妨假設在之前的例子中,我們隻希望得到節點c(乘法節點)的輸出。我們已經定義了完整的數據流圖,其中包含獨立於節點c和節點e(出現在節點c的後方)的節點d,那麼是否必須執行整個數據流圖的所有運算,即便並不需要節點d和節點e的輸出?答案當然是否定的。觀察該數據流圖,不難發現,如果隻需要節點c的輸出,那麼執行所有節點的運算便是浪費時間。但這裏的問題在於:如何確保計算機隻對必要的節點執行運算,而無需手工指定?答案是:利用節點之間的依賴關係!
這背後的概念相當簡單,我們唯一需要確保的是為每個節點的直接(而非間接)依賴節點維護一個列表。可從一個空棧開始,它最終將保存所有我們希望運行的節點。從你希望獲得其輸出的節點開始。顯然它必須得到執行,因此令其入棧。接下來查看該輸出節點的依賴節點列表,這意味著為計算輸出,那些節點必須運行,因此將它們全部入棧。然後,對所有那些節點進行檢查,看它們的直接依賴節點有哪些,然後將它們全部入棧。繼續這種追溯模式,直到數據流圖中的所有依賴節點均已入棧。按照這種方式,便可保證我們獲得運行該數據流圖所需的全部節點,且隻包含所有必需的節點。此外,利用上述棧結構,可對其中的節點進行排序,從而保證當遍曆該棧時,其中的所有節點都會按照一定的次序得到運行。唯一需要注意的是需要追蹤哪些節點已經完成了計算,並將它們的輸出保存在內存中,以避免對同一節點反複計算。按照這種方式,便可確保計算量盡可能地精簡,從而在規模較大的數據流圖上節省以小時計的寶貴處理時間。
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,可打開瀏覽器並在地址欄輸入https://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中每個元素的絕對值
二元運算符
運算符 相關TensorFlow運算 描 述
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對象,通常都需要這樣設置。
3.3 通過名稱作用域組織數據流圖
現在開始介紹構建任何TensorFlow數據流圖所必需的核心構件。到目前為止,我們隻接觸了包含少量節點和階數較小的張量的非常簡單的數據流圖,但現實世界中的模型往往會包含幾十或上百個節點,以及數以百萬計的參數。為使這種級別的複雜性可控,TensorFlow當前提供了一種幫助用戶組織數據流圖的機製—名稱作用域(name scope)。
名稱作用域非常易於使用,且在用TensorBoard對Graph對象可視化時極有價值。本質上,名稱作用域允許將Op劃分到一些較大的、有名稱的語句塊中。當以後用TensorBoard加載數據流圖時,每個名稱作用域都將對其自己的Op進行封裝,從而獲得更好的可視化效果。名稱作用域的基本用法是將Op添加到with tf.name_scope(<name>)語句塊中。
為了在TensorBoard中看到這些名稱作用域的效果,可打開一個SummaryWriter對象,並將Graph對象寫入磁盤。
由於SummaryWriter對象會將數據流圖立即導出,可在運行完上述代碼便啟動TensorBoard。導航到運行上述腳本的路徑,並啟動TensorBoard:
與之前一樣,上述命令將會在用戶的本地計算機啟動一個端口號為6006的TensorBoard服務器。打開瀏覽器,並在地址欄鍵入localhost:6006,導航至“Graph”標簽頁,用戶將看到類似於下圖的結果。
我們添加到該數據流圖中的add和mul Op並未立即顯示出來,所看到的是涵蓋它們的命名作用域。可通過單擊位於它們右上角的“+”圖標將名稱作用域的方框展開。
在每個作用域內,可看到已經添加到該數據流圖中的各個Op,也可將名稱作用域嵌入在其他名稱作用域內:
上述代碼並未使用默認的Graph對象,而是顯式創建了一個tf.Graph對象。下麵重新審視這段代碼,並聚焦於命名作用域,了解其組織方式:
現在對上述代碼進行分析就更加容易。這個模型擁有兩個標量占位節點作為輸入,一個TensorFlow常量,一個名為“Transformation”的中間塊,以及一個使用tf.maximum()作為其Op的最終輸出節點。可在TensorBoard內看到這種高層的表示:
在Transformation名稱作用域內有另外4個命名空間被安排到兩個“層”中。第一層由作用域“A”和“B”構成,該層將A和B的輸出傳給下一層“C”和“D”。最後的節點會將來自最後一層的輸出作為其輸入。在TensorBoard中展開Transformation名稱作用域,將看到類似下圖的效果。
這同時還賦予了我們一個展示TensorBoard另外一個特性的機會。在上圖中,可發現名稱作用域“A”和“B”的顏色一致(藍色),“C”和“D”的顏色也一致(綠色)。這是因為在相同的配置下,這些名稱作用域擁有相同的Op設置,即“A”和“B”都有一個tf.mul() Op傳給一個tf.sub() Op,而“C”和“D”都有一個tf.div() Op傳給tf.add() Op。如果開始用一些函數創建重複的Op序列,將會非常方便。
在上圖中可以看到,當在TensorBoard中顯示時,tf.constant對象的行為與其他Tensor對象或Op並不完全相同。即使我們沒有在任何名稱作用域內聲明static_value,它仍然會被放置在這些名稱作用域內,而且,static_value並非隻出現一個圖標,它會在被使用時創建一個小的視覺元素,其基本思想是常量可在任意時間使用,且在使用時無須遵循任何特定順序。為防止在數據流圖中出現從單點引出過多箭頭的問題,隻有當常量被使用時,它才會以一個很小的視覺元素的形式出現。
將一個規模較大的數據流圖分解為一些有意義的簇能夠使對模型的理解和編譯更加方便。
最後更新:2017-05-19 15:32:35