前言: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的值得吗?这实际上引出了下面的一个问题。
2.1 简单的例子
先看几个简单的例子:
In [59]: a = tf.constant([1,2,3,4,5])
In [60]: a
Out[60]:
In [61]: type(a)
Out[61]: tensorflow.python.framework.ops.EagerTensor
发现两个问题:
(1)这里的a的确是一个Tensor,而且它有属性numpy,我们可以通过a.numpy()来获取它的值
(2)它的类型本质上是一个 EagerTensor,
而上面的Tensor之所以没有numpy属性是因为它是这个样子的
# 类型
tf.Tensor([102 102 102], shape=(3,), dtype=int64) # Tensor
可见它是没有numpy属性的,所以会报错,
所以,在tensorflow2.x中,凡是可以用numpy获取值的都是指的是EagerTensor,虽然打印出来显示依然是下面的这种形式:
而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的值
我们需要自己定义函数的实现,用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)
'''运行结果为:
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.Tensor
。EagerTensor
和tf.Tensor
虽然非常相似,但是不完全一样,如果依赖于EagerTensor
特有的一些方法,会导致转换到静态图时tf.Tensor
没有这些方法而报错
我们很多时候不知道一个tensor到底是EagerTensor还是Tensor呢?最简单的方式就是使用
tf.print(type(tensor_name))
进行查看
必须承认是的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
的使用请参见后面的例子吧