📚 背景知識(読んでから問題へ)
num_ops個をランダムに選び、magnitudeという共通の強さで適用します。「毎回どのスパイスを何個使うかは店にお任せし、辛さのレベルだけ客が指定する」イメージです。λ(ラムダ)という比率でブレンドし、正解ラベルも同じ比率で混ぜます。猫70%・犬30%を混ぜたら、ラベルも「猫70%・犬30%」というソフトラベルになります。「二重露光の写真」のように2枚がうっすら重なって見えるイメージで、決定境界をなだらかにし過学習を抑えます。transforms.RandAugment(num_ops, magnitude): 変形候補からランダムに選んで適用する自動データ拡張timm.data.Mixup(...): Mixup・CutMixを確率的に切り替え、混合済みソフトラベルまで生成するクラスtimm.loss.SoftTargetCrossEntropy: ハードラベルではなく混合ソフトラベルに対応した損失関数
🔍 3手法の見え方比較 — 何が変わり、ラベルはどうなるか
RandAugmentは「1枚を変形するだけでラベルは不変」、Mixup/CutMixは「2枚を混ぜてラベルも比率どおりに混ぜる」という決定的な違いがある。
🎯 問題
Day118で構築したefficientnet_b0(特徴抽出モード)を土台に、CIFAR10の学習データにRandAugmentを適用し、さらに学習ループ内でMixup/CutMixを適用してファインチューニングする処理を実装してください。
transforms.RandAugment(num_ops=2, magnitude=9)をtrain側のみに追加し、val側と分ける理由をコメントで記述する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で構築するSoftTargetCrossEntropyで混合ソフトラベルに対応させ、検証は通常のCrossEntropyLossのまま。DataLoaderにdrop_last=Trueが必要な理由も記述する💡 ヒント
Day116までのAugmentationは「入力画像だけ」を変形し、ラベルはそのままでした。今日のMixup・CutMixの最大の違いは「ラベルまで混ぜる」という点です。損失関数がハードラベル(0か1)を前提にしたままだと、混合ラベルとかみ合わないことに注意しましょう。
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)で切り捨てておくと安全
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エポック)
🪜 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_alphaとcutmix_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(α, α)という確率分布からランダムに1つ選ばれます。Betaは「0〜1の間のどこかに落ちるサイコロ」のようなもので、αの値によってサイコロの出やすい目が変わります。αが小さい(例: 0.2)と、λは0付近か1付近(=ほとんど混ざらない)に出やすくなります。αが1に近づくほど、λは0〜1の間に均等に出やすくなります。「絵の具を混ぜる比率をサイコロで決めるが、サイコロの偏り方(α)を自分で調整できる」とイメージすると分かりやすいです。x̃ = λx_i + (1-λ)x_j, ỹ = λy_i + (1-λ)y_j, λ ~ Beta(α, α)→ これは「画像
x_iとx_jを、Beta分布から選んだ比率λでブレンドし、ラベルy_iとy_jも同じλでブレンドする」というMixupの定義そのものを数式にしたもの。CutMixも基本的な考え方は同じだが、λが「ピクセルの混合比率」ではなく「切り貼りした領域の面積比率」に対応する点が異なる。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 画像認識(CV) / ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 時系列(Time Series)
🧰 画像コンペ上位解法の定番構成
RandAugment/AutoAugment + Mixup/CutMix
Bengali.AI・Cassava Leaf Disease・Plant Pathologyなど過去コンペの上位解法の多くがこの組み合わせを使用。データ数が少ないコンペほど効果が上位入賞との差を生みやすい。
📉 過学習を抑え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_alphaやcutmix_alpha、switch_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テーマ予定)
Day 117で転移学習(ResNet)、Day 118でtimm/EfficientNet、Day 119でRandAugment・Mixup・CutMixを学んだ。CVブロック(11-15)は次に総括・バックボーン選定戦略へ進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: