什么是計(jì)算圖
??什么計(jì)算圖呢?計(jì)算圖跟圖計(jì)算不一樣,圖計(jì)算是對(duì)基于圖數(shù)據(jù)的計(jì)算的統(tǒng)稱。而計(jì)算圖是對(duì)一系列計(jì)算和數(shù)據(jù)流轉(zhuǎn)編排之后形成的有向無(wú)環(huán)圖的描述。搞過(guò)大數(shù)據(jù)的應(yīng)該都對(duì)大數(shù)據(jù)的調(diào)度及依賴任務(wù)編排比較熟悉,我們會(huì)將前后依賴的任務(wù)進(jìn)配置,設(shè)置個(gè)任務(wù)間的依賴關(guān)系,然后形成了一個(gè)關(guān)于各種數(shù)據(jù)加工和依賴任務(wù)的有向無(wú)環(huán)圖(DAG),在這個(gè)有向無(wú)環(huán)圖中,圖中的頂點(diǎn)(Vertex、Node)是一個(gè)個(gè)用于執(zhí)行某種計(jì)算的任務(wù),頂點(diǎn)直接的關(guān)系(Relationship也就是邊,Edge)有的是數(shù)據(jù)流轉(zhuǎn),有的是任務(wù)依賴。其實(shí)TensorFlow中的計(jì)算圖也基本上類似與這種有DAG圖。
??通常一個(gè)機(jī)器學(xué)習(xí)任務(wù)的核心是定義模型和求解參數(shù),在對(duì)模型定義和參數(shù)求解過(guò)程進(jìn)行抽象之后,在確定了數(shù)據(jù)的流轉(zhuǎn)方式、數(shù)據(jù)的計(jì)算方式以及各種計(jì)算之間的相互依賴關(guān)系之后,就可以確定一個(gè)唯一的計(jì)算執(zhí)行邏輯,然后將這個(gè)計(jì)算執(zhí)行邏輯用圖表示,最后我們稱這個(gè)有向無(wú)環(huán)圖為計(jì)算圖。
??TensorFlow中的計(jì)算圖由點(diǎn)(nodes)和邊(edges)組成,節(jié)點(diǎn)表示算子,邊表示算子間的依賴或數(shù)據(jù)(一般是張量)的傳遞方向,其中實(shí)線表示有數(shù)據(jù)傳遞依賴,傳遞的數(shù)據(jù)即為張量;而虛線通常表示控制依賴,即執(zhí)行先后順序,不存在數(shù)據(jù)傳遞依賴。所有的節(jié)點(diǎn)都通過(guò)邊連接,其中入度為0的節(jié)點(diǎn)沒(méi)有前置依賴,可以立即執(zhí)行;入度大于0的節(jié)點(diǎn),要等待其前置依賴的所有節(jié)點(diǎn)執(zhí)行結(jié)束之后才能執(zhí)行。下圖就是TensorFlow中一個(gè)簡(jiǎn)單的計(jì)算圖示例:

