經驗乾貨:使用tf.py_func函式增加Tensorflow程式的靈活性
不知不覺,筆者接觸Tensorflow也滿一年了。在這一年當中,筆者對Tensorflow的瞭解程度也逐漸加深。相比筆者接觸的第一個深度學習框架Caffe而言,筆者認為Tensorflow更適合科研一些,網路搭建與演算法設定的自由度也更大,使用Tensorflow實現自己的演算法也更迅速。
但是,筆者認為Tensorflow還是有不足的地方。第一體現在Tensorflow的資料機制,由於tensor只是佔位符,在沒有用tf.Session().run介面填充值之前是沒有實際值的。因此,在網路搭建的時候,是不能對tensor進行判值操作的,即不能插入if...else...之類的程式碼。第二
在筆者使用Tensorflow的一年中積累的程式設計經驗來看,擴充套件Tensorflow程式的靈活性,有一個重要的手段,就是使用tf.py_func介面。筆者先對這個介面做出解析:
在上圖中,我們看到,tf.py_func的核心是一個func函式(由使用者自己定義),該函式接收numpy array作為輸入,並返回numpy array型別的輸出。看到這裡,大家應該能夠明白為什麼建議使用py_func,因為在func函式中,可以對轉化成numpy array的tensor進行np.運算,這就大大擴充套件了程式的靈活性。
然後,我們來看看tf.py_func接受什麼引數:
在使用tf.py_func的過程中,主要核心是使用前三個引數。
第一個引數func,也是最重要的,是一個使用者自定製的函式,輸入numpy array,輸出也是numpy array,在該函式中,可以自由使用np.操作。
第二個引數inp,是func函式接收的輸入,是一個列表。
第三個引數Tout,指定了func函式返回的numpy array轉化成tensor後的格式,如果是返回多個值,就是一個列表或元組;如果只有一個返回值,就是一個單獨的dtype型別(當然也可以用列表括起來)。
最後來看看tf.py_func的輸出:
輸出是一個tensor列表或單個tensor。
到這裡,tf.py_func的原理也就逐漸明晰了。首先,tf.py_func接收的是tensor,然後將其轉化為numpy array送入func函式,最後再將func函式輸出的numpy array轉化為tensor返回。
在使用過程中,有兩個需要注意的地方,第一就是func函式的返回值型別一定要和Tout指定的tensor型別一致。第二就是,如下圖所示,tf.py_func中的func是脫離Graph的。在func中不能定義可訓練的引數參與網路訓練(反傳)。
上面就解析了tf.py_func的使用方法和原理。下面筆者舉幾個例子,一是向大家展示tf.py_func帶來的靈活性,二是通過筆者的親身體會說明一下如何使用tf.py_func完成一些Tensorflow基礎程式設計中較難的任務。
1) tf.py_func在Faster R-CNN中的介面中的使用。
在目標檢測演算法Faster R-CNN中,需要計算各種ground truth,介面比較複雜。因此,使用tf.py_func是一個比較好的途徑。對於tf.py_func的使用,可以參見計算RPN的ground truth和計算proposals的ground truth時的使用方法。可以看到,都是將tensor轉化成numpy array,再使用np.操作完成複雜運算。
下面筆者來舉兩個小例子,說明一下tf.py_func的強大功能。
2) 使用tf.py_func獲得未知tensor維度。
大家知道,我們在做資料佔位的時候,可能會傳入"None",即不知道資料的該維大小,取決於feed_dict中的實際值。可是,在運算中,要使用到資料的該維大小時應該怎麼辦呢?比如下面這個例子:
import tensorflow as tf
import numpy as np
def main():
a = tf.placeholder(tf.float32, shape=[1, 2], name = "tensor_a")
b = tf.placeholder(tf.float32, shape=[None, 2], name = "tensor_b")
tile_a = tf.tile(a, [b.get_shape()[0], 1])
sess = tf.Session()
array_a = np.array([[1., 2.]])
array_b = np.array([[3., 4.],[5., 6.],[7., 8.]])
feed_dict = {a: array_a, b: array_b}
tile_a_value = sess.run(tile_a, feed_dict = feed_dict)
print(tile_a_value)
if __name__ == '__main__':
main()
如上程式碼所示,要完成一個很簡單的功能,就是擴張tensor a,將其的維度變成和tensor b一樣,可是tensor b的維度暫時未知。我們來看看,執行上述程式能得到什麼結果:
可以看到,由於tensor b第一個維度未知,因此在給tile_a分配儲存空間時報錯,提示不能有None存在。
如何解決這個問題?稍微改寫一下上述程式碼,讓tensor擴張在tf.py_func中執行:
import tensorflow as tf
import numpy as np
from py_func_1 import *
def main():
a = tf.placeholder(tf.float32, shape=[1, 2], name = "tensor_a")
b = tf.placeholder(tf.float32, shape=[None, 2], name = "tensor_b")
tile_a = tile_tensor(a, b)
sess = tf.Session()
array_a = np.array([[1., 2.]])
array_b = np.array([[3., 4.],[5., 6.],[7., 8.]])
feed_dict = {a: array_a, b: array_b}
tile_a_value = sess.run(tile_a, feed_dict = feed_dict)
print(tile_a_value)
if __name__ == '__main__':
main()
在上面的程式碼中,tensor擴張在tile_tensor這個函式中執行。該函式定義在py_func_1.py檔案中,下面是py_func_1.py的程式碼:
import tensorflow as tf
import numpy as np
def tile_tensor(tensor_a, tensor_b):
tile_tensor_a = tf.py_func(_tile_tensor, [tensor_a, tensor_b], tf.float32)
return tile_tensor_a
def _tile_tensor(a, b):
tile_a = np.tile(a, (b.shape[0], 1))
return tile_a
大家可以看到,使用了tf.py_func介面,引數func就是_tile_tensor函式。在_tile_tensor函式中,將a擴張了,執行一下修改後的main函式,輸出結果:大家可以看到,在tile_tensor函式中,tensor a在tensor b的維度未知的情況下,根據tensor b的實際維度([3, 2])將其擴張了。並返回了一個tensor型別的tile_a。
3) 在tf.py_func中對tensor的值作出判斷。
筆者在之前的部落格中提到過,在tf.Session().run之前,是不能對Tensor的值做出判斷的。比如,我們想根據tensor a的值對tensor b做出擴張:
import tensorflow as tf
import numpy as np
def main():
a = tf.placeholder(tf.float32, shape=[1], name = "tensor_a")
b = tf.placeholder(tf.float32, shape=[1, 2], name = "tensor_b")
tile_b = b
if a[0]==1.:
tile_b = tf.tile(b, [4, 1])
sess = tf.Session()
array_a = np.array([1.])
array_b = np.array([[2., 3.]])
feed_dict = {a: array_a, b: array_b}
tile_b_value = sess.run(tile_b, feed_dict = feed_dict)
print(tile_b_value)
if __name__ == '__main__':
main()
如果a[0]的值為1.0,那麼就將tensor b擴張四倍。我們執行一下上述程式碼看看結果:
大家可以看到,由於在if語句執行時,tensor a裡面是空的。因此,不會執行if中的語句。儘管在feed_dict中a被填充了1.0,並且程式不報錯,可是沒有達到預想的目標。
如何解決這個問題?稍微改寫一下上述程式碼,讓判值進行tensor擴張在tf.py_func中執行:
import tensorflow as tf
import numpy as np
from py_func_2 import *
def main():
a = tf.placeholder(tf.float32, shape=[1], name = "tensor_a")
b = tf.placeholder(tf.float32, shape=[1, 2], name = "tensor_b")
tile_tensor_b = tile_b(a, b)
sess = tf.Session()
array_a = np.array([1.])
array_b = np.array([[2., 3.]])
feed_dict = {a: array_a, b: array_b}
tile_b_value = sess.run(tile_tensor_b, feed_dict = feed_dict)
print(tile_b_value)
if __name__ == '__main__':
main()
大家可以看到,在py_func_2.py中的tile_b函式中,對tensor b進行了判值擴張。py_func_2.py程式碼如下所示:
import tensorflow as tf
import numpy as np
def tile_b(tensor_a, tensor_b):
tile_tensor_b = tf.py_func(_tile_b, [tensor_a, tensor_b], tf.float32)
return tile_tensor_b
def _tile_b(a, b):
if a[0]==1.:
tile_b = np.tile(b, (4, 1))
else:
tile_b = b
return tile_b
大家可以看到,在tile_b函式中有一個tf.py_func函式,其中的func引數便是_tile_b函式。在_tile_b函式中,根據a的值對b進行了擴張。我們來執行一下main函式,輸出結果:
tensor b得到了擴張!
大家可以看到,在tensor輸入進tf.py_func並轉化成numpy array後,判值操作就有效了。
通過上面的三個例子,筆者向大家揭示了tf.py_func函式中的神奇之處。大家可以看到,在實際使用中,將tensor轉化為numpy array後,能夠執行更靈活的操作,達到更多的目標。總而言之,tf.py_func是一個很強大的介面,也希望大家能在Tensorflow程式中靈活運用。
歡迎閱讀筆者後續部落格,各位讀者朋友的支援與鼓勵是我最大的動力!
written by jiong
道阻且長,行則將至