📚 背景知識(読んでから問題へ)
timm(PyTorch Image Models)は「事前学習済みモデルの総合カタログ」です。timm.list_models(pretrained=True)と打つだけで、ResNet・EfficientNet・ConvNeXt・Vision Transformerなど1000種類以上の事前学習済みモデルが一覧できます。「どのメーカーの、どの型番の中古車が借りられるか」を横断的に検索できるカタログサイトのようなイメージです。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つを同時に、バランスよく
B0〜B7の番号は、この「つまみφ」の値の違いに対応する。番号が上がるほど深さ・幅・解像度が同じ比率のまま大きくなっていく。
🎯 問題
CIFAR10を題材に、timmライブラリでefficientnet_b0の事前学習済みモデルを特徴抽出(Feature Extraction)モードで利用する処理を実装してください。
timm.list_models()で名前にefficientnetを含む事前学習済みモデルを先頭5件表示するefficientnet_b0を特徴抽出モードで構築する関数を書く: timm.create_modelでpretrained=True, num_classes=10を指定して読み込み、分類ヘッド(get_classifier())以外の全パラメータを凍結するresnet18(Day117)とefficientnet_b0のパラメータ数を比較する: sum(p.numel() for p in model.parameters())でそれぞれの総パラメータ数を算出し比較する💡 ヒント
Day117では「モデルの最終層の名前(fc)を知っている」ことが前提でした。今日のポイントは、モデルの内部構造を細かく知らなくても分類ヘッドを扱えるようにするtimmの統一APIです。「名前がバラバラな最終層を、どうやって共通の方法で扱うか」を考えてみましょう。
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を条件に加える
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)
模範解答の実行例(実際の数値は初期化・環境により変動します)。
総パラメータ数
検証accuracy(特徴抽出モード・3エポック)
🪜 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)
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
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にすれば、凍結されていない実際に学習されるパラメータ数だけに絞ることもできる。🧮 数学・統計の補足(文系向け)
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のモデルはclassifier・head・fcなど属性名がモデルごとに異なる。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テーマ予定)
Day 117で転移学習(ResNet)を体験し、Day 118でtimmライブラリによる統一APIとEfficientNetのcompound scalingを学んだ。CVブロック(11-15)は次にAugmentation応用へ進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: