LESSON

演習:モデルの性能を改善しよう

田中VPoE:「ここまで正則化、最適化、デバッグの手法を学んだ。最後に、実際にモデルの性能を段階的に改善する演習に取り組もう。ベースラインから始めて、各テクニックの効果を検証してくれ。」

あなた:「1つずつ手法を追加して、どれがどれだけ効くかを確認するんですね。」

田中VPoE:「その通り。実務でも闇雲に全部入りにするのではなく、各手法の効果を計測しながら改善するのが正しいアプローチだ。」

ミッション概要

CIFAR-10 の分類タスクで、ベースラインモデルの性能を段階的に改善します。各テクニックの効果を記録し、最終的な改善レポートを作成します。


Mission 1: ベースラインの構築

意図的に最適化されていないモデルを作成し、ベースライン精度を記録してください。

要件

  1. BatchNorm、Dropout なしの CNN を構築する
  2. 固定学習率の SGD で10エポック学習する
  3. テスト精度を記録する
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# ここにベースラインを書く
解答例
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

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

transform = transforms.Compose([
    transforms.ToTensor(),
])

train_ds = datasets.CIFAR10('./data', train=True, download=True, transform=transform)
test_ds = datasets.CIFAR10('./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=256, shuffle=False)

class BaselineCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(),
            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
            nn.AdaptiveAvgPool2d(1),
        )
        self.fc = nn.Linear(128, 10)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)

def train_and_eval(model, train_loader, test_loader, optimizer, epochs=10):
    criterion = nn.CrossEntropyLoss()
    history = []
    for epoch in range(epochs):
        model.train()
        for imgs, labels in train_loader:
            imgs, labels = imgs.to(device), labels.to(device)
            optimizer.zero_grad()
            loss = criterion(model(imgs), labels)
            loss.backward()
            optimizer.step()

        model.eval()
        correct, total = 0, 0
        with torch.no_grad():
            for imgs, labels in test_loader:
                imgs, labels = imgs.to(device), labels.to(device)
                _, pred = model(imgs).max(1)
                total += labels.size(0)
                correct += pred.eq(labels).sum().item()
        acc = 100.0 * correct / total
        history.append(acc)
        print(f"  Epoch {epoch+1}: {acc:.1f}%")
    return history

# ベースライン
print("=== ベースライン ===")
model_base = BaselineCNN().to(device)
opt_base = torch.optim.SGD(model_base.parameters(), lr=0.01)
hist_base = train_and_eval(model_base, train_loader, test_loader, opt_base)
print(f"ベースライン最終精度: {hist_base[-1]:.1f}%")

Mission 2: テクニックを1つずつ追加して効果を検証する

以下のテクニックを1つずつ追加し、精度の変化を記録してください。

要件

  1. +BatchNorm: BatchNorm を追加
  2. +Dropout: Dropout(0.3) を追加
  3. +正規化: 入力データの正規化を追加
  4. +Adam: Optimizer を Adam に変更
  5. +学習率スケジューリング: CosineAnnealing を追加
  6. 各段階の精度を比較表にまとめる
# ここに段階的改善のコードを書く
解答例
results = {'ベースライン': hist_base[-1]}

# === +BatchNorm ===
class CNN_BN(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
            nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
            nn.AdaptiveAvgPool2d(1),
        )
        self.fc = nn.Linear(128, 10)
    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)

print("\n=== +BatchNorm ===")
m = CNN_BN().to(device)
h = train_and_eval(m, train_loader, test_loader, torch.optim.SGD(m.parameters(), lr=0.01))
results['+BatchNorm'] = h[-1]

# === +Dropout ===
class CNN_BN_DO(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
            nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
            nn.MaxPool2d(2), nn.Dropout2d(0.2),
            nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
            nn.AdaptiveAvgPool2d(1),
        )
        self.fc = nn.Sequential(nn.Dropout(0.3), nn.Linear(128, 10))
    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)

print("\n=== +Dropout ===")
m = CNN_BN_DO().to(device)
h = train_and_eval(m, train_loader, test_loader, torch.optim.SGD(m.parameters(), lr=0.01))
results['+Dropout'] = h[-1]

# === +正規化 ===
norm_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))
])
train_ds_n = datasets.CIFAR10('./data', train=True, transform=norm_transform)
test_ds_n = datasets.CIFAR10('./data', train=False, transform=norm_transform)
train_loader_n = DataLoader(train_ds_n, batch_size=128, shuffle=True)
test_loader_n = DataLoader(test_ds_n, batch_size=256, shuffle=False)

