tensorflow2.x之由dataset.map引发出的关于tf.py_function以及tf.numpy_function问题

前言:tensorflow是一个庞大的系统,里面的函数很多,实现了很多常规的一些操作,但是始终没有办法涵盖所有的操作,有时候我们需要定义一些自己的操作逻辑来实现制定的功能,发现没那么简单,本文是在编写tf.data.DataSet的时候出现的一个问题,做了一个集中化的总结,会涉及到以下概念:

EagerTensor和Tensor,tf.py_function以及tf.numpy_function,dataset.map等等

一、问题描述

需要解决的问题,现在有三个文本文件,分别存在files文件夹中,名称分别为file1.txt、file2.txt、file3.txt,里面的内容分别是如下:

file1.txt

1,2,3,4,5

file2.txt

11,22,33,44,55

file3.txt

111,222,333,444,555

每一个文件的标签分别为,1,2,3,现在假设已经经过独热编码,则类别分别为

[ [1,0,0],[0,1,0],[0,0,1] ]

先在我需要通过dataset标准pipeline来读取这三个样本,以便于放入神经网络进行训练,显然,我需要对每一个文本文件进行读取操作,需要使用到datase.map()函数,我的代码如下:

X=["file1.txt","file2.txt","file3.txt"]
Y=[[1,0,0],[0,1,0],[0,0,1]]
 
# 构建dataset对象
dataset = tf.data.Dataset.from_tensor_slices((X,Y))  # 第一步:构造dataset对象 
 
# 对每一个dataset的元素,实际上就是一个example进行解析
dataset = dataset.map(read_file)
 
for features,label in dataset:
    print(features)
    print(label)
    print("===========================================================")

 

解析函数read_file如下

def read_file(filename,label):
    tf.print(type(filename))
    tf.print(type(label))
    
    # filename_ = filename.numpy()
    # label_ = label.numpy()
    
    filename = "./files/" + filename
    tf.print("/")
    
    f =  open(filename,mode="r")
    s =f.readline()
    x_ =s.split(',')
    result =[]
    for i in x_:
        result.append(int(i))
    
    return result,label


代码看起来没什么问题,但是运行实际上显示下面错误:

TypeError: expected str, bytes or os.PathLike object, not Tensor

错误的位置在于

f =  open(filename,mode="r")

意思非常简单,就是说读取文件的这个filename应该是一个str,或者是表示路径的对象,而不应该是一个Tensor对象,

注意:这个问题足足困扰了我有2天之久,在google上面找了很久才找到解决方案,中文搜索几乎没合适的答案。

那怎么办呢?

看起来好像很简单,他既然说了这个filename和label是一个Tensor,那就是我们只要读取到这个Tensor里面的值就可以了啊,不就得到字符串嘛,事实上的确如此,tensorflow2.x中告诉我们获取tensor的值可以使用t.numpy()来获取,但是当我们使用了这两个方法的时候我们发现依然还是错误的,又显示下面的错误:

filename_ = filename.numpy()
label_ = label.numpy()
AttributeError: 'Tensor' object has no attribute 'numpy'

Tensor怎么会没有numpy属性呢?我们不都是通过t.numpy()来获取tensor的值得吗?这实际上引出了下面的一个问题。

二、区分tf.EagerTensor和tf.Tensor

2.1 简单的例子

先看几个简单的例子:​​​​​​

 
In [59]: a = tf.constant([1,2,3,4,5])
 
In [60]: a
Out[60]: <tf.Tensor: shape=(5,), dtype=int32, numpy=array([1, 2, 3, 4, 5])>
 
In [61]: type(a)
Out[61]: tensorflow.python.framework.ops.EagerTensor

发现两个问题:

(1)这里的a的确是一个Tensor,而且它有属性numpy,我们可以通过a.numpy()来获取它的值

(2)它的类型本质上是一个 EagerTensor,

而上面的Tensor之所以没有numpy属性是因为它是这个样子的 

<class 'tensorflow.python.framework.ops.Tensor'>   # 类型
 
tf.Tensor([102 102 102], shape=(3,), dtype=int64)  # Tensor

所以,在tensorflow2.x中,凡是可以用numpy获取值的都是指的是EagerTensor,虽然打印出来显示依然是下面的这种形式:

 <tf.Tensor: ... ...>

而Tensor到底是什么呢?它实际上是静态图中一种Tensor。虽然我们现在是使用的动态库,但是依然是在后台有一个构建graph的过程,Tensor的值并一定能够及时得到,而是需要为如数据之后才能得到,在tensorflow1.x 静态图中,我们需要采用以下方式来获取Tensor的值:

with tf.Session() as sess:
    result = sess.run([t])  # 获取Tensor t 的值
    print(result)
    
    # 或者是
    result = t.eval()

2.2 使用tensorflow2.x的注意事项

关于EagerTensor和Tensor使用的一些注意事项

(1)希望打印看看运算结果,使用tf.print(tensor)而非print(tensor.numpy())

