一、什么是Autograph
??在前一篇文章TensorFlow核心概念之計算圖中我們提到過,TensorFlow中的構建方式主要有三種,分別是:靜態計算圖構建、動態計算圖構建和Autograph。其中靜態計算圖主要是在TensorFlow1.0中支持的計算圖構建方式,這種方式構建的計算圖雖然執行效率高,但不便于編碼過程中的調試,交互體驗差。因此2.0之后TensorFlow開始支持動態計算圖,雖然便于了編碼過程中調試和交互體驗,但是執行效率問題隨之而來。于是就有了Autograph,Autograph是一種將動態圖轉換成靜態圖的實現機制,通過在普通python方法上使用@tf.function
進行裝飾,從而將動態圖轉換成靜態圖。
三、Autograph實現原理
??為了搞清楚Autograph的機制原理,我們需要知道,當我們使用@tf.function
裝飾一個函數后,在調用這些函數時,TensorFlow到底做了什么?下面我們詳細介紹Autograph的實現原理。當調用被@tf.function
時,TensorFlow一共做了兩件事:第一件事是創建靜態計算圖,第二件事是執行靜態計算圖。執行計算圖沒什么好講的,就是針對創建好的計算圖,根據輸入的參數進行執行,關鍵的問題是TensorFlow是如何創建計算這個靜態計算圖的。
??當執行被@tf.function
裝飾的函數時,TensorFlow會在后端隱式的創建一個靜態計算圖,靜態計算圖的創建過程大體時這樣的:跟蹤執行一遍函數體中的Python代碼,確定各個變量的Tensor類型,并根據執行順序將各TensorFlow的算子添加到計算圖中。 在該過程中,如果@tf.function(autograph=True)
(默認開啟autograph),TensorFlow會將Python控制流轉換成TensorFlow的靜態圖控制流。 主要是將if語句轉換成 tf.cond算子表達,將while和for循環語句轉換成tf.while_loop算子表達,并在必要的時候添加 tf.control_dependencies指定執行順序依賴關系。這里需要注意的是,非TensorFlow的函數不會被添加到計算圖中,也就是說,像Python原生支持的一些函數在構建靜態計算圖的過程中,只會被跟蹤執行,不會將該函數作為算子嵌入到TensorFlow的靜態計算圖中。
??另外還需要注意的一點是,當在調用@tf.function裝飾的函數時,如果輸入的參數是Tensor類型,此時TensorFlow會從性能的角度出發,去判斷當前入參類型下的靜態計算圖是否已經存在,如果已經存在,則直接執行計算圖,從而省去構建靜態計算圖的過程,進而提升效率。但是如果發現當前入參的靜態計算圖不存在,則需要重新創建新的計算圖。另外需要注意的是,如果調用被@tf.function裝飾的函數時,入參不是Tensor類型,則每次調用的時候都需要先創建靜態計算圖,然后執行計算圖。
三、Autograph的編碼規范
??介紹完TensorFlow的實現原理,下面我們簡單介紹一下Autograph的編碼規范和使用建議。并通過簡單的示例來演示為什么要有這些規范和建議。
1. 被@tf.function
修飾的函數應盡量使用TensorFlow中的函數,而非外部函數。
2. 不能在@tf.function
修飾的函數內部定義tf.Variable變量。
3. 被@tf.function
修飾的函數不可修改該函數外部的Python列表或字典等數據結構變量。
4. 調用被@tf.function
修飾的函數,入參盡量使用Tensor類型。
四、Autograph的編碼規范解析
1. 被@tf.function
修飾的函數應盡量使用TensorFlow中的函數,而非外部函數。
我們可以看下面一段代碼,我們定義了兩個@tf.function
修飾的函數,其中第一個函數體內使用了兩個外部函數,分別是np.random.randn(3,3)
和print('---------')
,第二個函數體內全部使用TensorFlow中的函數。
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('---------')
下面我們調用兩次第一個被@tf.function
修飾的函數:
print('第1次調用:')
np_random()
print('第2次調用:')
np_random()
結果如下:
第1次調用:
---------
array([[ 0.78826988, -0.05816027, 0.88905733],
[-1.98118034, -0.10032147, -0.51427141],
[ 0.50533615, -1.11163988, -0.87748809]])
第2次調用:
array([[ 0.78826988, -0.05816027, 0.88905733],
[-1.98118034, -0.10032147, -0.51427141],
[ 0.50533615, -1.11163988, -0.87748809]])
??這個時候我們會發現三個問題:
- 第一次調用的時候,
print('---------')
方法執行了,最起碼看起是執行了,也確實是執行了,而第二次調用的時候,print('---------')
方法并沒有執行; - 第一次調用的時候,
print('---------')
方法在tf.print(a)
之前調用了; - 兩次調用之后,變量
a
的結果是一樣的。
??下面針對以上問題,我們來詳細解釋一下:首先在第一次調用的是,會進行靜態計算圖的創建,這個時候Python后端會跟蹤執行一遍函數體Python的代碼,,并將方法體中的變量和算子進行映射和加入計算圖中,這里需要注意的是,由于np.random.randn(3,3)
和print('---------')
方法并不是TensorFlow中的方法,因此無法加入到計算圖中,因此只有tf.print(a)
方法加入到了靜態計算圖中,因此只有在第一次創建計算圖的時候進行跟蹤執行,而第二次執行時,如果計算圖已經存在,這個時候時不需要再執行的,這也就是為什么print('---------')
會先在tf.print(a)
前面執行,且執行一次。因為在實際執行計算圖的過程中,都只會執行tf.print(a)
這一個方法,這也導致了為什么多次調用之后,打印出來的a
的結果是一樣的。基于以上原因,我們再兩次調用一下第二個方法tf_random()
,示例代碼和結果如下:
print('第1次調用:')
tf_random()
print('第2次調用:')
tf_random()
結果如下:
第1次調用:
[[1.47568643 -0.204902112 0.694708228]
[-0.868299544 1.65556359 0.520012081]
[-0.215179399 -0.400003046 -0.393970907]]
---------
第2次調用:
[[0.0756372586 1.06571424 -0.579676867]
[-0.937381923 -2.79628611 -1.38038337]
[-0.762175 -1.79867613 0.329570293]]
---------
這個時候我們可以看出,全部使用TensorFlow函數的方法調用的結果是符合我們的預期的。
2. 不能在@tf.function
修飾的函數內部定義tf.Variable變量。
這個我們就直接示例,代碼如下:
@tf.function
def inner_var():
x = tf.Variable(1.0,dtype = tf.float32)
x.assign_add(1.0)
tf.print(x)
return(x)
這個時候執行的時候,代碼會直接報錯,報錯信息如下:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-12-c95a7c3c1ddd> in <module>
7
8 #執行將報錯
----> 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.
如果我們將這個變量拿到@tf.function
修飾的函數外,則可以直接執行,代碼如下:
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()
結果如下:
2
3
3. 被@tf.function
修飾的函數不可修改該函數外部的Python列表或字典等數據結構變量。
正對這個我們直接看代碼示例,首先我們在不用@tf.function
修飾的函數來演示一下執行結果,代碼如下:
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)
結果如下:
[<tf.Tensor: shape=(), dtype=float32, numpy=1.0>, <tf.Tensor: shape=(), dtype=float32, numpy=2.0>]
這個時候我們發現一切如我們的預期,沒有任何問題,接下來我們對這個append_tensor(x)
函數加上@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)
結果如下:
[<tf.Tensor 'x:0' shape=() dtype=float32>]
??其實出現這個問題的原因呢也很好解釋,那就是tensor_list.append(x)
不是一個TensorFlow的方法,在構建計算圖的時候呢,這個方法并不會作為算子加入到靜態計算圖中,那么在最后執行計算圖的時候,其實也就不會去執行這個方法了,這就是為啥最終這個列表內容為空的原因。
4. 調用被@tf.function
修飾的函數,入參盡量使用Tensor類型。
這一點是從性能的角度出發的,因為在調用被@tf.function
修飾的函數時,TensorFlow會根據入參類型來決定是否要重新創建靜態計算圖,這一點時從性能的角度出發的,對結果其實并沒有實際的影響。示例代碼如下:
import tensorflow as tf
import numpy as np
@tf.function(autograph=True)
def myadd(a,b):
c = a + b
print("tracing")#為了方便知道在創建計算圖
tf.print(c)
return c
首先我們使用Tensor類型的入參多次調用該函數:
print("第1次調用:")
myadd(tf.constant("Hello"), tf.constant("World"))
print("第2次調用:")
myadd(tf.constant("Good"), tf.constant("Bye"))
結果如下:
第1次調用:
tracing
HelloWorld
第2次調用:
GoodBye
而當我們使用非Tensor類型的入參多次調用該函數:
print("第1次調用:")
myadd("Hello","World")
print("第2次調用:")
myadd("Good","Bye")
結果如下:
第1次調用:
tracing
HelloWorld
第2次調用:
tracing
GoodBye
??這個時候我們發現,如果在調用@tf.function
修飾的函數時,如果入參的類型不是TensorFlow的類型,那么在多次調用該方法時,如果入參類型不變,內容變換的化,是需要多次創建靜態計算圖的,而如果使用Tensor類型的入參,則不會出現重復創建靜態計算圖的過程,除非入參類型改變,這樣可以大大的提高調用性能。OK,關于TensorFlow中的Autograph就簡單介紹這么多。