03 Β· Transforms π¨
Raw data is rarely model-ready. Transforms are the pipeline that turns a messy PIL image or raw array into a clean, normalized, augmented tensor.
1. The 'Why'
Neural networks are surprisingly picky eaters. A model expects its inputs to arrive as tensors of a consistent shape and a sensible numeric range β but raw data comes in all shapes and formats. Images load as PIL objects with pixel values from 0 to 255 and wildly varying dimensions; a network wants fixed-size float tensors with values around zero. Bridging that gap is the job of transforms: small, composable functions that each perform one preprocessing step, chained together into a pipeline that runs on every sample as it's fetched. Doing this consistently isn't optional polish β feed a model un-normalized inputs and it will train slowly or not at all.
Transforms do more than clean data, though β they also enable data augmentation, one of the most effective tricks in deep learning. By randomly flipping, rotating, or cropping training images on the fly, you show the model a slightly different version of each sample every epoch, which teaches it to generalize and dramatically reduces overfitting. Crucially, augmentation should apply to training data only; your validation and test data should be preprocessed but never randomly altered, so your metrics stay honest. torchvision packages all of this into torchvision.transforms, and slotting a transform pipeline into the Dataset you built in sub-module 01 completes the data story.
2. Core Concepts
Composing transforms. transforms.Compose([...]) chains steps into one callable. Each sample flows through them in order. You pass the composed transform to your Dataset, which applies it inside __getitem__.
ToTensor(). Converts a PIL image or NumPy array (HΓWΓC, values 0β255) into a float tensor (CΓHΓW, values 0.0β1.0). Note the channel dimension moves to the front β the layout PyTorch's vision layers expect.
Normalize(mean, std). Shifts and scales each channel to have a target mean and standard deviation. Networks train faster and more stably on normalized inputs. Apply it after ToTensor().
Augmentation vs. preprocessing. Preprocessing (resize, to-tensor, normalize) is deterministic and applies to all splits. Augmentation (random flip, rotation, crop) is random and applies to training only. Keep two separate transform pipelines.
The v2 API. Modern torchvision uses torchvision.transforms.v2, which is faster and supports more input types; the older transforms API still works and looks nearly identical.
3. Code in Action
A standard preprocessing pipeline
from torchvision import transforms
# Deterministic pipeline for ALL splits (train, val, test)
preprocess = transforms.Compose([
transforms.Resize((224, 224)), # force a fixed spatial size
transforms.ToTensor(), # PIL/ndarray -> float tensor, scales to [0,1], CxHxW
transforms.Normalize( # standardize each RGB channel
mean=[0.485, 0.456, 0.406], # ImageNet channel means (a common default)
std=[0.229, 0.224, 0.225], # ImageNet channel std-devs
),
])
A training pipeline with augmentation
from torchvision import transforms
# TRAINING pipeline: adds random augmentation on top of preprocessing
train_tfms = transforms.Compose([
transforms.RandomResizedCrop(224), # random crop + resize (augmentation)
transforms.RandomHorizontalFlip(p=0.5), # 50% chance to mirror (augmentation)
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
])
# VALIDATION pipeline: NO randomness β just preprocess
val_tfms = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224), # deterministic center crop
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
])
Plugging transforms into a Dataset
from torch.utils.data import Dataset
from PIL import Image
import torch
class ImageDataset(Dataset):
def __init__(self, paths, labels, transform=None):
self.paths, self.labels, self.transform = paths, labels, transform
def __len__(self): return len(self.paths)
def __getitem__(self, i):
img = Image.open(self.paths[i]).convert("RGB")
if self.transform:
img = self.transform(img) # apply the pipeline to THIS sample
return img, torch.tensor(self.labels[i])
# train_ds = ImageDataset(train_paths, train_labels, transform=train_tfms)
# val_ds = ImageDataset(val_paths, val_labels, transform=val_tfms)
4. Common Pitfalls
Pitfall 1 β Augmenting validation or test data. Random flips and crops belong to training only. Applying them to validation/test makes your metrics noisy and non-reproducible, and inflates or deflates scores randomly. Keep a separate, deterministic pipeline for evaluation.
Pitfall 2 β Wrong transform order. Order matters: Normalize expects a tensor, so it must come after ToTensor(). Putting Normalize before ToTensor() (i.e. on a PIL image) throws an error. The usual order is geometric transforms β ToTensor() β Normalize().
Pitfall 3 β Mismatched normalization between train and inference. Whatever mean/std you normalize training data with, you must use the exact same values at inference time. Different normalization at test time silently degrades predictions. Store these constants somewhere central.
5. Further Reading & Watch List
- π Deep Learning with PyTorch, Ch. 7β8 β normalizing image data
- π Learn PyTorch β Transforming data
- π οΈ torchvision transforms (v2) docs Β· Illustration of transforms
- π fast.ai β data augmentation in practice
β¬ οΈ Prev: 02 Β· The DataLoader Β· β‘οΈ Next: 04 Β· Splits & Built-in Datasets