Wskazówki dotyczące skuteczności

Ten dokument zawiera wskazówki dotyczące wydajności specyficzne dla zestawów danych TensorFlow (TFDS). Należy pamiętać, że TFDS udostępnia zbiory danych jako obiekty tf.data.Dataset , więc porady z przewodnika tf.data nadal mają zastosowanie.

Zestawy danych porównawczych

Użyj tfds.benchmark(ds) do porównania dowolnego obiektu tf.data.Dataset .

Upewnij się, że podano batch_size= , aby znormalizować wyniki (np. 100 iter/s -> 3200 ex/s). Działa to z dowolną iteracją (np. tfds.benchmark(tfds.as_numpy(ds)) ).

ds = tfds.load('mnist', split='train').batch(32).prefetch()
# Display some benchmark statistics
tfds.benchmark(ds, batch_size=32)
# Second iteration is much faster, due to auto-caching
tfds.benchmark(ds, batch_size=32)

Małe zbiory danych (mniej niż 1 GB)

Wszystkie zestawy danych TFDS przechowują dane na dysku w formacie TFRecord . W przypadku małych zbiorów danych (np. MNIST, CIFAR-10/-100) odczyt z pliku .tfrecord może spowodować znaczny narzut.

Ponieważ te zbiory danych mieszczą się w pamięci, można znacznie poprawić wydajność poprzez buforowanie lub wstępne ładowanie zbioru danych. Należy pamiętać, że TFDS automatycznie buforuje małe zbiory danych (szczegóły w poniższej sekcji).

Buforowanie zbioru danych

Oto przykład potoku danych, który jawnie buforuje zbiór danych po normalizacji obrazów.

def normalize_img(image, label):
  """Normalizes images: `uint8` -> `float32`."""
  return tf.cast(image, tf.float32) / 255., label


ds, ds_info = tfds.load(
    'mnist',
    split='train',
    as_supervised=True,  # returns `(img, label)` instead of dict(image=, ...)
    with_info=True,
)
# Applying normalization before `ds.cache()` to re-use it.
# Note: Random transformations (e.g. images augmentations) should be applied
# after both `ds.cache()` (to avoid caching randomness) and `ds.batch()` (for
# vectorization [1]).
ds = ds.map(normalize_img, num_parallel_calls=tf.data.AUTOTUNE)
ds = ds.cache()
# For true randomness, we set the shuffle buffer to the full dataset size.
ds = ds.shuffle(ds_info.splits['train'].num_examples)
# Batch after shuffling to get unique batches at each epoch.
ds = ds.batch(128)
ds = ds.prefetch(tf.data.experimental.AUTOTUNE)

Podczas iteracji po tym zbiorze danych druga iteracja będzie znacznie szybsza niż pierwsza dzięki buforowaniu.

Automatyczne buforowanie

Domyślnie TFDS automatycznie buforuje (za pomocą ds.cache() ) zestawy danych, które spełniają następujące ograniczenia:

  • Zdefiniowano całkowity rozmiar zbioru danych (wszystkie podziały) i < 250 MiB
  • shuffle_files jest wyłączone lub odczytywany jest tylko pojedynczy fragment

Można zrezygnować z automatycznego buforowania, przekazując try_autocaching=False do tfds.ReadConfig w tfds.load . Zajrzyj do dokumentacji katalogu zbiorów danych, aby sprawdzić, czy konkretny zbiór danych będzie korzystał z automatycznej pamięci podręcznej.

Ładowanie pełnych danych jako pojedynczego Tensora

Jeśli Twój zestaw danych mieści się w pamięci, możesz także załadować pełny zestaw danych jako pojedynczą tablicę Tensor lub NumPy. Można to zrobić, ustawiając batch_size=-1 aby wsadowo wszystkie przykłady umieścić w jednym tf.Tensor . Następnie użyj tfds.as_numpy do konwersji z tf.Tensor na np.array .

(img_train, label_train), (img_test, label_test) = tfds.as_numpy(tfds.load(
    'mnist',
    split=['train', 'test'],
    batch_size=-1,
    as_supervised=True,
))

Duże zbiory danych

Duże zbiory danych są dzielone na fragmenty (podzielone na wiele plików) i zazwyczaj nie mieszczą się w pamięci, dlatego nie należy ich buforować.

Mieszanie i trening

Podczas treningu ważne jest, aby dobrze przetasować dane – źle przetasowane dane mogą skutkować niższą dokładnością treningu.

Oprócz używania ds.shuffle do mieszania rekordów należy także ustawić shuffle_files=True , aby uzyskać dobre zachowanie podczas tasowania w przypadku większych zestawów danych podzielonych na wiele plików. W przeciwnym razie epoki będą czytać fragmenty w tej samej kolejności, więc dane nie będą naprawdę losowe.

ds = tfds.load('imagenet2012', split='train', shuffle_files=True)

Dodatkowo, gdy shuffle_files=True , TFDS wyłącza options.deterministic , co może dać niewielki wzrost wydajności. Aby uzyskać deterministyczne tasowanie, można zrezygnować z tej funkcji za pomocą tfds.ReadConfig : albo ustawiając read_config.shuffle_seed , albo nadpisując read_config.options.deterministic .

Automatyczne fragmentowanie danych pomiędzy procesami roboczymi (TF)

Podczas szkolenia wielu procesów roboczych można użyć argumentu input_context tfds.ReadConfig , aby każdy proces roboczy odczytał podzbiór danych.

input_context = tf.distribute.InputContext(
    input_pipeline_id=1,  # Worker id
    num_input_pipelines=4,  # Total number of workers
)
read_config = tfds.ReadConfig(
    input_context=input_context,
)
ds = tfds.load('dataset', split='train', read_config=read_config)

Jest to uzupełnienie interfejsu API subsplit. Najpierw stosowane jest subplit API: train[:50%] jest konwertowane na listę plików do odczytania. Następnie na tych plikach stosowana jest operacja ds.shard() . Na przykład, używając train[:50%] z num_input_pipelines=2 , każdy z 2 procesów roboczych odczyta 1/4 danych.

Gdy shuffle_files=True pliki są tasowane w ramach jednego procesu roboczego, ale nie pomiędzy procesami roboczymi. Każdy proces roboczy będzie czytał ten sam podzbiór plików pomiędzy epokami.

Automatyczne fragmentowanie danych pomiędzy procesami roboczymi (Jax)

Dzięki Jaxowi możesz używać API tfds.split_for_jax_process lub tfds.even_splits do dystrybucji danych pomiędzy pracownikami. Zobacz przewodnik po podzielonym interfejsie API .

split = tfds.split_for_jax_process('train', drop_remainder=True)
ds = tfds.load('my_dataset', split=split)

tfds.split_for_jax_process to prosty alias dla:

# The current `process_index` loads only `1 / process_count` of the data.
splits = tfds.even_splits('train', n=jax.process_count(), drop_remainder=True)
split = splits[jax.process_index()]

Szybsze dekodowanie obrazu

Domyślnie TFDS automatycznie dekoduje obrazy. Są jednak przypadki, w których bardziej wydajne może być pominięcie dekodowania obrazu za pomocą tfds.decode.SkipDecoding i ręczne zastosowanie opcji tf.io.decode_image :

Kod obu przykładów jest dostępny w przewodniku dekodowania .

Pomiń nieużywane funkcje

Jeśli używasz tylko podzbioru funkcji, możliwe jest całkowite pominięcie niektórych funkcji. Jeśli Twój zbiór danych ma wiele nieużywanych funkcji, ich dekodowanie może znacząco poprawić wydajność. Zobacz https://www.tensorflow.org/datasets/decode#only_decode_a_sub-set_of_the_features

tf.data wykorzystuje całą moją pamięć RAM!

Jeśli masz ograniczoną ilość pamięci RAM lub jeśli ładujesz wiele zestawów danych równolegle podczas korzystania tf.data , oto kilka opcji, które mogą pomóc:

Zastąp rozmiar bufora

builder.as_dataset(
  read_config=tfds.ReadConfig(
    ...
    override_buffer_size=1024,  # Save quite a bit of RAM.
  ),
  ...
)

Zastępuje to buffer_size przekazany do TFRecordDataset (lub odpowiednika): https://www.tensorflow.org/api_docs/python/tf/data/TFRecordDataset#args

Użyj tf.data.Dataset.with_options, aby zatrzymać magiczne zachowania

https://www.tensorflow.org/api_docs/python/tf/data/Dataset#with_options

options = tf.data.Options()

# Stop magic stuff that eats up RAM:
options.autotune.enabled = False
options.experimental_distribute.auto_shard_policy = (
  tf.data.experimental.AutoShardPolicy.OFF)
options.experimental_optimization.inject_prefetch = False

data = data.with_options(options)