Powiększanie danych w zakresie widzenia komputerowego: techniki i najlepsze praktyki
Jednym z najczęstszych problemów związanych z widzeniem komputerowym jest nadmierne dopasowanie: model uczy się na pamięć zestaw szkoleniowy zamiast uogólniać. Najbardziej skutecznym rozwiązaniem jest powiększanie danych: stosowanie losowych przekształceń do obrazów podczas treningu w celu sztucznego powiększania różnorodność danych i nauczenie niezmienności modelu wobec przekształceń niezwiązanych z zadaniem.
Dobrze zaprojektowana strategia powiększania może wystarczyć na podwojenie zbioru danych. Strategia źle może obniżyć wydajność. W tym artykule przyjrzymy się podstawowym technikom Albumentacje (najpotężniejsza biblioteka) e Torchvision.transformuje, zaawansowane techniki takie jak MixUp i CutMix oraz jak wybrać odpowiednie wzmocnienie dla każdej domeny. Omówimy także sposób wdrażania niestandardowych potoków, mierząc rzeczywisty wpływ każdego z nich transformację i wdrożenie w środowisku produkcyjnym przy minimalnym nakładzie obliczeniowym.
Czego się nauczysz
- dlaczego powiększanie danych działa: zasada niezmienności
- Albumentacje a widzenie torchowe: kiedy czego używać
- Techniki geometryczne: odwracanie, obrót, kadrowanie, transformacja perspektywy
- Techniki fotometryczne: jasność, kontrast, drgania kolorów, CLAHE
- Zaawansowane techniki: MixUp, CutMix, Mosaic, GridDistortion
- AutoAugment i RandAugment: Automatyczne wyszukiwanie zasad
- Augmentacja do wykrywania i segmentacji (ze współrzędnymi i maskami)
- Rozszerzenie specyficzne dla domeny: medyczne, przemysłowe, satelitarne
- Jak zmierzyć skuteczność augmentacji za pomocą badania ablacyjnego
- Potok zoptymalizowany pod kątem szybkiego szkolenia i minimalnych kosztów ogólnych
1. dlaczego powiększanie danych działa
Powiększanie danych opiera się na podstawowej zasadzie: transformacjach, które stosujemy nie mogą zmieniać znaczenia semantycznego obrazu (poprawnego wyjścia modelu), ale muszą zmienić piksele, aby model nie mógł po prostu przechowywać wzorów powierzchowne.
Z punktu widzenia teorii uczenia się, powiększanie danych jest formą: ukryta regularyzacja: rozszerza przestrzeń przekształceń pod względem względem których chcemy, aby model był niezmienniczy. Jeśli ćwiczymy z przewrotami poziomymi, model dowiaduje się, że lewa i prawa strona kota nie mają na to wpływu klasyfikacja. Jeśli trenujemy ze zmianami jasności, model się tego uczy ignorować warunki oświetleniowe.
# 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 Rodzaje niezmienności, których należy się nauczyć
Nie wszystkie aplikacje widzenia komputerowego wymagają tych samych niezmienności. Przed zbuduj rurociąg wzmacniający, zadajmy sobie pytanie: od tego, czego chcemy model jest solidny?
Mapa transformacji niezmienności dla domeny
| Domena | Przydatne niezmienniki | NIEBEZPIECZNE niezmienność |
|---|---|---|
| Naturalne zdjęcia | Odwróć H, przytnij, jasność, kolor | Odwróć V, obrót o 90 stopni |
| Tekst/OCR | Jasność, niewielki hałas | Obrót, odwrócenie, zniekształcenie |
| Ruch/sygnały | Jasność, rozmycie, kadrowanie | Odwróć V, obrót o 90 stopni |
| RTG (klatki piersiowej) | Odwróć H, lekki obrót, kontrast | Odwróć V, zmiana koloru, silny obrót |
| Histologia | Odwróć H/V, obrót o 90, niewielka zmiana koloru | Silny elastyczny obrót, ekstremalna skala |
| Inspekcja przemysłowa | Obrót o 360 stopni, jasność, rozmycie, szum | Bardzo ekstremalne skale (utraty szczegółów defektów) |
| Satelita | Obrót 90/180, przerzucanie H/V | Silna zmiana koloru (pasmo widmowe) |
2. Albumentacje: Biblioteka podręczna
Albumentacje to najpotężniejsza i najbardziej elastyczna biblioteka do powiększania danych wizja komputerowa. W przeciwieństwie do torchvision.transforms natywnie obsługuje:
- Obrazy + maski segmentacyjne (synchroniczne transformacje geometryczne)
- Obrazy + ramki ograniczające (współrzędne aktualizowane automatycznie)
- Obrazy + kluczowe punkty (kluczowe punkty pozostają spójne)
- Potoki zoptymalizowane za pomocą OpenCV (szybciej niż PIL o 15-40%)
- Ponad 70 transformacji dostępnych od razu po wyjęciu z pudełka
Jego architektura potokowa jest komponowalna: każda transformacja ma prawdopodobieństwo
p do zastosowania oraz przekształcenia takie jak A.OneOf pozwolić
aby pobrać próbkę ze zbioru alternatywnych transformacji. Tworzy to przestrzeń
wykładniczo duże rozszerzenie za pomocą zaledwie kilku linii kodu.
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 Albumentacje a transformacje torchvision: kiedy używać których
Porównanie albumentów i torchvision.transforms
| Charakterystyczny | Albumentacje | Torchvision.transformuje |
|---|---|---|
| obsługa bbox/maski | Natywny i automatyczny | Tylko obrazy (bez multimediów) |
| Liczba przekształceń | Ponad 70 transformacji | ~30 transformacji |
| Prędkość | Bardzo szybki (backend OpenCV) | Wolniejsze (zaplecza PKB) |
| Integracja z PyTorchem | Wymagany ToTensorV2 | Rodzinny |
| Automatyczne powiększanie/RandAugment | Realizacja niestandardowa | Natywny w torchvision 0.12+ |
| Zalecane użycie | Wykrywanie, segmentacja, niestandardowe potoki | Prosta klasyfikacja, AutoAugment |
3. Zaawansowane techniki powiększania
3.1 MixUp: Interpolacja pomiędzy obrazami
Mieszanie (Zhang i in., 2018) łączy dwa obrazy i odpowiadające im etykiety współczynnik lambda pobrany z rozkładu Beta. Zmuś model do zachowania liniowo pomiędzy klasami i znacząco zmniejsza pewność przewidywań, poprawa kalibracji i solidności. Stratę należy obliczyć jako średnią ważenie dwóch oddzielnych 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 i TrivialAugment
Automatyczne powiększanie (Cubuk i in., 2019) do wyszukiwania wykorzystuje uczenie się przez wzmacnianie automatycznie optymalną politykę powiększania zbioru danych. Problem: drogie badania (5000 godzin GPU na CIFAR-10). RandAugment uprość: zastosuj N operacji losowo wybrany z ustalonej listy o jednolitej wielkości M, z tylko 2 hiperparametrami do dostrojenia. Trywialne wzmocnienie idzie jeszcze dalej: często 1 losowa operacja o losowej wielkości przewyższa bardziej złożone 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 Czas testu wzmocnienia (TTA)
Il Wydłużenie czasu testu (TTA) zastosować transformacje także w czasie wnioskowania, wykonywanie wielu przewidywań na powiększonym obrazie i agregowanie wyników. Zwiększa niezawodność bez konieczności ponownego szkolenia, kosztem N-krotności czasu wnioskowania.
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. Rozszerzanie danych specyficznych dla domeny
4.1 Lekarz: zachowanie semantyki klinicznej
Obrazy medyczne wymagają bardzo konserwatywnych strategii powiększania. Kanały kolor niesie informację kliniczną (np. barwienie H&E w histologii), orientację anatomiczne i istotne (pionowe odwrócenie zdjęcia rentgenowskiego klatki piersiowej jest anatomicznie nieprawidłowe) a charakterystyka szumu zależy od typu skanera. Zawsze skonsultuj się ze specjalistą domeny przed zdefiniowaniem potoku.
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 Przemysł: odporność na wahania w zakresie nabycia
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 Teledetekcja i teledetekcja
Obrazy satelitarne mają unikalne właściwości: orientację niezmienną obrotowo (nr stałe „w górę” i „w dół”), wiele pasm widmowych (NIR, SWIR oprócz RGB) i rozdzielczość bardzo zmienna przestrzeń pomiędzy różnymi czujnikami.
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. Badanie ablacji: jak mierzyć skuteczność augmentacji
Zanim skomplikujesz potok za pomocą zaawansowanych transformacji, wykonuj systematyczne pomiary wpływ każdego. Badanie ablacyjne dotyczące augmentacji: ta sama architektura, ten sam harmonogram, te same hiperparametry treningowe - zmienia się tylko wzmocnienie.
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
Wpływ powiększania danych na CIFAR-10 (ResNet-18)
| Konfiguracja powiększania | Dokładność Val | Delta |
|---|---|---|
| Żadnego augmentacji | 84,3% | - |
| Odwróć + losowe przycięcie | 91,8% | +7,5% |
| + Drgania kolorów | 93,2% | +1,4% |
| + Wycięcie/grube wycięcie | 94,1% | +0,9% |
| + Mieszanie (alfa=0,2) | 95,3% | +1,2% |
| + CutMix (alfa=1,0) | 95,8% | +0,5% |
| AutoAugment (polityka CIFAR-10) | 97,1% | +1,3% |
| TrivialAugment + MixUp | 97,4% | +0,3% |
6. Typowe błędy i najlepsze praktyki
Najczęstsze błędy w augmentacji
- Rozszerzenie w zestawie walidacyjnym: NIGDY nie stosuj losowego wzmacniania w zestawie walidacyjnym lub testowym. Używaj tylko operacji deterministycznych (zmiana rozmiaru, normalizacja). Zestaw val służy do pomiaru rzeczywistej wydajności.
- Ignoruj domenę: Nie używaj opcji Color Jitter w przypadku obrazów w skali szarości. Nie używaj odwracania w pionie w przypadku obrazów tekstowych. Nie używaj obrotu o 90 stopni w przypadku naturalnych scen z horyzontem.
- Za dużo augmentacji: Potok z 20 transformacjami niekoniecznie jest lepszy niż ten z 5 dobrze wybranymi. Nadmierne dopasowanie augmentacyjne jest faktem: model może dowiedzieć się, że rozszerzone obrazy różnią się od rzeczywistych.
- Zapomnij o synchronizacji: W celu wykrycia i segmentacji transformacje geometryczne MUSZĄ zostać zastosowane identycznie do obrazu i adnotacji. Albumentations robi to automatycznie – torchvision.transforms nie.
- Zastosuj CutMix/MixUp bez odpowiednich strat: W przypadku miękkich etykiet (mieszanka klas) standardową stratę CrossEntropyLoss należy obliczyć jako średnią ważoną. Nie używaj argmax na etykietach do obliczania straty.
- Nie sprawdzaj wizualnie: Przed treningiem obejrzyj 20–30 powiększonych obrazów. Jeśli nawet dla ludzkiego oka wydają się „dziwne”, prawdopodobnie są zbyt agresywne.
- Ignorowanie rozmiaru partii podczas powiększania: MixUp/CutMix wymaga wielkości partii >= 2. Przy wielkości partii = 1 metody te nie mają znaczenia.
Najlepsze praktyki dotyczące optymalnych rurociągów wspomagających
- Zacznij od prostoty, stopniowo dodawaj złożoność: Zacznij od Flip + RandomCrop. Dodaj drgania koloru. Następnie MixUp. Mierz wpływ na każdym kroku za pomocą badania ablacji.
- Użyj odpowiednich num_workers: Wzmocnienie występuje u pracowników procesora. Dzięki num_workers=4 i pin_memory=True przetwarzanie wstępne nigdy nie jest wąskim gardłem, nawet w przypadku złożonych potoków.
- Profil przed optymalizacją: Użyj torch.profiler lub time.perf_counter, aby zmierzyć rzeczywisty czas modułu ładowania danych. Jeśli jest to mniej niż 10% etapu szkoleniowego, wzmocnienie nie jest wąskim gardłem.
- Trwałe rozszerzenia dla dużych zbiorów danych: W przypadku bardzo dużych zbiorów danych (ponad 100 000 obrazów) wstępnie wygeneruj rozszerzone wersje w trybie offline i zapisz je na dysku. Zmniejsza to moc obliczeniową w czasie rzeczywistym, ale zwiększa ilość miejsca na dane.
- Rozszerzenie programu nauczania: Stopniowo zwiększaj wielkość wzmocnienia podczas treningu. Zaczyna się od lekkiego wzmocnienia i staje się bardziej agresywny w późniejszych epokach.
Wnioski
Powiększanie danych jest jednym z najpotężniejszych i najtańszych narzędzi ulepszania modeli widzenia komputerowego. Przy zastosowaniu odpowiednich technik możliwe jest osiągnięcie wzrostu dokładności 5-15% bez gromadzenia ani jednej dodatkowej porcji danych. W tym artykule widzieliśmy:
- Podstawowa zasada: ważne rozszerzenie = transformacja zachowująca semantykę i niezmienna zadaniowa
- Albumentacje jako biblioteka referencyjna z natywną obsługą wykrywania i segmentacji za pomocą zsynchronizowanego bbox/maski
- MixUp, CutMix i Mosaic: zaawansowane techniki zwiększające dokładność o 2–5% poprzez interpolację między przykładami
- AutoAugment, RandAugment i TrivialAugment: automatyczne wyszukiwanie optymalnej polisy
- Wydłużenie czasu testu: poprawa wnioskowania bez ponownego szkolenia
- Rozszerzenie specyficzne dla domeny dla zastosowań medycznych, przemysłowych i satelitarnych
- Jak ustrukturyzować badanie ablacyjne, aby zmierzyć rzeczywisty wpływ każdej transformacji







