演習:モデルの性能を改善しよう
田中VPoE:「ここまで正則化、最適化、デバッグの手法を学んだ。最後に、実際にモデルの性能を段階的に改善する演習に取り組もう。ベースラインから始めて、各テクニックの効果を検証してくれ。」
あなた:「1つずつ手法を追加して、どれがどれだけ効くかを確認するんですね。」
田中VPoE:「その通り。実務でも闇雲に全部入りにするのではなく、各手法の効果を計測しながら改善するのが正しいアプローチだ。」
ミッション概要
CIFAR-10 の分類タスクで、ベースラインモデルの性能を段階的に改善します。各テクニックの効果を記録し、最終的な改善レポートを作成します。
Mission 1: ベースラインの構築
意図的に最適化されていないモデルを作成し、ベースライン精度を記録してください。
要件
- BatchNorm、Dropout なしの CNN を構築する
- 固定学習率の SGD で10エポック学習する
- テスト精度を記録する
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つずつ追加し、精度の変化を記録してください。
要件
- +BatchNorm: BatchNorm を追加
- +Dropout: Dropout(0.3) を追加
- +正規化: 入力データの正規化を追加
- +Adam: Optimizer を Adam に変更
- +学習率スケジューリング: CosineAnnealing を追加
- 各段階の精度を比較表にまとめる
# ここに段階的改善のコードを書く
解答例
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分