Day 119 — Data Augmentation応用 — RandAugment・Mixup・CutMix

2026-08-09 紫 / Phase 5 コーディング RandAugment / Mixup / CutMix / timm.data.Mixup

📚 背景知識(読んでから問題へ)

🎛️
RandAugmentは「変形の詰め合わせセットから、毎回ランダムに数個を選んで適用する」自動データ拡張です。Day116では回転・明るさ変更などを1つずつ自分でtransformに並べていましたが、RandAugmentは十数種類の変形候補を持ち、画像1枚ごとにnum_ops個をランダムに選び、magnitudeという共通の強さで適用します。「毎回どのスパイスを何個使うかは店にお任せし、辛さのレベルだけ客が指定する」イメージです。
🖼️
Mixupは2枚の画像をピクセルごとにλ(ラムダ)という比率でブレンドし、正解ラベルも同じ比率で混ぜます。猫70%・犬30%を混ぜたら、ラベルも「猫70%・犬30%」というソフトラベルになります。「二重露光の写真」のように2枚がうっすら重なって見えるイメージで、決定境界をなだらかにし過学習を抑えます。
✂️
CutMixもラベル混合はMixupと同じですが、ピクセルを薄くブレンドせず、片方の画像の一部を四角く切り取り(Cut)、もう片方の対応領域に貼り付けます(Mix)。ラベルの混合比率は「貼り付けた領域の面積比」で決まります。「コラージュ(切り貼り)」に近く、画像の一部がそのまま残るため自然な見た目になりやすい手法です。
🔑
キーワード
transforms.RandAugment(num_ops, magnitude): 変形候補からランダムに選んで適用する自動データ拡張
timm.data.Mixup(...): Mixup・CutMixを確率的に切り替え、混合済みソフトラベルまで生成するクラス
timm.loss.SoftTargetCrossEntropy: ハードラベルではなく混合ソフトラベルに対応した損失関数

🔍 3手法の見え方比較 — 何が変わり、ラベルはどうなるか

RandAugment — 1枚だけを変形(ラベルは変えない) 猫の画像 回転+彩度 ラベル: 猫 100%(変化なし) Mixup — ピクセルをブレンド(二重露光) + = 猫70% +犬30% ラベル: 猫70% / 犬30%(λで同比率混合) CutMix — 一部を切り取って貼り付け(コラージュ) + = ラベル: 猫78% / 犬22%(貼付け面積比で混合)

RandAugmentは「1枚を変形するだけでラベルは不変」、Mixup/CutMixは「2枚を混ぜてラベルも比率どおりに混ぜる」という決定的な違いがある。

🎯 問題

Day118で構築したefficientnet_b0(特徴抽出モード)を土台に、CIFAR10の学習データにRandAugmentを適用し、さらに学習ループ内でMixup/CutMixを適用してファインチューニングする処理を実装してください。

1
train用transformにRandAugmentを追加する: transforms.RandAugment(num_ops=2, magnitude=9)をtrain側のみに追加し、val側と分ける理由をコメントで記述する
2
timm.data.Mixupでmixup_fnを構築する: mixup_alpha=0.2, cutmix_alpha=1.0, prob=1.0, switch_prob=0.5, label_smoothing=0.1, num_classes=10で構築する
3
学習ループをMixup/CutMix対応に書き換える: 学習はSoftTargetCrossEntropyで混合ソフトラベルに対応させ、検証は通常のCrossEntropyLossのまま。DataLoaderdrop_last=Trueが必要な理由も記述する
4
3エポック学習し、Day118の結果(val_acc: 0.7823)と比較する(メイン): 少ないエポック数でMixup/CutMixの効果がどう出やすいかをコメントで記述する

💡 ヒント

ヒント1方向性

Day116までのAugmentationは「入力画像だけ」を変形し、ラベルはそのままでした。今日のMixup・CutMixの最大の違いは「ラベルまで混ぜる」という点です。損失関数がハードラベル(0か1)を前提にしたままだと、混合ラベルとかみ合わないことに注意しましょう。

ヒント2アプローチ
  • transforms.RandAugment(...)Compose内でResizeの後・ToTensorの前に挿入する(PIL Image形式に対して動作するため)
  • timm.data.Mixup(...)mixed_images, mixed_labels = mixup_fn(images, labels)のように呼び出せる。mixed_labelsはクラス数分のソフトラベルベクトル
  • 損失関数はfrom timm.loss import SoftTargetCrossEntropyを使い、criterion(outputs, mixed_labels)のように呼び出す
  • 検証時はラベルを混ぜる必要がないため、通常のnn.CrossEntropyLoss()と元の整数ラベルのままで評価する
  • Mixupはバッチ内の画像を2つずつペアにして混ぜるため、バッチサイズが半端だと不具合が起きやすくDataLoader(..., drop_last=True)で切り捨てておくと安全
