Datasets and DataLoaders
What is a Dataset?
Deriving the Dataset Interface from First Principles
Start with the problem: We have N samples (images, texts, etc.). During training, we need to:
- Access any sample by index (for batching)
- Know the total count (for epoch iteration)
Naive approach: Load everything into a list.
data = [load_image(i) for i in range(N)] # Uses N × image_size RAM!Problem: For ImageNet (1.2M images, ~150GB), this crashes.
Solution: Lazy loading via indexing.
class Dataset:
def __len__(self): return N
def __getitem__(self, idx): return load_image(idx) # Load on-demandNow we load one image at a time, keeping RAM constant.
Example1: Custom Image Dataset from Scratch
Problem: Load images from a folder where filenames encode labels (e.g., cat_001.jpg).
import os
from PIL import Image
import torch
from torch.utils.data import Dataset
class CustomImageDataset(Dataset):
def __init__(self, img_dir, transform=None):
self.img_dir = img_dir
self.transform = transform
# List all image paths
self.img_paths = [f for f in os.listdir(img_dir) if f.endswith('.jpg')]
# WHY: We store paths, not images (lazy loading)
def __len__(self):
return len(self.img_paths)
# WHY: Needed for batching and epoch calculation
def __getitem__(self, idx):
img_path = os.path.join(self.img_dir, self.img_paths[idx])
image = Image.open(img_path) # Load NOW (on-demand)
# WHY: Only load when requested, not during __init__
# Extract label from filename (e.g., "cat_001.jpg" → "cat")
label = self.img_paths[idx].split('_')[0]
if self.transform:
image = self.transform(image)
# WHY: Data augmentation (random crops, flips) happens per-access
return image, labelWhy this step (lazy loading)? If we loaded all images in __init__, we'd need gigabytes of RAM before training starts. Now we load ~1 image at a time.
What is a DataLoader?
Deriving DataLoader from Training Requirements
Training loop structure:
for epoch in range(num_epochs):
for batch in data: # Need batches, not individual samples
loss = model(batch)
loss.backward()Requirements analysis:
-
Batching: SGD/Adam need batches (e.g., 32 samples) for gradient estimation.
- Math: Gradient estimate
- Why batches? Reduces gradient variance, enables GPU parallelism.
-
Shuffling: Prevents model from learning sample order (overfitting to sequence).
- Why each epoch? Model sees same data, but in different order → better generalization.
-
Paralelism: Data loading (disk I/O, decompression) is CPU-bound. GPU waits idle.
- Solution: Use multiple CPU processes to pre-load next batch while GPU trains on current.
Example 2: DataLoader with All Features
from torch.utils.data import DataLoader
# Assume we have the CustomImageDataset from before
dataset = CustomImageDataset(img_dir='./data/images')
dataloader = DataLoader(
dataset,
batch_size=32, # Stack 32 samples per batch
shuffle=True, # Randomize order each epoch
num_workers=4, # 4 parallel CPU processes
pin_memory=True, # Speed up CPU → GPU transfer
drop_last=True # Drop incomplete last batch
)
# WHY each parameter:
# - batch_size=32: Trade-off between gradient accuracy and speed
# (larger batch → less noise, but slower per-iteration)
# - shuffle=True: Breaks correlation between consecutive samples
# (prevents overfitting to data order)
# - num_workers=4: While GPU trains batch t, CPUs load batch t+1
# (overlap computation and I/O)
# - pin_memory=True: Allocates tensors in page-locked RAM
# (faster DMA transfer to GPU, ~10-20% speedup)
# - drop_last=True: Ensures all batches have same size
# (some models/layers require fixed batch size)Using the DataLoader:
for epoch in range(10):
for batch_idx, (images, labels) in enumerate(dataloader):
# images: Tensor of shape (32, 3, 224, 224)
# labels: Tensor of shape (32,)
images = images.to(device) # Move to GPU
labels = labels.to(device)
# Train step...
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()Why this step (enumerate)? We get both batch index (for logging) and data. The DataLoader internally calls dataset.__getitem__() 32 times, stacks results, and yields.
Deep Dive: Collation
Custom Collate Function
def collate_variable_length(batch):
# batch: list of tuples [(seq1, label1), (seq2, label2), ...]
# where seq_i has shape (length_i, feature_dim)
# Step 1: Find max length
max_len = max([seq.shape[0] for seq, _ in batch])
# WHY: Tensors must be rectangular (all rows same length)
# Step 2: Pad sequences to max_len
padded_seqs = []
labels = []
for seq, label in batch:
pad_amount = max_len - seq.shape[0]
padded = torch.nn.functional.pad(seq, (0, 0, pad_amount))
# WHY: Pad on length dimension (dim 0), not feature dimension (dim 1)
padded_seqs.append(padded)
labels.append(label)
# Step 3: Stack into batch tensors
batch_seqs = torch.stack(padded_seqs) # (batch_size, max_len, feature_dim)
batch_labels = torch.tensor(labels) # (batch_size,)
return batch_seqs, batch_labels
# Use with DataLoader
dataloader = DataLoader(dataset, batch_size=32, collate_fn=collate_variable_length)Why padding? GPU operations (matrix multiplication) require fixed-size inputs. Padding wastes some computation but enables batching.
Common Patterns and Optimizations
Diagram Explanation