??計(jì)算圖創(chuàng)建好了之后,TensorFlow就會(huì)需要啟動(dòng)Session去執(zhí)行計(jì)算圖,在TensorFlow中,一個(gè)Session可以執(zhí)行多個(gè)計(jì)算圖,每個(gè)計(jì)算圖之間的執(zhí)行相互獨(dú)立。計(jì)算圖的執(zhí)行參考了拓?fù)渑判虻乃枷?,關(guān)于拓?fù)渑判?,如果有不清楚的,可以參考我的另外一篇文章—?a href="http://www.itdecent.cn/p/cd90e771d515" target="_blank">直觀理解:拓?fù)渑判?/a>。計(jì)算圖G的執(zhí)行大體可以分為如下4個(gè)步驟:
- a. 以節(jié)點(diǎn)id(node_id)作為
key、入度(in_degree)作為value創(chuàng)建哈希表map,并將計(jì)算圖G中的所有節(jié)點(diǎn)(nodes)加入map中。 - b. 為計(jì)算圖
G創(chuàng)建一個(gè)可執(zhí)行節(jié)點(diǎn)隊(duì)列queue,將map中入度為0的節(jié)點(diǎn)加入queue,并從map中刪除這些節(jié)點(diǎn)。 - c. 依次執(zhí)行
queue中的每一個(gè)節(jié)點(diǎn),執(zhí)行成功之后將此節(jié)點(diǎn)輸出指向的節(jié)點(diǎn)的入度減1,更新map中對(duì)應(yīng)節(jié)點(diǎn)的入度。 - d. 重復(fù)步驟b和c,直至
queue為空。
??TensorFlow在發(fā)展過(guò)程中一共提供了三種計(jì)算圖的構(gòu)建方式,分別是靜態(tài)計(jì)算圖、動(dòng)態(tài)計(jì)算圖和AutoGraph。其中靜態(tài)計(jì)算圖的構(gòu)建是TensorFlow在1.0提供的基礎(chǔ)功能,但是原生的靜態(tài)圖構(gòu)建這個(gè)功能在TensorFlow2.0之后被棄用,但為了保持對(duì)1.0版本的兼容,TensorFlow2.0在compat包中提供了兼容1.0版本的靜態(tài)圖構(gòu)建方式。關(guān)于三種計(jì)算圖的構(gòu)建方式的優(yōu)劣及不同,我們?cè)诤竺姘凑鹿?jié)進(jìn)行詳細(xì)講解。
靜態(tài)計(jì)算圖
??TensorFlow1.0是采用靜態(tài)計(jì)算圖的方式來(lái)構(gòu)建計(jì)算圖,需先用TensorFlow中的各種算子創(chuàng)建計(jì)算圖,然后開(kāi)啟一個(gè)Session來(lái)顯式地執(zhí)行計(jì)算圖。TensorFlow2.0為了保證對(duì)TensorFlow1.0項(xiàng)目的兼容性,在tf.compat.v1子模塊中保留了對(duì)TensorFlow1.0提供的靜態(tài)計(jì)算圖構(gòu)建方式的支持。但是在TensorFlow2.0中,這種靜態(tài)圖的構(gòu)建方式已經(jīng)不被推薦,后面漸漸可能會(huì)被舍棄。下面我們以兼容包里的靜態(tài)計(jì)算圖構(gòu)建方式來(lái)展示靜態(tài)計(jì)算圖的構(gòu)建方式。代碼如下:
import tensorflow as tf
#定義靜態(tài)計(jì)算圖g
g = tf.compat.v1.Graph()
with g.as_default():
#placeholder為占位符,會(huì)話執(zhí)行的時(shí)候會(huì)填充具體的對(duì)象內(nèi)容
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=" ")
#開(kāi)啟一個(gè)session,執(zhí)行計(jì)算圖g
result = None
with tf.compat.v1.Session(graph=g) as sess:
# fetches的結(jié)果非常像一個(gè)函數(shù)的返回值,而feed_dict中的占位符相當(dāng)于函數(shù)的參數(shù)序列。
result = sess.run(fetches=z, feed_dict={x:"Hello", y:"World!"})
#打印計(jì)算結(jié)果
tf.print(result)
結(jié)果如下:
b'Hello World!'
動(dòng)態(tài)計(jì)算圖
??動(dòng)態(tài)計(jì)算圖,也稱之為Eager Execution,其和靜態(tài)計(jì)算圖最大的區(qū)別在于,動(dòng)態(tài)計(jì)算圖無(wú)需顯式的定義計(jì)算圖,然后開(kāi)啟個(gè)session來(lái)執(zhí)行,在動(dòng)態(tài)計(jì)算圖中,默認(rèn)開(kāi)啟session,所有的算子定義之后立即執(zhí)行。示例代碼如下:
# 動(dòng)態(tài)計(jì)算圖在每個(gè)算子構(gòu)建后立即執(zhí)行
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)
結(jié)果如下:
x: "Hello"
y: "World!"
result: "Hello World!"
另外,從模塊化和函數(shù)化編程的角度出發(fā),也可以將上述的動(dòng)態(tài)計(jì)算圖進(jìn)行函數(shù)化封裝,從而將計(jì)算圖的輸入和輸出封裝在一個(gè)函數(shù)里面,示例代碼如下:
# 將x,y及result的輸入輸出關(guān)系封裝成函數(shù)
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)
結(jié)果如下:
x: "Hello"
y: "World!"
result: "Hello World!"
AutoGraph
??使用動(dòng)態(tài)計(jì)算圖(Eager Execution)的好處是方便代碼調(diào)試,因?yàn)樗械闹虚g過(guò)程可以在寫(xiě)代碼過(guò)程中立即執(zhí)行并顯示結(jié)果。但是動(dòng)態(tài)計(jì)算圖的運(yùn)行效率相對(duì)較低,因?yàn)镋ager Execution會(huì)有許多次Python進(jìn)程和TensorFlow的C++進(jìn)程之間的通信。而靜態(tài)計(jì)算圖構(gòu)建完成之后幾乎全部在TensorFlow內(nèi)核上使用C++代碼執(zhí)行,無(wú)需與Python進(jìn)程頻繁進(jìn)行交互通信,因而效率更高。此外靜態(tài)圖會(huì)對(duì)計(jì)算步驟進(jìn)行一定的優(yōu)化,省略和結(jié)果無(wú)關(guān)的計(jì)算步驟。鑒于此,TensorFlow2.0提供了tf.function讓算子從 Eager Execution 切換到靜態(tài)計(jì)算圖執(zhí)行??梢允褂聾tf.function注解將普通Python函數(shù)轉(zhuǎn)換成對(duì)應(yīng)的TensorFlow計(jì)算圖,而調(diào)用該函數(shù)就相當(dāng)于在TensorFlow1.0中開(kāi)啟一個(gè)Session執(zhí)行計(jì)算圖。這種使用tf.function構(gòu)建靜態(tài)圖的方式就叫做 Autograph。
??在TensorFlow2.0中,如果采用Autograph的方式使用計(jì)算圖,第一步需要定義函數(shù),第二步調(diào)用函數(shù)。無(wú)需顯示定義計(jì)算圖,然后顯式地開(kāi)啟session去執(zhí)行計(jì)算圖,執(zhí)行計(jì)算圖變得跟Python中函數(shù)的定義和調(diào)用一樣簡(jiǎn)單。下面用代碼來(lái)展示采用Autograph的計(jì)算圖執(zhí)行方式,代碼如下:
import tensorflow as tf
# 使用autograph構(gòu)建靜態(tài)圖
@tf.function
def str_join(x, y):
tf.print("x:", x)
tf.print("y:", y)
return tf.strings.join([x, y], separator=" ")
# 調(diào)用函數(shù)@tf.function裝飾的函數(shù),執(zhí)行計(jì)算圖
result = str_join(tf.constant("Hello"), tf.constant("World!"))
tf.print("result:", result)
結(jié)果如下:
hello world
tf.Tensor(b'hello world', shape=(), dtype=string)
三種計(jì)算圖對(duì)比
??靜態(tài)計(jì)算圖,動(dòng)態(tài)計(jì)算圖和AutoGraph在執(zhí)行效率和編程體驗(yàn)兩方面各有取舍。原始的使用靜態(tài)計(jì)算圖需要嚴(yán)格分兩步,第一步定義計(jì)算圖,第二步在會(huì)話中執(zhí)行計(jì)算圖。而在TensorFlow2.0中,在兼容原始靜態(tài)圖構(gòu)建方式的同時(shí),新推出了采用Autograph的方式使用計(jì)算圖,使得計(jì)算圖的定義和使用分別變成了定義函數(shù)和調(diào)用函數(shù),兼顧效率的同時(shí),極大的提升了編程體驗(yàn)和效率。關(guān)于三種計(jì)算圖的比較,分別從定義、執(zhí)行和效率三個(gè)方面做了簡(jiǎn)單的總結(jié):
- 定義:靜態(tài)計(jì)算圖需要嚴(yán)格遵循先定義,后使用的原則,定義過(guò)程比較麻煩;而動(dòng)態(tài)計(jì)算圖和AutoGraph的計(jì)算圖定義更接近普通的Python任務(wù)編程,定義過(guò)程比較簡(jiǎn)單。
- 執(zhí)行:靜態(tài)計(jì)算圖需要顯示開(kāi)啟session后執(zhí)行,比較麻煩;動(dòng)態(tài)計(jì)算圖無(wú)需顯示執(zhí)行,可以即刻執(zhí)行并查看中間結(jié)果;AutoGraph調(diào)用函數(shù)即可執(zhí)行,簡(jiǎn)單易用,調(diào)用后會(huì)在后臺(tái)會(huì)按照靜態(tài)圖一樣的方式執(zhí)行。
- 效率:靜態(tài)計(jì)算圖后臺(tái)有優(yōu)化策略,效率最高;AutoGraph裝飾后的函數(shù)也按靜態(tài)圖的方式去執(zhí)行;動(dòng)態(tài)計(jì)算圖由于Eager Execution,C++內(nèi)核和Python內(nèi)核需要進(jìn)行頻繁的交互,效率最低。