Navigation

Introduction to AI

Machine Learning

Deep Learning

Generative AI

Tools & Frameworks

General

Data Augmentation Strategies

Data augmentation generates additional training samples by applying label-preserving transformations to existing data. It is one of the most reliable tools in the ML practitioner’s toolkit: it reduces overfitting, improves generalization, and often provides significant accuracy gains at zero labeling cost. Modern augmentation strategies range from simple geometric transforms to learned policies that adapt to specific tasks.

Why Augmentation Works

From a regularization perspective, augmentation implicitly expands the training distribution to cover a larger region of input space — making the learned function smoother and more invariant to transformations that don’t change the label. From a data efficiency perspective, augmentation effectively multiplies dataset size: a single image with 10 augmentation operations becomes 10 training examples, each slightly different.

The key constraint: transformations must be label-preserving. A horizontal flip of a cat image is still a cat. Extreme color distortion that removes all visual features crosses into label-corruption.

Image Augmentation

Geometric Transforms

import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np
import cv2

# Standard geometric augmentation pipeline (ImageNet-style)
imagenet_train_transform = A.Compose([
    # Spatial transforms
    A.RandomResizedCrop(height=224, width=224, scale=(0.08, 1.0), ratio=(0.75, 1.33)),
    A.HorizontalFlip(p=0.5),
    
    # Color transforms
    A.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.1, p=0.8),
    A.ToGray(p=0.2),
    
    # Regularization transforms
    A.GaussianBlur(blur_limit=(3, 7), p=0.1),
    
    # Normalization
    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ToTensorV2()
])

# Heavier augmentation for small datasets or few-shot learning
heavy_transform = A.Compose([
    A.RandomResizedCrop(height=224, width=224, scale=(0.2, 1.0)),
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.1),           # useful for aerial/satellite imagery
    A.RandomRotate90(p=0.3),
    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=30, p=0.5),
    
    # Cutout / random erasing: masks a random patch
    A.CoarseDropout(max_holes=8, max_height=32, max_width=32,
                     min_holes=1, fill_value=0, p=0.5),
    
    # Advanced color augmentation
    A.OneOf([
        A.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.2),
        A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20),
        A.RGBShift(r_shift_limit=20, g_shift_limit=20, b_shift_limit=20),
    ], p=0.8),
    
    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ToTensorV2()
])

Mixup: Interpolating Between Samples

Mixup (Zhang et al., 2018) creates convex combinations of pairs of training examples and their labels:

$$\tilde{x} = \lambda x_i + (1 - \lambda) x_j, \quad \tilde{y} = \lambda y_i + (1 - \lambda) y_j$$

where $\lambda \sim \text{Beta}(\alpha, \alpha)$, typically with $\alpha \in [0.2, 1.0]$. The model must predict a soft distribution rather than hard one-hot labels, which acts as a strong regularizer and improves calibration:

import torch
import numpy as np

def mixup_data(x: torch.Tensor, y: torch.Tensor,
               alpha: float = 0.4) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:
    """
    Apply Mixup to a batch of (image, label) pairs.
    Returns mixed images, original labels a and b, and mixing coefficient lambda.
    """
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1.0
    
    batch_size = x.size(0)
    # Random permutation for pairing
    index = torch.randperm(batch_size, device=x.device)
    
    mixed_x = lam * x + (1 - lam) * x[index]
    y_a, y_b = y, y[index]
    
    return mixed_x, y_a, y_b, lam


def mixup_criterion(criterion, pred: torch.Tensor, y_a: torch.Tensor,
                    y_b: torch.Tensor, lam: float) -> torch.Tensor:
    """Mixup loss: weighted combination of losses for both labels."""
    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)


# Training loop with Mixup
def train_with_mixup(model, loader, optimizer, criterion, alpha=0.4):
    model.train()
    for images, labels in loader:
        images, labels = images.cuda(), labels.cuda()
        
        mixed_images, labels_a, labels_b, lam = mixup_data(images, labels, alpha)
        
        outputs = model(mixed_images)
        loss = mixup_criterion(criterion, outputs, labels_a, labels_b, lam)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

CutMix: Pasting Patches Between Images

CutMix (Yun et al., 2019) cuts a rectangular region from one image and pastes it onto another. Labels are mixed proportionally to the area of each image in the final sample:

$$\lambda = 1 - \frac{W_{box} \cdot H_{box}}{W \cdot H}$$

