tfrecord文件的map在使用的时候所踩的坑总结(map、py_function、numpy_function)

前言:在使用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

关于这两个函数的使用,会在下面一篇文章里面详细讨论

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值