使用tf.print(tensor)能够无论在动态图还是静态图下都能够打印出张量的内容,而print(tensor.numpy())只能在动态图下使用,而且只能够对EagerTensor使用,以下是一个正确的示范:

(2)使用tf.device而非tensor.gpu()、tensor.cpu()

新版本中创建张量时会自动分配到优先级高的设备上,比如存在gpu时,直接会分配到gpu上:

# 需要GPU版本才能看出创建的张量会直接放置到gpu上!CPU版本不行
import tensorflow as tf
print(tf.test.is_gpu_available())
# True
r = tf.random.normal((3, 4))
print(r.device)
# '/job:localhost/replica:0/task:0/device:GPU:0'

对于新版本的设备指定,EagerTensor可以直接通过.cpu().gpu()方法直接将张量移动到对应的设备上,但是tf.Tensor并没有,兼容两者的方法是在tf.device创建的scope下操作。一个在gpu下创建张量并移动到cpu下进行sin操作的错误例子为:

(3)不要遍历张量,尽量使用向量化的操作

EagerTensor是可以被遍历的,但是tf.Tensor不行,所以尽量不要对张量进行遍历,多想一想应该怎么进行向量化的操作,不光动静态图的兼容性都有,向量化之后的速度的提升也是非常大的。

2.3 分析与理解,

我们可以这样理解,

EagerTensor是实时的,可以在任何时候获取到它的值,即通过numpy获取

Tensor是非实时的,它是静态图中的组件,只有当喂入数据、运算完成才能获得该Tensor的值,

那为什么datastep.map(function)

给解析函数function传递进去的参数,即上面的read_file(filename,label)中的filename和label是Tensor呢?

因为对一个数据集dataset.map,并没有预先对每一组样本先进行map中映射的函数运算,而仅仅是告诉dataset,你每一次拿出来的样本时要先进行一遍function运算之后才使用的,所以function的调用是在每次迭代dataset的时候才调用的,但是预先的参数filename和label只是一个“坑”,迭代的时候采用数据将这个“坑”填起来,而在运算的时候,虽然将数据填进去了,但是filename和label依然还是一个Tensor而不是EagerTensor,所以才会出现上面的问题。

注意:两个问题:

(1)Tensor和EagerTensor没有办法直接转化

(2)Tensor没有办法在python函数中直接使用,因为我没办法在python函数中获取到Tensor的值

三、tensorflow与python代码交互的方式——tf.py_function

我们需要自己定义函数的实现,用python编写的函数没有办法直接来与Tensor交互,那怎么办呢?

tensorflow2.x版本提供了函数tf.py_function来时实现自己定义的功能。

3.1 函数原型

tf.py_function(func, inp, Tout, name=None)

作用:包装Python函数,让Python底阿妈可以与tensorflow进行交互

参数:

func :自己定义的python函数名称

inp :自己定义python函数的参数列表,写成列表的形式,[tensor1,tensor2,tensor3] 列表的每一个元素是一个Tensor对象,

         注意与定义的函数参数进行匹配

Tout:它与自定义的python函数的返回值相对应的,

  • 当Tout是一个列表的时候 ,如 [ tf.string,tf,int64,tf.float] 表示自定义函数有三个返回值,即返回三个tensor,每一个tensor的元素的类型与之对应
  • 当Tout只有一个值的时候,如tf.int64,表示自定义函数返回的是一个整型列表或整型tensor
  • 当Tout没有值的时候,表示自定义函数没有返回值

3.2 上面所出现的问题的解决方案

(1)定义自己实现的python函数

# dataset.map函数没有直接使用它,而是先用tf.py_function来包装他
def read_file(filename,label):
    tf.print(type(filename))     # 包装之后类型不再是Tensor,而是EagerTensor
    tf.print(type(label))
    
    filename_ = filename.numpy() # 因为是EagerTensor,可以使用numpy获取值,在tensorflow中,字符串以byte存储,所以它的值是  b'xxxxx'  的形式
    label_ = label.numpy()
    
    new_filename = filename_.decode()  # 将byte解码得到str
    new_filename = "./files/" + new_filename
    
    # 先在的new_filename就是纯python字符串了,可以直接打开了
    f =  open(new_filename,mode="r")
    s =f.readline()
    x_ =s.split(',')
    result =[]
    for i in x_:
        result.append(int(i))
    
    return result,label  # 返回,result是一个列表list

(2)定义一个函数来使用tf.py_function来包装自己定义的python函数

z 注意参数的匹配以及类型的匹配
def wrap_function(x,y):
    x, y = tf.py_function(read_file, inp=[x, y], Tout=[tf.int32, tf.int32])
    return x,y

当然我们也可以不用编写包装函数,直接使用lambda表达式一步到位,

如果不使用tf.py_function()来包装这里的读取函数read_file,则read_file的两个参数都是Tensor

