📚 背景知識(読んでから問題へ)
transforms.RandomHorizontalFlip(): 確率0.5で左右反転するAugmentationtransforms.RandomRotation(degrees): ±degrees度の範囲でランダムに回転過学習(Overfitting): 訓練データへの適合度は上がり続けるのに、検証データへの適合度が頭打ち・悪化する現象
Early Stopping: 検証lossが改善しなくなったら学習を自動的に止めるテクニック(今日の判定ロジックがその原型)
🔍 過学習の兆候は「開いていく2本の線」で見つける
訓練lossは順調に下がり続けているのに、検証lossはある地点(黄色い縦線)を境に反転して上昇し始めています。この「ベストエポック以降にギャップが開いていく」形が、今日のタスク3で機械的に判定する対象です。
🎯 問題
Day 115で組み立てたFashionMNISTの学習ループをベースに、以下3つを実装してください。
transformにRandomHorizontalFlip()・RandomRotation(10)を追加し、検証用はAugmentationなしにするtrain_losses・train_accs・val_losses・val_accsの4リストに毎エポックの値を追加するval_losses[-1]とmin(val_losses)を比較して過学習の有無をprintする💡 ヒント
なぜ検証用データにはAugmentationをかけないのか考えてみましょう。Augmentationは「訓練を難しくして頑健性を高める」ための工夫であり、検証・テストの役割は「本番でどれくらいの実力が出るかを正確に測ること」です。測定条件そのものを毎回ランダムに変えてしまうと、モデルの実力を公平に評価できなくなります。
transforms.Compose([...])は上から順に適用されるリスト。訓練用と検証用で「中身が違う2つのCompose」を用意し、Datasetにそれぞれ別のtransformを渡す- 1エポックの平均lossは「
loss.item() * images.size(0)を積算し、最後にtotal(サンプル総数)で割る」のが正確な出し方(バッチサイズが端数でも歪まない) - 学習曲線のギャップは、
val_lossesの中で「谷(最小値)を打った後、また上がっているか」をmin()とインデックスで確認すれば判定できる val_losses[-1] > min(val_losses)が真なら「最終エポックの検証lossがベスト時点より悪化している」=過学習の兆候あり
train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) val_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) train_losses, train_accs, val_losses, val_accs = [], [], [], [] for epoch in range(10): # --- 学習ループ(Day115と同様)--- # --- 評価ループ(Day115と同様)--- train_losses.append(...) val_losses.append(...) # 過学習判定: min(val_losses) の位置 vs 最後の要素 if val_losses[-1] > min(val_losses): print("過学習の兆候あり")
✅ 模範解答
タスク1: Augmentation付きtransformの定義
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms import matplotlib.pyplot as plt device = torch.device("cuda" if torch.cuda.is_available() else "cpu") train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) val_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) train_dataset = datasets.FashionMNIST(root="./data", train=True, download=True, transform=train_transform) val_dataset = datasets.FashionMNIST(root="./data", train=False, download=True, transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
タスク2: モデル定義+記録付き学習ループ
class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 8, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2) self.conv2 = nn.Conv2d(8, 16, kernel_size=3, padding=1) self.flatten = nn.Flatten() self.fc = nn.Linear(16 * 7 * 7, 10) def forward(self, x): x = self.pool(torch.relu(self.conv1(x))) # (N,1,28,28) -> (N,8,14,14) x = self.pool(torch.relu(self.conv2(x))) # (N,8,14,14) -> (N,16,7,7) x = self.flatten(x) return self.fc(x) model = SimpleCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) train_losses, train_accs, val_losses, val_accs = [], [], [], [] EPOCHS = 10 for epoch in range(EPOCHS): # 学習フェーズ model.train() running_loss, correct, total = 0.0, 0, 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) train_losses.append(running_loss / total) train_accs.append(correct / total) # 評価フェーズ model.eval() running_loss, correct, total = 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 = criterion(outputs, labels) running_loss += loss.item() * images.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) val_losses.append(running_loss / total) val_accs.append(correct / total) print(f"Epoch {epoch+1}/{EPOCHS}: train_loss={train_losses[-1]:.4f}, val_loss={val_losses[-1]:.4f}")
タスク3: 学習曲線の可視化と過学習判定
epochs_range = range(1, EPOCHS + 1) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(epochs_range, train_losses, label="train_loss") axes[0].plot(epochs_range, val_losses, label="val_loss") axes[0].set_title("Loss Curve") axes[0].legend() axes[1].plot(epochs_range, train_accs, label="train_acc") axes[1].plot(epochs_range, val_accs, label="val_acc") axes[1].set_title("Accuracy Curve") axes[1].legend() plt.tight_layout() plt.show() # 過学習判定 best_epoch = val_losses.index(min(val_losses)) + 1 print(f"検証lossが最小だったエポック: {best_epoch} (val_loss={min(val_losses):.4f})") print(f"最終エポック: {EPOCHS} (val_loss={val_losses[-1]:.4f})") if val_losses[-1] > min(val_losses): print("過学習の兆候あり") else: print("過学習の兆候なし") # 検証lossが最小だったエポック: 6 (val_loss=0.2981) # 最終エポック: 10 (val_loss=0.3204) # 過学習の兆候あり(実際の値は初期化・環境により変動する)
📊 学習曲線の実行結果イメージ(loss推移)
模範解答の実行例(実際の数値は初期化・Augmentationの乱数により変動します)。
🪜 Step-by-Step 解説
1訓練用と検証用でtransformを分ける
train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) val_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ])
RandomHorizontalFlipとRandomRotationは呼び出すたびに結果が変わるランダムな変換。訓練データにだけ適用することで毎エポック少しずつ違う見た目の画像を学習させ、「たまたまの見た目」への過剰な適応を防げる。検証の目的は「今のモデルの実力を毎回同じ条件で測る」ことなので、ランダム性を持ち込むとエポック間の公平な比較ができなくなる。2学習ループの中で4つの指標を記録する
train_losses.append(running_loss / total) train_accs.append(correct / total)
running_lossはloss.item() * images.size(0)(バッチ内合計loss)を積み上げたものなので、最後にtotal(サンプル総数)で割ることで1サンプルあたりの平均lossに正規化している。バッチ数ではなくサンプル数で割ることで、最後のバッチが端数でも正しい平均になる。3model.eval()とtorch.no_grad()を検証フェーズで使う
model.eval() with torch.no_grad(): for images, labels in val_loader: ...
model.eval()とno_grad()によって学習中の特殊な動作・パラメータ更新を完全に排除し、モデルの素の実力だけを測る。4学習曲線を2つ並べてプロットする
axes[0].plot(epochs_range, train_losses, label="train_loss") axes[0].plot(epochs_range, val_losses, label="val_loss")
5「検証lossの最小値」と「最終エポックの検証loss」を比較する
if val_losses[-1] > min(val_losses): print("過学習の兆候あり")
min(val_losses)で一番調子が良かった時点のlossを取得し、それをval_losses[-1](学習を止めた時点のloss)と比較することで、学習を続けたことでむしろ悪化していないかを機械的に判定できる。🧮 数学・統計の補足(文系向け)
train_accとval_accの開きがまさにこれに対応します。RandomRotation(10)は画像を-10度〜+10度の範囲でランダムに回転させるという意味。角度は「見た目をどれだけ変えるか」の調整ノブに近く、回転させすぎると(例えば90度)服の形が不自然になり学習を妨げてしまうため、「元の意味を壊さない範囲で少しだけ形を変える」塩梅が重要になる。この「やりすぎない」調整感覚は、Day 004で扱った正則化の強さの調整とも通じる考え方。🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 画像認識(CV) / ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 時系列(Time Series)
🧵 Augmentationはデータの性質次第
意味を壊す変換は逆効果
服の画像では左右反転は自然だが、文字認識(6と9)では左右反転や180度回転でラベルの意味が変わってしまう。ドメイン知識に基づいた取捨選択がスコアに直結する。
🗺️ 学習曲線はチューニングの地図
グラフを見てから次の一手を決める
「まだ伸びしろがあるから学習を続ける」「ギャップが開き始めたのでAugmentationを強める・正則化を足す」といった判断を学習曲線を見ながら行う。
💾 ベストモデルを保存して提出する
最終エポック≠最良モデル
過学習が進んだ最終エポックのモデルをそのまま提出するとLBスコアが落ちるリスクがある。val_lossが最小だったエポックの重みを別途保存し提出用に使うのが基本戦術。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 検証データにもAugmentationをかけてしまう | 訓練用のコードをそのままコピーする | 検証の目的は毎回同じ条件で実力を測ること。Augmentationはランダム性を持ち込むため検証には不適切 |
| Augmentationをかけるほど精度が上がると思い込む | 「データが増える=良いこと」という単純な連想 | 回転・反転を強くしすぎると画像の意味が変わったりノイズだらけになったりして逆に精度が下がることがある。強さは実験しながら調整する |
| 訓練accuracyが高い=良いモデルだと判断する | 「正解率」という言葉のわかりやすさに引っ張られる | 訓練accuracyは問題集を覚えた度合いにすぎない。本当の実力は必ず検証accuracyで判断する |
| 過学習の兆候が出たら即座に学習を止めるべきだと思う | グラフが開いた瞬間=失敗という短絡的な理解 | 一時的にギャップが開いても再び縮まることがある。複数エポックの傾向を見て、必要ならpatienceを設けたEarly Stoppingで対応する |
running_lossをバッチ数で割ってしまう | 「エポックのloss=バッチのloss平均」という単純化 | 最後のバッチが端数だとバッチ数で割ると歪む。loss.item() * images.size(0)を積み上げてサンプル総数で割るのが正確 |
🚀 次のステップ
- 発展:
transforms.ColorJitter(brightness=0.2, contrast=0.2)をtrain_transformに追加し、明るさ・コントラストの変化がAugmentationなし・回転反転のみの場合と比べて学習曲線にどう影響するかを比較してみましょう - 次回予告: Day 117 — 転移学習入門(事前学習済みモデル・ResNetの活用・Fine-tuningの基本)。今日までゼロから組んできた自作CNNを卒業し、大規模データで学習済みのモデルを流用する転移学習に進みます
Phase 5 の学習マップ(全20テーマ予定)
Day 114でCNNの構造を組み、Day 115で学習ループを回し、Day 116でAugmentationと過学習検知まで到達。次回からは自作CNNを卒業し、ResNetなど事前学習済みモデルの転移学習へ進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: