TensorFlow Data Pipelines
TensorFlow Data Pipelines
You have seen basic tf.data usage in the beginner tutorial. At the intermediate level, the focus shifts to performance building pipelines that keep your GPU saturated, handle large datasets that cannot fit in memory, and apply augmentation efficiently without slowing training.
Introduction to tf.data
tf.data.A dataset represents a lazy sequence of elements. Operations like map(), filter(), and batch() are chained as transformations and only execute when the model requests the next batch. This lazy evaluation is what makes tf.data scalable to arbitrarily large datasets — you never need to hold more than one batch in memory at a time.
Creating Datasets
tf.data supports multiple data sources. The most common are shown below:
import tensorflow as tf
# From NumPy arrays (in-memory)
ds_numpy = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# From image files on disk
ds_images = tf.data.Dataset.list_files('data/images/*.jpg')
# From TFRecord files (most efficient for large datasets)
ds_tfrecord = tf.data.TFRecordDataset('data/train.tfrecord')
# From a Python generator (useful for complex data loading logic)
def generator():
for i in range(1000):
yield (tf.random.normal([28, 28]), i % 10)
ds_gen = tf.data.Dataset.from_generator(
generator,
output_signature=(
tf.TensorSpec(shape=(28,28), dtype=tf.float32),
tf.TensorSpec(shape=(), dtype=tf.int32)
)
)
Batching and Shuffling Data
The standard pipeline order is: map → cache → shuffle → batch → prefetch. The cache() call stores preprocessed data after the first epoch, avoiding redundant work. According to PythonGuides' deep tf.data analysis, batching after shuffling is non-negotiable — shuffle first, then batch:
BATCH_SIZE = 64
BUFFER_SIZE = 10000
train_ds = (
tf.data.Dataset.from_tensor_slices((x_train, y_train))
.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
.cache() # cache after preprocessing
.shuffle(BUFFER_SIZE) # shuffle individual samples
.batch(BATCH_SIZE) # group into batches
.prefetch(tf.data.AUTOTUNE) # overlap data prep with training
)
Data Augmentation
At the intermediate level, augmentation goes beyond simple flips and rotations. You can apply MixUp, CutMix, or random erasing. Here is a practical augmentation pipeline for image classification:
augmentation = tf.keras.Sequential([
tf.keras.layers.RandomFlip('horizontal_and_vertical'),
tf.keras.layers.RandomRotation(0.15),
tf.keras.layers.RandomZoom((-0.1, 0.1)),
tf.keras.layers.RandomTranslation(0.1, 0.1),
tf.keras.layers.RandomContrast(0.2),
tf.keras.layers.RandomBrightness(0.2),
], name='augmentation')
def augment(image, label):
image = tf.cast(image, tf.float32) / 255.0
image = augmentation(image, training=True)
return image, label
train_ds = (
raw_train_ds
.map(augment, num_parallel_calls=tf.data.AUTOTUNE)
.shuffle(5000)
.batch(64)
.prefetch(tf.data.AUTOTUNE)
)
Optimizing Input Pipelines
A poorly built pipeline leaves your GPU waiting for data. Here are the most impactful optimisations:
Technique | Code | Impact |
AUTOTUNE parallelism | .map(fn, num_parallel_calls=tf.data.AUTOTUNE) | Dynamically sets CPU thread count, often 2–4x throughput gain |
Caching | .cache() | Eliminates repeated preprocessing after epoch 1 |
Prefetching | .prefetch(tf.data.AUTOTUNE) | GPU never waits for the next batch prepared in the background |
TFRecord format | tf.data.TFRecordDataset(...) | Sequential disk reads much faster than random image file reads |
Vectorised map | .map(batch_fn) on batches | Applying preprocessing to the whole batch at once avoids Python loop overhead. |










