Znaczenie wartości buffer_size w Dataset.map, Dataset.prefetch i Dataset.shuffle

101

Zgodnie TensorFlow dokumentacji , to prefetchi mapmetody tf.contrib.data.Datasetklasy, obie posiadają parametr zwany buffer_size.

W przypadku prefetchmetody parametr jest znany jako buffer_sizei zgodnie z dokumentacją:

buffer_size: tf.int64 skalarny tf.Tensor, reprezentujący maksymalną liczbę elementów, które będą buforowane podczas wstępnego pobierania.

W mapmetodzie parametr jest znany jako output_buffer_sizei zgodnie z dokumentacją:

output_buffer_size: (opcjonalny). Tf.Tensor skalarny tf.int64, reprezentujący maksymalną liczbę przetwarzanych elementów, które będą buforowane.

Podobnie dla shufflemetody pojawia się ta sama ilość i zgodnie z dokumentacją:

buffer_size: skalarny tf.int64 tf.Tensor, reprezentujący liczbę elementów z tego zbioru danych, z których będzie próbkowany nowy zestaw danych.

Jaka jest zależność między tymi parametrami?

Załóżmy, że utworzę Datasetobiekt w następujący sposób:

 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)

Jaką rolę odgrywają bufferparametry w powyższym fragmencie?

Ujjwal
źródło
1
Nie znaleziono łącza 404 do „dokumentacji”.
Pradeep Singh

Odpowiedzi:

151

TL; DR Pomimo podobnych nazw argumenty te mają całkiem odmienne znaczenie. buffer_sizeW Dataset.shuffle()może wpłynąć na przypadkowość swojego zbioru danych, a tym samym porządku, w którym elementy są produkowane. buffer_sizeW Dataset.prefetch()wpływa jedynie na czas potrzebny do wytworzenia następnego elementu.


buffer_sizeArgument tf.data.Dataset.prefetch()i output_buffer_sizeargument tf.contrib.data.Dataset.map()zapewnić sposób dostroić wydajność swojego rurociągu Wejście: oba argumenty powiedzieć TensorFlow stworzyć bufor w większości buffer_sizepierwiastków, a wątek tła, aby wypełnić tę bufor w tle. (Zauważ, że usunęliśmy output_buffer_sizeargument z miejsca, w Dataset.map()którym został przeniesiony z tf.contrib.datado tf.data. Nowy kod powinien używać Dataset.prefetch()after, map()aby uzyskać takie samo zachowanie).

Dodanie buforu pobierania wstępnego może poprawić wydajność poprzez nałożenie wstępnego przetwarzania danych na dalsze obliczenia. Zazwyczaj najbardziej przydatne jest dodanie małego buforu pobierania wstępnego (z być może tylko jednym elementem) na samym końcu potoku, ale bardziej złożone potoki mogą skorzystać z dodatkowego pobierania wstępnego, zwłaszcza gdy czas potrzebny na wyprodukowanie pojedynczego elementu może się różnić.

Z kolei buffer_sizeargument przemawiający za tf.data.Dataset.shuffle()wpływa na losowość transformacji. Zaprojektowaliśmy Dataset.shuffle()transformację (podobnie jak tf.train.shuffle_batch()funkcję, którą zastępuje), aby obsługiwać zestawy danych, które są zbyt duże, aby zmieścić się w pamięci. Zamiast tasować cały zbiór danych, utrzymuje bufor buffer_sizeelementów i losowo wybiera następny element z tego bufora (zastępując go kolejnym elementem wejściowym, jeśli jest dostępny). Zmiana wartości buffer_sizewpływa na jednolitość tasowania: jeśli buffer_sizejest większa niż liczba elementów w zbiorze danych, otrzymasz jednolite tasowanie; Jeśli to jest1wtedy nie ma żadnego tasowania. W przypadku bardzo dużych zbiorów danych typowym „wystarczająco dobrym” podejściem jest losowe podzielenie danych na wiele plików jeden raz przed uczeniem, a następnie jednolite przetasowanie nazw plików, a następnie użycie mniejszego buforu do odtwarzania losowego. Jednak właściwy wybór będzie zależał od dokładnego charakteru Twojej pracy szkoleniowej.


mrry
źródło
Jeśli chodzi o to wyjaśnienie, nadal mam pewne nieporozumienia tf.data.Dataset.shuffle(). Chciałbym poznać dokładny proces tasowania. Powiedzmy, że pierwsze batch_sizepróbki są wybierane losowo z pierwszych buffer_sizeelementów i tak dalej.
Bs He,
1
@mrry IIUC tasowanie nazw plików jest ważne, ponieważ w przeciwnym razie każda epoka będzie widzieć ten sam element w partiach 0 ... 999; oraz partiami 1000.1999; itd., gdzie zakładam, że 1 plik = 1000 partii. Nawet przy tasowaniu nazw plików nadal istnieje pewna nielosowość: to dlatego, że przykłady z pliku #k są blisko siebie w każdej epoce. To może nie być takie złe, ponieważ sam plik #k jest losowy; w niektórych przypadkach nawet to może zepsuć szkolenie. Jedynym sposobem na uzyskanie idealnego tasowania byłoby ustawienie buffer_sizerównej wielkości pliku (i oczywiście tasowanie plików).
maksymalnie
Tensorflow rc 15.0.1 Z dataset.shuffle(buffer_size=1)tasowaniem nadal występuje. jakieś pomysły?
Sergey Bushmanov
@SergeyBushmanov może to zależeć od transformacji przed twoim shuffle, np. List_files (), która domyślnie tasuje nazwy plików na początku każdej epoki.
Xiaolong,
130

Znaczenie buffer_sizewshuffle()

Chciałem pójść w górę na poprzedniej odpowiedzi od @mrry podkreślić znaczenie z buffer_sizew tf.data.Dataset.shuffle().

Niski poziom buffer_sizenie tylko w niektórych przypadkach spowoduje gorsze tasowanie : może zepsuć cały trening.


Praktyczny przykład: klasyfikator kotów

Załóżmy na przykład, że trenujesz klasyfikator kotów na obrazach, a twoje dane są zorganizowane w następujący sposób (z 10000obrazami w każdej kategorii):

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

Standardowym sposobem wprowadzania danych za pomocą tf.datamoże być lista nazw plików i lista odpowiadających im etykiet oraz użycie tf.data.Dataset.from_tensor_slices()do utworzenia zestawu danych:

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

Duży problem z kodem powyżej jest to, że zestaw danych nie będzie faktycznie być tasuje we właściwy sposób. Przez mniej więcej pierwszą połowę epoki będziemy widzieć tylko obrazy kotów, a przez drugą połowę tylko obrazy inne niż koty. To bardzo zaszkodzi treningowi.
Na początku uczenia zestaw danych weźmie pierwsze 1000nazwy plików i umieści je w swoim buforze, a następnie wybierze losowo jedną spośród nich. Ponieważ wszystkie pierwsze 1000obrazy są obrazami kotów, wybierzemy tylko obrazy kotów na początku.

Rozwiązaniem jest tutaj upewnienie się, że buffer_sizejest większy niż 20000lub przetasowanie z wyprzedzeniem filenamesi labels(oczywiście z tymi samymi indeksami).

Ponieważ przechowywanie wszystkich nazw plików i etykiet w pamięci nie stanowi problemu, możemy faktycznie użyć buffer_size = len(filenames)go, aby upewnić się, że wszystko zostanie przetasowane. Pamiętaj, aby wywołać tf.data.Dataset.shuffle()przed zastosowaniem ciężkich przekształceń (takich jak czytanie obrazów, przetwarzanie ich, przetwarzanie wsadowe ...).

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

Na wynos jest zawsze podwójne sprawdzenie, co da tasowanie. Dobrym sposobem na wychwycenie tych błędów może być wykreślenie rozkładu partii w czasie (upewnij się, że partie zawierają mniej więcej taki sam rozkład, jak zbiór uczący, w naszym przykładzie w połowie cat i half non cat).

