前言:在使用tensorflow解析tfrecord文件的时候,踩过很多坑,其中一个便是关于tensor,eagertensor的坑,前面的一片文章已经有所说明,本文基于tensorflow2.1,在前面一篇文章的基础之上进行补充,前面的文章参考:
tensorflow2.x之由dataset.map引发出的关于tf.py_function以及tf.numpy_function问题
问题的原因分析,当输入X所对应的值是字符串的时候,比如前面文章中的文件路径,没有办法使用map直接解析,需要借助于tf.py_function来实现,但是X如果是数字的时候,又是没有问题的,瞎弄下面的例子来看。
一、问题引出
1.1 当X是数值的时候
def map_function(x,y):
'''
定义一个简单的函数,给每一个X的特征值加上100
'''
tf.print(type(x)) # 这里的每一个样本都是Tensor,不再是EagerTensor
tf.print(type(y)) # <class 'tensorflow.python.framework.ops.Tensor'>
tf.print("++++++++++++++++++++++++++++++++++++")
# x_ = x.numpy() # 这句话会出错,因为x,y不再是EagerTensor,所以提示没有numpy属性
x = x + 100 # 给每一个X的值加上 100 再返回,这是正确的,先不管为什么
return x,y
下面是解析代码
X=[[1,2,3],[4,5,6],[7,8,9]]
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(map_function)
for features,label in dataset:
print(f"type is {type(features)}") # 因为是tensorflow,所以默认的是EagerTensor
print(f"type is {type(label)}") # EagerTensor,<class 'tensorflow.python.framework.ops.EagerTensor'>
print(features)
print(label)
print("===========================================================")
1.2 现在当我们的X是string的时候
def map_function(x,y):
tf.print(type(x)) # 这里的每一个样本都是Tensor,不再是EagerTensor
tf.print(type(y)) # <class 'tensorflow.python.framework.ops.Tensor'>
tf.print("++++++++++++++++++++++++++++++++++++")
# 希望将每一个X的小写字母变成大写字母
return x.numpy().decode('utf-8').upper(),y # 出错,提示没numpy属性
X=[["thumb"],["index"],["middle"]]
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(map_function)
for features,label in dataset:
print(f"type is {type(features)}") # 因为是tensorflow,所以默认的是EagerTensor
print(f"type is {type(label)}") # EagerTensor,<class 'tensorflow.python.framework.ops.EagerTensor'>
print(features)
print(label)
print("===========================================================")
1.3 原因分析——为什么明明X,Y是EagerTensor,到了map_function里面却变成了Tensor了呢?
官方文档中有这样的描述,
链接为:https://www.tensorflow.org/api_docs/python/tf/data/Dataset#map
map_func
is defined (eager vs. graph), tf.data traces the function and executes it as a graph. To use Python code inside of the function you have two options:
简单理解即为:tf.data追踪执行这个函数是作为一个 graph (静态图)来执行的,所以传给他的参数都会转化成静态图使用的Tensor,而不再是EagerTensor,所以我们在使用这个函数的时候有两个注意事项如下,后面再说。
总结:map_function里面的内容是静态的graph。
(1)注意事项:
map_function是没有办法断点调试,因为他是静态graph,在上面的1.1中,我们在 x = x+100 这句话上面设置断点,然后启动调试,发现他并不会命中断点,也不会暂停,而是自己执行完了。
(2)既然都是Tensor,为什么1.1正确运行,但是1.2却不行呢?
这个地方具体的原因我还真不是很清楚,但是记住一点,对于数值类型,如int、float,我们在map_function里面进行简单的运算是没有问题的,即便它是Tensor,但是对于字符串string是不行的,我甚至都没有办法取得字符串的值。
二、解决方案
2.1 关于map的两个注意事项
续接 1.3
如果我们需要在map_function里面编写python风格的代码,我们需要注意下面这两个注意事项:
(1) Rely on AutoGraph to convert Python code into an equivalent graph computation. The downside of this approach is that AutoGraph can convert some but not all Python code.
(2)Use
tf.py_function
, which allows you to write arbitrary Python code but will generally result in worse performance than (1)
(1)我们可以将map_function函数通过tf.autograph转化成AutoGraph,但是需要注意的是,并不是所有的Python代码都可以转化成AutoGraph的;
(2)使用tf.py_function来包装map_function函数,这允许我们在map_function里面编写任意风格的python代码,但是这样做的缺点是相较于(1)的AutoGraph,执行性能差一些。
我们针对方式二来解决上面得问题
2.2 使用tf.py_function来解决map所存在的问题
(1)针对 1.1 中的代码
import tensorflow as tf
import numpy as np
def map_function(x,y):
tf.print(type(x)) # 通过tf.py_function包装之后的map_function,x,y都是EagerTensor
tf.print(type(y)) # 'tensorflow.python.framework.ops.EagerTensor'>
tf.print("++++++++++++++++++++++++++++++++++++")
# x_ = x.numpy() # 这句话正确,因为x,y现在是EagerTensor,可以使用numpy属性
x = x + 100 # 给每一个X的值加上 100 再返回,这是正确的,先不管为什么
return x,y
# 再定义一个包装函数,包装map_function,注意参数的匹配以及类型的匹配
def wrap_map_function(x,y):
# 通过tf.py_function()包装map_function函数
x, y = tf.py_function(map_function, inp=[x, y], Tout=[tf.int32, tf.int32])
return x,y
# X=[["thumb"],["index"],["middle"]]
X=[[1,2,3],[4,5,6],[7,8,9]]
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(wrap_map_function)
for features,label in dataset:
print(f"type is {type(features)}") # 因为是tensorflow,所以默认的是EagerTensor
print(f"type is {type(label)}") # EagerTensor,<class 'tensorflow.python.framework.ops.EagerTensor'>
print(features)
print(label)
print("===========================================================")
当然我也可以不定义包装函数,直接通过给dataset.map()函数传递一个lambda表达式来完成也是一样的。
如下:
dataset = dataset.map(lambda x, y: tf.py_function(map_function, inp=[x, y], Tout=[tf.int32, tf.int32]))
(2)针对1.2 中的代码
import tensorflow as tf
import numpy as np
'''
https://www.tensorflow.org/api_docs/python/tf/data/Dataset#map
'''
def map_function(x,y):
tf.print(type(x)) # 由于map_function经过了tf.py_function包装这里的每一个样本都是EagerTensor,不再是Tensor
tf.print(type(y)) # <class 'tensorflow.python.framework.ops.EagerTensor'>
# tf.print("++++++++++++++++++++++++++++++++++++")
x_str = x.numpy()[0] # EagerTensor有numpy属性
x_str = x_str.decode("utf-8") # tensorflow中的string组成的tensor全部都是二进制的,即 b'xxx'
x_str = x_str.upper() # 将字符串转化成大写
print(x_str)
return x_str,y
X=[["thumb"],["index"],["middle"]]
Y=[[1,0,0],[0,1,0],[0,0,1]]
# 构建dataset对象
dataset = tf.data.Dataset.from_tensor_slices((X,Y)) # 第一步:构造dataset对象
# 对每一个dataset的元素,实际上就是一个example进行解析
# 这里不使用包装函数了,直接使用lambda表达式一步到位,需要注意返回的类型第一个为tf.string
dataset = dataset.map(lambda x,y:tf.py_function(map_function, inp=[x, y], Tout=[tf.string, tf.int32]))
for features,label in dataset:
print(f"type is {type(features)}") # 因为是tensorflow,所以默认的是EagerTensor
print(f"type is {type(label)}") # EagerTensor,<class 'tensorflow.python.framework.ops.EagerTensor'>
print(features)
print(label)
print("===========================================================")
运行结果如下:
tf.Tensor(b'THUMB', shape=(), dtype=string)
tf.Tensor([1 0 0], shape=(3,), dtype=int32)
===========================================================
tf.Tensor(b'INDEX', shape=(), dtype=string)
tf.Tensor([0 1 0], shape=(3,), dtype=int32)
===========================================================
tf.Tensor(b'MIDDLE', shape=(), dtype=string)
tf.Tensor([0 0 1], shape=(3,), dtype=int32)
可见已经转换成了大写字母,说明执行成功了。
三、再看一个实例
现在有三个文件,
file1.txt,里面的内容是1,2,3,4,5
file2.txt,里面的内容是11,22,33,44,55
file3.txt,里面的内容是111,222,333,444,555
我的X,Y是下面这样的形式:
X=["file1.txt","file2.txt","file3.txt"]
Y=[[1,0,0],[0,1,0],[0,0,1]]
现在我需要每次解析一条样本,解析的过程即从文件中读取相应的数据,然后返回,
首先定义解析函数:
def map_function(filename,label):
tf.print(type(filename)) # 这两个应该是EagerTensor
tf.print(type(label))
filename_ = filename.numpy() # 通过numpy获取每一条样本的文件名称
filename_ = filename_.decode("utf-8") # 需要将bytes转化成str,得到 file1.txt、file2.txt
# 现在可以使用纯python操作了,获取文件路径
filename = "./" + filename_
f = open(filename,mode="r")
s =f.readline()
x_ =s.split(',')
result =[]
for i in x_:
result.append(int(i))
return result,label
然后定义包装函数,通过tf.py_function来包装
def wrap_map_function(filename,label):
x, y = tf.py_function(map_function, inp=[filename, label], Tout=[tf.int32, tf.int32])
return x, y
迭代数据
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(wrap_map_function)
for features,label in dataset:
print(features)
print(label)
print("===========================================================")
运行结果为:
tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32)
tf.Tensor([1 0 0], shape=(3,), dtype=int32)
===========================================================
tf.Tensor([11 22 33 44 55], shape=(5,), dtype=int32)
tf.Tensor([0 1 0], shape=(3,), dtype=int32)
===========================================================
tf.Tensor([111 222 333 444 555], shape=(5,), dtype=int32)
tf.Tensor([0 0 1], shape=(3,), dtype=int32)
===========================================================
总结:
关于map_function函数的参数定义与返回值说明:
(1)参数:参数的个数一般是与每一条样本需要解析的数据来决定的,只要使用了tf.py_function()函数来包装,参数的类型总是会变成EagerTensor,如果不使用tf.py_function()函数来包装,参数的类型总是Tensor,这样在map_function里面编写一些对Tensor进行操作的python操作,可能会出现意想不到的问题。所以一般在编写的时候首先使用type(x)来确定参数的类型是Tensor还是EagerTensor,然后看是不是普通的Python函数若果直接对Tensor操作是否可行,来决定是否是需要包装。
在上面的两个例子中,1.1 由于仅仅是对数据进行加法100操作,我们可以有以下几种选择,
第一:不使用任何包装,因为Tensor本身也是支持加法+操作的,即 tensor + 100 不会有问题
第二:可以使用tf.function()来将map_function包装成AutoGraph,因为加法操作是支持转化成graph的operation的;
第三:使用tf.py_function()来包装。
但是在1.2 的例子中就不行了,不管是转化成大写操作还是从字符换读取文件,普通的Python操作与tensorflow的操作不统一,所以没办法直接针对Tensor来完成,所以需要转化成EagerTensor,然后获取其值,用Python的方式来完成;
另外,字符串的转化与文件读取操作也不支持转化成AutoGraph的操作,所以只能够通过第三种方式,那就是使用tf.py_function()来包装map_function。
(2)返回值:
在上面的两个例子中,一个是返回了转化成大写之后的字符串,一个是从文本文件读取数据之后的整数,返回的类型分别如下:
对于字符串,返回的是:
x_str = x_str.upper() # 将字符串转化成大写
return x_str,y # 这里的x_str就是一个普通的Python字符串,不再需要包装成Tensor了哦!
而解析之后的到的结果是:
tf.Tensor(b'INDEX', shape=(), dtype=string)
对于从文件读写,返回的是:
result =[]
for i in x_:
result.append(int(i))
return result,label # 这里的result就是一个普通的python列表,也不需要额外的包装了
返回的结果却是:
tf.Tensor([111 222 333 444 555], shape=(5,), dtype=int32)
由此可见,不管map_function的返回值是一个什么类型的数据,她总是会被转化成与之对应的Tensor对象来返回,不需要手动再包装,这个过程是它自己来完成的,我们只需要按照编写python代码的逻辑来编写map_function就可以了。
总结:
map_function默认是使用静态graph来执行,所以传入给map_function的参数都会自动被包装成Tensor,要想在map_function里面编写解析的Python代码,有两种解决办法。
一种是使用tf.function()来包装map_function函数,然后将里面的python代码转化成静态图graph,但是这样容易出错,因为很多的Python代码并不能够转化成graph运算,
另外一种方法就是通过tf.py_function()来包装map_function函数。
注意一点,一个自定义的解析函数,通过dataset.map(func),之后,不管解析的数据本身是EagerTensor还是Tensor,都会变成Tensor,因为map函数会将它所包装的func转化成静态graph,所以总是Tensor。
四、关于tf.py_function与tf.numpy_function
关于这两个函数的使用,会在下面一篇文章里面详细讨论