而使用了tf.py_function()来包装read_file函数之后,它的参数就变成了EagerTensor,

至于为什么是这样子,我还不是很清楚,望有大神告知!

即如下:

dataset = dataset.map(lambda x, y: tf.py_function(read_file, inp=[x, y], Tout=[tf.int32, tf.int32]))

(3)编写dataset的pipeline

X=["file1.txt","file2.txt","file3.txt"]
Y=[[1,0,0],[0,1,0],[0,0,1]]
 
dataset = tf.data.Dataset.from_tensor_slices((X,Y))  # 第一步:构造dataset对象 
 
dataset = dataset.map(wrap_function)
 
dataset=dataset.repeat(3)       # 重复三次                                   
dataset=dataset.batch(3)        # 每次3个样本一个batch
 
 
for features,label in dataset:
    print(features)
    print(label)
    print("=================================================================")

运行结果如下:

tf.Tensor(
[[  1   2   3   4   5]
 [ 11  22  33  44  55]
 [111 222 333 444 555]], shape=(3, 5), dtype=int32)
tf.Tensor(
[[1 0 0]
 [0 1 0]
 [0 0 1]], shape=(3, 3), dtype=int32)
=======================================================================================================
tf.Tensor(
[[  1   2   3   4   5]
 [ 11  22  33  44  55]
 [111 222 333 444 555]], shape=(3, 5), dtype=int32)
tf.Tensor(
[[1 0 0]
 [0 1 0]
 [0 0 1]], shape=(3, 3), dtype=int32)
=======================================================================================================
tf.Tensor(
[[  1   2   3   4   5]
 [ 11  22  33  44  55]
 [111 222 333 444 555]], shape=(3, 5), dtype=int32)
tf.Tensor(
[[1 0 0]
 [0 1 0]
 [0 0 1]], shape=(3, 3), dtype=int32)
=======================================================================================================

可以发现,现在的结果完全吻合!

3.3 关于Tensor与EagerTensor的进一步说明

注意:EagerTensor是可以直接与python代码进行交互的,也可以进行迭代便利操作,不支持与Python直接进行交互的实际上是Tensor,这需要格注意,如下所示的例子:

(1)EagerTensor与python函数的交互

def iterate_tensor(tensor):
    tf.print(type(tensor))  # EagerTensor
    (x1, x2, x3), (x4, x5, x6) = tensor
    return tf.stack([x2, x4, x6])
 
 
const = tf.constant(range(6), shape=(2, 3)) # EagerTensor
o = iterate_tensor(const)
print(o)
'''运行结果为:
<class 'tensorflow.python.framework.ops.EagerTensor'>
tf.Tensor([1 3 5], shape=(3,), dtype=int32)
'''

(2)Tensor与python函数的交互

使用tf.function来修饰函数,如下:

@tf.function
def iterate_tensor(tensor):
    tf.print(type(tensor))  # Tensor
    (x1, x2, x3), (x4, x5, x6) = tensor
    return tf.stack([x2, x4, x6])
 
 
const = tf.constant(range(6), shape=(2, 3)) # EagerTensor
o = iterate_tensor(const)
print(o)

因为使用了tf.function来修饰Python函数,会将其编译为静态图的操作,此时的tensor变为了Tensor,所以上面的代码会出错:

OperatorNotAllowedInGraphError: iterating over `tf.Tensor` is not allowed: AutoGraph did not convert this function. Try decorating it directly with @tf.function.

由此可见tensor变成了Tensor,不允许对其进行迭代操作,会出现错误。

总结:一定要注意区分EagerTensor和tf.Tensor

在动态图下创建的张量是EagerTensor(引用方式为from tensorflow.python.framework.ops import EagerTensor),在静态图下创建的张量是tf.TensorEagerTensortf.Tensor虽然非常相似,但是不完全一样,如果依赖于EagerTensor特有的一些方法,会导致转换到静态图时tf.Tensor没有这些方法而报错

我们很多时候不知道一个tensor到底是EagerTensor还是Tensor呢?最简单的方式就是使用

tf.print(type(tensor_name))

进行查看

四、补充——关于tf.py_function和tf.numpy_function

必须承认是的TensorFlow的存在的这么多(len(dir(tf.raw_ops))个,大约1227个)的Op依然不足以完全覆盖numpy所有的功能,因此在一些情况下找不到合适的Op(或者Op组合)表达运算逻辑时,能用上numpy的函数也是挺好的,因此可能会有人会想到先EagerTensor转换成numpy然后用numpy运算完再转换成Tensor,tf.function可不允许这么做,还是老老实实用tf.numpy_function吧。(当然可以自己写Op Kernel然后编译使用,后续看看有没有额外的时间做自定义Op的总结,目前还是把早年立的填2.0的总结的坑的flag搞定再说> <)

关于更多

tf.py_function

tf.numpy_function

的使用请参见后面的例子吧

猜你喜欢

转载自blog.csdn.net/yxpandjay/article/details/109053539