ヒント3コード骨格
from timm.data import Mixup
from timm.loss import SoftTargetCrossEntropy

transform_train = transforms.Compose([
    transforms.Resize(224),
    transforms.RandAugment(num_ops=2, magnitude=9),
    transforms.ToTensor(),
    transforms.Normalize(...),
])

def build_mixup_fn():
    return Mixup(
        mixup_alpha=0.2, cutmix_alpha=1.0,
        prob=1.0, switch_prob=0.5,
        label_smoothing=0.1, num_classes=10,
    )

def train_one_epoch_mixup(model, loader, mixup_fn, optimizer, device):
    model.train()
    criterion = SoftTargetCrossEntropy()
    for images, labels in loader:
        images, labels = images.to(device), labels.to(device)
        images, mixed_labels = mixup_fn(images, labels)  # ここでラベルもソフト化される
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, mixed_labels)
        loss.backward()
        optimizer.step()

模範解答

タスク1: train用transformにRandAugmentを追加、val用は変えない

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import timm
from timm.data import Mixup
from timm.loss import SoftTargetCrossEntropy

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

transform_train = transforms.Compose([
    transforms.Resize(224),
    transforms.RandAugment(num_ops=2, magnitude=9),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# valは「そのモデルが本番でどう見るか」を測る基準なので、学習データだけを人工的に増やし、
# 評価の物差し(val transform)は変えずに固定しておく

transform_val = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

train_dataset = datasets.CIFAR10(root="./data", train=True, download=True, transform=transform_train)
val_dataset = datasets.CIFAR10(root="./data", train=False, download=True, transform=transform_val)

# Mixupはバッチ内の画像を2つずつペアにして混ぜるため、半端な最後のバッチを避けるためdrop_last=True
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, drop_last=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)

Day118と同じ: efficientnet_b0を特徴抽出モードで構築

def build_efficientnet_feature_extractor():
    model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=10)
    classifier = model.get_classifier()
    classifier_param_ids = {id(p) for p in classifier.parameters()}
    for param in model.parameters():
        if id(param) not in classifier_param_ids:
            param.requires_grad = False
    return model.to(device)

タスク2: Mixup/CutMixインスタンスの構築

def build_mixup_fn():
    return Mixup(
        mixup_alpha=0.2,
        cutmix_alpha=1.0,
        cutmix_minmax=None,
        prob=1.0,
        switch_prob=0.5,
        mode="batch",
        label_smoothing=0.1,
        num_classes=10,
    )

タスク3: Mixup/CutMix対応の学習ループ・通常の検証ループ

def train_and_eval_mixup(model, epochs, lr):
    mixup_fn = build_mixup_fn()
    train_criterion = SoftTargetCrossEntropy()
    val_criterion = nn.CrossEntropyLoss()  # 検証は混合しない、通常のハードラベルで評価する
    trainable_params = filter(lambda p: p.requires_grad, model.parameters())
    optimizer = optim.Adam(trainable_params, lr=lr)

    for epoch in range(epochs):
        model.train()
        running_loss, total = 0.0, 0
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)
            images, mixed_labels = mixup_fn(images, labels)

            optimizer.zero_grad()
            outputs = model(images)
            loss = train_criterion(outputs, mixed_labels)
            loss.backward()
            optimizer.step()

            running_loss += loss.item() * images.size(0)
            total += images.size(0)

        train_loss = running_loss / total

        # 検証: Mixup/CutMixは使わず、通常のCrossEntropyLossと元のラベルで評価する
        model.eval()
        val_correct, val_total, val_loss_sum = 0, 0, 0.0
        with torch.no_grad():
            for images, labels in val_loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                loss = val_criterion(outputs, labels)
                val_loss_sum += loss.item() * images.size(0)
                preds = outputs.argmax(dim=1)
                val_correct += (preds == labels).sum().item()
                val_total += labels.size(0)
        val_acc = val_correct / val_total
        val_loss = val_loss_sum / val_total

        print(f"Epoch {epoch+1}/{epochs}: train_loss(mixed)={train_loss:.4f}, val_loss={val_loss:.4f}, val_acc={val_acc:.4f}")

    return val_acc

タスク4: 学習実行・Day118との比較

model = build_efficientnet_feature_extractor()
mixup_val_acc = train_and_eval_mixup(model, epochs=3, lr=0.001)