The diagram shows the data pipeline flow:
- Dataset: On-disk data (images, CSVs) with lazy
__getitem__()access - Sampler: Generates indices (shuffled or sequential)
- Workers: Parallel CPU processes fetch samples via indices
- Collate: Stacks samples into batch tensors
- GPU: Receives batched tensors for training
Key insight: The DataLoader is a producer-consumer system where CPU workers (producers) run ahead of GPU (consumer) to eliminate wait time.
Recall Explain to a 12-year-old
Imagine you're studying for a test with flashcards. You have 1000 cards in a big box.
Dataset is like organizing those cards. Each card has a number (index), and you can pull out any card by its number. But you DON'T pull out all1000 cards at once—that would cover your whole desk!
DataLoader is like your study buddy who:
- Shuffles the cards so you don't memorize the order (that's cheating!)
- Hands you 10 cards at a time (a batch) so you can study them together
- Has 4 friends (workers) who pre-fetch the next batch while you study the current one—so you never wait
The magic: You study 1000 cards without ever having all of them on your desk at once, and your friends make sure the next batch is always ready!
Connections
- 3.3.01-Introduction-to-PyTorch: DataLoader is part of PyTorch's
torch.utils.datamodule - 3.3.05-Training-Loop-Basics: DataLoader feeds batches to the training loop
- 2.2.03-Mini-Batch-Gradient-Descent: Batch size parameter comes from DataLoader
- 3.2.04-Data-Augmentation: Transforms applied in Dataset's
__getitem__() - 4.1.02-Transfer-Learning: Pre-trained models expect specific normalization (applied in Dataset)
- 3.3.08-Distributed-Training: DataLoader with DistributedSampler for multi-GPU
#flashcards/ai-ml
What are the two methods a PyTorch Dataset must implement? :: __len__() (returns total samples) and __getitem__(idx) (returns sample at index idx)
Why do we use lazy loading in Datasets instead of loading all data in init?
What is the mathematical relationship between dataset size N, batch size B, and number of batches?
What does the DataLoader's collate function do?
Why use num_workers > 0 in DataLoader?
What does pin_memory=True do in DataLoader?
Why should shuffling be done in DataLoader, not inside Dataset.getitem?
What is prefetch_factor in DataLoader? :: Number of batches each worker pre-loads ahead. With prefetch_factor=2 and num_workers=4, the system loads 8 batches ahead of current GPU batch, maximizing overlap between computation and I/O.
Why use drop_last=True in DataLoader?
What is the time complexity of accessing a sample in a well-designed Dataset?
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Datasets aur DataLoaders ka sabse important kaam hai efficiently data feed karna model ko during training. Socho tumhare pas ImageNet jaisa dataset hai—millions of images. Sabko RAM me load karna impossible hai. Toh kya kare? Yahi pe Dataset aur DataLoader ka concept ata hai.
Datasetek blueprint hai jo bata hai ki tumhara data kahan hai aur kaise access karna hai. Jaise library me har book ka index hota hai, waise hi Dataset me har sample ka index hota hai. Jab tum kehte ho "mujhe sample number42 chahiye," tab Dataset disk se exactly woh sample load karta hai—lazy loading kehte hain isko. RAM me sab kuch ek sath nahi, bas jo chahiye woh load karo.
DataLoader ek smart librarian ki tarah kaam karta hai. Woh Dataset se samplesuthata hai, lekin efficiently. Pehle woh shuffles karta hai (taki model order yad na kare), phir batches banata hai (32 samples ek sath, kyunki GPU parallel processing me fast hai), aur multiple workers lagata hai (4-5