Signification de buffer_size dans Dataset.map, Dataset.prefetch et Dataset.shuffle


94

Selon la documentation de TensorFlow , les méthodes prefetchet mapde la tf.contrib.data.Datasetclasse ont toutes deux un paramètre appelé buffer_size.

Pour la prefetchméthode, le paramètre est appelé buffer_sizeet selon la documentation:

buffer_size: Un tf.Tensor scalaire tf.int64, représentant le nombre maximum d'éléments qui seront mis en mémoire tampon lors de la prélecture.

Pour la mapméthode, le paramètre est appelé output_buffer_sizeet selon la documentation:

output_buffer_size: (Facultatif.) Un tf.Tensor scalaire tf.int64, représentant le nombre maximum d'éléments traités qui seront mis en mémoire tampon.

De même pour la shuffleméthode, la même quantité apparaît et selon la documentation:

buffer_size: Un tf.Tensor scalaire tf.int64, représentant le nombre d'éléments de cet ensemble de données à partir desquels le nouvel ensemble de données sera échantillonné.

Quelle est la relation entre ces paramètres?

Supposons que je crée un Datasetobjet comme suit:

 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)

Quel rôle jouent les bufferparamètres dans l'extrait ci-dessus?


1
404 lien vers "documentation" introuvable.
Pradeep Singh

Réponses:


145

TL; DR Malgré leurs noms similaires, ces arguments ont des significations assez différentes. Le buffer_sizein Dataset.shuffle()peut affecter le caractère aléatoire de votre ensemble de données, et donc l'ordre dans lequel les éléments sont produits. Le buffer_sizeen Dataset.prefetch()affecte uniquement le temps nécessaire pour produire l'élément suivant.


L' buffer_sizeargument in tf.data.Dataset.prefetch()et l' output_buffer_sizeargument in tf.contrib.data.Dataset.map()fournissent un moyen d'ajuster les performances de votre pipeline d'entrée: les deux arguments indiquent à TensorFlow de créer un tampon d'au plus buffer_sizeéléments, et un thread d'arrière-plan pour remplir ce tampon en arrière-plan. (Notez que nous avons supprimé l' output_buffer_sizeargument de Dataset.map()quand il a été déplacé de tf.contrib.dataà tf.data. Le nouveau code doit utiliser Dataset.prefetch()after map()pour obtenir le même comportement.)

L'ajout d'un tampon de prélecture peut améliorer les performances en chevauchant le prétraitement des données avec le calcul en aval. En règle générale, il est plus utile d'ajouter un petit tampon de prélecture (avec peut-être un seul élément) à la toute fin du pipeline, mais les pipelines plus complexes peuvent bénéficier d'une prélecture supplémentaire, en particulier lorsque le temps de production d'un seul élément peut varier.

En revanche, l' buffer_sizeargument to tf.data.Dataset.shuffle()affecte le caractère aléatoire de la transformation. Nous avons conçu la Dataset.shuffle()transformation (comme la tf.train.shuffle_batch()fonction qu'elle remplace) pour gérer les ensembles de données trop volumineux pour tenir en mémoire. Au lieu de mélanger l'ensemble de données, il maintient un tampon d' buffer_sizeéléments et sélectionne au hasard l'élément suivant de ce tampon (en le remplaçant par l'élément d'entrée suivant, s'il en existe un). La modification de la valeur de buffer_sizeaffecte l'uniformité du brassage: si buffer_sizeest supérieur au nombre d'éléments de l'ensemble de données, vous obtenez un brassage uniforme; Si c'est1alors vous n'obtenez aucun mélange. Pour les très grands ensembles de données, une approche typique «assez bonne» consiste à fragmenter de manière aléatoire les données en plusieurs fichiers une fois avant l'entraînement, puis à mélanger les noms de fichiers de manière uniforme, puis à utiliser un tampon de mélange plus petit. Cependant, le choix approprié dépendra de la nature exacte de votre travail de formation.



Pour cette explication, j'ai encore quelques confusions tf.data.Dataset.shuffle(). J'aimerais connaître le processus de remaniement exact. Disons que les premiers batch_sizeéchantillons sont choisis au hasard parmi les premiers buffer_sizeéléments, et ainsi de suite.
Bs He

1
@mrry IIUC mélanger les noms de fichiers est important car sinon, chaque époque verra le même élément dans les lots 0 ... 999; et par lots 1000.1999; etc., où je suppose 1 fichier = 1000 lots. Même avec le brassage des noms de fichiers, il y a toujours un certain non-aléatoire: c'est parce que les exemples du fichier #k sont tous proches les uns des autres à chaque époque. Ce n'est peut-être pas trop grave car le fichier #k lui-même est aléatoire; encore dans certains cas, même cela pourrait gâcher la formation. La seule façon d'obtenir un mélange parfait serait de définir buffer_sizeune taille égale à la taille du fichier (et de mélanger les fichiers bien sûr).
max

Tensorflow rc 15.0. Avec la dataset.shuffle(buffer_size=1)lecture aléatoire se produit toujours. Des pensées?
Sergey Bushmanov le

@SergeyBushmanov cela peut dépendre de la transformation avant votre shuffle, par exemple list_files (), qui mélange les noms de fichiers au début de chaque époque par défaut.
Xiaolong

125

Importance de buffer_sizedansshuffle()

Je voulais faire suite à la réponse précédente de @mrry pour souligner l' importance de buffer_sizein tf.data.Dataset.shuffle().

Avoir un faible buffer_sizene vous donnera pas seulement un mélange inférieur dans certains cas: cela peut gâcher tout votre entraînement.


Un exemple pratique: classificateur de chat