print("\n=== +正規化 ===")
m = CNN_BN_DO().to(device)
h = train_and_eval(m, train_loader_n, test_loader_n, torch.optim.SGD(m.parameters(), lr=0.01))
results['+正規化'] = h[-1]

# === +Adam ===
print("\n=== +Adam ===")
m = CNN_BN_DO().to(device)
h = train_and_eval(m, train_loader_n, test_loader_n, torch.optim.Adam(m.parameters(), lr=1e-3))
results['+Adam'] = h[-1]

# === +学習率スケジューリング ===
print("\n=== +学習率スケジューリング ===")
m = CNN_BN_DO().to(device)
opt = torch.optim.Adam(m.parameters(), lr=1e-3)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=10)
criterion = nn.CrossEntropyLoss()
for epoch in range(10):
    m.train()
    for imgs, labels in train_loader_n:
        imgs, labels = imgs.to(device), labels.to(device)
        opt.zero_grad()
        loss = criterion(m(imgs), labels)
        loss.backward()
        opt.step()
    scheduler.step()
    m.eval()
    correct, total = 0, 0
    with torch.no_grad():
        for imgs, labels in test_loader_n:
            imgs, labels = imgs.to(device), labels.to(device)
            _, pred = m(imgs).max(1)
            total += labels.size(0)
            correct += pred.eq(labels).sum().item()
    print(f"  Epoch {epoch+1}: {100.0*correct/total:.1f}%")
results['+LR Schedule'] = 100.0 * correct / total

# === 結果まとめ ===
print("\n" + "=" * 50)
print("=== 改善レポート ===")
print(f"{'手法':<20s} {'精度':>8s} {'改善幅':>8s}")
print("-" * 40)
baseline = results['ベースライン']
for name, acc in results.items():
    diff = acc - baseline
    print(f"{name:<20s} {acc:>7.1f}% {diff:>+7.1f}%")

Mission 3: 学習曲線の異常を診断する

意図的にバグを含んだコードを修正し、正常に学習できるようにしてください。

要件

以下のコードには3つの問題があります。問題を特定し、修正してください。

# バグあり: 3つの問題を見つけて修正してください
model = CNN_BN_DO().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.5)  # 問題1?

for epoch in range(10):
    for imgs, labels in train_loader_n:
        imgs, labels = imgs.to(device), labels.to(device)
        # optimizer.zero_grad()  # 問題2?
        outputs = model(imgs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

    # 検証
    correct, total = 0, 0
    for imgs, labels in test_loader_n:  # 問題3?
        imgs, labels = imgs.to(device), labels.to(device)
        outputs = model(imgs)
        _, pred = outputs.max(1)
        total += labels.size(0)
        correct += pred.eq(labels).sum().item()
    print(f"Epoch {epoch+1}: {100.0*correct/total:.1f}%")
答えを見る

問題1: 学習率 0.5 が大きすぎる。Adam の場合は 1e-3 程度が適切。

問題2: optimizer.zero_grad() がコメントアウトされている。勾配がバッチごとに累積され、正しく学習できない。

問題3: 検証時に model.eval()torch.no_grad() が使われていない。Dropout が有効なまま評価され、結果が不安定になる。また、不要な勾配計算でメモリが浪費される。

# 修正版
model = CNN_BN_DO().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)  # 修正1

for epoch in range(10):
    model.train()
    for imgs, labels in train_loader_n:
        imgs, labels = imgs.to(device), labels.to(device)
        optimizer.zero_grad()  # 修正2: コメント解除
        outputs = model(imgs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

    # 検証
    model.eval()  # 修正3a: 評価モードに切り替え
    correct, total = 0, 0
    with torch.no_grad():  # 修正3b: 勾配計算を無効化
        for imgs, labels in test_loader_n:
            imgs, labels = imgs.to(device), labels.to(device)
            outputs = model(imgs)
            _, pred = outputs.max(1)
            total += labels.size(0)
            correct += pred.eq(labels).sum().item()
    print(f"Epoch {epoch+1}: {100.0*correct/total:.1f}%")

達成度チェック

  • ベースラインモデルを構築し、精度を記録した
  • BatchNorm、Dropout、正規化、Adam、スケジューラーの効果を個別に検証した
  • 各テクニックの効果を比較表にまとめた
  • バグのあるコードを3つ修正できた

推定所要時間: 60分