什么是計算圖
??什么計算圖呢?計算圖跟圖計算不一樣,圖計算是對基于圖數據的計算的統稱。而計算圖是對一系列計算和數據流轉編排之后形成的有向無環圖的描述。搞過大數據的應該都對大數據的調度及依賴任務編排比較熟悉,我們會將前后依賴的任務進配置,設置個任務間的依賴關系,然后形成了一個關于各種數據加工和依賴任務的有向無環圖(DAG),在這個有向無環圖中,圖中的頂點(Vertex、Node)是一個個用于執行某種計算的任務,頂點直接的關系(Relationship也就是邊,Edge)有的是數據流轉,有的是任務依賴。其實TensorFlow中的計算圖也基本上類似與這種有DAG圖。
??通常一個機器學習任務的核心是定義模型和求解參數,在對模型定義和參數求解過程進行抽象之后,在確定了數據的流轉方式、數據的計算方式以及各種計算之間的相互依賴關系之后,就可以確定一個唯一的計算執行邏輯,然后將這個計算執行邏輯用圖表示,最后我們稱這個有向無環圖為計算圖。
??TensorFlow中的計算圖由點(nodes)和邊(edges)組成,節點表示算子,邊表示算子間的依賴或數據(一般是張量)的傳遞方向,其中實線表示有數據傳遞依賴,傳遞的數據即為張量;而虛線通常表示控制依賴,即執行先后順序,不存在數據傳遞依賴。所有的節點都通過邊連接,其中入度為0的節點沒有前置依賴,可以立即執行;入度大于0的節點,要等待其前置依賴的所有節點執行結束之后才能執行。下圖就是TensorFlow中一個簡單的計算圖示例:
??計算圖創建好了之后,TensorFlow就會需要啟動Session去執行計算圖,在TensorFlow中,一個Session可以執行多個計算圖,每個計算圖之間的執行相互獨立。計算圖的執行參考了拓撲排序的思想,關于拓撲排序,如果有不清楚的,可以參考我的另外一篇文章——直觀理解:拓撲排序。計算圖G
的執行大體可以分為如下4個步驟:
- a. 以節點id(node_id)作為
key
、入度(in_degree)作為value
創建哈希表map
,并將計算圖G
中的所有節點(nodes)加入map
中。 - b. 為計算圖
G
創建一個可執行節點隊列queue
,將map
中入度為0的節點加入queue
,并從map
中刪除這些節點。 - c. 依次執行
queue
中的每一個節點,執行成功之后將此節點輸出指向的節點的入度減1,更新map
中對應節點的入度。 - d. 重復步驟b和c,直至
queue
為空。
??TensorFlow在發展過程中一共提供了三種計算圖的構建方式,分別是靜態計算圖、動態計算圖和AutoGraph。其中靜態計算圖的構建是TensorFlow在1.0提供的基礎功能,但是原生的靜態圖構建這個功能在TensorFlow2.0之后被棄用,但為了保持對1.0版本的兼容,TensorFlow2.0在compat
包中提供了兼容1.0版本的靜態圖構建方式。關于三種計算圖的構建方式的優劣及不同,我們在后面按章節進行詳細講解。
靜態計算圖
??TensorFlow1.0是采用靜態計算圖的方式來構建計算圖,需先用TensorFlow中的各種算子創建計算圖,然后開啟一個Session來顯式地執行計算圖。TensorFlow2.0為了保證對TensorFlow1.0項目的兼容性,在tf.compat.v1
子模塊中保留了對TensorFlow1.0
提供的靜態計算圖構建方式的支持。但是在TensorFlow2.0中,這種靜態圖的構建方式已經不被推薦,后面漸漸可能會被舍棄。下面我們以兼容包里的靜態計算圖構建方式來展示靜態計算圖的構建方式。代碼如下:
import tensorflow as tf
#定義靜態計算圖g
g = tf.compat.v1.Graph()
with g.as_default():
#placeholder為占位符,會話執行的時候會填充具體的對象內容
x = tf.compat.v1.placeholder(name='x', shape=[], dtype=tf.string)
y = tf.compat.v1.placeholder(name='y', shape=[], dtype=tf.string)
z = tf.strings.join([x, y], name="join", separator=" ")
#開啟一個session,執行計算圖g
result = None
with tf.compat.v1.Session(graph=g) as sess:
# fetches的結果非常像一個函數的返回值,而feed_dict中的占位符相當于函數的參數序列。
result = sess.run(fetches=z, feed_dict={x:"Hello", y:"World!"})
#打印計算結果
tf.print(result)
結果如下:
b'Hello World!'
動態計算圖
??動態計算圖,也稱之為Eager Execution,其和靜態計算圖最大的區別在于,動態計算圖無需顯式的定義計算圖,然后開啟個session來執行,在動態計算圖中,默認開啟session,所有的算子定義之后立即執行。示例代碼如下:
# 動態計算圖在每個算子構建后立即執行
x = tf.constant("Hello")
tf.print("x:", x)
y = tf.constant("World!")
tf.print("y:", y)
result = tf.strings.join([x, y], separator=" ")
tf.print("result:", result)
結果如下:
x: "Hello"
y: "World!"
result: "Hello World!"
另外,從模塊化和函數化編程的角度出發,也可以將上述的動態計算圖進行函數化封裝,從而將計算圖的輸入和輸出封裝在一個函數里面,示例代碼如下:
# 將x,y及result的輸入輸出關系封裝成函數
def str_join(x,y):
tf.print("x:", x)
tf.print("y:", y)
return tf.strings.join([x, y], separator=" ")
result = str_join(tf.constant("Hello"), tf.constant("World!"))
tf.print("result:", result)
結果如下:
x: "Hello"
y: "World!"
result: "Hello World!"
AutoGraph
??使用動態計算圖(Eager Execution)的好處是方便代碼調試,因為所有的中間過程可以在寫代碼過程中立即執行并顯示結果。但是動態計算圖的運行效率相對較低,因為Eager Execution會有許多次Python進程和TensorFlow的C++進程之間的通信。而靜態計算圖構建完成之后幾乎全部在TensorFlow內核上使用C++代碼執行,無需與Python進程頻繁進行交互通信,因而效率更高。此外靜態圖會對計算步驟進行一定的優化,省略和結果無關的計算步驟。鑒于此,TensorFlow2.0提供了tf.function
讓算子從 Eager Execution 切換到靜態計算圖執行。可以使用@tf.function注解將普通Python函數轉換成對應的TensorFlow計算圖,而調用該函數就相當于在TensorFlow1.0中開啟一個Session執行計算圖。這種使用tf.function構建靜態圖的方式就叫做 Autograph。
??在TensorFlow2.0中,如果采用Autograph的方式使用計算圖,第一步需要定義函數,第二步調用函數。無需顯示定義計算圖,然后顯式地開啟session去執行計算圖,執行計算圖變得跟Python中函數的定義和調用一樣簡單。下面用代碼來展示采用Autograph的計算圖執行方式,代碼如下:
import tensorflow as tf
# 使用autograph構建靜態圖
@tf.function
def str_join(x, y):
tf.print("x:", x)
tf.print("y:", y)
return tf.strings.join([x, y], separator=" ")
# 調用函數@tf.function裝飾的函數,執行計算圖
result = str_join(tf.constant("Hello"), tf.constant("World!"))
tf.print("result:", result)
結果如下:
hello world
tf.Tensor(b'hello world', shape=(), dtype=string)
三種計算圖對比
??靜態計算圖,動態計算圖和AutoGraph在執行效率和編程體驗兩方面各有取舍。原始的使用靜態計算圖需要嚴格分兩步,第一步定義計算圖,第二步在會話中執行計算圖。而在TensorFlow2.0中,在兼容原始靜態圖構建方式的同時,新推出了采用Autograph的方式使用計算圖,使得計算圖的定義和使用分別變成了定義函數和調用函數,兼顧效率的同時,極大的提升了編程體驗和效率。關于三種計算圖的比較,分別從定義、執行和效率三個方面做了簡單的總結:
- 定義:靜態計算圖需要嚴格遵循先定義,后使用的原則,定義過程比較麻煩;而動態計算圖和AutoGraph的計算圖定義更接近普通的Python任務編程,定義過程比較簡單。
- 執行:靜態計算圖需要顯示開啟session后執行,比較麻煩;動態計算圖無需顯示執行,可以即刻執行并查看中間結果;AutoGraph調用函數即可執行,簡單易用,調用后會在后臺會按照靜態圖一樣的方式執行。
- 效率:靜態計算圖后臺有優化策略,效率最高;AutoGraph裝飾后的函數也按靜態圖的方式去執行;動態計算圖由于Eager Execution,C++內核和Python內核需要進行頻繁的交互,效率最低。