一、什么是Autograph
??在前一篇文章TensorFlow核心概念之計(jì)算圖中我們提到過,TensorFlow中的構(gòu)建方式主要有三種,分別是:靜態(tài)計(jì)算圖構(gòu)建、動(dòng)態(tài)計(jì)算圖構(gòu)建和Autograph。其中靜態(tài)計(jì)算圖主要是在TensorFlow1.0中支持的計(jì)算圖構(gòu)建方式,這種方式構(gòu)建的計(jì)算圖雖然執(zhí)行效率高,但不便于編碼過程中的調(diào)試,交互體驗(yàn)差。因此2.0之后TensorFlow開始支持動(dòng)態(tài)計(jì)算圖,雖然便于了編碼過程中調(diào)試和交互體驗(yàn),但是執(zhí)行效率問題隨之而來。于是就有了Autograph,Autograph是一種將動(dòng)態(tài)圖轉(zhuǎn)換成靜態(tài)圖的實(shí)現(xiàn)機(jī)制,通過在普通python方法上使用@tf.function進(jìn)行裝飾,從而將動(dòng)態(tài)圖轉(zhuǎn)換成靜態(tài)圖。
三、Autograph實(shí)現(xiàn)原理
??為了搞清楚Autograph的機(jī)制原理,我們需要知道,當(dāng)我們使用@tf.function裝飾一個(gè)函數(shù)后,在調(diào)用這些函數(shù)時(shí),TensorFlow到底做了什么?下面我們詳細(xì)介紹Autograph的實(shí)現(xiàn)原理。當(dāng)調(diào)用被@tf.function時(shí),TensorFlow一共做了兩件事:第一件事是創(chuàng)建靜態(tài)計(jì)算圖,第二件事是執(zhí)行靜態(tài)計(jì)算圖。執(zhí)行計(jì)算圖沒什么好講的,就是針對創(chuàng)建好的計(jì)算圖,根據(jù)輸入的參數(shù)進(jìn)行執(zhí)行,關(guān)鍵的問題是TensorFlow是如何創(chuàng)建計(jì)算這個(gè)靜態(tài)計(jì)算圖的。
??當(dāng)執(zhí)行被@tf.function裝飾的函數(shù)時(shí),TensorFlow會在后端隱式的創(chuàng)建一個(gè)靜態(tài)計(jì)算圖,靜態(tài)計(jì)算圖的創(chuàng)建過程大體時(shí)這樣的:跟蹤執(zhí)行一遍函數(shù)體中的Python代碼,確定各個(gè)變量的Tensor類型,并根據(jù)執(zhí)行順序?qū)⒏鱐ensorFlow的算子添加到計(jì)算圖中。 在該過程中,如果@tf.function(autograph=True)(默認(rèn)開啟autograph),TensorFlow會將Python控制流轉(zhuǎn)換成TensorFlow的靜態(tài)圖控制流。 主要是將if語句轉(zhuǎn)換成 tf.cond算子表達(dá),將while和for循環(huán)語句轉(zhuǎn)換成tf.while_loop算子表達(dá),并在必要的時(shí)候添加 tf.control_dependencies指定執(zhí)行順序依賴關(guān)系。這里需要注意的是,非TensorFlow的函數(shù)不會被添加到計(jì)算圖中,也就是說,像Python原生支持的一些函數(shù)在構(gòu)建靜態(tài)計(jì)算圖的過程中,只會被跟蹤執(zhí)行,不會將該函數(shù)作為算子嵌入到TensorFlow的靜態(tài)計(jì)算圖中。
??另外還需要注意的一點(diǎn)是,當(dāng)在調(diào)用@tf.function裝飾的函數(shù)時(shí),如果輸入的參數(shù)是Tensor類型,此時(shí)TensorFlow會從性能的角度出發(fā),去判斷當(dāng)前入?yún)㈩愋拖碌撵o態(tài)計(jì)算圖是否已經(jīng)存在,如果已經(jīng)存在,則直接執(zhí)行計(jì)算圖,從而省去構(gòu)建靜態(tài)計(jì)算圖的過程,進(jìn)而提升效率。但是如果發(fā)現(xiàn)當(dāng)前入?yún)⒌撵o態(tài)計(jì)算圖不存在,則需要重新創(chuàng)建新的計(jì)算圖。另外需要注意的是,如果調(diào)用被@tf.function裝飾的函數(shù)時(shí),入?yún)⒉皇荰ensor類型,則每次調(diào)用的時(shí)候都需要先創(chuàng)建靜態(tài)計(jì)算圖,然后執(zhí)行計(jì)算圖。
三、Autograph的編碼規(guī)范
??介紹完TensorFlow的實(shí)現(xiàn)原理,下面我們簡單介紹一下Autograph的編碼規(guī)范和使用建議。并通過簡單的示例來演示為什么要有這些規(guī)范和建議。
1. 被@tf.function修飾的函數(shù)應(yīng)盡量使用TensorFlow中的函數(shù),而非外部函數(shù)。
2. 不能在@tf.function修飾的函數(shù)內(nèi)部定義tf.Variable變量。
3. 被@tf.function修飾的函數(shù)不可修改該函數(shù)外部的Python列表或字典等數(shù)據(jù)結(jié)構(gòu)變量。
4. 調(diào)用被@tf.function修飾的函數(shù),入?yún)⒈M量使用Tensor類型。
四、Autograph的編碼規(guī)范解析
1. 被@tf.function修飾的函數(shù)應(yīng)盡量使用TensorFlow中的函數(shù),而非外部函數(shù)。
我們可以看下面一段代碼,我們定義了兩個(gè)@tf.function修飾的函數(shù),其中第一個(gè)函數(shù)體內(nèi)使用了兩個(gè)外部函數(shù),分別是np.random.randn(3,3)和print('---------'),第二個(gè)函數(shù)體內(nèi)全部使用TensorFlow中的函數(shù)。
import numpy as np
import tensorflow as tf
@tf.function
def np_random():
a = np.random.randn(3,3)
tf.print(a)
print('---------')
@tf.function
def tf_random():
a = tf.random.normal((3,3))
tf.print(a)
tf.print('---------')
下面我們調(diào)用兩次第一個(gè)被@tf.function修飾的函數(shù):
print('第1次調(diào)用:')
np_random()
print('第2次調(diào)用:')
np_random()
結(jié)果如下:
第1次調(diào)用:
---------
array([[ 0.78826988, -0.05816027, 0.88905733],
[-1.98118034, -0.10032147, -0.51427141],
[ 0.50533615, -1.11163988, -0.87748809]])
第2次調(diào)用:
array([[ 0.78826988, -0.05816027, 0.88905733],
[-1.98118034, -0.10032147, -0.51427141],
[ 0.50533615, -1.11163988, -0.87748809]])
??這個(gè)時(shí)候我們會發(fā)現(xiàn)三個(gè)問題:
- 第一次調(diào)用的時(shí)候,
print('---------')方法執(zhí)行了,最起碼看起是執(zhí)行了,也確實(shí)是執(zhí)行了,而第二次調(diào)用的時(shí)候,print('---------')方法并沒有執(zhí)行; - 第一次調(diào)用的時(shí)候,
print('---------')方法在tf.print(a)之前調(diào)用了; - 兩次調(diào)用之后,變量
a的結(jié)果是一樣的。
??下面針對以上問題,我們來詳細(xì)解釋一下:首先在第一次調(diào)用的是,會進(jìn)行靜態(tài)計(jì)算圖的創(chuàng)建,這個(gè)時(shí)候Python后端會跟蹤執(zhí)行一遍函數(shù)體Python的代碼,,并將方法體中的變量和算子進(jìn)行映射和加入計(jì)算圖中,這里需要注意的是,由于np.random.randn(3,3)和print('---------')方法并不是TensorFlow中的方法,因此無法加入到計(jì)算圖中,因此只有tf.print(a)方法加入到了靜態(tài)計(jì)算圖中,因此只有在第一次創(chuàng)建計(jì)算圖的時(shí)候進(jìn)行跟蹤執(zhí)行,而第二次執(zhí)行時(shí),如果計(jì)算圖已經(jīng)存在,這個(gè)時(shí)候時(shí)不需要再執(zhí)行的,這也就是為什么print('---------')會先在tf.print(a)前面執(zhí)行,且執(zhí)行一次。因?yàn)樵趯?shí)際執(zhí)行計(jì)算圖的過程中,都只會執(zhí)行tf.print(a)這一個(gè)方法,這也導(dǎo)致了為什么多次調(diào)用之后,打印出來的a的結(jié)果是一樣的?;谝陨显?,我們再兩次調(diào)用一下第二個(gè)方法tf_random(),示例代碼和結(jié)果如下:
print('第1次調(diào)用:')
tf_random()
print('第2次調(diào)用:')
tf_random()
結(jié)果如下:
第1次調(diào)用:
[[1.47568643 -0.204902112 0.694708228]
[-0.868299544 1.65556359 0.520012081]
[-0.215179399 -0.400003046 -0.393970907]]
---------
第2次調(diào)用:
[[0.0756372586 1.06571424 -0.579676867]
[-0.937381923 -2.79628611 -1.38038337]
[-0.762175 -1.79867613 0.329570293]]
---------
這個(gè)時(shí)候我們可以看出,全部使用TensorFlow函數(shù)的方法調(diào)用的結(jié)果是符合我們的預(yù)期的。
2. 不能在@tf.function修飾的函數(shù)內(nèi)部定義tf.Variable變量。
這個(gè)我們就直接示例,代碼如下:
@tf.function
def inner_var():
x = tf.Variable(1.0,dtype = tf.float32)
x.assign_add(1.0)
tf.print(x)
return(x)
這個(gè)時(shí)候執(zhí)行的時(shí)候,代碼會直接報(bào)錯(cuò),報(bào)錯(cuò)信息如下:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-12-c95a7c3c1ddd> in <module>
7
8 #執(zhí)行將報(bào)錯(cuò)
----> 9 inner_var()
10 inner_var()
~/anaconda3/lib/python3.7/site-packages/tensorflow_core/python/eager/def_function.py in __call__(self, *args, **kwds)
566 xla_context.Exit()
567 else:
--> 568 result = self._call(*args, **kwds)
569
570 if tracing_count == self._get_tracing_count():
......
ValueError: tf.function-decorated function tried to create variables on non-first call.
如果我們將這個(gè)變量拿到@tf.function修飾的函數(shù)外,則可以直接執(zhí)行,代碼如下:
x = tf.Variable(1.0,dtype=tf.float32)
@tf.function
def outer_var():
x.assign_add(1.0)
tf.print(x)
return(x)
outer_var()
outer_var()
結(jié)果如下:
2
3
3. 被@tf.function修飾的函數(shù)不可修改該函數(shù)外部的Python列表或字典等數(shù)據(jù)結(jié)構(gòu)變量。
正對這個(gè)我們直接看代碼示例,首先我們在不用@tf.function修飾的函數(shù)來演示一下執(zhí)行結(jié)果,代碼如下:
tensor_list = []
def append_tensor(x):
tensor_list.append(x)
return tensor_list
append_tensor(tf.constant(1.0))
append_tensor(tf.constant(2.0))
print(tensor_list)
結(jié)果如下:
[<tf.Tensor: shape=(), dtype=float32, numpy=1.0>, <tf.Tensor: shape=(), dtype=float32, numpy=2.0>]
這個(gè)時(shí)候我們發(fā)現(xiàn)一切如我們的預(yù)期,沒有任何問題,接下來我們對這個(gè)append_tensor(x)函數(shù)加上@tf.function修飾,代碼如下:
tensor_list = []
@tf.function
def append_tensor(x):
tensor_list.append(x)
return tensor_list
append_tensor(tf.constant(1.0))
append_tensor(tf.constant(2.0))
print(tensor_list)
結(jié)果如下:
[<tf.Tensor 'x:0' shape=() dtype=float32>]
??其實(shí)出現(xiàn)這個(gè)問題的原因呢也很好解釋,那就是tensor_list.append(x)不是一個(gè)TensorFlow的方法,在構(gòu)建計(jì)算圖的時(shí)候呢,這個(gè)方法并不會作為算子加入到靜態(tài)計(jì)算圖中,那么在最后執(zhí)行計(jì)算圖的時(shí)候,其實(shí)也就不會去執(zhí)行這個(gè)方法了,這就是為啥最終這個(gè)列表內(nèi)容為空的原因。
4. 調(diào)用被@tf.function修飾的函數(shù),入?yún)⒈M量使用Tensor類型。
這一點(diǎn)是從性能的角度出發(fā)的,因?yàn)樵谡{(diào)用被@tf.function修飾的函數(shù)時(shí),TensorFlow會根據(jù)入?yún)㈩愋蛠頉Q定是否要重新創(chuàng)建靜態(tài)計(jì)算圖,這一點(diǎn)時(shí)從性能的角度出發(fā)的,對結(jié)果其實(shí)并沒有實(shí)際的影響。示例代碼如下:
import tensorflow as tf
import numpy as np
@tf.function(autograph=True)
def myadd(a,b):
c = a + b
print("tracing")#為了方便知道在創(chuàng)建計(jì)算圖
tf.print(c)
return c
首先我們使用Tensor類型的入?yún)⒍啻握{(diào)用該函數(shù):
print("第1次調(diào)用:")
myadd(tf.constant("Hello"), tf.constant("World"))
print("第2次調(diào)用:")
myadd(tf.constant("Good"), tf.constant("Bye"))
結(jié)果如下:
第1次調(diào)用:
tracing
HelloWorld
第2次調(diào)用:
GoodBye
而當(dāng)我們使用非Tensor類型的入?yún)⒍啻握{(diào)用該函數(shù):
print("第1次調(diào)用:")
myadd("Hello","World")
print("第2次調(diào)用:")
myadd("Good","Bye")
結(jié)果如下:
第1次調(diào)用:
tracing
HelloWorld
第2次調(diào)用:
tracing
GoodBye
??這個(gè)時(shí)候我們發(fā)現(xiàn),如果在調(diào)用@tf.function修飾的函數(shù)時(shí),如果入?yún)⒌念愋筒皇荰ensorFlow的類型,那么在多次調(diào)用該方法時(shí),如果入?yún)㈩愋筒蛔?,?nèi)容變換的化,是需要多次創(chuàng)建靜態(tài)計(jì)算圖的,而如果使用Tensor類型的入?yún)?,則不會出現(xiàn)重復(fù)創(chuàng)建靜態(tài)計(jì)算圖的過程,除非入?yún)㈩愋透淖?,這樣可以大大的提高調(diào)用性能。OK,關(guān)于TensorFlow中的Autograph就簡單介紹這么多。