Rozšíření dat pro počítačové vidění: techniky a osvědčené postupy
Jedním z nejčastějších problémů v počítačovém vidění je nadměrné vybavení: model se učí nazpaměť tréninková sada místo zobecňování. Nejúčinnější řešení je augmentace dat: aplikace náhodných transformací na obrázky během tréninku k umělému rozšíření rozmanitost dat a naučit model invariance transformacím, které nejsou relevantní.
Dobře navržená strategie rozšiřování může být stejně dobrá jako zdvojnásobení datové sady. Strategie špatné může snížit výkon. V tomto článku se podíváme na základní techniky s Albumentace (nejvýkonnější knihovna) e torchvision.transformuje, pokročilé techniky, jako je MixUp a CutMix, a jak vybrat správnou augmentaci pro každou doménu. Probereme také, jak implementovat vlastní kanály a měřit skutečný dopad každého z nich transformace a nasazení do výroby s minimální výpočetní režií.
Co se naučíte
- proč funguje augmentace dat: princip invariance
- Albumentace vs torchvision: kdy co použít
- Geometrické techniky: převrácení, rotace, oříznutí, perspektivní transformace
- Fotometrické techniky: jas, kontrast, barevný jitter, CLAHE
- Pokročilé techniky: MixUp, CutMix, Mosaic, GridDistortion
- AutoAugment a RandAugment: Automatické vyhledávání zásad
- Augmentace pro detekci a segmentaci (se souřadnicemi a maskami)
- Rozšíření specifické pro doménu: lékařské, průmyslové, satelitní
- Jak měřit účinnost augmentace s ablační studií
- Potrubí optimalizované pro rychlý trénink a minimální režii
1. proč Data Augmentation funguje
Rozšiřování dat je založeno na základním principu: na transformacích, které aplikujeme nesmí měnit sémantický význam obrázku (správný výstup modelu), ale musí změnit pixely, aby model nemohl pouze ukládat vzory povrchní.
Z pohledu teorie učení je augmentace dat formou implicitní regularizace: rozšiřuje prostor proměn s respektem ke kterému chceme, aby byl model invariantní. Pokud trénujeme s horizontálními obraty, model se naučí, že levá a pravá strana kočky neovlivňují klasifikaci. Pokud trénujeme s variacemi jasu, model se to naučí ignorovat světelné podmínky.
# Esempio: classificazione gatti/cani
# Trasformazioni CORRETTE (preservano la semantica):
# - Flip orizzontale: un gatto capovolto orizzontalmente è ancora un gatto ✓
# - Variazione luminosita: un gatto in penombra è ancora un gatto ✓
# - Crop random: un dettaglio del gatto è ancora riconoscibile come gatto ✓
# - Rotazione lieve: un gatto ruotato di 15 gradi è ancora un gatto ✓
# Trasformazioni PERICOLOSE (potrebbero cambiare la semantica):
# - Flip VERTICALE per traffico stradale: "stop" capovolto perde significato ✗
# - Rotazione >45 gradi per testo/numeri: "6" ruotato diventa "9" ✗
# - Scala estrema: un oggetto crop al 5% potrebbe perdere contesto ✗
# - Color jitter estremo su diagnostica medica: il colore è semanticamente rilevante ✗
# Regola d'oro:
# "Un'augmentation è valida se un umano, vedendo l'immagine aumentata,
# darebbe ancora la stessa label"
# Impatto pratico su benchmark (stesso modello ResNet-18, stessi iperparametri):
# CIFAR-10 senza augmentation: ~84.3% accuracy
# + Flip + Crop: ~91.8% accuracy (+7.5%)
# + Color Jitter: ~93.2% accuracy (+1.4%)
# + Cutout/CoarseDropout: ~94.1% accuracy (+0.9%)
# + MixUp (alpha=0.2): ~95.3% accuracy (+1.2%)
# + CutMix (alpha=1.0): ~95.8% accuracy (+0.5%)
# + AutoAugment (CIFAR-10 policy): ~97.1% accuracy (+1.3%)
# TrivialAugment + MixUp: ~97.4% accuracy (miglior combinazione)
# Nota: ogni +% è applicato sul modello SENZA aggiungere dati reali.
# Data augmentation = dataset virtualmente infinito da dati finiti.
1.1 Typy invariance, které je třeba se naučit
Ne všechny aplikace počítačového vidění vyžadují stejné invarianty. Předtím vybudovat potrubí pro augmentaci, zeptejme se sami sebe: od toho, co chceme je model robustní?
Mapa invariance-transformace pro doménu
| Doména | Užitečné invarianty | NEBEZPEČNÉ invariance |
|---|---|---|
| Přírodní fotografie | Překlopení H, oříznutí, jas, barva | Flip V, otočení o 90 stupňů |
| Text/OCR | Jas, mírný šum | Rotace, převrácení, zkreslení |
| Provoz/Signály | Jas, rozostření, oříznutí | Flip V, otočení o 90 stupňů |
| rentgen (hrudník) | Flip H, mírná rotace, kontrast | Flip V, barevný posun, silná rotace |
| Histologie | Flip H/V, otočení o 90, mírný barevný posun | Silná elastická rotace, extrémní měřítko |
| Průmyslová kontrola | 360 rotace, jas, rozostření, šum | Velmi extrémní měřítka (ztráta defektů detailů) |
| Satelit | Rotace 90/180, překlápění H/V | Silná změna barvy (spektrální pásmo) |
2. Albumentace: Referenční knihovna
Albumentace je nejvýkonnější a nejflexibilnější knihovna pro rozšiřování dat počítačové vidění. Na rozdíl od torchvision.transforms nativně podporuje:
- Obrázky + segmentační masky (synchronizované geometrické transformace)
- Obrázky + ohraničovací rámečky (automaticky se aktualizují souřadnice)
- Obrázky + klíčové body (klíčové body jsou konzistentní)
- Pipelines optimalizované pomocí OpenCV (rychlejší než PIL o 15-40%)
- Více než 70 transformací dostupných ihned po vybalení
Jeho pipeline architektura je komponovatelná: každá transformace má pravděpodobnost
p které mají být aplikovány, a transformace jako např A.OneOf dovolit
vzorkovat ze sady alternativních transformací. Vznikne tak prostor
exponenciálně velké rozšíření s několika řádky kódu.
import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np
import cv2
# ---- Pipeline standard per classificazione ----
def get_classification_transforms(img_size: int = 224, is_train: bool = True):
if is_train:
return A.Compose([
# --- Geometriche ---
A.RandomResizedCrop(img_size, img_size, scale=(0.7, 1.0),
ratio=(0.75, 1.33), p=1.0),
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.15,
rotate_limit=15, border_mode=cv2.BORDER_REFLECT, p=0.7),
A.OneOf([
A.Perspective(scale=(0.05, 0.1)),
A.GridDistortion(num_steps=5, distort_limit=0.3),
A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50)
], p=0.3),
# --- Fotometriche ---
A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.7),
A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30,
val_shift_limit=20, p=0.5),
A.OneOf([
A.GaussNoise(var_limit=(10, 50)),
A.GaussianBlur(blur_limit=(3, 7)),
A.MotionBlur(blur_limit=7),
A.MedianBlur(blur_limit=5)
], p=0.4),
A.ImageCompression(quality_lower=70, quality_upper=100, p=0.2),
# --- Dropout e occlusione ---
A.CoarseDropout(max_holes=8, max_height=32, max_width=32,
min_holes=1, p=0.3), # simile a Cutout
A.RandomGridShuffle(grid=(3, 3), p=0.1),
# --- Normalizzazione ImageNet ---
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
else:
# Validation: solo operazioni deterministiche
return A.Compose([
A.Resize(int(img_size * 1.14), int(img_size * 1.14)),
A.CenterCrop(img_size, img_size),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
# ---- Pipeline per object detection (aggiorna bounding boxes!) ----
def get_detection_transforms(img_size: int = 640, is_train: bool = True):
if is_train:
return A.Compose([
A.RandomResizedCrop(img_size, img_size, scale=(0.5, 1.0)),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.7),
A.HueSaturationValue(p=0.5),
A.OneOf([
A.GaussNoise(),
A.GaussianBlur(blur_limit=3)
], p=0.3),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
],
# CRITICO: specifica formato bbox per aggiornamento automatico
bbox_params=A.BboxParams(
format='yolo', # o 'pascal_voc', 'coco', 'albumentations'
label_fields=['class_labels'],
min_visibility=0.3, # rimuovi bbox se visibilità < 30%
min_area=100 # rimuovi bbox se area < 100 pixel
))
else:
return A.Compose([
A.Resize(img_size, img_size),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
],
bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
# ---- Pipeline per segmentazione (aggiorna maschere!) ----
def get_segmentation_transforms(img_size: int = 512, is_train: bool = True):
if is_train:
return A.Compose([
A.RandomResizedCrop(img_size, img_size, scale=(0.7, 1.0)),
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1,
rotate_limit=10, p=0.5),
A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, p=0.3),
A.RandomBrightnessContrast(p=0.5),
A.GaussNoise(var_limit=(10, 30), p=0.3),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
# NOTA: la maschera viene passata come argomento 'mask' - aggiornata automaticamente!
else:
return A.Compose([
A.Resize(img_size, img_size),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
# ---- Utilizzo con detection ----
transform = get_detection_transforms(is_train=True)
image = cv2.imread('image.jpg')
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
bboxes = [(0.5, 0.5, 0.3, 0.4)] # formato YOLO [x_c, y_c, w, h]
labels = [0]
result = transform(image=image_rgb, bboxes=bboxes, class_labels=labels)
transformed_image = result['image'] # tensor [3, H, W]
transformed_boxes = result['bboxes'] # bbox aggiornate automaticamente!
print(f"Bbox originale: {bboxes} -> Bbox trasformata: {transformed_boxes}")
2.1 Albumentace vs torchvision.transforms: Kdy použít který
Srovnání Albumentations vs torchvision.transforms
| Charakteristický | Albumentace | torchvision.transformuje |
|---|---|---|
| podpora bbox/masky | Nativní a automatické | Pouze obrázky (žádná média) |
| Počet transformací | 70+ transformací | ~30 transformací |
| Rychlost | Velmi rychlé (backend OpenCV) | Pomalejší (backend GDP) |
| Integrace PyTorch | Je vyžadován ToTensorV2 | Rodák |
| AutoAugment/RandAugment | Zakázková realizace | Nativní v torchvision 0.12+ |
| Doporučené použití | Detekce, segmentace, vlastní potrubí | Jednoduchá klasifikace, AutoAugment |
3. Pokročilé techniky augmentace
3.1 MixUp: Interpolace mezi obrázky
MixUp (Zhang et al., 2018) míchá dva obrázky a jejich příslušné štítky koeficient lambda odebraný z distribuce Beta. Přinutit model, aby se choval lineárně mezi třídami a výrazně snižuje důvěru v předpovědi, zlepšení kalibrace a robustnosti. Ztrátu je třeba vypočítat jako průměr vážení dvou samostatných CrossEntropyLoss.
import torch
import numpy as np
def mixup_batch(images: torch.Tensor, labels: torch.Tensor,
alpha: float = 0.2) -> tuple:
"""
MixUp: interpola linearmente due immagini e le loro label.
Output: immagine mista, label miste (soft labels).
lambda ~ Beta(alpha, alpha)
image_mixed = lambda * image_a + (1 - lambda) * image_b
label_mixed = lambda * label_a + (1 - lambda) * label_b
Nota: con alpha=0.2, lambda e tipicamente vicino a 0 o 1 (quasi pura),
con alpha=1.0 (uniform Beta), le immagini sono equamente miscelate.
"""
batch_size = images.size(0)
lam = np.random.beta(alpha, alpha)
# Indici random per la seconda immagine nel batch
perm = torch.randperm(batch_size)
mixed_images = lam * images + (1 - lam) * images[perm]
labels_a = labels
labels_b = labels[perm]
# Per calcolo loss: loss = lam * CE(pred, a) + (1-lam) * CE(pred, b)
return mixed_images, labels_a, labels_b, lam
def cutmix_batch(images: torch.Tensor, labels: torch.Tensor,
alpha: float = 1.0) -> tuple:
"""
CutMix: sostituisce una regione rettangolare di un'immagine con quella di un'altra.
Più efficace di MixUp per task di detection (preserva regioni intatte).
lambda ~ Beta(alpha, alpha) # determina la proporzione dell'area tagliata
"""
batch_size, C, H, W = images.size()
lam = np.random.beta(alpha, alpha)
perm = torch.randperm(batch_size)
# Calcola dimensioni del box da tagliare
cut_ratio = np.sqrt(1 - lam)
cut_w = int(W * cut_ratio)
cut_h = int(H * cut_ratio)
# Centro del box random
cx = np.random.randint(W)
cy = np.random.randint(H)
# Coordinate box (clipped ai bordi)
x1 = np.clip(cx - cut_w // 2, 0, W)
x2 = np.clip(cx + cut_w // 2, 0, W)
y1 = np.clip(cy - cut_h // 2, 0, H)
y2 = np.clip(cy + cut_h // 2, 0, H)
# Applica CutMix (immutabile: crea una copia)
mixed_images = images.clone()
mixed_images[:, :, y1:y2, x1:x2] = images[perm, :, y1:y2, x1:x2]
# Ricalcola lambda effettivo basato sull'area reale del box
lam = 1 - (x2 - x1) * (y2 - y1) / (W * H)
return mixed_images, labels, labels[perm], lam
def mosaic_augmentation(images: list, labels: list) -> tuple:
"""
Mosaic augmentation (introdotto in YOLOv5):
Combina 4 immagini in un mosaico 2x2 con crop random centrale.
Particolarmente efficace per detection su oggetti piccoli:
ogni immagine nel mosaico e al 25% della dimensione originale,
simulando oggetti a distanza maggiore.
"""
assert len(images) == 4, "Mosaic richiede esattamente 4 immagini"
_, H, W = images[0].shape
mosaic = torch.zeros(3, H * 2, W * 2)
# Posiziona le 4 immagini nel mosaico
mosaic[:, 0:H, 0:W] = images[0] # top-left
mosaic[:, 0:H, W:2*W] = images[1] # top-right
mosaic[:, H:2*H, 0:W] = images[2] # bottom-left
mosaic[:, H:2*H, W:2*W] = images[3] # bottom-right
# Crop centrale random (simula diverse prospettive)
crop_y = np.random.randint(H // 2, H)
crop_x = np.random.randint(W // 2, W)
mosaic_cropped = mosaic[:, crop_y-H//2:crop_y+H//2,
crop_x-W//2:crop_x+W//2]
# In pratica i bbox vanno aggiornati di conseguenza (offset per posizione)
combined_labels = []
for i, lbl in enumerate(labels):
combined_labels.extend(lbl)
return mosaic_cropped, combined_labels
# ---- Training loop con MixUp/CutMix ----
def train_with_advanced_augmentation(
model, train_loader, optimizer, criterion, device,
mixup_alpha: float = 0.2, cutmix_alpha: float = 1.0,
mixup_prob: float = 0.5, cutmix_prob: float = 0.5
) -> float:
"""
Training step che applica MixUp o CutMix con probabilità data.
La scelta e esclusiva: se entrambe superano threshold, si usa la prima.
"""
model.train()
total_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
r = np.random.rand()
if r < mixup_prob:
mixed_images, labels_a, labels_b, lam = mixup_batch(images, labels, mixup_alpha)
outputs = model(mixed_images)
loss = lam * criterion(outputs, labels_a) + (1 - lam) * criterion(outputs, labels_b)
elif r < mixup_prob + cutmix_prob:
mixed_images, labels_a, labels_b, lam = cutmix_batch(images, labels, cutmix_alpha)
outputs = model(mixed_images)
loss = lam * criterion(outputs, labels_a) + (1 - lam) * criterion(outputs, labels_b)
else:
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad(set_to_none=True)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
total_loss += loss.item()
return total_loss / len(train_loader)
3.2 AutoAugment, RandAugment a TrivialAugment
AutoAugment (Cubuk et al., 2019) využívá k hledání posilovací učení automaticky optimální politiku rozšiřování na datové sadě. Problém: drahý výzkum (5000 hodin GPU na CIFAR-10). RandAugment zjednodušit: použít N operací náhodně vybrané z pevného seznamu s jednotnou velikostí M, s pouze 2 hyperparametry k ladění. TrivialAugment jde ještě dále: 1 náhodná operace s náhodnou velikostí, často překonává složitější metody.
from torchvision import transforms
MEAN = [0.485, 0.456, 0.406]
STD = [0.229, 0.224, 0.225]
# ---- AutoAugment (policy appresa su ImageNet) ----
train_auto = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.AutoAugment(
policy=transforms.AutoAugmentPolicy.IMAGENET, # o CIFAR10, SVHN
interpolation=transforms.InterpolationMode.BILINEAR
),
transforms.ToTensor(),
transforms.Normalize(mean=MEAN, std=STD)
])
# ---- RandAugment (N=2, M=9 sono i valori ottimali tipici) ----
train_rand = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.RandAugment(num_ops=2, magnitude=9),
transforms.ToTensor(),
transforms.Normalize(mean=MEAN, std=STD)
])
# ---- TrivialAugment: ancora più semplice, spesso meglio ----
# Seleziona 1 operazione casuale con magnitude casuale uniforme
train_trivial = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.TrivialAugmentWide(), # PyTorch 1.13+
transforms.ToTensor(),
transforms.Normalize(mean=MEAN, std=STD),
transforms.RandomErasing(p=0.1) # Aggiunge cutout
])
# ---- Augmentation Mix Strategy (raccomandato) ----
# TrivialAugment + MixUp/CutMix = la combinazione con il miglior rapporto
# semplicità/performance su quasi tutti i task di classificazione
# Non usare AutoAugment su dataset diversi da quello per cui e stata appresa
# (la policy di ImageNet non e ottimale per CIFAR-10 o per dataset medici)
3.3 Doba testu augmentace (TTA)
Il Rozšíření testovací doby (TTA) aplikujte transformace také v inferenčním čase, provádění více předpovědí na rozšířeném obrázku a agregace výsledků. Zvyšuje robustnost bez nutnosti přeškolování za cenu N-násobku inferenční doby.
import torch
import torch.nn.functional as F
import torchvision.transforms.functional as TF
@torch.no_grad()
def tta_predict(model: torch.nn.Module,
image: torch.Tensor, # [1, C, H, W]
n_augmentations: int = 5) -> torch.Tensor:
"""
Test-Time Augmentation: mediatura di predizioni su immagini aumentate.
Trucchi pratici:
- TTA di flip orizzontale e crop centrale sono le più affidabili
- Evitare TTA con rotazioni forti (possono degradare le performance)
- n=5 e un buon compromesso velocità/performance
"""
device = image.device
model.eval()
predictions = []
# 1. Immagine originale
pred = F.softmax(model(image), dim=1)
predictions.append(pred)
# 2. Flip orizzontale
flipped = TF.hflip(image)
pred = F.softmax(model(flipped), dim=1)
predictions.append(pred)
# 3. Flip verticale (solo se semanticamente valido)
# vflipped = TF.vflip(image)
# predictions.append(F.softmax(model(vflipped), dim=1))
# 4-N. Crop random dall'immagine scalata al 90%
_, C, H, W = image.shape
scale = 0.9
for _ in range(n_augmentations - 2):
# Scala lievemente
new_h, new_w = int(H * scale), int(W * scale)
resized = TF.resize(image, (new_h, new_w))
# Crop casuale alla dimensione originale
top = torch.randint(0, H - new_h + 1, (1,)).item()
left = torch.randint(0, W - new_w + 1, (1,)).item()
cropped = TF.crop(resized, top, left, new_h, new_w)
cropped = TF.resize(cropped, (H, W)) # rimetti a dimensione originale
pred = F.softmax(model(cropped), dim=1)
predictions.append(pred)
# Media delle probabilità (ensemble di N predizioni)
mean_pred = torch.stack(predictions).mean(dim=0)
return mean_pred.argmax(dim=1) # classe predetta
4. Rozšíření dat specifických pro doménu
4.1 Lékař: Zachování klinické sémantiky
Lékařské snímky vyžadují velmi konzervativní augmentační strategie. Kanály barva nést klinickou informaci (např. H&E barvení v histologii), orientaci anatomické a relevantní (vertikální převrácení rentgenového snímku hrudníku je anatomicky neplatné) a šumové charakteristiky závisí na typu skeneru. Vždy se poraďte s odborníkem domény před definováním potrubí.
import albumentations as A
from albumentations.pytorch import ToTensorV2
def get_medical_transforms(img_size: int = 512, modality: str = 'xray'):
"""
Pipeline augmentation per immagini mediche.
Diversa per modalità: radiografia, ecografia, risonanza, istologia.
"""
common = [
A.Resize(img_size, img_size),
]
if modality == 'xray':
augmentations = [
# Flip solo orizzontale (anatomicamente valido per torace)
A.HorizontalFlip(p=0.5),
# Rotazione lieve (paziente non sempre perfettamente allineato)
A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05,
rotate_limit=10, p=0.5),
# CLAHE migliora contrasto su radiografie
A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=0.5),
# Rumore realistico del sensore
A.GaussNoise(var_limit=(5, 25), p=0.4),
# Variazione contrasto lieve
A.RandomGamma(gamma_limit=(80, 120), p=0.5),
# NON usare: flip verticale, color jitter, hue shift
]
elif modality == 'histology':
augmentations = [
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.5), # OK per istologia (no orientamento fisso)
A.RandomRotate90(p=0.5),
# Variazione colore importante per istologia (diversi laboratori/staining)
A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20,
val_shift_limit=20, p=0.7),
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.7),
A.ElasticTransform(alpha=120, sigma=120 * 0.05,
alpha_affine=120 * 0.03, p=0.3),
]
elif modality == 'mri':
augmentations = [
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1,
rotate_limit=15, p=0.5),
# MRI: variazioni di intensità tra scanner diversi
A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.7),
# Simula artifact di movimento MRI
A.GaussianBlur(blur_limit=3, p=0.3),
# Elastic deformation anatomica realistica
A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, p=0.2),
]
else:
augmentations = []
norm = [
A.Normalize(mean=[0.5], std=[0.5]), # Grayscale normalization
# Per immagini RGB: A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]
return A.Compose(common + augmentations + norm + [ToTensorV2()])
4.2 Průmysl: Odolnost vůči akvizici Variace
import albumentations as A
from albumentations.pytorch import ToTensorV2
def get_industrial_transforms(img_size: int = 256, is_train: bool = True):
"""
Pipeline per ispezione visiva industriale.
Obiettivo: robustezza a variazioni di illuminazione, rotazione parziale,
rumore del sensore e piccole deformazioni meccaniche.
"""
if is_train:
return A.Compose([
A.RandomResizedCrop(img_size, img_size, scale=(0.8, 1.0)),
# Rotazione: prodotti su nastro trasportatore hanno orientamenti variabili
A.RandomRotate90(p=0.5),
A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1,
rotate_limit=30, border_mode=0, p=0.7),
# Illuminazione: variazioni da illuminazione industriale (LED, fluorescente)
A.OneOf([
A.RandomBrightnessContrast(brightness_limit=0.4, contrast_limit=0.4),
A.CLAHE(clip_limit=4.0),
A.RandomGamma(gamma_limit=(70, 130))
], p=0.8),
# Sensore: rumore e blur da sistemi di acquisizione industriali
A.OneOf([
A.GaussNoise(var_limit=(10, 60)),
A.ISONoise(color_shift=(0.01, 0.05), intensity=(0.1, 0.5)),
A.MultiplicativeNoise(multiplier=[0.9, 1.1]),
], p=0.4),
A.OneOf([
A.GaussianBlur(blur_limit=(3, 5)),
A.Defocus(radius=(1, 3)),
A.MotionBlur(blur_limit=5)
], p=0.3),
# Artefatti da riflessione/ombra
A.RandomShadow(shadow_roi=(0, 0, 1, 1), p=0.2),
A.Downscale(scale_min=0.7, scale_max=0.9, p=0.2), # simula bassa risoluzione
# CoarseDropout simula occlusione parziale del pezzo
A.CoarseDropout(max_holes=3, max_height=32, max_width=32, p=0.2),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
else:
return A.Compose([
A.Resize(img_size, img_size),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
4.3 Satelitní a dálkový průzkum Země
Satelitní snímky mají jedinečné vlastnosti: rotačně invariantní orientaci (č pevné "nahoru" a "dolů"), více spektrálních pásem (NIR, SWIR kromě RGB) a rozlišení velmi variabilní prostor mezi různými senzory.
import albumentations as A
import numpy as np
def get_satellite_transforms(img_size: int = 256, n_bands: int = 4,
is_train: bool = True):
"""
Pipeline per immagini satellitari multi-banda.
n_bands: numero di bande spettrali (es. 3=RGB, 4=RGBI con NIR, 13=Sentinel-2)
"""
if is_train:
return A.Compose([
A.RandomCrop(img_size, img_size, p=1.0),
# Rotazione: immagini satellitari non hanno orientamento fisso
A.D4(p=1.0), # tutte le 8 simmetrie del quadrato (rotazioni 0/90/180/270 + flip)
# Shift/scale lieve per variazione di zoom/scala
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.15,
rotate_limit=45, border_mode=4, p=0.5),
# Variazioni radiometriche tra diverse scene e stagioni
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.6),
A.RandomGamma(gamma_limit=(80, 120), p=0.5),
# Rumore da sensore satellitare
A.GaussNoise(var_limit=(5, 30), p=0.3),
# Blur da risoluzione atmosferica
A.GaussianBlur(blur_limit=(3, 5), p=0.2),
# CloudDropout: simula copertura nuvolosa parziale
# (custom transform: riempie regioni con valori "cloud")
A.CoarseDropout(
max_holes=5, max_height=64, max_width=64,
fill_value=255, # bianco = nuvola
p=0.2
),
# Normalizzazione per multibanda (media/std per banda)
# Usa valori specifici del sensore, qui esempio per Sentinel-2 4 bande
A.Normalize(
mean=[0.485, 0.456, 0.406, 0.4], # 4 bande: R, G, B, NIR
std=[0.229, 0.224, 0.225, 0.22]
),
])
else:
return A.Compose([
A.CenterCrop(img_size, img_size),
A.Normalize(
mean=[0.485, 0.456, 0.406, 0.4],
std=[0.229, 0.224, 0.225, 0.22]
),
])
5. Ablační studie: Jak měřit účinnost augmentace
Než svůj kanál zkomplikujete pokročilými transformacemi, měřte systematicky dopad každého z nich. Ablační studie o augmentaci: stejná architektura, stejný plánovač, stejné tréninkové hyperparametry - mění se pouze augmentace.
import torch
import albumentations as A
from albumentations.pytorch import ToTensorV2
from torchvision import datasets
import pandas as pd
def run_augmentation_ablation(
model_class,
dataset_path: str,
augmentation_configs: dict, # nome -> A.Compose
n_epochs: int = 30,
device: str = 'cuda'
) -> pd.DataFrame:
"""
Esegue ablation study sistematico su diverse configurazioni di augmentation.
Confronta tutte le configurazioni in condizioni identiche.
augmentation_configs = {
'baseline': A.Compose([A.Resize(224,224), A.Normalize(...), ToTensorV2()]),
'flip+crop': A.Compose([A.RandomResizedCrop(224,224), A.HorizontalFlip(0.5), ...]),
'flip+crop+col': A.Compose([..., A.ColorJitter(0.3,0.3,0.3), ...]),
'full_pipeline': get_classification_transforms(is_train=True),
}
"""
results = []
for config_name, transform in augmentation_configs.items():
print(f"\n=== Ablation: {config_name} ===")
# Dataset con questa specifica augmentation
train_dataset = datasets.ImageFolder(
f"{dataset_path}/train",
transform=lambda img: transform(image=np.array(img))['image']
)
val_dataset = datasets.ImageFolder(f"{dataset_path}/val",
transform=get_val_transform(224))
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=64, shuffle=True,
num_workers=4, pin_memory=True
)
val_loader = torch.utils.data.DataLoader(
val_dataset, batch_size=128, shuffle=False, num_workers=4
)
# Modello fresco per ogni configurazione (stessi pesi iniziali!)
torch.manual_seed(42)
model = model_class(num_classes=len(train_dataset.classes)).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=n_epochs)
criterion = torch.nn.CrossEntropyLoss()
best_val_acc = 0.0
for epoch in range(n_epochs):
# Training
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
loss = criterion(model(images), labels)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
scheduler.step()
# Validation
model.eval()
correct = total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
preds = model(images).argmax(1)
correct += preds.eq(labels).sum().item()
total += labels.size(0)
val_acc = 100.0 * correct / total
best_val_acc = max(best_val_acc, val_acc)
results.append({
'config': config_name,
'best_val_acc': round(best_val_acc, 2)
})
print(f"Best val accuracy: {best_val_acc:.2f}%")
df = pd.DataFrame(results).sort_values('best_val_acc', ascending=False)
print("\n=== Risultati Ablation Study ===")
print(df.to_string(index=False))
return df
Vliv rozšíření dat na CIFAR-10 (ResNet-18)
| Konfigurace augmentace | Val Přesnost | Delta |
|---|---|---|
| Bez augmentace | 84,3 % | - |
| Flip + náhodné oříznutí | 91,8 % | +7,5 % |
| + Jitter barev | 93,2 % | +1,4 % |
| + Výřez/hrubé vysazení | 94,1 % | +0,9 % |
| + MixUp (alfa=0,2) | 95,3 % | +1,2 % |
| + CutMix (alfa=1,0) | 95,8 % | +0,5 % |
| AutoAugment (zásady CIFAR-10) | 97,1 % | +1,3 % |
| TrivialAugment + MixUp | 97,4 % | +0,3 % |
6. Běžné chyby a osvědčené postupy
Časté chyby při augmentaci
- Rozšíření v ověřovací sadě: NIKDY nepoužívejte náhodné rozšíření ve validační nebo testovací sadě. Používejte pouze deterministické operace (změna velikosti, normalizace). Sada val slouží k měření skutečného výkonu.
- Ignorovat doménu: Nepoužívejte Color Jitter pro obrázky ve stupních šedi. U textových obrázků nepoužívejte vertikální převrácení. Nepoužívejte otočení o 90 stupňů pro přirozené scény s horizontem.
- Příliš mnoho augmentace: Potrubí s 20 transformacemi není nutně lepší než potrubí s 5 dobře vybranými. Augmentační overfitting je skutečný: model se může naučit, že rozšířené obrázky se liší od skutečných.
- Zapomeňte na synchronizaci: Pro detekci a segmentaci MUSÍ být geometrické transformace aplikovány identicky na obrázek a anotace. Albumentations to dělá automaticky - torchvision.transforms ne.
- Aplikujte CutMix/MixUp bez správných ztrát: U měkkých štítků (směs tříd) by se standardní CrossEntropyLoss měla vypočítat jako vážený průměr. Nepoužívejte argmax na štítcích k výpočtu ztráty.
- Neověřujte vizuálně: Před tréninkem si prohlédněte 20-30 rozšířených obrázků. Pokud se i lidskému oku zdají „divné“, pravděpodobně jsou příliš agresivní.
- Ignorování velikosti dávky při augmentaci: MixUp/CutMix vyžaduje velikost dávky >= 2. S velikostí dávky = 1 tyto metody nemají smysl.
Nejlepší postupy pro optimální augmentační potrubí
- Začněte jednoduše, postupně přidávejte složitost: Začněte s Flip + RandomCrop. Přidejte chvění barev. Poté MixUp. Změřte dopad na každém kroku pomocí ablační studie.
- Použijte správné num_workers: K augmentaci dochází u pracovníků CPU. S num_workers=4 a pin_memory=True není předzpracování nikdy zúženo ani u složitých kanálů.
- Profil před optimalizací: Použijte torch.profiler nebo time.perf_counter k měření skutečného času dataloaderu. Pokud je pod 10 % tréninkového kroku, není augmentace úzkým hrdlem.
- Přetrvávat augmentace pro velké datové sady: Pro velmi velké datové sady (více než 100 000 obrázků) předem vygenerujte rozšířené verze offline a uložte je na disk. To snižuje výpočet v reálném čase, ale zvyšuje úložiště.
- Rozšíření osnov: Během tréninku postupně zvyšujte velikost augmentace. Začíná to lehkou augmentací a v pozdějších epochách se stává agresivnější.
Závěry
Rozšiřování dat je jedním z nejvýkonnějších a nejlevnějších nástrojů pro vylepšování modelů počítačového vidění. Se správnými technikami je možné dosáhnout zvýšení přesnosti 5–15 % bez sběru jediného dalšího kusu dat. V tomto článku jsme viděli:
- Základní princip: platná augmentace = sémantická a úkolově invariantní transformace
- Albumentace jako referenční knihovna s nativní podporou detekce a segmentace se synchronizovaným bboxem/maskou
- MixUp, CutMix a Mosaic: pokročilé techniky pro zvýšení přesnosti o 2–5 % díky interpolaci mezi příklady
- AutoAugment, RandAugment a TrivialAugment: automatické vyhledávání optimální politiky
- Rozšíření doby testu: Zlepšení inference bez přeškolování
- Doménově specifické rozšíření pro lékařské, průmyslové a satelitní aplikace
- Jak strukturovat ablační studii k měření skutečného dopadu každé transformace







