Day 118 — EfficientNet・timmライブラリ入門 — 統一APIでのバックボーン選定

2026-08-08 紫 / Phase 5 コーディング timm / EfficientNet / compound scaling / get_classifier

📚 背景知識(読んでから問題へ)

📦
timm(PyTorch Image Models)は「事前学習済みモデルの総合カタログ」です。timm.list_models(pretrained=True)と打つだけで、ResNet・EfficientNet・ConvNeXt・Vision Transformerなど1000種類以上の事前学習済みモデルが一覧できます。「どのメーカーの、どの型番の中古車が借りられるか」を横断的に検索できるカタログサイトのようなイメージです。
⚖️
EfficientNetは「深さ(層の数)」「幅(チャンネル数)」「解像度(入力サイズ)」の3つを、決まった比率でバランスよく同時に大きくしていくcompound scaling(複合スケーリング)という考え方で設計されたモデルです。3つのうち1つだけを闇雲に大きくするより、3つを揃えてバランスよく大きくした方が「同じ計算コストでより高い精度が出る」ことが実験的に示されています。B0(最小)〜B7(最大)のバリエーションがあり、B0でもResNet18やResNet50に匹敵する精度を、より少ないパラメータ数で達成します。
🔑
キーワード
timm.create_model(name, pretrained=True, num_classes=10): モデルの種類を問わず、分類ヘッドをそのクラス数用に自動で差し替えてくれる統一関数
model.get_classifier(): モデルごとに名前が異なる分類ヘッド(fc/classifier/head等)を、名前を意識せず取得するメソッド
compound scaling: 深さ・幅・解像度を決まった比率で同時に拡大する設計思想

📐 compound scalingのイメージ — 3つを同時に、バランスよく

従来型スケーリング(1つだけを大きくする) 深さ↑ 解像度 → 頭打ちしやすい(例: ResNetの深層化) 深さ 幅↑ 解像度 EfficientNetのcompound scaling(3つを同じ比率で同時に) 深さ↑ 幅↑ 解像度↑ → 同じ計算コストでより高い精度(B0→B7) φ(ファイ)というつまみ1つで、α・β・γの比率を保ったまま3つ同時にスケールする

B0〜B7の番号は、この「つまみφ」の値の違いに対応する。番号が上がるほど深さ・幅・解像度が同じ比率のまま大きくなっていく。

🎯 問題

CIFAR10を題材に、timmライブラリでefficientnet_b0の事前学習済みモデルを特徴抽出(Feature Extraction)モードで利用する処理を実装してください。

1
利用可能なEfficientNet系モデルを一覧する: timm.list_models()で名前にefficientnetを含む事前学習済みモデルを先頭5件表示する
2
efficientnet_b0を特徴抽出モードで構築する関数を書く: timm.create_modelpretrained=True, num_classes=10を指定して読み込み、分類ヘッド(get_classifier())以外の全パラメータを凍結する
3
resnet18(Day117)とefficientnet_b0のパラメータ数を比較する: sum(p.numel() for p in model.parameters())でそれぞれの総パラメータ数を算出し比較する
4
特徴抽出モードで3エポック学習し、Day117のresnet18の結果と比較する(メイン): 学習率0.001で学習し、パラメータ数と精度の両面でどちらが効率的だったかをコメントで記述する

💡 ヒント

ヒント1方向性

Day117では「モデルの最終層の名前(fc)を知っている」ことが前提でした。今日のポイントは、モデルの内部構造を細かく知らなくても分類ヘッドを扱えるようにするtimmの統一APIです。「名前がバラバラな最終層を、どうやって共通の方法で扱うか」を考えてみましょう。

ヒント2アプローチ
  • timm.list_models("*efficientnet*", pretrained=True)のようにワイルドカードでモデル名を検索できる
  • timm.create_model("efficientnet_b0", pretrained=True, num_classes=10)で事前学習済み・分類ヘッド差し替え済みのモデルが一度に手に入る
  • 分類ヘッドの層そのものはmodel.get_classifier()で取得できる。これが返す層のパラメータのid()と、model.parameters()で回している各パラメータのid()を比較すれば「どれが分類ヘッドのパラメータか」を判定できる
  • パラメータ数の集計はsum(p.numel() for p in model.parameters())。学習対象だけに絞るならif p.requires_gradを条件に加える
