LESSON

転移学習で画像分類モデルを構築する

「まずは転移学習でベースラインを作ろう。ResNetとEfficientNet、どちらが我々のタスクに適しているか検証してくれ。」

田中VPoEがKaggleのデータセットページを開きながら指示する。

「最初から完璧を目指す必要はない。まずは動くものを作り、そこから改善していく。」

転移学習の基本戦略

Feature Extraction vs Fine-tuning

import torch
import torch.nn as nn
from torchvision import models

# 戦略1: Feature Extraction(特徴抽出)
# 事前学習済みの層を凍結し、最終層のみ学習
def create_feature_extractor(num_classes=2):
    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, num_classes)
    return model

# 戦略2: Fine-tuning(微調整)
# 一部の層を解凍して学習
def create_finetuned_model(num_classes=2):
    model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
    # 全パラメータを凍結
    for param in model.parameters():
        param.requires_grad = False
    # 最後の2ブロック + 全結合層を解凍
    for param in model.layer4.parameters():
        param.requires_grad = True
    model.fc = nn.Linear(model.fc.in_features, num_classes)
    return model

戦略の使い分け

条件推奨戦略理由
データ少量(数百枚)Feature Extraction過学習リスクが低い
データ中量(数千枚)Fine-tuning(後半の層)精度と汎化のバランス
データ大量(数万枚)Full Fine-tuningタスク固有の特徴を十分学習可能

ResNet vs EfficientNet

ResNet(Residual Network)

# ResNetの特徴: Skip Connection(残差接続)
# 層を深くしても勾配消失を防ぐ

model_resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
print(f"パラメータ数: {sum(p.numel() for p in model_resnet.parameters()):,}")
# 約25.6M パラメータ

EfficientNet

# EfficientNetの特徴: Compound Scaling
# 幅・深さ・解像度を統合的にスケーリング

model_effnet = models.efficientnet_b3(
    weights=models.EfficientNet_B3_Weights.IMAGENET1K_V1
)
print(f"パラメータ数: {sum(p.numel() for p in model_effnet.parameters()):,}")
# 約12.2M パラメータ
モデルパラメータ数ImageNet Top-1推論速度特徴
ResNet-5025.6M80.9%速い安定、実績豊富
EfficientNet-B312.2M82.0%やや遅い高精度、軽量
EfficientNet-B05.3M77.7%速い軽量、エッジ向き

データの準備

from torchvision import transforms, datasets
from torch.utils.data import DataLoader

# 画像の前処理パイプライン
train_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    ),
])

val_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    ),
])

# Chest X-Rayデータセットの読み込み
train_dataset = datasets.ImageFolder(
    root="chest_xray/train",
    transform=train_transform
)
val_dataset = datasets.ImageFolder(
    root="chest_xray/val",
    transform=val_transform
)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

print(f"クラス: {train_dataset.classes}")
print(f"学習データ数: {len(train_dataset)}")

学習ループの実装

import torch.optim as optim

def train_model(model, train_loader, val_loader, epochs=10, lr=1e-3):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = model.to(device)

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(
        filter(lambda p: p.requires_grad, model.parameters()),
        lr=lr
    )
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)

    best_val_acc = 0.0

    for epoch in range(epochs):
        # 学習フェーズ
        model.train()
        train_loss, train_correct = 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()

            train_loss += loss.item() * images.size(0)
            train_correct += (outputs.argmax(1) == labels).sum().item()

        # 検証フェーズ
        model.eval()
        val_loss, val_correct = 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)
                val_loss += loss.item() * images.size(0)
                val_correct += (outputs.argmax(1) == labels).sum().item()

        train_acc = train_correct / len(train_loader.dataset)
        val_acc = val_correct / len(val_loader.dataset)

        print(f"Epoch {epoch+1}/{epochs} - "
              f"Train Loss: {train_loss/len(train_loader.dataset):.4f}, "
              f"Train Acc: {train_acc:.4f}, "
              f"Val Acc: {val_acc:.4f}")

        if val_acc > best_val_acc:
            best_val_acc = val_acc
            torch.save(model.state_dict(), "best_model.pth")

        scheduler.step()

    return model

まとめ

項目ポイント
Feature Extraction少量データ向け、全パラメータ凍結 + 最終層のみ学習
Fine-tuning中〜大量データ向け、後半の層を解凍して微調整
ResNet-50安定性が高い、実績豊富、25.6Mパラメータ
EfficientNet-B3高精度かつ軽量、12.2Mパラメータ

チェックリスト

  • Feature ExtractionとFine-tuningの違いを説明できる
  • ResNetとEfficientNetの特徴を比較できる
  • PyTorchで転移学習のコードを書ける
  • 学習ループの各要素(損失関数、最適化、スケジューラ)を理解した

次のステップへ

ベースラインモデルを構築したところで、次はデータ拡張でモデルの汎化性能を向上させよう。

推定読了時間: 30分