Dataset.map,Dataset.prefetch和Dataset.shuffle中buffer_size的含义


94

根据TensorFlow 文档,类的prefetchmap方法tf.contrib.data.Dataset都有一个名为的参数buffer_size

对于prefetchmethod,该参数称为,buffer_size并且根据文档:

buffer_size:一个tf.int64标量tf.Tensor,表示预取时将要缓冲的最大元素数。

对于该map方法,output_buffer_size根据文档,该参数称为和:

output_buffer_size:(可选。)tf.int64标量tf.Tensor,表示将要缓冲的最大已处理元素数。

同样,对于shuffle方法,根据文档显示相同的数量:

buffer_size:一个tf.int64标量tf.Tensor,表示此数据集中要从中采样新数据集的元素数。

这些参数之间有什么关系?

假设我创建一个Dataset对象,如下所示:

 tr_data = TFRecordDataset(trainfilenames)
    tr_data = tr_data.map(providefortraining, output_buffer_size=10 * trainbatchsize, num_parallel_calls\
=5)
    tr_data = tr_data.shuffle(buffer_size= 100 * trainbatchsize)
    tr_data = tr_data.prefetch(buffer_size = 10 * trainbatchsize)
    tr_data = tr_data.batch(trainbatchsize)

buffer上面片段中的参数在扮演什么角色?


1
找不到指向“文档”的404链接。
Pradeep Singh

Answers:


145

TL; DR尽管名称相似,但这些参数具有完全不同的含义。的buffer_sizeDataset.shuffle()可以影响你的数据集的随机性,因此在其中的元件所产生的顺序。该buffer_sizeDataset.prefetch()只影响它需要产生下一个元素的时间。


中的buffer_size参数tf.data.Dataset.prefetch()和中的output_buffer_size参数tf.contrib.data.Dataset.map()提供一种调整输入管道性能的方式:两个参数都告诉TensorFlow创建最多一个buffer_size元素的缓冲区,以及一个背景线程以在后台填充该缓冲区。(请注意,我们去掉了output_buffer_size从参数Dataset.map()时,从移动tf.contrib.datatf.data。新代码应该使用Dataset.prefetch()map()获得相同的行为。)

通过将数据的预处理与下游计算重叠,添加预取缓冲区可以提高性能。通常,最有用的是在流水线的末端添加一个小的预取缓冲区(可能只有一个元素),但是更复杂的流水线可以从附加的预取中受益,尤其是在产生单个元素的时间可能不同的情况下。

相比之下,buffer_size参数to tf.data.Dataset.shuffle()影响转换的随机性。我们设计了Dataset.shuffle()转换(如tf.train.shuffle_batch()它所替换的函数)来处理太大而无法容纳在内存中的数据集。它没有改组整个数据集,而是维护一个buffer_size元素缓冲区,并从该缓冲区中随机选择下一个元素(如果可用,将其替换为下一个输入元素)。更改值的值buffer_size会影响混洗的均匀程度:ifbuffer_size大于数据集中的元素数量,则会得到均匀的混洗;如果是1那么您根本不会洗牌。对于非常大的数据集,典型的“足够好”的方法是在训练之前将数据随机分片到多个文件中,然后均匀地对文件名进行混洗,然后使用较小的混洗缓冲区。但是,适当的选择将取决于培训工作的确切性质。



对于这个解释,我仍然有些困惑tf.data.Dataset.shuffle()。我想知道确切的改组过程。可以说,第一个batch_size样本是从第一个buffer_size元素中随机选择的,依此类推。
Bs他

1
@mrry IIUC改组文件名很重要,因为否则每个时期将在批处理0 ... 999中看到相同的元素;并分批1000.1999; 等等,我假设1个文件= 1000批。即使使用文件名改组,仍然存在一些非随机性:这是因为文件#k中的示例在每个时期都彼此接近。可能还不错,因为文件#k本身是随机的。在某些情况下,即使那样也可能使培训混乱。获得完美改组的唯一方法是将buffer_size文件大小设置为相等(当然也改组文件)。
最大

