컴퓨터 비전을 위한 데이터 증강: 기술 및 모범 사례
컴퓨터 비전의 가장 일반적인 문제 중 하나는 과적합(overfitting)입니다. 모델은 이를 암기합니다. 일반화 대신 훈련 세트. 가장 효과적인 해결책은 데이터 증대: 인위적으로 강화하기 위해 훈련 중 이미지에 무작위 변환 적용 데이터의 다양성을 높이고 작업과 관련 없는 변환에 대한 모델 불변성을 가르칩니다.
잘 설계된 증강 전략은 데이터세트를 두 배로 늘리는 것만큼이나 효과적일 수 있습니다. 전략 잘못하면 성능이 저하될 수 있습니다. 이 기사에서는 기본 기술을 살펴 보겠습니다. 앨범화 (가장 강력한 라이브러리) e 토치비전.변환, MixUp 및 CutMix와 같은 고급 기술과 각 도메인에 적합한 확장을 선택하는 방법. 또한 커스텀 파이프라인을 구현하는 방법과 각 파이프라인의 실제 영향을 측정하는 방법도 다룹니다. 최소한의 계산 오버헤드로 변환하고 프로덕션에 배포합니다.
무엇을 배울 것인가
- 데이터 증대가 작동하는 이유: 불변의 원리
- Albummentations vs torchvision: 언제 무엇을 사용해야 할까요?
- 기하학적 기술: 뒤집기, 회전, 자르기, 원근감 변환
- 측광 기술: 밝기, 대비, 색상 지터, CLAHE
- 고급 기술: MixUp, CutMix, 모자이크, GridDistortion
- AutoAugment 및 RandAugment: 자동 정책 검색
- 감지 및 분할을 위한 확대(좌표 및 마스크 사용)
- 도메인별 증강: 의료, 산업, 위성
- 절제 연구를 통해 확대 효과를 측정하는 방법
- 빠른 훈련과 최소한의 오버헤드에 최적화된 파이프라인
1. 데이터 확대가 작동하는 이유
데이터 증대는 기본 원칙, 즉 우리가 적용하는 변환을 기반으로 합니다. 이미지의 의미론적 의미(모델의 올바른 출력)를 변경해서는 안 됩니다. 하지만 모델이 패턴만 저장할 수 없도록 픽셀을 변경해야 합니다. 피상적.
학습 이론의 관점에서 볼 때, 데이터 증강은 다음과 같은 형태입니다. 암시적 정규화: 존중을 통해 변형의 공간을 확장합니다. 우리는 모델이 변하지 않기를 원합니다. 수평 뒤집기로 훈련하면 모델은 고양이의 왼쪽과 오른쪽이 영향을 미치지 않는다는 것을 학습합니다. 분류. 밝기의 변화를 가지고 훈련하면 모델은 다음을 학습합니다. 조명 조건을 무시하십시오.
# 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 학습해야 할 불변성의 유형
모든 컴퓨터 비전 애플리케이션에 동일한 불변성이 필요한 것은 아닙니다. 이전 증강 파이프라인을 구축하면서 스스로에게 질문해 봅시다. 우리가 원하는 것에서 모델이 견고합니까?
도메인에 대한 불변-변환 맵
| 도메인 | 유용한 불변성 | 위험한 불변성 |
|---|---|---|
| 자연스러운 사진 | H 뒤집기, 자르기, 밝기, 색상 | V 뒤집기, 90도 회전 |
| 텍스트/OCR | 밝기, 약간의 소음 | 회전, 뒤집기, 왜곡 |
| 교통/신호등 | 밝기, 흐림, 자르기 | V 뒤집기, 90도 회전 |
| 엑스레이(가슴) | H 뒤집기, 약간 회전, 대비 | V 뒤집기, 색상 이동, 강한 회전 |
| 조직학 | H/V 뒤집기, 90도 회전, 약간의 색상 변화 | 강력한 탄성회전, 극한의 스케일 |
| 산업검사 | 360도 회전, 밝기, 흐림, 노이즈 | 매우 극단적인 규모(손실 세부 결함) |
| 위성 | 90/180 회전, H/V 플립 | 강한 색상 변화(스펙트럼 밴드) |
2. 앨범 구성: 참고 자료실
앨범화 가장 강력하고 유연한 데이터 증대 라이브러리입니다. 컴퓨터 비전. torchvision.transforms와 달리 기본적으로 다음을 지원합니다.
- 이미지 + 분할 마스크(동기화된 기하학적 변환)
- 이미지 + 경계 상자(좌표가 자동으로 업데이트됨)
- 이미지 + 키포인트(키포인트는 일관되게 유지됨)
- OpenCV로 최적화된 파이프라인(PIL보다 15-40% 빠름)
- 즉시 사용 가능한 70개 이상의 변환
파이프라인 아키텍처는 구성 가능합니다. 각 변환에는 확률이 있습니다.
p 적용하고 다음과 같은 변환을 수행합니다. A.OneOf 허용하다
일련의 대체 변환에서 샘플링합니다. 이는 다음과 같은 공간을 생성합니다.
단 몇 줄의 코드만으로 기하급수적으로 큰 규모의 확장이 가능합니다.
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 앨범 구성과 torchvision.transforms 비교: 언제 어느 것을 사용해야 할까요?
앨범 구성과 torchvision.transforms 비교
| 특성 | 앨범화 | 토치비전.변환 |
|---|---|---|
| bbox/마스크 지원 | 기본 및 자동 | 이미지만(미디어 없음) |
| 변환 수 | 70개 이상의 변형 | ~30개 변형 |
| 속도 | 매우 빠름(OpenCV 백엔드) | 느림(백엔드 GDP) |
| 파이토치 통합 | ToTensorV2 필요 | 토종의 |
| 자동증강/랜드증강 | 맞춤 구현 | 토치비전 0.12+의 기본 |
| 권장 용도 | 감지, 세분화, 커스텀 파이프라인 | 단순 분류, AutoAugment |
3. 고급 증강 기술
3.1 MixUp: 이미지 간 보간
믹스업 (Zhang et al., 2018)은 두 개의 이미지와 각각의 라벨을 다음과 같이 혼합합니다. 베타 분포에서 샘플링된 람다 계수. 모델이 행동하도록 강제하기 클래스 간 선형적으로 예측에 대한 신뢰도가 크게 감소합니다. 교정 및 견고성을 향상시킵니다. 손실은 평균으로 계산되어야 합니다. 두 개의 개별 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 및 TrivialAugment
자동증강 (Cubuk et al., 2019) 강화 학습을 사용하여 검색 데이터세트에 대한 최적의 증강 정책을 자동으로 생성합니다. 문제: 비용이 많이 드는 연구 (CIFAR-10에서는 5000 GPU 시간) 랜드증강 단순화: N 연산 적용 균일한 크기 M을 갖는 고정 목록에서 무작위로 선택되며, 조정할 하이퍼파라미터는 2개뿐입니다. 사소한 증강 더 나아가: 무작위 크기를 갖는 1개의 무작위 연산, 종종 더 복잡한 방법을 능가합니다.
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 증강 테스트 시간(TTA)
Il 테스트 시간 확대(TTA) 추론 시에도 변환을 적용합니다. 증강 이미지에 대해 다중 예측을 수행하고 결과를 집계합니다. 추론 시간의 N배 비용으로 재교육 없이 견고성을 높입니다.
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. 도메인별 데이터 확대
4.1 의사: 임상 의미 보존
의료 영상에는 매우 보수적인 확대 전략이 필요합니다. 채널 색상은 임상 정보(예: 조직학의 H&E 염색), 방향을 전달합니다. 해부학적이며 관련성이 있음(흉부 엑스레이의 수직 뒤집기는 해부학적으로 유효하지 않음) 소음 특성은 스캐너 유형에 따라 다릅니다. 항상 전문가와 상담하세요 파이프라인을 정의하기 전에 도메인.
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 산업: 획득 변화에 대한 견고성
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 위성 및 원격 탐사
위성 이미지에는 회전 불변 방향(회전 불변 방향)이라는 고유한 속성이 있습니다. 고정된 "위" 및 "아래"), 다중 스펙트럼 대역(RGB 외에 NIR, SWIR) 및 해상도 서로 다른 센서 사이의 공간이 매우 다양합니다.
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. 절제 연구: 증강의 효과를 측정하는 방법
고급 변환으로 파이프라인을 복잡하게 만들기 전에 체계적으로 측정하세요. 각각의 영향. 증강에 대한 절제 연구: 동일한 아키텍처, 동일한 스케줄러, 동일한 교육 하이퍼파라미터 - 증강만 변경됩니다.
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
CIFAR-10(ResNet-18)에 대한 데이터 확대의 영향
| 증강 구성 | 발 정확도 | 델타 |
|---|---|---|
| 증강 없음 | 84.3% | - |
| 뒤집기 + 무작위 자르기 | 91.8% | +7.5% |
| + 색상 지터 | 93.2% | +1.4% |
| + 컷아웃/대략 드롭아웃 | 94.1% | +0.9% |
| + 믹스업(알파=0.2) | 95.3% | +1.2% |
| + CutMix(알파=1.0) | 95.8% | +0.5% |
| 자동 증강(CIFAR-10 정책) | 97.1% | +1.3% |
| TrivialAugment + MixUp | 97.4% | +0.3% |
6. 일반적인 실수와 모범 사례
증강의 일반적인 실수
- 검증 세트의 확대: 검증 또는 테스트 세트에 무작위 확대를 적용하지 마십시오. 결정적 작업(크기 조정, 정규화)만 사용하세요. val 세트는 실제 성능을 측정하는 데 사용됩니다.
- 도메인을 무시하십시오. 회색조 이미지에는 색상 지터를 사용하지 마십시오. 텍스트 이미지에는 수직 뒤집기를 사용하지 마세요. 수평선이 있는 자연스러운 장면에는 90도 회전을 사용하지 마십시오.
- 너무 많은 확장: 20개의 변환이 있는 파이프라인이 잘 선택된 5개의 변환이 있는 파이프라인보다 반드시 더 나은 것은 아닙니다. 증강 과적합은 실제입니다. 모델은 증강 이미지가 실제 이미지와 다르다는 것을 학습할 수 있습니다.
- 동기화는 잊어버리세요: 탐지 및 분할을 위해 기하학적 변환은 이미지와 주석에 동일하게 적용되어야 합니다. Albumentations는 이 작업을 자동으로 수행하지만 torchvision.transforms는 그렇지 않습니다.
- 올바른 손실 없이 CutMix/MixUp을 적용합니다. 소프트 레이블(클래스 혼합)을 사용하면 표준 CrossEntropyLoss를 가중 평균으로 계산해야 합니다. 손실을 계산하기 위해 레이블에 argmax를 사용하지 마세요.
- 시각적으로 검증하지 마세요: 훈련하기 전에 20~30개의 증강 이미지를 봅니다. 인간의 눈에도 "이상하게" 보인다면 아마도 너무 공격적일 것입니다.
- 확대 시 배치 크기 무시: MixUp/CutMix에는 배치 크기가 2보다 커야 합니다. 배치 크기가 1이면 이러한 방법은 의미가 없습니다.
최적의 증강 파이프라인을 위한 모범 사례
- 간단하게 시작하고 점차적으로 복잡성을 추가하세요. Flip + RandomCrop으로 시작하세요. 색상 지터를 추가합니다. 그럼 믹스업. 절제 연구의 모든 단계에서 영향을 측정합니다.
- 적절한 num_workers를 사용하십시오. 확장은 CPU 작업자에서 발생합니다. num_workers=4 및 pin_memory=True를 사용하면 복잡한 파이프라인에서도 전처리에 병목 현상이 발생하지 않습니다.
- 최적화 전 프로필: 실제 데이터로더 시간을 측정하려면 torch.profiler 또는 time.perf_counter를 사용하십시오. 훈련 단계의 10% 미만이면 증강으로 인해 병목 현상이 발생하지 않습니다.
- 대규모 데이터세트에 대한 지속적인 확장: 매우 큰 데이터세트(100,000개 이상의 이미지)의 경우 오프라인으로 증강 버전을 미리 생성하고 디스크에 저장하세요. 이렇게 하면 실시간 컴퓨팅이 줄어들지만 스토리지가 늘어납니다.
- 커리큘럼 확대: 훈련 중에 증강의 크기를 점진적으로 늘리십시오. 가벼운 강화로 시작하여 후기 시대에 더욱 공격적으로 변합니다.
결론
데이터 증대는 모델 개선을 위한 가장 강력하고 저렴한 도구 중 하나입니다. 컴퓨터 비전의. 올바른 기술을 사용하면 정확도를 높일 수 있습니다. 단일 추가 데이터 수집 없이 5~15%. 이 기사에서 우리는 다음을 보았습니다:
- 기본 원칙: 유효한 증대 = 의미 보존 및 작업 불변 변환
- 동기화된 bbox/마스크를 사용한 감지 및 분할을 기본적으로 지원하는 참조 라이브러리로서의 앨범 정리
- MixUp, CutMix 및 mosaic: 예제 간의 보간을 통해 2~5% 정확도 향상을 위한 고급 기술
- AutoAugment, RandAugment 및 TrivialAugment: 최적의 정책 자동 검색
- 테스트 시간 확대: 재교육 없이 추론 개선
- 의료, 산업 및 위성을 위한 도메인별 확장
- 각 변환의 실제 영향을 측정하기 위해 절제 연구를 구성하는 방법