ヒント3コード骨格
import timm
import torch.nn as nn

def build_efficientnet_feature_extractor():
    model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=10)

    classifier = model.get_classifier()
    classifier_param_ids = {id(p) for p in classifier.parameters()}

    for param in model.parameters():
        if id(param) not in classifier_param_ids:
            param.requires_grad = False
        # 分類ヘッド自身のパラメータは requires_grad=True のまま触らない

    return model.to(device)

def count_params(model, only_trainable=False):
    if only_trainable:
        return sum(p.numel() for p in model.parameters() if p.requires_grad)
    return sum(p.numel() for p in model.parameters())

模範解答

タスク1: 利用可能なEfficientNet系モデルを一覧する

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
from torchvision.models import ResNet18_Weights
import timm

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

efficientnet_models = timm.list_models("*efficientnet*", pretrained=True)
print("EfficientNet系モデル(先頭5件):", efficientnet_models[:5])
# 出力例: ['efficientnet_b0', 'efficientnet_b1', 'efficientnet_b1_pruned', 'efficientnet_b2', 'efficientnet_b2_pruned']

データ準備(timmのEfficientNet-B0も224×224・ImageNet統計量が前提)

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

train_dataset = datasets.CIFAR10(root="./data", train=True, download=True, transform=transform)
val_dataset = datasets.CIFAR10(root="./data", train=False, download=True, transform=transform)

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

タスク2: efficientnet_b0を特徴抽出モードで構築

def build_efficientnet_feature_extractor():
    model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=10)

    classifier = model.get_classifier()
    classifier_param_ids = {id(p) for p in classifier.parameters()}

    for param in model.parameters():
        if id(param) not in classifier_param_ids:
            param.requires_grad = False

    return model.to(device)


# --- 比較用: Day117のresnet18特徴抽出モデル ---
def build_resnet18_feature_extractor():
    model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
    for param in model.parameters():
        param.requires_grad = False
    model.fc = nn.Linear(model.fc.in_features, 10)
    return model.to(device)


def count_params(model, only_trainable=False):
    if only_trainable:
        return sum(p.numel() for p in model.parameters() if p.requires_grad)
    return sum(p.numel() for p in model.parameters())

タスク3: パラメータ数の比較

resnet_model = build_resnet18_feature_extractor()
efficientnet_model = build_efficientnet_feature_extractor()

print(f"resnet18       総パラメータ数: {count_params(resnet_model):,}")
print(f"efficientnet_b0 総パラメータ数: {count_params(efficientnet_model):,}")
# 出力例:
# resnet18        総パラメータ数: 11,181,642
# efficientnet_b0 総パラメータ数: 4,020,358
# → efficientnet_b0はresnet18の1/3弱のパラメータ数。compound scalingにより
#   少ないパラメータでも表現力の高い特徴を抽出できる設計になっているため。

タスク4: 学習・評価・比較

def train_and_eval(model, epochs, lr):
    criterion = nn.CrossEntropyLoss()
    trainable_params = filter(lambda p: p.requires_grad, model.parameters())
    optimizer = optim.Adam(trainable_params, lr=lr)

    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_loss = running_loss / total
        train_acc = correct / total

        model.eval()
        val_correct, val_total = 0, 0
        with torch.no_grad():
            for images, labels in val_loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                preds = outputs.argmax(dim=1)
                val_correct += (preds == labels).sum().item()
                val_total += labels.size(0)
        val_acc = val_correct / val_total

        print(f"Epoch {epoch+1}/{epochs}: train_loss={train_loss:.4f}, train_acc={train_acc:.4f}, val_acc={val_acc:.4f}")

    return val_acc


print("=== efficientnet_b0 特徴抽出モード ===")
eff_val_acc = train_and_eval(efficientnet_model, epochs=3, lr=0.001)

print(f"efficientnet_b0 最終val_acc: {eff_val_acc:.4f}")
print("(比較対象)resnet18 特徴抽出モード 最終val_acc(Day117実行例): 0.7412")
# 出力例:
# efficientnet_b0 最終val_acc: 0.7823
# → resnet18特徴抽出モード(0.7412)より高い精度。パラメータ数は1/3弱にもかかわらず
#   精度が上回っており、EfficientNet-B0の「少ないパラメータで効率よく特徴を捉える」
#   設計の効果が特徴抽出モードでも表れている。

📊 実行結果の比較イメージ(パラメータ数・検証accuracy)

模範解答の実行例(実際の数値は初期化・環境により変動します)。

総パラメータ数

resnet18
11,181,642
Day117
efficientnet_b0
4,020,358
約1/3弱のサイズ

検証accuracy(特徴抽出モード・3エポック)

resnet18(Day117)
0.7412
パラメータ数 大
efficientnet_b0
0.7823
パラメータ数 小・精度 上回る
🏁
パラメータ数が resnet18 の1/3弱にもかかわらず、efficientnet_b0 の特徴抽出モードは検証accuracyで上回る傾向がある。compound scalingにより「少ないパラメータで効率よく特徴を捉える」設計の効果が表れている。ただし実測の学習・推論速度はパラメータ数だけで決まらない点に注意(後述「よくある誤解」参照)。

🪜 Step-by-Step 解説

1timm.list_modelsでモデルを検索する

efficientnet_models = timm.list_models("*efficientnet*", pretrained=True)
🔍
なぜこうするか: timmには1000種類を超えるモデルが登録されているため、名前を正確に覚えていなくてもワイルドカード検索でシリーズ全体を把握できる。pretrained=Trueを指定すると、事前学習済み重みが実際に配布されているモデルのみに絞り込まれる。

2num_classesを渡すだけで分類ヘッドが差し替わる

model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=10)
📝
なぜこうするか: Day117ではresnet18の最終層の名前がfcであることを知っている前提で手動置き換えが必要だった。timmはモデルごとに異なる最終層の名前・構造の違いを内部で吸収しており、num_classesを渡すだけでそのモデルに適した形で新しい分類ヘッドが自動的に作られる。モデルを別のアーキテクチャに変えても、呼び出し方は変わらない。

3get_classifier()で分類ヘッドを特定し、それ以外を凍結する

classifier = model.get_classifier()
classifier_param_ids = {id(p) for p in classifier.parameters()}

for param in model.parameters():
    if id(param) not in classifier_param_ids:
        param.requires_grad = False
🧩
なぜこうするか: Day117ではmodel.layer4のように「凍結を解除したい層の名前」を知っている前提で書けたが、モデルによって内部の層の名前は大きく異なる。get_classifier()は「このモデルの分類ヘッドはどれか」をモデル自身に問い合わせる汎用的な方法であり、id()(Pythonオブジェクトの識別子)でパラメータ集合を比較することで、モデルの内部構造を細かく知らなくても同じロジックを別のモデルに使い回せる。

4パラメータ数をnumel()の合計で数える

def count_params(model, only_trainable=False):
    if only_trainable:
        return sum(p.numel() for p in model.parameters() if p.requires_grad)
    return sum(p.numel() for p in model.parameters())
🧮
なぜこうするか: p.numel()はテンソルpが持つ要素の総数を返す。全パラメータのnumel()を合計することでモデル全体のパラメータ数(=サイズ・複雑さの目安)を比較できる。only_trainable=Trueにすれば、凍結されていない実際に学習されるパラメータ数だけに絞ることもできる。

🧮 数学・統計の補足(文系向け)

🍳
compound scalingの直感的な理解: 料理で例えると、ResNetのような従来のモデルの多くは「鍋を大きくする(層を深くする)」ことだけで美味しさ(精度)を上げようとしてきました。しかしEfficientNetの著者たちは「鍋の大きさ(深さ)」「具材の量(幅=チャンネル数)」「火加減の細かさ(解像度)」の3つを、決まった比率でバランスよく同時に増やした方が、同じ食材コスト(計算コスト)でより美味しくなる、ということを実験で確かめました。
📐
もし数式を見たら:
depth = α^φ, width = β^φ, resolution = γ^φ, s.t. α・β²・γ² ≈ 2

→ これは「深さ・幅・解像度を、φ(ファイ)という1つの共通のつまみを回すだけで、決まった比率を保ったまま同時に大きくする」ことを数式で表したもの。EfficientNet-B0〜B7の番号は、このφの値の違いに対応している。α・β・γという3つの係数の比率は最初に小さいモデル(B0)で探索で決められており、番号が上がるほど3つが同じ比率のまま大きくなっていく。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 画像認識(CV) / ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 時系列(Time Series)

事実上の標準

📚 timmは画像コンペの標準ライブラリ

上位ノートブックはほぼ timm から始まる

数百種類のバックボーンを同じコードパターンで試せるため、「どのバックボーンが自分のデータに合うか」を素早く比較検討できる。

運用テクニック

⚖️ 精度・速度・メモリのトレードオフ探索

モデル名を差し替えるだけで規模を変えられる

コンペ序盤は軽量なB0で素早くパイプラインを検証し、終盤により大きなB4・B5に載せ替えてスコアを伸ばすのが定石。

上級テクニック

🧬 num_classes=0で特徴ベクトルを取り出す

分類ヘッドなしの特徴抽出器として使う

複数のバックボーンから得た特徴量を結合してアンサンブルしたり、GBDTなど別モデルに特徴量として渡す使い方もよく行われる。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
num_classesを渡せば分類ヘッドも自動で凍結解除されると誤解するtimmの便利さから「全部自動でよしなにやってくれる」と思い込むnum_classesは新しい分類ヘッドを作るだけ。凍結(requires_gradの制御)は依然として自分でロジックを書く必要がある
EfficientNetは常にResNetより高精度だと思い込むパラメータ数が少ないのに精度が良いという説明だけを見て一般化する同程度の計算コストで比較した場合に効率が良い、という設計思想。データセットやタスクによってはResNet系が有利な場合もあり、実際にCVで比較して確認する姿勢が重要
model.fcのようにモデル固有の属性名を直接指定して操作しようとするDay117のresnet18の書き方をそのまま流用してしまうtimmのモデルはclassifierheadfcなど属性名がモデルごとに異なる。get_classifier()という統一メソッドを使えばモデルを差し替えてもコードを変更せずに済む
パラメータ数が少ない=学習・推論が速いと単純に考えるパラメータ数だけを効率の指標にしてしまう実際の速度はパラメータ数だけでなく演算の種類(depthwise convolutionなど)やメモリアクセスパターンにも左右される。EfficientNetは理論演算量(FLOPs)は少なくてもGPU上の実測速度がResNetより遅くなる場合がある

🚀 次のステップ

  • 発展: timm.create_model("efficientnet_b0", pretrained=True, num_classes=0)で分類ヘッドなしの特徴抽出器を作り、resnet18の特徴量とefficientnet_b0の特徴量をtorch.catで結合してから1つのnn.Linearに通す簡易アンサンブルを実装し、単独モデルより精度が上がるか確認してみましょう
  • 次回予告: Day 119 — Data Augmentation応用(timmのRandAugment・Mixup・CutMix)。今日はモデル側(バックボーン)の選定を扱いましたが、次回はデータ側の工夫として、Kaggleの画像コンペで定番の高度なAugmentation手法を学びます

Phase 5 の学習マップ(全20テーマ予定)

1-5 DL基礎(PyTorch) 完了
6-10 NLP
11-15 CV ← 本日(②EfficientNet/timm)
16-18 時系列
19-20 マルチモーダル

Day 117で転移学習(ResNet)を体験し、Day 118でtimmライブラリによる統一APIとEfficientNetのcompound scalingを学んだ。CVブロック(11-15)は次にAugmentation応用へ進む。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: