Background
Sections
IntroductionModule 01 β€” Tensors 🧊01 Β· Creating Tensors 🧊02 Β· Indexing & Reshaping πŸ”ͺ03 Β· Tensor Math βž—04 Β· Device Placement πŸ–₯️⚑Module 02 β€” Autograd βš™οΈ01 Β· The Computational Graph πŸ•ΈοΈ02 Β· Backward Pass & Gradients ⬅️03 Β· Turning Autograd Off πŸ›‘04 Β· Gradient Gotchas πŸͺ€Module 03 β€” Neural Networks01 Β· The `nn.Module` Basics 🧱02 Β· Common Layers 🧩03 Β· Building a Network πŸ—οΈ04 Β· Inspecting Models πŸ”Module 04 β€” Data Handling πŸ—‚οΈ01 Β· Dataset Basics πŸ“‡02 Β· The DataLoader 🚚03 Β· Transforms 🎨04 Β· Splits & Built-in Datasets βœ‚οΈModule 05 β€” The Training Loop πŸ”01 Β· Loss Functions 🎯02 Β· Optimizers βš™οΈ03 Β· The Training Loop πŸ”04 Β· Evaluation & Metrics πŸ“ŠModule 06 β€” Saving & Loading πŸ’Ύ01 Β· `state_dict` Basics πŸ’Ύ02 Β· Checkpoints & Resuming ⏸️03 Β· Loading for Inference πŸš€04 Β· Best Model & Early Stopping πŸ…Module 07 β€” Computer Vision πŸ‘οΈ01 Β· Convolutions πŸ”²02 Β· CNN Architecture πŸ›οΈ03 Β· Transfer Learning πŸ”04 Β· Image Classification Project πŸ§ͺModule 08 β€” NLP & Transformers πŸ’¬01 Β· Text Data & Tokenization πŸ”€02 Β· Embeddings 🧭03 Β· Recurrent Layers & LSTMs πŸ”„04 Β· Intro to Transformers ⚑Module 09 β€” Ecosystem: PyTorch Lightning ⚑01 Β· Why Lightning? πŸ€”02 Β· The LightningModule 🧩03 Β· The Trainer πŸŽ›οΈ04 Β· DataModules & Callbacks 🧰Module 10 β€” Deployment & Optimization πŸš€01 Β· Exporting Models πŸ“¦02 Β· `torch.compile` ⚑03 Β· Inference Optimization πŸͺΆ04 Β· Serving & Next Steps πŸŽ“

03 · Transforms 🎨

4 min read

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


⬅️ Prev: 02 Β· The DataLoader Β· ➑️ Next: 04 Β· Splits & Built-in Datasets