Significado de buffer_size em Dataset.map, Dataset.prefetch e Dataset.shuffle

94

De acordo com a documentação do TensorFlow , os métodos prefetche mapda tf.contrib.data.Datasetclasse, ambos têm um parâmetro chamado buffer_size.

Para o prefetchmétodo, o parâmetro é conhecido como buffer_sizee de acordo com a documentação:

buffer_size: Um tf.int64 escalar tf.Tensor, representando o número máximo de elementos que serão armazenados em buffer durante a pré-busca.

Para o mapmétodo, o parâmetro é conhecido como output_buffer_sizee de acordo com a documentação:

output_buffer_size: (Opcional.) Um tf.int64 escalar tf.Tensor, representando o número máximo de elementos processados ​​que serão armazenados em buffer.

Da mesma forma para o shufflemétodo, aparece a mesma quantidade e de acordo com a documentação:

buffer_size: Um tf.int64 escalar tf.Tensor, representando o número de elementos deste conjunto de dados do qual o novo conjunto de dados terá uma amostra.

Qual é a relação entre esses parâmetros?

Suponha que eu crie um Datasetobjeto da seguinte maneira:

 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)

Que papel está sendo desempenhado pelos bufferparâmetros no snippet acima?

Ujjwal
fonte
1
Link 404 para "documentação" não encontrado.
Pradeep Singh

Respostas:

145

TL; DR Apesar de seus nomes semelhantes, esses argumentos têm significados bastante diferentes. O buffer_sizein Dataset.shuffle()pode afetar a aleatoriedade de seu conjunto de dados e, portanto, a ordem em que os elementos são produzidos. O buffer_sizeem Dataset.prefetch()somente afeta o tempo que leva para produzir o próximo elemento.


O buffer_sizeargumento in tf.data.Dataset.prefetch()e o output_buffer_sizeargumento in tf.contrib.data.Dataset.map()fornecem uma maneira de ajustar o desempenho do pipeline de entrada: ambos os argumentos dizem ao TensorFlow para criar um buffer de no máximo buffer_sizeelementos e um thread de segundo plano para preencher esse buffer no segundo plano. (Observe que removemos o output_buffer_sizeargumento de Dataset.map()quando ele mudou de tf.contrib.datapara tf.data. O novo código deve ser usado Dataset.prefetch()depois map()para obter o mesmo comportamento.)

Adicionar um buffer de pré-busca pode melhorar o desempenho ao sobrepor o pré-processamento de dados à computação downstream. Normalmente, é mais útil adicionar um pequeno buffer de pré-busca (talvez com apenas um único elemento) no final do pipeline, mas pipelines mais complexos podem se beneficiar de pré-busca adicional, especialmente quando o tempo para produzir um único elemento pode variar.

Em contraste, o buffer_sizeargumento para tf.data.Dataset.shuffle()afeta a aleatoriedade da transformação. Projetamos a Dataset.shuffle()transformação (como a tf.train.shuffle_batch()função que ela substitui) para lidar com conjuntos de dados que são muito grandes para caber na memória. Em vez de embaralhar todo o conjunto de dados, ele mantém um buffer de buffer_sizeelementos e seleciona aleatoriamente o próximo elemento desse buffer (substituindo-o pelo próximo elemento de entrada, se houver um disponível). Alterar o valor de buffer_sizeafeta o quão uniforme é o embaralhamento: se buffer_sizefor maior que o número de elementos no conjunto de dados, você obtém um embaralhamento uniforme; se for1então você não terá nenhum embaralhamento. Para conjuntos de dados muito grandes, uma abordagem "boa o suficiente" típica é fragmentar aleatoriamente os dados em vários arquivos uma vez antes do treinamento, depois embaralhar os nomes de arquivo uniformemente e usar um buffer de embaralhamento menor. No entanto, a escolha apropriada dependerá da natureza exata do seu trabalho de treinamento.


senhor
fonte
Para esta explicação, ainda tenho algumas confusões escritas tf.data.Dataset.shuffle(). Gostaria de saber o processo exato de embaralhamento. Digamos que as primeiras batch_sizeamostras sejam escolhidas aleatoriamente a partir dos primeiros buffer_sizeelementos e assim por diante.
Bs He
1
@mrry IIUC embaralhar nomes de arquivos é importante porque, caso contrário, cada época verá o mesmo elemento nos lotes 0 ... 999; e em lotes 1000.1999; etc., onde suponho que 1 arquivo = 1000 lotes. Mesmo com o embaralhamento de nome de arquivo, ainda há alguma não aleatoriedade: isso porque os exemplos do arquivo #k estão todos próximos uns dos outros em todas as épocas. Isso pode não ser tão ruim, já que o próprio arquivo #k é aleatório; ainda em alguns casos, mesmo isso poderia atrapalhar o treinamento. A única maneira de obter a ordem aleatória perfeita seria configurá-lo buffer_sizepara igualar o tamanho do arquivo (e embaralhar os arquivos, é claro).
máx.
Tensorflow rc 15.0. Com dataset.shuffle(buffer_size=1)embaralhamento ainda ocorre. Alguma ideia?
Sergey Bushmanov
@SergeyBushmanov pode depender da transformação antes de seu embaralhamento, por exemplo list_files (), que embaralha os nomes dos arquivos no início de cada época por padrão.
Xiaolong
125

Importância de buffer_sizeemshuffle()

Eu queria continuar a resposta anterior de @mrry para enfatizar a importância de buffer_sizein tf.data.Dataset.shuffle().