Supposons par exemple que vous entraînez un classificateur de chat sur des images et que vos données sont organisées de la manière suivante (avec des 10000images dans chaque catégorie):

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

Un moyen standard de saisir des données avec tf.datapeut être d'avoir une liste de noms de fichiers et une liste d'étiquettes correspondantes, et d'utiliser tf.data.Dataset.from_tensor_slices()pour créer l'ensemble de données:

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

Le gros problème avec le code ci-dessus est que l'ensemble de données ne sera pas mélangé de la bonne manière. Pendant environ la première moitié d'une époque, nous ne verrons que des images de chats, et pour la seconde moitié uniquement des images non félines. Cela nuira beaucoup à l'entraînement.
Au début de la formation, l'ensemble de données prendra les premiers 1000noms de fichiers et les mettra dans sa mémoire tampon, puis en choisira un au hasard parmi eux. Puisque toutes les premières 1000images sont des images de chat, nous ne choisirons que des images de chat au début.

Le correctif ici est de s'assurer que buffer_sizec'est plus grand que 20000, ou de mélanger à l'avance filenameset labels(avec les mêmes indices évidemment).

Étant donné que le stockage de tous les noms de fichiers et étiquettes en mémoire n'est pas un problème, nous pouvons en fait l'utiliser buffer_size = len(filenames)pour nous assurer que tout sera mélangé ensemble. Assurez-vous d'appeler tf.data.Dataset.shuffle()avant d'appliquer les transformations lourdes (comme la lecture des images, leur traitement, le traitement par lots ...).

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 chose à retenir est de toujours vérifier ce que fera le brassage. Un bon moyen d'attraper ces erreurs pourrait être de tracer la distribution des lots au fil du temps (assurez-vous que les lots contiennent à peu près la même distribution que l'ensemble d'entraînement, moitié chat et moitié non chat dans notre exemple).


1
L'échantillon suivant est toujours choisi dans le tampon (ici de taille 1000). Ainsi, le premier échantillon est tiré des 1000 premiers noms de fichiers. Le tampon diminue à la taille 999, il prend donc l'entrée suivante ( filename_01001) et l'ajoute. Le deuxième échantillon est pris au hasard parmi ces 1000 noms de fichiers (1001 premiers noms de fichiers moins le premier échantillon).
Olivier Moindrot du

1
Le problème avec cette faible taille de tampon est que vous n'aurez que des chats dans vos premiers lots. Ainsi, le modèle apprendra trivialement à ne prédire que "chat". La meilleure façon de former le réseau est d'avoir des lots avec la même quantité de "chat" et "non chat".
Olivier Moindrot du

1
Vous pouvez utiliser tf.summary.histogrampour tracer la distribution des étiquettes au fil du temps.
Olivier Moindrot

3
Ce n'est pas une faute de frappe :) L'ensemble de données contient 10k images de chaque classe, donc la taille totale du tampon doit être supérieure à 20k. Mais dans l'exemple ci-dessus, j'ai pris une taille de tampon de 1k qui est trop faible.
Olivier Moindrot le

1
Oui, définir la taille de la mémoire tampon sur la taille de l'ensemble de données est généralement bien. Tout ce qui dépasse la taille de l'ensemble de données serait de toute façon inutile (et à moins que vous ne répétiez votre ensemble de données avant le mélange, le tampon ne pourrait pas être plus grand que l'ensemble de données).
Olivier Moindrot

7

Code

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

Production

[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
Cela indique que pour chaque élément produit par l'itérateur, le tampon est rempli avec l'élément suivant respectif de l'ensemble de données qui n'était pas dans le tampon auparavant.
Alex

2

En fait, la réponse de @ olivier-moindrot n'est pas correcte.

Vous pouvez le vérifier en créant des noms de fichiers et des étiquettes au fur et à mesure qu'il / elle mentionne et imprime les valeurs de lecture aléatoire.

Vous verrez que chaque procédure de mélange générera un échantillon de manière aléatoire avec la taille égale à la taille de la mémoire tampon à partir de l'ensemble de données.

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

J'ai trouvé que @ olivier-moindrot est en effet correct, j'ai essayé le code fourni par @Houtarou Oreki, en utilisant les modifications pointées par @max. Le code que j'ai utilisé était le suivant:

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 sortie de code était en effet un nombre allant de 1 à (buffer_size + (i * batch_size)), où i est le nombre de fois que vous avez exécuté next_element . Je pense que la façon dont cela fonctionne est la suivante. Tout d'abord, les échantillons buffer_size sont sélectionnés dans l'ordre dans fake_data . Ensuite, un par un, les échantillons batch_size sont prélevés dans le tampon. Chaque fois qu'un échantillon de lot est sélectionné dans le tampon, il est remplacé par un nouveau, pris dans l'ordre de fake_data . J'ai testé cette dernière chose en utilisant le code suivant:

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)

La valeur maximale produite par le code était de 109. Vous devez donc garantir un échantillon équilibré au sein de votre batch_size pour garantir un échantillonnage uniforme pendant la formation.

J'ai également testé ce que @mrry a dit à propos des performances, j'ai trouvé que batch_size prélèverait cette quantité d'échantillons en mémoire. J'ai testé cela en utilisant le code suivant:

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

La modification de la quantité dataset.prefetch (10) n'a entraîné aucune modification de la mémoire (RAM) utilisée. Ceci est important lorsque vos données ne rentrent pas dans la RAM. Je pense que le meilleur moyen est de mélanger vos données / noms de fichier avant de les alimenter dans tf.dataset, puis de contrôler la taille de la mémoire tampon en utilisant buffer_size .

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.