Dataset可以看作是相同类型“元素”的有序 列表。在实际使用时,单个“元素”可以是向量,也可以是字符串、图片,甚至是tuple或者dict。Dataset是google点名建议的读取数据的方式,所以在tf使用中有很重要的地位。
上面这个定义其实已经描述了dataset,他是一个可迭代对象,而且是有序的,而且每个元素的类型相同。在工作中为了方便理解,我一般把它看作java中的List
应该注意的是,他和List
Dataset的作用是为了更加高效便捷的读取数据。知道这个目的,也就自然而然的知道Dataset应该怎么生成了:它应该在读取数据的过程中生成,即把数据读取成一个Dataset,以便后面的处理。
所以下面的这张图片也就不难理解了,TextLineDataset,TFRecordDataset和FixedLengthRecordDataset 分别是三种生成Dataset的方式。TextLineDataset用于读取文本数据,TFRecordDataset用于读取tfrecord数据,FixedLengthRecordDataset用于从二进制文件数据数据。
上述的三种方法的用法也都极其简单:
tf.data.TFRecordDataset(
filenames, compression_type=None, buffer_size=None, num_parallel_reads=None
)
tf.data.TextLineDataset(
filenames, compression_type=None, buffer_size=None, num_parallel_reads=None
)
tf.data.FixedLengthRecordDataset(
filenames, record_bytes, header_bytes=None, footer_bytes=None, buffer_size=None,
compression_type=None, num_parallel_reads=None
)
其中,TextLineDataset和TFRecordDataset 都只需要输入文件名,FixedLengthRecordDataset需要输入文件名和每个样本的字节长度。
除此之外,还有一些用的比较少,一般用于学习或者测试代码的生成Dataset的方法。例如从list或者numpy中生成:
dataset = tf.data.Dataset.from_tensor_slices([1, 2, 3])
直接生成一个依次递增1的dataset:
dataset = tf.data.Dataset.range(10)
还有一些更冷门的用的就更少了,这里不多介绍
通过TextLineDataset,TFRecordDataset和FixedLengthRecordDataset,其实数据读取工作就已经完成了。后面的工作全部都是对Dataset的处理。由于平时tfrecord用的最多,而且处理也最复杂,所以下面以TFRecordDataset为例介绍Dataset的后处理工作。中间会夹杂稍微介绍一点TextLineDataset的处理。
对Dataset的处理一般通过调用Dataset的各种方法来实现。这些方法在tensorflow的官方文档中都有介绍。但无论是对Dataset多么复杂的操作,我们都只需要记住:
Dataset,就是一个统一了元素类型的List。
Dataset,就是一个统一了元素类型的List。
Dataset,就是一个统一了元素类型的List。
只要记住了这一点,无论对这个Dataset做了多么复杂的操作,我们都能对当前状态有一个清醒的了解。下面介绍常用的Dataset的操作
1. map
其实map方法并不是Dataset独有的方法,其本质是Python所有的可迭代对象的一个方法。map方法的用法如下:
map(
map_func, num_parallel_calls=None, deterministic=None
)
在Python中,可迭代对象是指有__iter__属性的对象,这类对象可以用循环取迭代,所以可以放在for中迭代,例如list,dict,DataFrame等都是可迭代对象。而不可迭代对象例如整型,float等不是可迭代对象,放在循环中会报错 “object is not iterable”。
map方法只有一个必须的入参,是一个函数。map方法会对Dataset的每个元素调用这个函数。
举例来说,我们都知道读取了tfrecord以后,一般会用tf.io.parse_single_example来处理一下,代码如下所示
data3 = data.map(lambda x : tf.io.parse_single_example(x, features = feature))
那么这一行代码的是什么意思呢?在https://blog.csdn.net/kangshuangzhu/article/details/106814664 中我已经介绍了,tfrecord文件本质上还是一个二进制文件,所以读进来的就是二进制字符,所以我们要解析成example来操作。
tf.io.parse_single_example的作用就是把每一行的二进制字符解析成example。
其实如果我们在读进来tfrecord以后,完全可以直接把每个元素输出,可以看到是一行行的乱码字符串:
tfrecordpath = "./tfrecord" #假设有这么一个tfrecord文件
data = tf.data.TFRecordDataset(tfrecordpath)
for line in data:
print(line)
我们完全不理会是不是乱码,就把它当成一个字符串来处理
def line_settle(x):
tf.string_split(x,"\\") # 把每一行的字符串张量用"\"分隔开
tfrecordpath = "./tfrecord" #假设有这么一个tfrecord文件
data = tf.data.TFRecordDataset(tfrecordpath)
data.map(line_settle)
同样的道理,现在我们有一个文本他的内容是这样的
name:john age:20 gender:male address:beijing
name:joe age:12 gender:female address:beijing
name:mike age:23 gender:male address:shanghai
name:wolf age:33 gender:female address:shenzhen
把上面的内容保存成一个文本,名为info,并且把里面的空格替换成tab,用TextLineDataset读取后的Dataset每个元素同样得到的就是上面每一行的string tensor。我们可以通过下面的代码进行处理,处理以后得Dataset每个元素是tuple[tensor],格式如下:
def line_settle(x):
info_str = str(x.numpy(), encoding = "utf-8")
info_list = info_str.split(" ")
return info_list
infopath = "info.txt" #文本文件
# infopath = "cowper.txt" #文本文件
data = tf.data.TextLineDataset(infopath)
data2 = data.map(labmda x: tf.py_function(line_settle.[x],[tf.string,tf.string,tf.string])
for batch in data2:
print(batch)
这里有一个非常复杂的语法
data2 = data.map(labmda x: tf.py_function(line_settle.[x],[tf.string,tf.string,tf.string])
之所以这么写,是因为tensorflow2.0留下了一个非常大的坑。如果Dataset的每个元素是tensor,那么Dataset进行for循环的时候,得到的每个元素是eagerTensor,可以直接读取其中的内容。但是,在使用map的时候却是通过Tensor来实现的。也就是说,map的时候,实际上还是用tensorflow1.x那一套静态图的逻辑,这就导致我们是无法得到每个元素中的内容的。tensorflow2.0 中的一些语法,例如tensor.numpy()这类写法失效了。这时候就需要用tf.py_function把静态图中的tensor进行eager excution。
py_function的语法如下:
tf.py_function( func, inp, Tout, name=None )
三个必须入参是
Args
func
A Python function which accepts a list of Tensor
objects having element types that match the correspondingtf.Tensor
objects ininp
and returns a list ofTensor
objects (or a singleTensor
, orNone
) having element types that match the corresponding values inTout
.inp
A list of Tensor
objects.Tout
A list or tuple of tensorflow data types or a single tensorflow data type if there is only one, indicating what func
returns; an empty list if no value is returned (i.e., if the return value isNone
).name
A name for the operation (optional). 其中,比较值得注意的是Tout规定了输出只能是tensorflow的类型,不能是dict或者其他类型。这就导致tensor为元素的Dataset的map方法没有非常灵活的输出。
总结一下,map其实就是对Dataset的每个元素都执行了同一个函数。但,如果Dataset的元素是tensor那么map会通过静态图的方法调用函数。
apply
apply很容易和map搞混,他们的区别是map堆Dataset的每个元素进行同样的操作,而apply则是对每个Dataset做一个操作。例如
dataset = tf.data.Dataset.range(100)
def dataset_fn(ds):
return ds.filter(lambda x: x < 5)
dataset = dataset.apply(dataset_fn)
list(dataset.as_numpy_iterator())
batch
batch的作用是把dataset的元素合并,我们可以理解成这样,原有一个list
a = [1,2,3,4,5,6,7,8,9,10]
a.batch(3)
[ [1,2,3], [4,5,6], [7,8,9], [10] ]
如果Dataset的元素本社那就是一个集合,由于dataset的每个元素类型相同,会把相应位置的进行合并,例如:
a = [[john,20,male,beijing],
[joe,12,female,beijing],
[mike,23 ,male,shanghai],
[wolf,33,female,shenzhen]]
a.batch(2) = [ [ [john,joe],[20,12], [male,female],[beijing,beijing] ],
[ [mike,wolf],[23,33], [mael,female],[shanghai,shenzhen] ]
]
应该注意的是,用了batch以后,dataset就只能用parse_example 来解析了
filter
与map非常类似,同样用静态图的方法调用函数。
repeat
dataset.repeate(n), dataset重复n遍,是123,123,123的重复,而不是111,222,333的重复
如果省略n那么是无穷次重复
list_file
这个函数用于处理文件路径,假设我们的文件都放在文件夹holder 中,其中有文件a.text,b.text,c.text。如果我们想把所有的数据都读取进来,如果只写data = tf.data.TextLineDataset("./holder/*")是不行的。首先应该先读取所有的文件名:
filename = tf.data.Dataset.list_files(["./holder/*"])
for batch in filename :
print(batch)
tf.Tensor(b'.\\holder\\a.txt', shape=(), dtype=string)
tf.Tensor(b'.\\holder\\b.txt', shape=(), dtype=string)
tf.Tensor(b'.\\holder\\c.txt', shape=(), dtype=string)
那如果我们的训练文件放在多个文件夹中,也可以用这种方式把所有的文件名都读出来。假设有一个文件夹holder2,里面放着d.txt, e.txt
filename = tf.data.Dataset.list_files(["./holder/*","./holder2/*"])
for batch in filename :
print(batch)
tf.Tensor(b'.\\holder\\a.txt', shape=(), dtype=string)
tf.Tensor(b'.\\holder\\b.txt', shape=(), dtype=string)
tf.Tensor(b'.\\holder\\c.txt', shape=(), dtype=string)
tf.Tensor(b'.\\holder\\e.txt', shape=(), dtype=string)
tf.Tensor(b'.\\holder\\d.txt', shape=(), dtype=string)
interleave
用法
interleave(
map_func, cycle_length=None, block_length=None, num_parallel_calls=None,
deterministic=None
)
上面list_file可以把文件夹中的文件都列举出来,interleave可以把这些文件全部读取出来。假设a.txt中内容就是字母a,b.txt中内容就是字母b。。。那么:
word = rfile.interleave(lambda x: tf.data.TextLineDataset(x))
for batch in word:
print(batch)
tf.Tensor(b'e', shape=(), dtype=string)
tf.Tensor(b'd', shape=(), dtype=string)
tf.Tensor(b'b', shape=(), dtype=string)
tf.Tensor(b'a', shape=(), dtype=string)
tf.Tensor(b'c', shape=(), dtype=string)
当然,interleave并不是专门为了读取数据而准备的 ,interleave也是一个对dataset元素进行操作的方法。下面举一个tensorflow2.3官方的例子:
dataset = Dataset.range(1, 6) # ==> [ 1, 2, 3, 4, 5 ]
# NOTE: New lines indicate "block" boundaries.
dataset = dataset.interleave(
lambda x: Dataset.from_tensors(x).repeat(6),
cycle_length=2, block_length=4)
list(dataset.as_numpy_iterator())
结果为:
interleave命令中cycle_length和block_length虽然是可选入参,但实际应用也很多。
interleave是根据dataset的每个元素生产新的元素,那么新的元素是怎么排列的呢?
比如原来dataset的元素是 [1,2,3]。现在产生的新元素是:
"1" 产生了 [a, a, a, a, a] 五个元素
"2"产生了 [b, b, b, b, b]
"3" 产生了[c, c, c, c, c]
那这新产生的15个元素怎么排列,是[a,b,c,a,b,c.....] 还是[a,a,a,a,a,b,b,b,......] 还是 [a,a,b,b,a,a,b,b,a,b]。就是通过cycle_length和block_length来控制。
cycle_length:
cycle_length:n ,原dataset的元素每n个一组,上一组的元素产生的新元素输出完了,才开始输出下一组。
还是刚才的例子,cycle=2的时候,原dataset元素每2个一组,就是 [1,2]一组,[3]一组。那么产生的新元素顺序就是:
a, b, a, b, a, b, a, b, a, b, #第一组的元素产生的新元素输出完了
c, c, c, c, c #第二组元素产生的新元素
所以新元素的排列顺序就是[ a, b, a, b, a, b, a, b, a, b, c, c, c, c, c]
block_length:
block_length:m 同一组内元素,先输出第一个元素产生的m个新元素,再输出第二个元素产生的m个新元素,依次循环,知道把所有元素生成完。
还是刚才的例子 cycle=2,block_length=2的时候。产生的新元素顺序是:
#block_length=2
#cycle_length=2
a,a ##第一组第一个元素1产生的新元素输出block_length=2个
b,b ##第一组第二个元素2产生的新元素输出block_length=2个
a,a ##第一组第一个元素1产生的新元素再输出block_length=2个
b,b ##第一组第二个元素2产生的新元素再输出block_length=2个
a ##第一组第一个元素1产生的新元素全部输出完毕
b ##第一组第二个元素2产生的新元素全部输出完毕
c,c ##第二组第一个元素1产生的新元素输出block_length=2个
c,c ##第二组第一个元素1产生的新元素再输出block_length=2个
c ##第二组第一个元素2产生的新元素全部输出完毕
所以新元素的排列顺序就是[ a, a, b, b, a, a, b, b, a, b, c, c, c, c, c]
cycle_length与block_length和是否多线程没有关系
map和interleave的区别
map和interleave的用法十分类似,他们的共同点是他们都是堆Tensor做的操作,也就是说interleave也是通过静态图的方法进行操作。
他们的区别是
map是对dataset的每个元素做处理,原dataset的一个元素“变成了”一个新的元素。这个过程中,dataset的元素个数不会发生变化
而interleave则是根据dataset 的元素,原dataset 的一个元素“生成了”n个新的元素,新生成的元素组成了新的dataset。
注意这2个过程的区别,map是对元素进行了变换,所以新老dataset的元素是一样多的,可以意义对应。而interleave的中文就是插入新的,所以新的dataset是由新生成的元素拼装起来的,新老dataset的元素不一定一样多。