Significado de buffer_size en Dataset.map, Dataset.prefetch y Dataset.shuffle

101

Según la documentación de TensorFlow , los métodos prefetchy mapde la tf.contrib.data.Datasetclase, ambos tienen un parámetro llamado buffer_size.

Por prefetchmétodo, el parámetro se conoce como buffer_sizey según la documentación:

buffer_size: Un tf.int64 escalar tf.Tensor, que representa el número máximo de elementos que se almacenarán en búfer al realizar la captación previa.

Para el mapmétodo, el parámetro se conoce como output_buffer_sizey según la documentación:

output_buffer_size: (Opcional). Un tf.Tensor escalar tf.int64, que representa el número máximo de elementos procesados ​​que se almacenarán en búfer.

De igual manera para el shufflemétodo aparece la misma cantidad y según documentación:

buffer_size: Un tf.int64 escalar tf.Tensor, que representa el número de elementos de este conjunto de datos del que se tomará una muestra del nuevo conjunto de datos.

¿Cuál es la relación entre estos parámetros?

Supongamos que creo un Datasetobjeto de la siguiente manera:

 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)

¿Qué papel desempeñan los bufferparámetros del fragmento anterior?

Ujjwal
fuente
1
404 enlace a "documentación" no encontrado.
Pradeep Singh

Respuestas:

151

TL; DR A pesar de sus nombres similares, estos argumentos tienen significados bastante diferentes. El buffer_sizeen Dataset.shuffle()puede afectar a la aleatoriedad de su conjunto de datos, y por lo tanto el orden en que se producen los elementos. El buffer_sizein Dataset.prefetch()solo afecta el tiempo que se tarda en producir el siguiente elemento.


El buffer_sizeargumento en tf.data.Dataset.prefetch()y el output_buffer_sizeargumento en tf.contrib.data.Dataset.map()proporcionan una manera de ajustar el rendimiento de su canalización de entrada: ambos argumentos le dicen a TensorFlow que cree un búfer de la mayoría de los buffer_sizeelementos y un hilo de fondo para llenar ese búfer en segundo plano. (Tenga en cuenta que eliminamos el output_buffer_sizeargumento de Dataset.map()cuando se movió de tf.contrib.dataa tf.data. El nuevo código debe usarse Dataset.prefetch()después map()para obtener el mismo comportamiento).

Agregar un búfer de captación previa puede mejorar el rendimiento al superponer el procesamiento previo de datos con el cálculo posterior. Normalmente, es más útil agregar un búfer de captación previa pequeño (quizás con un solo elemento) al final de la canalización, pero las canalizaciones más complejas pueden beneficiarse de la captación previa adicional, especialmente cuando el tiempo para producir un solo elemento puede variar.

Por el contrario, el buffer_sizeargumento a tf.data.Dataset.shuffle()afecta la aleatoriedad de la transformación. Diseñamos la Dataset.shuffle()transformación (como la tf.train.shuffle_batch()función que reemplaza) para manejar conjuntos de datos que son demasiado grandes para caber en la memoria. En lugar de mezclar todo el conjunto de datos, mantiene un búfer de buffer_sizeelementos y selecciona aleatoriamente el siguiente elemento de ese búfer (reemplazándolo con el siguiente elemento de entrada, si hay uno disponible). Cambiar el valor de buffer_sizeafecta la uniformidad de la mezcla: si buffer_sizees mayor que el número de elementos del conjunto de datos, se obtiene una mezcla uniforme; si esto es1entonces no tendrás que barajar en absoluto. Para conjuntos de datos muy grandes, un enfoque típico "suficientemente bueno" es dividir aleatoriamente los datos en varios archivos una vez antes del entrenamiento, luego mezclar los nombres de los archivos de manera uniforme y luego usar un búfer aleatorio más pequeño. Sin embargo, la elección adecuada dependerá de la naturaleza exacta de su trabajo de formación.


señor
fuente
Para esta explicación, todavía tengo algunas confusiones wrt tf.data.Dataset.shuffle(). Me gustaría saber el proceso exacto de barajar. Digamos que las primeras batch_sizemuestras se eligen aleatoriamente de los primeros buffer_sizeelementos, y así sucesivamente.
Bs He
1
@mrry IIUC barajar los nombres de los archivos es importante porque, de lo contrario, cada época verá el mismo elemento en los lotes 0 ... 999; y en lotes 1000.1999; etc., donde supongo que 1 archivo = 1000 lotes. Incluso con la mezcla de nombres de archivo, todavía hay algo de no aleatoriedad: eso se debe a que los ejemplos del archivo #k están todos cerca entre sí en cada época. Eso podría no ser tan malo ya que el archivo #k en sí es aleatorio; aun así, en algunos casos, incluso eso podría estropear el entrenamiento. La única forma de obtener una reproducción aleatoria perfecta sería establecer buffer_sizeque el tamaño del archivo sea igual (y, por supuesto, mezclar los archivos).
máximo
Tensorflow rc 15.0. Con dataset.shuffle(buffer_size=1)barajar todavía se produce. ¿Alguna idea?
Sergey Bushmanov
@SergeyBushmanov puede depender de la transformación antes de su barajado, por ejemplo list_files (), que baraja los nombres de archivo al principio de cada época por defecto.
Xiaolong
130

Importancia de buffer_sizeenshuffle()

Quería hacer un seguimiento de la respuesta anterior de @mrry para enfatizar la importancia de buffer_sizein tf.data.Dataset.shuffle().