Tensorflow RC 15.0。随着dataset.shuffle(buffer_size=1)改组仍然发生。有什么想法吗?
谢尔盖·布什曼诺夫

@SergeyBushmanov,这可能取决于改组之前的转换,例如list_files(),默认情况下会在每个时期的开始对文件名进行改组。
小龙

125

的重要性buffer_sizeshuffle()

我想跟进从@mrry以前的答案强调重要性buffer_sizetf.data.Dataset.shuffle()

处于低水平不仅buffer_size会在某些情况下使您的洗牌效果不佳:还会使您的整个训练变得混乱。


一个实际的例子:猫分类器

例如,假设您正在针对图像训练猫分类器,并且您的数据是以以下方式组织的(10000每个类别中都有图像):

train/
    cat/
        filename_00001.jpg
        filename_00002.jpg
        ...
    not_cat/
        filename_10001.jpg
        filename_10002.jpg
        ...

输入数据的标准方法tf.data可以是拥有文件名列表和对应标签列表,并用于tf.data.Dataset.from_tensor_slices()创建数据集:

filenames = ["filename_00001.jpg", "filename_00002.jpg", ..., 
             "filename_10001.jpg", "filename_10002.jpg", ...]
labels = [1, 1, ..., 0, 0...]  # 1 for cat, 0 for not_cat

dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.shuffle(buffer_size=1000)  # 1000 should be enough right?
dataset = dataset.map(...)  # transform to images, preprocess, repeat, batch...

大问题与上面的代码是数据集实际上将不会以正确的方式洗牌。在大约前半段,我们只会看到猫的图像,而在下半段,只会看到非猫的图像。这将极大地伤害训练。
在训练开始时,数据集将采用第一个1000文件名并将其放入缓冲区,然后在其中随机选择一个。由于所有最初的1000图像都是猫的图像,因此我们仅在开始时选择猫的图像。

这里的解决方法是确保buffer_size大于20000或提前洗牌filenameslabels(具有相同指数明显)。

由于将所有文件名和标签存储在内存中不是问题,因此我们可以buffer_size = len(filenames)用来确保将所有内容混在一起。确保tf.data.Dataset.shuffle()在应用繁琐的转换之前先致电(例如,读取图像,对其进行处理,批量处理...)。

dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.shuffle(buffer_size=len(filenames)) 
dataset = dataset.map(...)  # transform to images, preprocess, repeat, batch...

要注意的是,总是要仔细检查改组将要做什么。捕获这些错误的一种好方法是绘制随时间变化的批次分布(确保批次包含与训练集大致相同的分布,在我们的示例中为一半cat和一半non cat)。


1
始终从缓冲区(此处为1000)中选择下一个样本。因此,第一个样本取自前1000个文件名。缓冲区的大小减小到999,因此它接受下一个输入(filename_01001)并将其相加。从这1000个文件名中随机抽取第二个样本(第一个文件名减去第一个样本1001个)。
奥利维尔·莫恩德罗

1
这种低缓冲区大小的问题在于,您的第一批中只会有猫。因此,模型将简单地学习仅预测“猫”。训练网络的最佳方法是使批次中的“猫”和“非猫”数量相同。
奥利维尔·莫恩德罗

1
您可以tf.summary.histogram用来绘制标签随时间的分布。
奥利维尔·莫恩德罗

3
不是错别字:)数据集每个类有10k张图像,因此总缓冲区大小应大于20k。但是在上面的示例中,我将缓冲区大小设置为1k,这太小了。
奥利维尔·莫恩德罗

1
是的,通常将缓冲区大小设置为数据集大小是可以的。无论如何,任何超出数据集大小的东西都是无用的(并且除非在重排之前重复数据集,否则缓冲区不能大于数据集)。
奥利维尔·莫恩德罗特