Olivier Moindrot
źródło
1
Następna próbka jest zawsze wybierana z bufora (tutaj o rozmiarze 1000). Zatem pierwsza próbka jest pobierana z pierwszych 1000 nazw plików. Bufor zmniejsza się do rozmiaru 999, więc pobiera następne wejście ( filename_01001) i dodaje je. Druga próbka jest pobierana losowo z tych 1000 nazw plików (1001 pierwszych nazw plików minus pierwsza próbka).
Olivier Moindrot,
1
Problem z tym małym rozmiarem bufora polega na tym, że koty będą miały tylko pierwsze partie. Tak więc model w trywialny sposób nauczy się przewidywać tylko „kota”. Najlepszym sposobem na przeszkolenie sieci jest posiadanie partii z taką samą ilością „cat” i „non cat”.
Olivier Moindrot,
1
Można go użyć tf.summary.histogramdo wykreślenia rozkładu etykiet w czasie.
Olivier Moindrot
3
To nie jest literówka :) Zbiór danych zawiera 10k obrazów każdej klasy, więc całkowity rozmiar bufora powinien być większy niż 20k. Ale w powyższym przykładzie wziąłem rozmiar bufora 1k, który jest za mały.
Olivier Moindrot
1
Tak, ustawienie rozmiaru bufora na rozmiar zestawu danych jest zazwyczaj prawidłowe. Cokolwiek powyżej rozmiaru zbioru danych byłoby i tak bezużyteczne (a jeśli nie powtórzysz zbioru danych przed tasowaniem, bufor nie może być większy niż zbiór danych).
Olivier Moindrot
7

Kod

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

Wynik

[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
źródło
2
Oznacza to, że dla każdego elementu uzyskanego przez iterator bufor jest zapełniany odpowiednim kolejnym elementem zestawu danych, którego wcześniej nie było w buforze.
Alex
2

W rzeczywistości odpowiedź @ olivier-moindrot jest nieprawidłowa.

Możesz to zweryfikować, tworząc nazwy plików i etykiety, gdy on / ona wspomina, i drukując wartości losowe.

Zobaczysz, że każda procedura tasowania wygeneruje losowo próbkę o rozmiarze równym rozmiarowi bufora z zestawu danych.

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
źródło
2

Okazało się, że @ olivier-moindrot jest rzeczywiście poprawne, wypróbowałem kod dostarczony przez @Houtarou Oreki, używając modyfikacji wskazanych przez @max. Kod, którego użyłem, był następujący:

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

Kod wyjściowy był rzeczywiście liczbą z zakresu od 1 do (rozmiar_bufora + (i * rozmiar_batchu)), gdzie i to liczba uruchomień next_element . Myślę, że sposób, w jaki to działa, jest następujący. Najpierw próbki buffer_size są pobierane w kolejności z fake_data . Następnie próbki batch_size są pobierane jedna po drugiej z bufora. Za każdym razem, gdy próbka partii jest pobierana z bufora, jest ona zastępowana nową, pobraną w kolejności z fake_data . Przetestowałem tę ostatnią rzecz używając następującego kodu:

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)

Maksymalna wartość wygenerowana przez kod wynosiła 109. Dlatego musisz zapewnić zrównoważoną próbkę w ramach swojego batch_size, aby zapewnić jednolite pobieranie próbek podczas uczenia.

Przetestowałem również to, co @mrry powiedział o wydajności, stwierdziłem, że batch_size wstępnie pobierze tę liczbę próbek do pamięci. Przetestowałem to za pomocą następującego kodu:

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

Zmiana ilości dataset.prefetch (10) nie spowodowała zmiany w używanej pamięci (RAM). Jest to ważne, gdy dane nie mieszczą się w pamięci RAM. Myślę, że najlepszym sposobem jest shuffle dane / file_names przed wprowadzeniem ich do tf.dataset, a następnie kontrolować wielkość bufora używając BUFFER_SIZE .

Ramiro RC
źródło