コンピューター ビジョンのデータ拡張: テクニックとベスト プラクティス
コンピューター ビジョンで最も一般的な問題の 1 つは過学習です。モデルは暗記します。 一般化するのではなく、トレーニングセットを作成します。最も効果的な解決策は、 データ拡張: トレーニング中に画像にランダムな変換を適用して人為的に拡張する データの多様性を認識し、タスクに関係のない変換に対するモデルの不変性を教えます。
適切に設計された拡張戦略は、データセットを 2 倍にするのと同じくらい効果的です。戦略 間違っているとパフォーマンスが低下する可能性があります。この記事では、基本的なテクニックを見ていきます。 アルバムメンテーション (最も強力なライブラリ) e torchvision.transforms、 MixUp や CutMix などの高度なテクニック、および各ドメインに適切なオーグメンテーションを選択する方法。 また、カスタム パイプラインを実装し、それぞれの実際の影響を測定する方法についても説明します。 最小限の計算オーバーヘッドで変換して本番環境にデプロイします。
何を学ぶか
- データ拡張が機能する理由: 不変性の原理
- アルバムとトーチビジョン: いつ何を使用するか
- 幾何学的なテクニック: 反転、回転、クロップ、透視変換
- 測光技術: 明るさ、コントラスト、カラージッター、CLAHE
- 高度なテクニック: MixUp、CutMix、モザイク、グリッドディストーション
- 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 度回転 |
| X線(胸部) | 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 Albumentations と torchvision.transforms: いつどちらを使用するか
Albumentations と torchvision.transforms の比較
| 特性 | アルバムメンテーション | torchvision.transforms |
|---|---|---|
| bbox/マスクのサポート | ネイティブかつ自動 | 画像のみ (メディアなし) |
| 変換の数 | 70以上の変換 | ~30 の変換 |
| スピード | 非常に高速 (OpenCV バックエンド) | 遅い(バックエンドGDP) |
| PyTorch の統合 | ToTensorV2 が必要です | ネイティブ |
| オートオーグメント/ランドオーグメント | カスタム実装 | torchvision 0.12+ でネイティブ |
| 推奨用途 | 検出、セグメンテーション、カスタム パイプライン | 簡易分類、AutoAugment |
3. 高度な拡張技術
3.1 MixUp: 画像間の補間
ミックスアップ (Zhang et al., 2018) は 2 つの画像とそれぞれのラベルを混合します。 ベータ分布からサンプリングされたラムダ係数。モデルに動作を強制する クラス間で直線的に変化し、予測の信頼性が大幅に低下します。 キャリブレーションと堅牢性が向上します。損失は平均として計算する必要があります 2 つの別々の 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 つだけです。 TrivialAugment さらに詳しく言えば、ランダムな大きさで 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 染色など)、向きを伝えます。 解剖学的かつ関連性がある(胸部 X 線写真の垂直方向の反転は解剖学的に無効です) ノイズ特性はスキャナの種類によって異なります。必ず専門家に相談してください パイプラインを定義する前にドメインを指定します。
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% |
| + カットミックス (アルファ=1.0) | 95.8% | +0.5% |
| AutoAugment (CIFAR-10 ポリシー) | 97.1% | +1.3% |
| TrivialAugment + MixUp | 97.4% | +0.3% |
6. よくある間違いとベストプラクティス
オーグメンテーションにおけるよくある間違い
- 検証セットの拡張: 検証セットまたはテストセットにはランダムな拡張を決して適用しないでください。決定的な操作 (サイズ変更、正規化) のみを使用してください。 val set は、実際のパフォーマンスを測定するために使用されます。
- ドメインを無視します。 グレースケール画像にはカラージッターを使用しないでください。テキスト画像には垂直反転を使用しないでください。地平線のある自然なシーンには 90 度回転を使用しないでください。
- 拡張が多すぎる: 20 の変換を含むパイプラインが、適切に選択された 5 つの変換を含むパイプラインより必ずしも優れているとは限りません。拡張の過学習は現実のものです。モデルは、拡張された画像が実際の画像とは異なることを学習できます。
- 同期のことは忘れてください: 検出とセグメンテーションでは、幾何学的変換を画像と注釈に同様に適用しなければなりません。 Albumentations はこれを自動的に行いますが、torchvision.transforms は行いません。
- 正しい損失を発生させずに CutMix/MixUp を適用します。 ソフト ラベル (クラス ミックス) の場合、標準 CrossEntropyLoss は加重平均として計算される必要があります。損失を計算するためにラベルに argmax を使用しないでください。
- 視覚的に検証しないでください。 トレーニングの前に、20 ~ 30 枚の拡張画像を表示します。人間の目にも「奇妙」に見える場合は、おそらく攻撃的すぎるでしょう。
- 拡張でのバッチ サイズの無視: MixUp/CutMix にはバッチ サイズ >= 2 が必要です。バッチ サイズ = 1 では、これらのメソッドは無意味です。
最適な拡張パイプラインのベスト プラクティス
- シンプルに始めて、徐々に複雑さを加えていきます。 まずは「フリップ + ランダムクロップ」から始めます。カラージッターを追加します。次にミックスアップ。アブレーションスタディであらゆる段階での影響を測定します。
- 適切な num_workers を使用します。 拡張は CPU ワーカーで発生します。 num_workers=4 および pin_memory=True を使用すると、複雑なパイプラインでも前処理がボトルネックになることはありません。
- 最適化前のプロファイル: torch.profiler または time.perf_counter を使用して、実際のデータローダー時間を測定します。トレーニング ステップの 10% 未満であれば、拡張はボトルネックではありません。
- 大規模なデータセットの拡張を維持します。 非常に大規模なデータセット (100,000 以上の画像) の場合は、拡張バージョンをオフラインで事前生成し、ディスクに保存します。これにより、リアルタイム コンピューティングは削減されますが、ストレージは増加します。
- カリキュラムの強化: トレーニング中に増強の大きさを段階的に増加させます。それは軽い増強から始まり、後の時代ではより攻撃的になります。
結論
データ拡張は、モデルを改善するための最も強力で低コストのツールの 1 つです コンピュータービジョンのこと。適切な技術を使用すれば、精度を向上させることが可能です 追加のデータを 1 つも収集せずに 5 ~ 15%。この記事では次のことがわかりました。
- 基本原則: 有効な拡張 = 意味を保持し、タスクに不変な変換
- 同期された bbox/マスクによる検出とセグメンテーションのネイティブ サポートを備えたリファレンス ライブラリとしてのアルバムメンテーション
- MixUp、CutMix、Mosaic: サンプル間の補間により 2 ~ 5% の精度を向上させる高度なテクニック
- AutoAugment、RandAugment、TrivialAugment: 最適なポリシーの自動検索
- テスト時の拡張: 再トレーニングせずに推論を改善する
- 医療、産業、衛星向けのドメイン固有の拡張
- 各変換の実際の影響を測定するためにアブレーション研究を構成する方法