Tener un nivel bajo buffer_sizeno solo le dará un barajado inferior en algunos casos: puede arruinar todo su entrenamiento.


Un ejemplo práctico: clasificador de gatos

Supongamos, por ejemplo, que está entrenando un clasificador de gatos en imágenes y sus datos están organizados de la siguiente manera (con 10000imágenes en cada categoría):

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

Una forma estándar de ingresar datos tf.datapuede ser tener una lista de nombres de archivo y una lista de etiquetas correspondientes, y usarla tf.data.Dataset.from_tensor_slices()para crear el conjunto de datos:

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...

El gran problema con el código anterior es que el conjunto de datos no se barajará de la manera correcta. Durante aproximadamente la primera mitad de una época, solo veremos imágenes de gatos, y durante la segunda mitad solo imágenes que no sean de gatos. Esto dolerá mucho el entrenamiento.
Al comienzo del entrenamiento, el conjunto de datos tomará los primeros 1000nombres de archivo y los colocará en su búfer, luego elegirá uno al azar entre ellos. Dado que todas las primeras 1000imágenes son imágenes de gatos, solo seleccionaremos imágenes de gatos al principio.

La solución aquí es asegurarse de que buffer_sizesea ​​mayor que 20000, o mezclar de antemano filenamesy labels(con los mismos índices obviamente).

Dado que almacenar todos los nombres de archivo y etiquetas en la memoria no es un problema, podemos usarlo buffer_size = len(filenames)para asegurarnos de que todo se mezcle. Asegúrese de llamar tf.data.Dataset.shuffle()antes de aplicar las transformaciones pesadas (como leer las imágenes, procesarlas, procesar por lotes ...).

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...

La conclusión es siempre verificar dos veces lo que hará la mezcla. Una buena forma de detectar estos errores podría ser trazar la distribución de lotes a lo largo del tiempo (asegúrese de que los lotes contengan aproximadamente la misma distribución que el conjunto de entrenamiento, mitad gato y mitad no gato en nuestro ejemplo).

Olivier Moindrot
fuente
1
La siguiente muestra siempre se elige del búfer (de tamaño 1000 aquí). Entonces, la primera muestra se toma de los primeros 1000 nombres de archivo. El búfer disminuye al tamaño de 999, por lo que toma la siguiente entrada ( filename_01001) y la agrega. La segunda muestra se toma al azar de estos 1000 nombres de archivo (1001 primeros nombres de archivo menos la primera muestra).
Olivier Moindrot
1
El problema con este tamaño de búfer bajo es que solo tendrá gatos en sus primeros lotes. De modo que el modelo aprenderá trivialmente a predecir solo "gato". La mejor manera de entrenar a la red es tener lotes con la misma cantidad de "gato" y "no gato".
Olivier Moindrot
1
Puede utilizar tf.summary.histogrampara trazar la distribución de etiquetas a lo largo del tiempo.
Olivier Moindrot
3
No es un error tipográfico :) El conjunto de datos tiene 10k imágenes de cada clase, por lo que el tamaño total del búfer debe ser superior a 20k. Pero en el ejemplo anterior, tomé un tamaño de búfer de 1k que es demasiado bajo.
Olivier Moindrot
1
Sí, establecer el tamaño del búfer en el tamaño del conjunto de datos generalmente está bien. Cualquier cosa por encima del tamaño del conjunto de datos sería inútil de todos modos (y a menos que repita su conjunto de datos antes de barajar, el búfer no podría ser más grande que el conjunto de datos).
Olivier Moindrot
7

Código

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()

Salida

[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]

Vladimir
fuente
2
Esto indica que por cada elemento generado por el iterador, el búfer se está llenando con el siguiente elemento respectivo del conjunto de datos que antes no estaba en el búfer.
Alex
2

En realidad, la respuesta de @ olivier-moindrot no es correcta.

Puede verificarlo creando nombres de archivo y etiquetas a medida que mencione e imprima los valores aleatorios.

Verá que cada procedimiento de reproducción aleatoria generará una muestra aleatoriamente con el tamaño igual al tamaño del búfer del conjunto de datos.

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))
Isaac Cheng
fuente
2

Descubrí que @ olivier-moindrot es realmente correcto, probé el código proporcionado por @Houtarou Oreki, usando las modificaciones señaladas por @max. El código que utilicé fue el siguiente:

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())

La salida del código era de hecho un número que iba de 1 a (buffer_size + (i * batch_size)), donde i es el número de veces que ejecutó next_element . Creo que la forma en que está funcionando es la siguiente. Primero, las muestras de buffer_size se seleccionan en orden de fake_data . Luego, una por una, las muestras de tamaño de lote se extraen del búfer. Cada vez que se toma una muestra por lotes del búfer, se reemplaza por una nueva, tomada en orden de fake_data . Probé esto último usando el siguiente código:

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)

El valor máximo producido por el código fue 109. Por lo tanto, debe garantizar una muestra equilibrada dentro de su tamaño de lote para garantizar un muestreo uniforme durante el entrenamiento.

También probé lo que dijo @mrry sobre el rendimiento, descubrí que batch_size precargará esa cantidad de muestras en la memoria. Probé esto usando el siguiente código:

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

Cambiar la cantidad de dataset.prefetch (10) resultó en ningún cambio en la memoria (RAM) utilizada. Esto es importante cuando sus datos no caben en la RAM. Creo que la mejor manera es mezclar sus datos / nombres de archivo antes de alimentarlos a tf.dataset, y luego controlar el tamaño del búfer usando buffer_size .

Ramiro RC
fuente