Augmentarea datelor pentru viziunea computerizată: tehnici și bune practici
Una dintre cele mai frecvente probleme în viziunea computerizată este supraadaptarea: modelul învață pe de rost set de antrenament în loc să generalizeze. Cea mai eficientă soluție este mărirea datelor: aplicarea de transformări aleatorii imaginilor în timpul antrenamentului pentru a crește artificial varietatea datelor și învață invarianța modelului la transformări irelevante pentru sarcină.
O strategie de augmentare bine concepută poate fi la fel de bună ca și dublarea setului de date. O strategie greșit poate degrada performanța. În acest articol vom vedea tehnicile fundamentale cu Albumentații (cea mai puternică bibliotecă) e torță viziune.transformă, tehnici avansate precum MixUp și CutMix și cum să alegeți mărirea potrivită pentru fiecare domeniu. Vom aborda, de asemenea, modul de implementare a conductelor personalizate, măsurând impactul real al fiecăruia transformare și implementare în producție cu cheltuieli de calcul minime.
Ce vei învăța
- de ce funcţionează mărirea datelor: principiul invarianţei
- Albumentații vs viziune cu torță: când să folosiți ce
- Tehnici geometrice: flip, rotation, crop, perspective transform
- Tehnici fotometrice: luminozitate, contrast, jitter de culoare, CLAHE
- Tehnici avansate: MixUp, CutMix, Mosaic, GridDistortion
- AutoAugment și RandAugment: căutare automată a politicilor
- Augmentare pentru detectare și segmentare (cu coordonate și măști)
- Augmentare specifică domeniului: medical, industrial, satelit
- Cum se măsoară eficiența creșterii cu studiul ablației
- Conductă optimizată pentru antrenament rapid și cheltuieli minime
1. de ce funcționează Data Augmentation
Augmentarea datelor se bazează pe un principiu fundamental: transformările pe care le aplicăm nu trebuie să modifice sensul semantic al imaginii (ieșirea corectă a modelului), dar trebuie să schimbe pixelii, astfel încât modelul să nu poată doar stoca modelele superficial.
Din perspectiva teoriei învățării, creșterea datelor este o formă de regularizare implicită: extinde spaţiul transformărilor cu respect faţă de care dorim ca modelul să fie invariant. Dacă ne antrenăm cu răsturnări orizontale, modelul învață că partea stângă și partea dreaptă a unei pisici nu afectează clasificarea. Dacă ne antrenăm cu variații de luminozitate, modelul învață ignora conditiile de iluminare.
# 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 Tipuri de invarianță de învățat
Nu toate aplicațiile de viziune computerizată necesită aceleași invarianțe. Înainte construim conducta de augmentare, haideți să ne întrebăm: din ceea ce ne dorim noastre modelul este robust?
Harta de transformare a invarianței pentru domeniu
| Domeniu | Invarianțe utile | Invarianțe PERICULOASE |
|---|---|---|
| Fotografii naturale | Întoarceți H, decupați, luminozitate, culoare | Flip V, rotație de 90 de grade |
| Text/OCR | Luminozitate, zgomot ușor | Rotire, răsturnare, distorsiune |
| Trafic/Semnale | Luminozitate, estompare, decupare | Flip V, rotație de 90 de grade |
| radiografie (piept) | Flip H, ușoară rotație, contrast | Flip V, schimbare de culoare, rotație puternică |
| Histologie | Flip H/V, rotație de 90, ușoară schimbare de culoare | Rotire elastică puternică, scară extremă |
| Inspecție industrială | Rotire 360, luminozitate, estompare, zgomot | Scale foarte extreme (pierderi defecte de detaliu) |
| Satelit | Rotire 90/180, flip H/V | Schimbare puternică de culoare (bandă spectrală) |
2. Albumentații: Biblioteca de referință
Albumentații este cea mai puternică și flexibilă bibliotecă de creștere a datelor pentru viziune computerizată. Spre deosebire de torchvision.transforms, acceptă în mod nativ:
- Imagini + măști de segmentare (transformări geometrice sincronizate)
- Imagini + casete de delimitare (coordonatele actualizate automat)
- Imagini + puncte cheie (punctele cheie păstrate consistente)
- Conducte optimizate cu OpenCV (mai rapid decât PIL cu 15-40%)
- Peste 70 de transformări disponibile imediat
Arhitectura conductei sale este componabilă: fiecare transformare are o probabilitate
p de aplicat, și transformări precum A.OneOf permite
pentru a eșantiona dintr-un set de transformări alternative. Acest lucru creează un spațiu de
mărire exponențială mare cu doar câteva linii de cod.
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 Albumentații vs torchvision.transforms: Când să folosiți Care
Comparație albumentații vs torchvision.transforms
| Caracteristică | Albumentații | torță viziune.transformă |
|---|---|---|
| suport bbox/mask | Nativ și automat | Numai imagini (fără media) |
| Numărul de transformări | 70+ transformări | ~30 de transformări |
| Viteză | Foarte rapid (backend OpenCV) | Mai lent (PIB de backend) |
| Integrarea PyTorch | Este necesar ToTensorV2 | Nativ |
| AutoAugment/RandAugment | Implementare personalizată | Nativ în torchvision 0.12+ |
| Utilizare recomandată | Detectare, segmentare, conducte personalizate | Clasificare simplă, AutoAugment |
3. Tehnici avansate de creștere
3.1 MixUp: Interpolare între imagini
MixUp (Zhang et al., 2018) amestecă două imagini și etichetele lor respective cu un coeficient lambda eșantionat dintr-o distribuție Beta. Forțați modelul să se comporte liniar între clase și reduce semnificativ încrederea în predicții, îmbunătățirea calibrării și robusteței. Pierderea trebuie calculată ca medie ponderarea a două CrossEntropyLoss separate.
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 și TrivialAugment
AutoAugment (Cubuk et al., 2019) folosește învățarea prin întărire pentru a căuta automat politica optimă de creștere a unui set de date. Problema: cercetarea costisitoare (5000 de ore GPU pe CIFAR-10). RandAugment simplifica: aplica N operatii ales aleatoriu dintr-o listă fixă cu magnitudine uniformă M, cu doar 2 hiperparametri de reglat. Trivial Augment merge și mai departe: 1 operație aleatoare cu magnitudine aleatorie, adesea depășește metodele mai complexe.
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 Augmentation Test-Time (TTA)
Il Augmentarea timpului de testare (TTA) aplicați transformările și în timpul inferenței, efectuarea de predicții multiple asupra imaginii augmentate și agregarea rezultatelor. Mărește robustețea fără a necesita recalificare, cu prețul de N ori timpul de inferență.
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. Augmentarea datelor specifice domeniului
4.1 Doctor: Păstrarea semanticii clinice
Imaginile medicale necesită strategii de augmentare foarte conservatoare. Canalele culoarea transportă informații clinice (de exemplu, colorarea H&E în histologie), orientare anatomic și relevant (întoarcerea verticală a unei radiografii toracice este invalidă din punct de vedere anatomic) iar caracteristicile zgomotului depind de tipul de scaner. Consultați întotdeauna un expert domeniu înainte de a defini conducta.
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 Industrial: variații de robustețe la achiziție
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 Sateliți și teledetecție
Imaginile din satelit au proprietăți unice: orientare invariantă la rotație (nr fix „sus” și „jos”), benzi spectrale multiple (NIR, SWIR în plus față de RGB) și rezoluție spațiu foarte variabil între diferiți senzori.
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. Studiu de ablație: Cum se măsoară eficacitatea creșterii
Înainte de a vă complica conducta cu transformări avansate, măsurați sistematic impactul fiecăruia. Un studiu de ablație asupra creșterii: aceeași arhitectură, același planificator, aceiași hiperparametri de antrenament - se schimbă doar augmentarea.
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
Impactul creșterii datelor asupra CIFAR-10 (ResNet-18)
| Configurație de creștere | Precizie Val | Delta |
|---|---|---|
| Fără mărire | 84,3% | - |
| Întoarceți + Decupare aleatorie | 91,8% | +7,5% |
| + Jitter de culoare | 93,2% | +1,4% |
| + Decupaj/CoarseDroout | 94,1% | +0,9% |
| + MixUp (alfa=0,2) | 95,3% | +1,2% |
| + CutMix (alfa=1,0) | 95,8% | +0,5% |
| AutoAugment (politica CIFAR-10) | 97,1% | +1,3% |
| TrivialAugment + MixUp | 97,4% | +0,3% |
6. Greșeli comune și cele mai bune practici
Greșeli frecvente în creștere
- Creștere în setul de validare: NU aplicați NICIODATĂ mărirea aleatorie în setul de validare sau de testare. Utilizați numai operații deterministe (redimensionare, normalizare). Val set este folosit pentru a măsura performanța reală.
- Ignora domeniul: Nu utilizați Color Jitter pentru imagini în tonuri de gri. Nu utilizați flip vertical pentru imagini text. Nu utilizați rotația de 90 de grade pentru scene naturale cu orizont.
- Prea multă mărire: O conductă cu 20 de transformări nu este neapărat mai bună decât una cu 5 bine alese. Supraadaptarea de creștere este reală: modelul poate afla că imaginile augmentate sunt diferite de cele reale.
- Uitați de sincronizare: Pentru detectare și segmentare, transformările geometrice TREBUIE să fie aplicate identic imaginii și adnotărilor. Albumentații face acest lucru automat - torchvision.transforms nu.
- Aplicați CutMix/MixUp fără pierderile corecte: Cu etichete soft (mix de clasă), CrossEntropyLoss standard ar trebui calculat ca o medie ponderată. Nu utilizați argmax pe etichete pentru a calcula pierderea.
- Nu validați vizual: Înainte de antrenament, vizualizați 20-30 de imagini augmentate. Dacă par „ciudați” chiar și pentru ochiul uman, probabil că sunt prea agresivi.
- Ignorarea dimensiunii lotului în creștere: MixUp/CutMix necesită dimensiunea lotului >= 2. Cu dimensiunea lotului = 1, aceste metode sunt lipsite de sens.
Cele mai bune practici pentru conducte de creștere optimă
- Începe simplu, adaugă complexitate treptat: Începeți cu Flip + RandomCrop. Adăugați Color Jitter. Apoi MixUp. Măsurați impactul la fiecare pas cu studiul de ablație.
- Utilizați num_workers adecvat: Augmentarea are loc la lucrătorii CPU. Cu num_workers=4 și pin_memory=True, preprocesarea nu este niciodată blocată chiar și cu conducte complexe.
- Profil înainte de optimizare: Utilizați torch.profiler sau time.perf_counter pentru a măsura timpul real de încărcare a datelor. Dacă este sub 10% din pasul de antrenament, creșterea nu este blocajul.
- Continuați creșterile pentru seturi mari de date: Pentru seturi de date foarte mari (mai mult de 100.000 de imagini), pre-generați versiuni augmentate offline și salvați-le pe disc. Acest lucru reduce calculul în timp real, dar crește stocarea.
- Mărirea curriculumului: Creșteți amploarea creșterii progresiv în timpul antrenamentului. Începe cu creșterea ușoară și devine mai agresivă în epocile ulterioare.
Concluzii
Mărirea datelor este unul dintre cele mai puternice și mai ieftine instrumente pentru îmbunătățirea modelelor a vederii computerizate. Cu tehnicile potrivite, este posibil să obțineți creșteri ale preciziei 5-15% fără a colecta o singură bucată suplimentară de date. În acest articol am văzut:
- Principiul fundamental: creșterea validă = transformarea semantică-conservare și sarcină-invariantă
- Albumentații ca bibliotecă de referință cu suport nativ pentru detectarea și segmentarea cu bbox/mask sincronizate
- MixUp, CutMix și Mosaic: tehnici avansate pentru câștiguri de precizie de 2-5% prin interpolare între exemple
- AutoAugment, RandAugment și TrivialAugment: căutare automată pentru politica optimă
- Test-Time Augmentation: Îmbunătățirea inferenței fără reinstruire
- Augmentare specifică domeniului pentru medical, industrial și satelit
- Cum să structurați un studiu de ablație pentru a măsura impactul real al fiecărei transformări