print(f"efficientnet_b0 + RandAugment + Mixup/CutMix 最終val_acc: {mixup_val_acc:.4f}")
print("(比較対象)efficientnet_b0 特徴抽出モード(Day118, Augmentationなし)val_acc: 0.7823")
# 出力例:
# efficientnet_b0 + RandAugment + Mixup/CutMix 最終val_acc: 0.7605
# → Day118(0.7823)よりむしろ低下する場合がある。Mixup/CutMixは「学習をあえて難しくして
#   汎化性能を上げる」正則化手法であり、効果が表れるには十分なエポック数が必要。
#   わずか3エポックの特徴抽出(学習対象パラメータが分類ヘッドのみ)では、
#   正則化の強さに対してモデルの学習余地が少なく、逆に学習不足(未収束)になりやすい。
#   エポック数を増やすか、Fine-tuning(Day117)で学習対象パラメータを増やすと
#   Mixup/CutMixの正則化効果が精度向上として表れやすくなる。

📊 実行結果の比較イメージ(検証accuracy)

模範解答の実行例(実際の数値は初期化・環境により変動します)。

検証accuracy(efficientnet_b0・特徴抽出モード・3エポック)

Day118: Augmentationなし
0.7823
基準値
Day119: RandAugmentのみ
0.7690(例)
やや低下しやすい
Day119: RandAugment+Mixup/CutMix
0.7605(例)
3epochでは未収束気味
正則化を強めるほど短期的なaccuracyは下がりやすい。Mixup/CutMixの真価は「エポック数を十分に確保し、汎化性能(未知データへの強さ)が問われる場面」で発揮される。3エポックという短い学習では、正則化の強さがそのまま学習不足として表れやすい点に注意する。

🪜 Step-by-Step 解説

1RandAugmentはPIL Imageの段階で適用する

transform_train = transforms.Compose([
    transforms.Resize(224),
    transforms.RandAugment(num_ops=2, magnitude=9),
    transforms.ToTensor(),
    transforms.Normalize(...),
])
🔍
なぜこうするか: RandAugmentが内部で使う回転・シアー・コントラスト調整などの多くはPIL Image形式を前提に実装されているため、ToTensor()(テンソル化)より前に挿入する必要がある。num_ops=2は「1枚あたり何種類の変形を組み合わせるか」、magnitude=9は「それぞれの変形をどれくらい強くかけるか」を表す。val用のtransformには入れないことで、評価の物差しを常に一定に保つ。

2Mixupは「MixupとCutMixを確率的に切り替えるスイッチ」

mixup_fn = Mixup(
    mixup_alpha=0.2, cutmix_alpha=1.0,
    prob=1.0, switch_prob=0.5,
    label_smoothing=0.1, num_classes=10,
)
🎲
なぜこうするか: mixup_alphacutmix_alphaはそれぞれの手法で使われるBeta分布のパラメータで、値が小さいほどλが0か1に近い極端な値になりやすく(あまり混ざらない)、大きいほど0.5付近に寄りやすい(しっかり混ざる)。switch_prob=0.5によりバッチごとにMixupとCutMixを半々の確率で切り替える。label_smoothing=0.1は混合ラベルにさらに軽いスムージングを加え、モデルの過信を抑える。

3mixup_fnはバッチ単位で画像とラベルを同時に混ぜて返す

images, mixed_labels = mixup_fn(images, labels)
🔀
なぜこうするか: mixup_fnはバッチ内の画像を2つずつ組み合わせ、Mixup/CutMixのどちらかを適用したうえで、対応する混合ソフトラベル(num_classes次元のベクトル)まで一括生成する。呼び出し側は「元のラベルが整数だったか混合ベクトルだったか」を意識せず、常にmixed_labelsをそのまま損失関数に渡せばよい設計になっている。

4損失関数をソフトラベル対応のSoftTargetCrossEntropyに切り替える

train_criterion = SoftTargetCrossEntropy()
val_criterion = nn.CrossEntropyLoss()
⚠️
なぜこうするか: 通常のnn.CrossEntropyLoss()は「正解クラスは1つだけ」というハードラベルを前提にしている。Mixup/CutMixが生成するのはソフトラベル(確率分布)なので、そのままでは形が合わない。SoftTargetCrossEntropyはソフトラベルに対応した交差エントロピーで、予測確率分布と正解確率分布のズレを計算できる。検証データにはMixup/CutMixを適用しないため、検証ループでは従来どおりCrossEntropyLossと整数ラベルのままでよい。

🧮 数学・統計の補足(文系向け)

