LESSON

演習:画像分類モデルを構築しよう

「ここからが本番だ。Chest X-Rayデータセットで肺炎判定モデルを構築し、Grad-CAMで判断根拠を可視化してくれ。」

田中VPoEが要件を示す。

「精度だけでなく、モデルが正しい根拠で判断しているかの確認まで含めて、一連のパイプラインを完成させてほしい。」

ミッション概要

Kaggle Chest X-Ray Images (Pneumonia) データセットを使い、肺炎判定の画像分類モデルを構築する。転移学習、データ拡張、Grad-CAMによる解釈性確保までを一貫して実装する。


Mission 1: データの準備と探索(20分)

タスク:

  1. Chest X-Ray データセットをダウンロードし、ディレクトリ構造を確認する
  2. 各クラス(NORMAL/PNEUMONIA)の画像数を集計する
  3. サンプル画像を表示し、視覚的な特徴を確認する
  4. クラス不均衡の度合いを計算する
# ヒント: データセットの読み込み
from torchvision import datasets
import matplotlib.pyplot as plt

train_dataset = datasets.ImageFolder("chest_xray/train")
print(f"クラス: {train_dataset.classes}")
print(f"クラス別画像数: {dict(zip(train_dataset.classes,
    [sum(1 for _, l in train_dataset.samples if l == i)
     for i in range(len(train_dataset.classes))]))}")
解答例
import os
from collections import Counter

# ディレクトリ構造の確認
for split in ["train", "val", "test"]:
    for cls in ["NORMAL", "PNEUMONIA"]:
        path = f"chest_xray/{split}/{cls}"
        count = len(os.listdir(path))
        print(f"{split}/{cls}: {count}枚")

# サンプル画像の表示
fig, axes = plt.subplots(2, 5, figsize=(20, 8))
for i, cls in enumerate(["NORMAL", "PNEUMONIA"]):
    imgs = os.listdir(f"chest_xray/train/{cls}")[:5]
    for j, img_name in enumerate(imgs):
        img = Image.open(f"chest_xray/train/{cls}/{img_name}")
        axes[i][j].imshow(img, cmap="gray")
        axes[i][j].set_title(cls)
        axes[i][j].axis("off")
plt.tight_layout()
plt.show()

Mission 2: ベースラインモデルの構築(25分)

タスク:

  1. ResNet-50の転移学習(Feature Extraction)でベースラインモデルを構築する
  2. 基本的なデータ拡張パイプラインを実装する
  3. クラス不均衡に対処する(WeightedRandomSampler or 重み付き損失)
  4. 学習を実行し、学習曲線(Loss/Accuracy)をプロットする
# ヒント: モデル構築
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
for param in model.parameters():
    param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 2)
解答例
# データ拡張
train_transform = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.RandomCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])

# クラス不均衡対策
class_counts = [1341, 3875]
class_weights = torch.tensor([sum(class_counts)/c for c in class_counts])
criterion = nn.CrossEntropyLoss(weight=class_weights.to(device))

# 学習実行
model = train_model(model, train_loader, val_loader, epochs=10, lr=1e-3)

Mission 3: Fine-tuningで精度向上(20分)

タスク:

  1. ResNet-50のlayer4を解凍してFine-tuningする
  2. EfficientNet-B3でも同様のFine-tuningを実施する
  3. 両モデルのテストデータでの性能を比較する(Accuracy, Sensitivity, Specificity)
  4. 混同行列を可視化する
解答例
from sklearn.metrics import confusion_matrix, classification_report
import seaborn as sns

# Fine-tuning(layer4を解凍)
for param in model.layer4.parameters():
    param.requires_grad = True

optimizer = optim.Adam([
    {"params": model.layer4.parameters(), "lr": 1e-4},
    {"params": model.fc.parameters(), "lr": 1e-3},
])

# テストデータでの評価
model.eval()
all_preds, all_labels = [], []
with torch.no_grad():
    for images, labels in test_loader:
        outputs = model(images.to(device))
        preds = outputs.argmax(1).cpu()
        all_preds.extend(preds.numpy())
        all_labels.extend(labels.numpy())

print(classification_report(all_labels, all_preds,
                          target_names=["NORMAL", "PNEUMONIA"]))

# 混同行列
cm = confusion_matrix(all_labels, all_preds)
sns.heatmap(cm, annot=True, fmt="d",
           xticklabels=["NORMAL", "PNEUMONIA"],
           yticklabels=["NORMAL", "PNEUMONIA"])
plt.xlabel("予測")
plt.ylabel("実際")
plt.title("混同行列")
plt.show()

Mission 4: Grad-CAMによる解釈性確認(25分)

タスク:

  1. 最良モデルに対してGrad-CAMを実装する
  2. 正解/誤答それぞれ5枚ずつGrad-CAMを可視化する
  3. ショートカット学習の兆候がないか確認する
  4. モデルの注目領域が医学的に妥当か考察する
解答例
# Grad-CAMの生成と可視化
target_layer = model.layer4[-1]  # ResNet-50の最終畳み込み層
grad_cam = GradCAM(model, target_layer)

# 正解ケースの可視化
correct_cases = [(img, label) for img, label, pred
                 in zip(test_images, test_labels, predictions)
                 if label == pred][:5]

# 誤答ケースの可視化
wrong_cases = [(img, label) for img, label, pred
               in zip(test_images, test_labels, predictions)
               if label != pred][:5]

for img, label in correct_cases + wrong_cases:
    visualize_gradcam(img, model, target_layer,
                     class_names=["NORMAL", "PNEUMONIA"])

確認ポイント:

  • 肺炎ケース: 浸潤影のある領域に注目しているか
  • 正常ケース: 特定の領域に偏らず分散しているか
  • ショートカット: 画像端や非肺野領域に注目していないか

提出物

  1. 学習済みモデルファイル(best_model.pth)
  2. モデル比較レポート(ResNet vs EfficientNet の性能比較表)
  3. Grad-CAM可視化結果(正解5枚、誤答5枚)
  4. 考察レポート(ショートカット学習の有無、改善提案)

推定所要時間: 90分