1. 程式人生 > >經驗乾貨:使用tf.py_func函式增加Tensorflow程式的靈活性

經驗乾貨:使用tf.py_func函式增加Tensorflow程式的靈活性

   不知不覺,筆者接觸Tensorflow也滿一年了。在這一年當中,筆者對Tensorflow的瞭解程度也逐漸加深。相比筆者接觸的第一個深度學習框架Caffe而言,筆者認為Tensorflow更適合科研一些,網路搭建與演算法設定的自由度也更大,使用Tensorflow實現自己的演算法也更迅速。

   但是,筆者認為Tensorflow還是有不足的地方。第一體現在Tensorflow的資料機制,由於tensor只是佔位符,在沒有用tf.Session().run介面填充值之前是沒有實際值的。因此,在網路搭建的時候,是不能對tensor進行判值操作的,即不能插入if...else...之類的程式碼。第二

,相較於numpy array,Tensorflow中對tensor的操作介面靈活性並沒有那麼高,使得Tensorflow的靈活性減弱。

   在筆者使用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

道阻且長,行則將至