7

import tensorflow as tf
def shuffle():
    ds = list(range(0,1000))
    dataset = tf.data.Dataset.from_tensor_slices(ds)
    dataset=dataset.shuffle(buffer_size=500)
    dataset = dataset.batch(batch_size=1)
    iterator = dataset.make_initializable_iterator()
    next_element=iterator.get_next()
    init_op = iterator.initializer
    with tf.Session() as sess:
        sess.run(init_op)
        for i in range(100):
            print(sess.run(next_element), end='')

shuffle()

输出量

[298] [326] [2] [351] [92] [398] [72] [134] [404] [378] [238] [131] [369] [324] [35] [182] [441 ] [370] [372] [144] [77] [11] [199] [65] [346] [418] [493] [343] [444] [470] [222] [83] [61] [ 81] [366] [49] [295] [399] [177] [507] [288] [524] [401] [386] [89] [371] [181] [489] [172] [159] [195] [232] [160] [352] [495] [241] [435] [127] [268] [429] [382] [479] [519] [116] [395] [165] [233] ] [37] [486] [553] [111] [525] [170] [571] [215] [530] [47] [291] [558] [21] [245] [514] [103] [ 45] [545] [219] [468] [338] [392] [54] [139] [339] [448] [471] [589] [321] [223] [311] [234] [314]


2
这表明,对于迭代器产生的每个元素,缓冲区中的数据集之前都未包含在缓冲区中。
亚历克斯

2

实际上,@ olivier-moindrot的回答是不正确的。

您可以通过在提及时创建文件名和标签并打印随机值来进行验证。

您将看到每个随机播放过程将随机生成样本,其大小等于数据集中的缓冲区大小。

dataset = dataset.shuffle(buffer_size=1000)
iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()
with tf.Session() as sess:
    for i in range(1000):
        print(sess.run(next_element))

2

我发现@ olivier-moindrot确实是正确的,我使用@max指向的修改尝试了@Houtarou Oreki提供的代码。我使用的代码如下:

fake_data = np.concatenate((np.arange(1,500,1),np.zeros(500)))

dataset = tf.data.Dataset.from_tensor_slices(fake_data)
dataset=dataset.shuffle(buffer_size=100)
dataset = dataset.batch(batch_size=10)
iterator = dataset.make_initializable_iterator()
next_element=iterator.get_next()

init_op = iterator.initializer

with tf.Session() as sess:
    sess.run(init_op)
    for i in range(50):
        print(i)
        salida = np.array(sess.run(next_element))
        print(salida)
        print(salida.max())

输出的代码确实是从1到(buffer_size +(i * batch_size))的数字,其中i是您运行next_element的次数。我认为其工作方式如下。首先,从fake_data中按顺序选择buffer_size样本。然后从缓冲区中一个一个地选择batch_size样本。每次从缓冲区中提取一批样本时,都会将其替换为一个新的样本,该样本将按顺序从fake_data获取。我使用以下代码测试了这最后一件事:

aux = 0
for j in range (10000):
    with tf.Session() as sess:
        sess.run(init_op)
        salida = np.array(sess.run(next_element))
        if salida.max() > aux:
            aux = salida.max()

print(aux)

该代码产生的最大值为109。因此,您需要确保batch_size内的样本均衡,以确保训练期间进行均匀的采样。

我还测试了@mrry关于性能的内容,发现batch_size会将预采样的数量预取到内存中。我使用以下代码对此进行了测试:

dataset = dataset.shuffle(buffer_size=20)
dataset = dataset.prefetch(10)
dataset = dataset.batch(batch_size=5)

更改dataset.prefetch(10)数量不会导致使用的内存(RAM)发生变化。当您的数据不适合RAM时,这一点很重要。我认为最好的方法是在将数据/文件名提供给tf.dataset之前先对其进行洗牌,然后使用buffer_size控制缓冲区大小。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.