Ter uma baixa buffer_sizenão vai apenas causar embaralhamento inferior em alguns casos: pode atrapalhar todo o seu treinamento.


Um exemplo prático: classificador gato

Suponha, por exemplo, que você esteja treinando um classificador de gato em imagens e seus dados sejam organizados da seguinte maneira (com 10000imagens em cada categoria):

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

Uma maneira padrão de inserir dados tf.datapode ser ter uma lista de nomes de arquivos e uma lista de rótulos correspondentes e usar tf.data.Dataset.from_tensor_slices()para criar o conjunto de dados:

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

O grande problema com o código acima é que o conjunto de dados não será embaralhado da maneira certa. Por volta da primeira metade de uma época, veremos apenas imagens de gatos e, na segunda metade, apenas imagens que não sejam de gatos. Isso vai prejudicar muito o treinamento.
No início do treinamento, o conjunto de dados 1000pegará os primeiros nomes de arquivo e os colocará em seu buffer, em seguida, escolherá um aleatoriamente entre eles. Como todas as primeiras 1000imagens são imagens de gato, escolheremos apenas imagens de gato no início.

A correção aqui é ter certeza de que buffer_sizeé maior que 20000, ou embaralhar antecipadamente filenamese labels(com os mesmos índices, obviamente).

Uma vez que armazenar todos os nomes de arquivos e rótulos na memória não é um problema, podemos usar buffer_size = len(filenames)para garantir que tudo seja misturado. Certifique-se de chamar tf.data.Dataset.shuffle()antes de aplicar as transformações pesadas (como ler as imagens, processá-las, lote ...).

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

A lição é sempre verificar novamente o que o embaralhamento fará. Uma boa maneira de detectar esses erros pode ser traçar a distribuição dos lotes ao longo do tempo (certifique-se de que os lotes contêm aproximadamente a mesma distribuição do conjunto de treinamento, metade gato e metade não gato em nosso exemplo).

Olivier Moindrot
fonte
1
A próxima amostra é sempre escolhida do buffer (de tamanho 1000 aqui). Portanto, a primeira amostra é obtida dos primeiros 1000 nomes de arquivos. O buffer diminui para o tamanho 999, então ele pega a próxima entrada ( filename_01001) e a adiciona. A segunda amostra é retirada aleatoriamente desses 1000 nomes de arquivos (1001 primeiros nomes de arquivos menos a primeira amostra).
Olivier Moindrot
1
O problema com esse tamanho de buffer baixo é que você só terá gatos em seus primeiros lotes. Portanto, o modelo trivialmente aprenderá a prever apenas "gato". A melhor forma de treinar a rede é ter lotes com a mesma quantidade de "gato" e "não gato".
Olivier Moindrot
1
Você pode usar tf.summary.histogrampara traçar a distribuição dos rótulos ao longo do tempo.
Olivier Moindrot
3
Não é um erro de digitação :) O conjunto de dados tem 10k imagens de cada classe, então o tamanho total do buffer deve ser acima de 20k. Mas no exemplo acima, peguei um tamanho de buffer de 1k, que é muito baixo.
Olivier Moindrot
1
Sim, definir o tamanho do buffer para o tamanho do conjunto de dados geralmente é adequado. Qualquer coisa acima do tamanho do conjunto de dados seria inútil de qualquer maneira (e a menos que você repita seu conjunto de dados antes de embaralhar, o buffer não poderia ser maior que o conjunto de dados).
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()

Resultado

[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
fonte
2
Isso indica que, para cada elemento gerado pelo iterador, o buffer está sendo preenchido com o próximo elemento respectivo do conjunto de dados que não estava no buffer antes.
Alex
2

Na verdade, a resposta de @olivier-moindrot não está correta.

Você pode verificá-lo criando nomes de arquivos e etiquetas conforme ele menciona e imprime os valores aleatórios.

Você verá que cada procedimento de embaralhamento gerará amostra aleatoriamente com o tamanho igual ao tamanho do buffer do conjunto de dados.

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
fonte
2

Achei que @olivier-moindrot está mesmo correto, tentei o código fornecido por @Houtarou Oreki, usando as modificações apontadas por @max. O código que usei foi o seguinte:

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

A saída do código era de fato um número variando de 1 a (buffer_size + (i * batch_size)), onde i é o número de vezes que você executou next_element . Eu acho que a forma como está funcionando é a seguinte. Primeiro, as amostras buffer_size são selecionadas em ordem a partir de fake_data . Então, uma a uma, as amostras batch_size são retiradas do buffer. Cada vez que uma amostra de lote é retirada do buffer, ela é substituída por uma nova, obtida em ordem de fake_data . Testei essa última coisa usando o seguinte 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)

O valor máximo produzido pelo código foi 109. Portanto, você precisa garantir uma amostra balanceada dentro de batch_size para garantir uma amostragem uniforme durante o treinamento.

Eu também testei o que @mrry disse sobre desempenho, descobri que batch_size irá pré- buscar essa quantidade de amostras na memória. Testei isso usando o seguinte código:

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

Alterar a quantidade de dataset.prefetch (10) resultou em nenhuma alteração na memória (RAM) usada. Isso é importante quando seus dados não cabem na RAM. Acho que a melhor maneira é embaralhar seus dados / file_names antes de alimentá-los para tf.dataset e, em seguida, controlar o tamanho do buffer usando buffer_size .

Ramiro RC
fonte