def cutmix_data(x: torch.Tensor, y: torch.Tensor,
                alpha: float = 1.0) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:
    """
    CutMix augmentation.
    
    Compared to Mixup:
    - Preserves local texture structure (patches from real images, not blended)
    - Better for tasks requiring local features (detection, segmentation)
    - Superior to Mixup for ImageNet-scale classification
    """
    lam = np.random.beta(alpha, alpha)
    batch_size = x.size(0)
    index = torch.randperm(batch_size, device=x.device)
    
    B, C, H, W = x.shape
    
    # Sample random bounding box
    cut_ratio = (1.0 - lam) ** 0.5   # box occupies (1-lam) of image area
    cut_h = int(H * cut_ratio)
    cut_w = int(W * cut_ratio)
    
    # Random center point
    cx = np.random.randint(W)
    cy = np.random.randint(H)
    
    x1 = max(0, cx - cut_w // 2)
    x2 = min(W, cx + cut_w // 2)
    y1 = max(0, cy - cut_h // 2)
    y2 = min(H, cy + cut_h // 2)
    
    # Paste patch from shuffled image
    mixed_x = x.clone()
    mixed_x[:, :, y1:y2, x1:x2] = x[index, :, y1:y2, x1:x2]
    
    # Recompute lambda based on actual box area
    lam = 1 - (x2 - x1) * (y2 - y1) / (W * H)
    
    return mixed_x, y, y[index], lam

Automated Augmentation: AutoAugment and RandAugment

AutoAugment (Cubuk et al., 2019) learns an augmentation policy for a dataset using reinforcement learning — selecting which operations to apply and at what magnitude/probability. It is expensive to search but the resulting policies transfer well.

RandAugment (Cubuk et al., 2020) simplifies AutoAugment: apply $N$ randomly chosen operations from a fixed set at a uniform magnitude $M$. Only two hyperparameters to tune, no search required:

import torchvision.transforms.v2 as T

# RandAugment: N=2 operations, M=9 magnitude (on a 0-30 scale)
randaugment_transform = T.Compose([
    T.RandomResizedCrop(224),
    T.RandomHorizontalFlip(),
    T.RandAugment(num_ops=2, magnitude=9),
    T.ToTensor(),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# TrivialAugment: even simpler — one random op at a random magnitude each step
trivialaugment_transform = T.Compose([
    T.RandomResizedCrop(224),
    T.RandomHorizontalFlip(),
    T.TrivialAugmentWide(),   # state-of-the-art on many benchmarks with zero tuning
    T.ToTensor(),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# AugMix: creates diverse augmented views via augmentation chains + mixing
augmix_transform = T.Compose([
    T.RandomResizedCrop(224),
    T.RandomHorizontalFlip(),
    T.AugMix(severity=3, mixture_width=3),   # robust to distribution shift
    T.ToTensor(),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

Text Augmentation

import random
import nlpaug.augmenter.word as naw
import nlpaug.augmenter.sentence as nas

# Synonym replacement (WordNet)
syn_aug = naw.SynonymAug(aug_src='wordnet', aug_p=0.15)

# Back-translation: translate → target language → translate back
# Produces semantically equivalent but lexically different text
back_translation_aug = naw.BackTranslationAug(
    from_model_name='facebook/wmt19-en-de',
    to_model_name='facebook/wmt19-de-en',
    device='cuda'
)

# Contextual word embedding insertion (BERT-based)
bert_aug = naw.ContextualWordEmbsAug(
    model_path='bert-base-uncased',
    action='insert',
    aug_p=0.1,
    device='cuda'
)

def augment_text_dataset(texts: list[str], labels: list[int],
                          n_augments: int = 3) -> tuple[list[str], list[int]]:
    """
    Create augmented text samples for low-resource classification.
    Each original sample generates n_augments augmented versions.
    """
    augmenters = [syn_aug, bert_aug]
    aug_texts, aug_labels = list(texts), list(labels)
    
    for text, label in zip(texts, labels):
        for _ in range(n_augments):
            augmenter = random.choice(augmenters)
            try:
                aug_text = augmenter.augment(text)[0]
                aug_texts.append(aug_text)
                aug_labels.append(label)
            except Exception:
                pass   # skip failed augmentations
    
    return aug_texts, aug_labels

Test-Time Augmentation (TTA)

TTA applies augmentations at inference time and aggregates predictions across augmented views — trading compute for accuracy:

def predict_with_tta(model: torch.nn.Module, image: torch.Tensor,
                     num_augments: int = 10) -> torch.Tensor:
    """
    Test-time augmentation: average predictions over multiple augmented views.
    Typically improves top-1 accuracy by 0.5–2% at 10× inference cost.
    """
    tta_transforms = [
        T.Compose([T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]),
        T.Compose([T.RandomHorizontalFlip(p=1.0),
                   T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]),
        T.Compose([T.CenterCrop(200), T.Resize(224),
                   T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]),
        # ... additional crops and flips
    ]
    
    model.eval()
    predictions = []
    
    with torch.no_grad():
        for aug in tta_transforms[:num_augments]:
            aug_image = aug(image.clone())
            pred = torch.softmax(model(aug_image.unsqueeze(0)), dim=-1)
            predictions.append(pred)
    
    return torch.stack(predictions).mean(0)  # average softmax probabilities

Augmentation Strategy Comparison

StrategyTypeKey ideaBest for
Flips + cropsGeometricInvariance to viewpointMost vision tasks
Color jitterPhotometricInvariance to lightingNatural images
MixupLabel mixingConvex combination of pairsClassification, robustness
CutMixPatch mixingReplace patch, mix labelsImageNet-scale classification
RandAugmentAutomatedRandom ops at fixed magnitudeZero-cost search
TrivialAugmentAutomatedSingle random opStrong baseline, minimal tuning
AugMixConsistencyMix augmented streams + JSD lossDistribution shift robustness
Back-translationTextParaphrase via MTLow-resource NLP
SMOTETabularSynthetic minority oversamplingImbalanced classification

The most impactful augmentations depend heavily on the task, dataset size, and modality. For images: RandAugment or TrivialAugment combined with CutMix is a strong default. For text: back-translation and synonym replacement work well for small datasets. For tabular data: Mixup applied in feature space often outperforms domain-specific transforms.