🎨
Beta分布と混合比率λの直感的な理解: Mixup/CutMixで使われる混合比率λは、Beta(α, α)という確率分布からランダムに1つ選ばれます。Betaは「0〜1の間のどこかに落ちるサイコロ」のようなもので、αの値によってサイコロの出やすい目が変わります。αが小さい(例: 0.2)と、λは0付近か1付近(=ほとんど混ざらない)に出やすくなります。αが1に近づくほど、λは0〜1の間に均等に出やすくなります。「絵の具を混ぜる比率をサイコロで決めるが、サイコロの偏り方(α)を自分で調整できる」とイメージすると分かりやすいです。
📐
もし数式を見たら:
x̃ = λx_i + (1-λ)x_j, ỹ = λy_i + (1-λ)y_j, λ ~ Beta(α, α)

→ これは「画像x_ix_jを、Beta分布から選んだ比率λでブレンドし、ラベルy_iy_jも同じλでブレンドする」というMixupの定義そのものを数式にしたもの。CutMixも基本的な考え方は同じだが、λが「ピクセルの混合比率」ではなく「切り貼りした領域の面積比率」に対応する点が異なる。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 画像認識(CV) / ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 時系列(Time Series)

ほぼ標準装備

🧰 画像コンペ上位解法の定番構成

RandAugment/AutoAugment + Mixup/CutMix

Bengali.AI・Cassava Leaf Disease・Plant Pathologyなど過去コンペの上位解法の多くがこの組み合わせを使用。データ数が少ないコンペほど効果が上位入賞との差を生みやすい。

CV-LB乖離対策

📉 過学習を抑えCVとLBの乖離を防ぐ

正則化効果でLeaderboardでも伸びやすくなる

Augmentationなしのモデルは手元CVでは高スコアでもLBで伸び悩みがち(過学習)。Mixup/CutMixはこの乖離を抑える効果がある。

設計上の注意

⏱️ エポック数・学習率とセットで調整

正則化を強めたら学習量も増やす

Mixup/CutMixは学習をあえて難しくするため、少ないエポック数では逆に精度が下がる。Augmentationを強めるのと同時にエポック数・学習率スケジューラも調整するのが定石。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
検証データにもRandAugmentやMixup/CutMixを適用してしまう「Augmentationは常に性能を上げるはず」と思い込み、train/valで同じtransformを使い回す検証は「本番でモデルがどう振る舞うか」を測る物差しであり、物差し自体を毎回変形させると評価が安定しない。Augmentationは学習データだけに適用する
Mixup/CutMix適用後もnn.CrossEntropyLoss()をそのまま使ってしまう通常の分類問題の損失関数としてCrossEntropyLossに慣れきっている混合されたソフトラベルにはハードラベル前提のCrossEntropyLossは対応していない。SoftTargetCrossEntropyなどソフトラベル対応の損失関数に切り替える必要がある
Mixup/CutMixを導入すればエポック数を変えなくても必ず精度が上がると思い込む「正則化=常に良い効果」という単純化したイメージを持ってしまう正則化は「学習をわざと難しくして汎化性能を上げる」トレードオフの手法。十分なエポック数がないと正則化の強さがそのまま学習不足として表れ、逆に精度が下がることがある
Mixupのλは常に0.5固定だと誤解する「2枚を混ぜる」という説明だけを見て常に半々にブレンドされると思い込むλはBeta分布からバッチごと・ペアごとにランダムに選ばれる値であり0.5固定ではない。αの設定次第でほとんど混ざらない場合も強く混ざる場合もある

🚀 次のステップ

  • 発展: mixup_alphacutmix_alphaswitch_probをいくつか変えて(例えばmixup_alpha=1.0にする、switch_prob=1.0にしてCutMixのみにする)3エポック学習を繰り返し、val_accがどう変化するかを比較してみましょう。あわせてエポック数を3から10程度に増やした場合にMixup/CutMixの効果が逆転するか(Day118のAugmentationなしを上回るか)も確認してみましょう
  • 次回予告: Day 120 — CVブロック総括・実践的なバックボーン選定戦略。Day114〜119でCNN基礎からResNet・EfficientNet・Augmentation応用までを一通り学んだので、次回はこれらを踏まえてKaggle画像コンペで実際にどうモデルを選び、どの順序で試すかという戦略面を整理します

Phase 5 の学習マップ(全20テーマ予定)

1-5 DL基礎(PyTorch) 完了
6-10 NLP
11-15 CV ← 本日(③Augmentation応用)
16-18 時系列
19-20 マルチモーダル

Day 117で転移学習(ResNet)、Day 118でtimm/EfficientNet、Day 119でRandAugment・Mixup・CutMixを学んだ。CVブロック(11-15)は次に総括・バックボーン選定戦略へ進む。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: