Day 120 — CVブロック総括 — 実践的なバックボーン選定戦略

2026-08-10 紫 / Phase 5 戦略 実験計画 / GPU予算配分 / 過学習判定

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

🧰
Day114〜119の6日間で「画像コンペを1人で戦うための武器」が揃いました。今日はこれらをバラバラな知識のまま終わらせず、「実際のコンペで何を・どの順番で試すか」という1つの意思決定フローに統合する回です。理論・コーディング問題では「個々の技術が動くか」を確認してきましたが、戦略問題では「限られた計算予算・時間の中で、どの武器をいつ使うか」を問います。
⏱️
なぜ「順番」が重要なのか: Kaggleの無料GPU環境は週あたりの利用時間に上限があります。強力な武器(大きいバックボーン・Fine-tuning・強いAugmentation)を最初から全部使うと、1回の実験に時間がかかりすぎて「何を変えたらスコアが伸びたか」が分からなくなります。「まず一番安い実験で当たりを付けてから、当たりが出た方向にだけ予算をかけて深掘りする」という考え方が、Silver・Gold帯のコンペティタに共通する型です。
🧭
判断の3つの軸
①データの量とクラス不均衡: 少ない・不均衡が強いほど、正則化(Augmentation)とFeature Extraction(凍結)が有効になりやすい
②計算予算: 少ないほど「軽いバックボーン → Feature Extraction → 段階的に重くする」の順で試す
③過学習の兆候: train/val lossの乖離(Day116)を見て、Augmentation強化・データ追加・モデル縮小のどれを選ぶか判断する

🗂️ CVブロック6日間まとめ(Day114〜119)

Dayテーマ身についた武器
114CNN入門①(畳み込み・プーリング)画像がなぜCNNで扱えるか、nn.Conv2d/nn.MaxPool2dの直感
115CNN入門②(DataLoader・学習ループ)torchvision.datasetstransforms・学習/評価ループの型
116CNN入門③(Augmentation基礎・過学習検知)基本Augmentation、学習曲線から過学習を見抜く目
117転移学習入門(ResNet)事前学習済みモデルの再利用、Feature Extraction と Fine-tuning の違い
118EfficientNet・timm入門timm.create_modelの統一API、compound scaling
119Augmentation応用(RandAugment・Mixup・CutMix)強い正則化でCV-LB乖離を抑える、エポック数とのトレードオフ

今日(Day120)はこの6日間を1つの意思決定フローに統合し、CVブロック(テーマ11-15相当)を完走する。

🎯 問題

架空のKaggle画像コンペ 「Plant Pathology 2026」(葉の病気を12クラスに分類、評価指標はmacro F1)に参加しています。

項目
train6,500枚 / 12クラス(不均衡: 最小80枚・最大1,400枚)
test2,800枚
評価指標macro F1
実行制限1回の実行につきGPU連続9時間まで
今週のGPU残り時間24時間
コンペ残り日数12日
1
実験の優先順位付け(戦略): Day114〜119の武器の中から、GPU予算24時間を踏まえて試すべき実験を優先度順に5つ挙げ、それぞれの狙いを一言で説明する
2
意思決定のコード化(コーディング・メイン): データ規模・クラス不均衡・残りGPU予算から次の実験を提案するrecommend_cv_experiment_plan(...)を実装する。不均衡比率10倍以上でRandAugment開始、平均枚数300枚未満でFeature Extraction開始、Mixup/CutMixはepochs≥5限定、予算超過は提案しない、の4ルールを満たす
3
結果を踏まえた次の一手(戦略・分析): 実験1(efficientnet_b0+Feature Extraction+RandAugment、3epoch)の学習曲線から、Fine-tuning切替・Mixup/CutMix追加・バックボーン大型化のどれを最優先すべきか、根拠つきで結論を出す

💡 ヒント

ヒント1方向性

新しい技術は何も出てきません。「Day114〜119の各技術を、いつ・どの順番で・どんな条件で使うか」という意思決定のルールを言語化し、コードのif分岐として表現する回です。タスク3は「train_lossは順調に下がっているのにval_lossが下がらない/悪化している」というパターンをどう解釈するかがポイントです。

ヒント2アプローチ
  • 不均衡比率はmax_class_count / min_class_countで計算し、10倍を「一部のクラスがほぼ無視される」目安として使う
  • 平均枚数num_train_images / num_classesが300枚未満なら、Day117の「データが少ないほどFeature Extractionを優先」をそのままコード化する
  • タスク3では、Epoch2以降val_lossが下がらずtrain_lossだけ下がり続けている点(Day116の過学習の兆候)と、今のモードがすでにFeature Extraction(凍結)である点を突き合わせて考える
  • 「Fine-tuning」「Mixup/CutMix」「バックボーン大型化」のどれが表現力を上げる方向で、どれが正則化を強める方向かを整理してから、今の症状に合う方を選ぶ
ヒント3コード骨格
def recommend_cv_experiment_plan(num_train_images, num_classes, min_class_count, max_class_count, gpu_hours_left):
    plan = []
    imbalance_ratio = max_class_count / min_class_count
    avg_per_class = num_train_images / num_classes

    first_mode = "feature_extraction" if avg_per_class < 300 else "fine_tuning"
    first_aug = "randaugment" if imbalance_ratio >= 10 else "basic"

    # 実験1: 最も軽い efficientnet_b0 のベースライン確認
    plan.append({
        "backbone": "efficientnet_b0", "mode": first_mode, "augmentation": first_aug,
        "epochs": 3, "est_hours": 3.0,
        "reason": "最も安い実験でまず当たりを付ける(Day118: 軽量・高効率バックボーン)",
    })

    # 実験2以降は残り予算を見ながら追加していく
    # ... Mixup/CutMixはepochs>=5の実験にのみ提案する、大きいバックボーンは2倍のest_hoursで見積もる ...
    return plan

模範解答

タスク1: 優先度順の実験リスト(GPU予算24時間を前提)

  1. 1
    efficientnet_b0 + Feature Extraction + 基本/RandAugment、3epoch — 最も安い実験でパイプライン全体が動くこと・ベースラインF1を確認する(Day118: 軽量バックボーン、Day117: 凍結で高速)
  2. 2
    実験1の学習曲線を見て過学習の有無を判定(Day116) — 新規学習コストゼロで、次の一手のボトルネック(データ不足かモデル力不足か)を切り分ける
  3. 3
    Fine-tuningへ切り替えて再学習(Day117) — Feature Extractionで頭打ちなら学習対象パラメータを増やす。ただし過学習の兆候が強い場合はAugmentation強化を先に行う
  4. 4
    RandAugment→Mixup/CutMixへ段階的に強化(epochs≥5、Day119) — 正則化を一段ずつ強め、val_f1の頭打ちが過学習由来であることを確認しながら潰す
  5. 5
    予算に余裕があればefficientnet_b3等へスケールアップ(Day118) — データ・正則化側の改善余地を使い切ってから、計算コストの高い打ち手を最後に残り予算の範囲で試す
🧩
設計思想: 「安い実験から高い実験へ」「データ・正則化側の改善を先に、モデルを大きくするのは最後」という2つの原則で並べている。GPU予算24時間の中でいきなり全部乗せから始めると、1回の実験で予算の大半を使い切り、何が効いたか分からなくなる。

タスク2: recommend_cv_experiment_plan の実装

def recommend_cv_experiment_plan(
    num_train_images: int,
    num_classes: int,
    min_class_count: int,
    max_class_count: int,
    gpu_hours_left: float,
) -> list[dict]:
    """Day114〜119の判断基準をコード化し、次に試すべき実験を優先度順に返す。"""
    plan = []
    imbalance_ratio = max_class_count / min_class_count
    avg_per_class = num_train_images / num_classes

    # --- ルール1: 平均枚数300枚未満ならFeature Extractionから開始(Day117) ---
    first_mode = "feature_extraction" if avg_per_class < 300 else "fine_tuning"

    # --- ルール2: 不均衡比率10倍以上ならRandAugmentから開始(Day119) ---
    first_aug = "randaugment" if imbalance_ratio >= 10 else "basic"

    remaining = gpu_hours_left

    # --- 実験1: 必ずefficientnet_b0のベースライン確認から(Day118) ---
    exp1_hours = 3.0
    if exp1_hours <= remaining:
        plan.append({
            "backbone": "efficientnet_b0", "mode": first_mode, "augmentation": first_aug,
            "epochs": 3, "est_hours": exp1_hours,
            "reason": "最も安い実験で当たりを付ける(軽量バックボーン・Day118)",
        })
        remaining -= exp1_hours

    # --- 実験2: Fine-tuningへの切り替え(実験1がfeature_extractionの場合のみ意味を持つ) ---
    exp2_hours = 4.0
    if first_mode == "feature_extraction" and exp2_hours <= remaining:
        plan.append({
            "backbone": "efficientnet_b0", "mode": "fine_tuning", "augmentation": first_aug,
            "epochs": 4, "est_hours": exp2_hours,
            "reason": "Feature Extractionで頭打ちの場合に学習対象パラメータを増やす(Day117)",
        })
        remaining -= exp2_hours

    # --- 実験3: Mixup/CutMixはepochs>=5の実験としてのみ提案(Day119の教訓) ---
    exp3_hours = 5.5
    if exp3_hours <= remaining:
        plan.append({
            "backbone": "efficientnet_b0", "mode": "fine_tuning", "augmentation": "mixup_cutmix",
            "epochs": 5, "est_hours": exp3_hours,
            "reason": "十分なエポック数を確保したうえで強い正則化を試す(Day119: 3epochでは未収束)",
        })
        remaining -= exp3_hours

    # --- 実験4: 予算に余裕があれば重いバックボーンへスケールアップ(学習時間は実験1の2倍と見積もる) ---
    exp4_hours = exp1_hours * 2
    if exp4_hours <= remaining:
        plan.append({
            "backbone": "efficientnet_b3", "mode": "fine_tuning", "augmentation": first_aug,
            "epochs": 4, "est_hours": exp4_hours,
            "reason": "軽量・正則化側の改善余地を使い切ってから、計算コストの高いスケールアップを最後に試す(Day118)",
        })
        remaining -= exp4_hours

    return plan


# --- 動作確認: Plant Pathology 2026設定 ---
plan = recommend_cv_experiment_plan(
    num_train_images=6500, num_classes=12,
    min_class_count=80, max_class_count=1400, gpu_hours_left=24.0,
)
for i, exp in enumerate(plan, 1):
    print(f"実験{i}: {exp['backbone']} / {exp['mode']} / {exp['augmentation']} / {exp['epochs']}epoch / 見積{exp['est_hours']}h")
# 出力例:
# 実験1: efficientnet_b0 / feature_extraction / randaugment / 3epoch / 見積3.0h
# 実験2: efficientnet_b0 / fine_tuning / randaugment / 4epoch / 見積4.0h
# 実験3: efficientnet_b0 / fine_tuning / mixup_cutmix / 5epoch / 見積5.5h
# 実験4: efficientnet_b3 / fine_tuning / randaugment / 4epoch / 見積6.0h
# (合計 18.5h ≤ 24h、予算内に収まっている)
⚠️
判定確認: avg_per_class = 6500/12 ≈ 541.7枚(300枚以上)に対しimbalance_ratio = 1400/80 = 17.5倍(10倍以上)が同時に成立する。今回のルール設計は平均枚数のみでモードを決めるため機械的にはfine_tuningになるが、タスク3の出題設定は「80枚しかない少数クラスが過学習しやすい」という追加リスクを人間側が読み取り、あえてFeature Extractionから始めたケースとして提示している。関数の機械的な判定と、人間が状況を見て下す最終判断は必ずしも一致しなくてよく、関数はあくまで「たたき台」である点が今回の設計上の重要な学び。

タスク3: 実験1の結果から次の一手を判断する

Epoch 1: train_loss=1.82, val_loss=1.61, train_f1=0.41, val_f1=0.39
Epoch 2: train_loss=1.20, val_loss=1.55, train_f1=0.63, val_f1=0.42  # val_f1のピーク
Epoch 3: train_loss=0.85, val_loss=1.58, train_f1=0.76, val_f1=0.41  # val_lossが悪化
選択肢適合度理由
Fine-tuningに切り替える✗ 低い現状はすでに過学習気味。学習対象パラメータを増やす方向は過学習をさらに悪化させるリスクが高い
Mixup/CutMixを追加する◎ 最優先過学習を抑える最も直接的な打ち手。ただしエポック数を5以上に増やしたうえで導入する必要がある(Day119)
バックボーンを大きくする✗ 低い表現力不足が原因である証拠はまだない。正則化側を先に試すべき段階
🏁
結論: 最優先の一手は「Mixup/CutMixを追加し、エポック数を5以上に増やして再学習する」。観測されている症状(train/valの乖離拡大、val_f1の早期頭打ち)は典型的な過学習パターンであり、Fine-tuningやバックボーン大型化はどちらも表現力を上げる=過学習を悪化させやすい方向。正則化を強める方向が症状への正しい処方箋であり、タスク2の意思決定関数の実験3と一致する。GPU予算21時間に対し見積り5.5時間は十分収まる。

📊 GPU予算配分の可視化(合計24時間のうち何にいくら使うか)

予算24.0h の内訳(優先度順に積み上げ) 実験1 3.0h 実験2 4.0h 実験3 5.5h 実験4 6.0h バッファ 5.5h 実験1: b0 / FeatExt / 基本 or RandAug(ベースライン確認) 実験2: b0 / Fine-tuning へ切替(表現力を上げる) 実験3: b0 / Fine-tuning + Mixup/CutMix(5epoch以上、正則化強化) 実験4: b3 / Fine-tuning(予算が余れば大型化) 未使用バッファ: 想定外の再実行・失敗リカバリ用

安い実験(実験1)から高い実験(実験4)へ積み上げ、合計18.5h/24hに収める。残り5.5hはタスク3で判明した「想定外の過学習」への対応やコンペ終盤の再実行用バッファとして残す。

📉 学習曲線の読み解き(実験1の結果)

Loss(実線: train / 破線: val) 2.0 1.0 0 Epoch1 Epoch2 Epoch3 train 0.85 val 1.58 ▲悪化 → trainは下がり続けるがvalはEpoch3で悪化 = 典型的な過学習の兆候(Day116)
🔍
val_f1もEpoch2の0.42をピークにEpoch3で0.41へ低下している。「trainだけ改善し続け、valが早期に頭打ち・悪化」というパターンは、モデルの表現力不足ではなく訓練データへの過剰適合を示す。次の一手は表現力を上げる方向(Fine-tuning・大型化)ではなく、正則化を強める方向(Mixup/CutMix)が適切。

🪜 Step-by-Step 解説

1判断基準を「閾値付きのif分岐」として明文化する

first_mode = "feature_extraction" if avg_per_class < 300 else "fine_tuning"
first_aug = "randaugment" if imbalance_ratio >= 10 else "basic"
🔍
なぜこうするか: Day117・Day119で学んだ経験則を、具体的な数値の閾値(300枚・10倍)を決めてコード化する。閾値の絶対的な正しさより、毎回同じ基準で機械的に判断できること自体に価値がある。コンペが変わっても同じ関数を使い回せる。

2予算を消費しながら実験を積み上げる

remaining = gpu_hours_left
if exp1_hours <= remaining:
    plan.append({...})
    remaining -= exp1_hours
💰
なぜこうするか: 「使える予算の範囲でしか提案しない」というガードを入れることで、予算オーバーの計画を防ぐ。優先度の高い実験から順に予算を割り当て、足りなくなった時点で提案を打ち切ることで、常に実行可能な計画になる。

3Mixup/CutMixにはエポック数の下限を設ける

exp3_epochs = 5
if exp3_hours <= remaining:
    plan.append({..., "augmentation": "mixup_cutmix", "epochs": exp3_epochs, ...})
なぜこうするか: Day119で「3エポックではMixup/CutMixの正則化効果が学習不足として表れる」ことを実験で確認済みなので、「Mixup/CutMixを使う実験は必ず5エポック以上を割り当てる」というコード上の制約として反映する。過去の失敗パターンを再現しないためのガードレールになっている。

4学習曲線のパターンから原因を切り分ける

train_loss: 1.82 → 1.20 → 0.85(下がり続ける)
val_loss:   1.61 → 1.55 → 1.58(Epoch3で悪化)
⚖️
なぜこうするか: 「train・valともに高止まり」なら表現力不足(モデル大型化・Fine-tuningが有効)、「trainだけ下がりvalが頭打ち・悪化」なら過学習(正則化が有効)、という2パターンの切り分けがDay116の核心。今回は後者のパターンなので、正則化を強める打ち手(Mixup/CutMix)を選ぶ。

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

🎲
「優先度スコア」を作らず条件分岐で表現した理由: 今回の関数は各実験に点数をつけて並べ替える方式ではなく、「この条件ならこの実験を先に置く」という決め打ちの順番で実装しました。理由は、優先度を数式で表そうとすると「精度改善見込み」自体を事前に見積もる必要があり、それこそが実験してみないと分からない未知数だからです。「まず一番安い実験を置き、その結果を見てから次を決める」という逐次的な意思決定の方が、実験するまで効果が分からない状況には適しています。
📐
もし数式的な表現を見たら:
Expected Value = P(改善する) × 改善幅 − 実験コスト

→ これは「探索と活用のトレードオフ(Exploration vs Exploitation)」を数式にしたもの。今日のタスク1〜2は、P(改善する)改善幅を正確に見積もる代わりに、「安くて情報量の多い実験を先に、コストの高い実験を後に」という経験則ベースの近似解で意思決定している、と理解すると良い。

🏆 Kaggleでの実践的な使い方

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

必需品

📋 「実験計画表」はコンペ参加者の必需品

試す順番を書き出してから実装を始める

上位入賞者の多くはNotebookを書き始める前に、今日のタスク1のような「試す順番のメモ」をスプレッドシートやREADMEに書き出している。GPU予算が有限な個人参加者ほど、計画的な実験順序が最終スコアに直結する。

セルフチェック

🩺 学習曲線の自動判定をサブミット前に

過学習の兆候を見逃さない簡易チェック

タスク3のような「train/valの乖離パターンから次の一手を選ぶ」ロジックは、簡易チェックとしてノートブックに常備しておくと、過学習の兆候を見逃さずに済む。

実験管理

🗃️ 自作の意思決定関数は実験管理の第一歩

MLflow/W&Bの前段階として有効

本格的なコンペではMLflowやWeights & Biasesを使うが、個人開発の初期段階では今日作った自作の意思決定関数でも十分に「何を・なぜ試したか」を後から振り返れる仕組みになる。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
過学習の兆候が出たら、とりあえずモデルを大きくして精度を上げようとする「モデルが弱いから精度が伸びない」という直感に頼ってしまうモデルを大きくするのは表現力不足のケースに有効。train_lossだけ下がりval_lossが頭打ちのケース(過学習)ではむしろ逆効果になりやすい
GPU予算を考えず、最初から一番強い設定で実験を始める「強い設定の方が良い結果が出るはず」と考え、段階を踏むのを面倒に感じる1回の実験に予算を使い切ると、何が効いたのか切り分けられなくなる。安い実験から始める方が限られた予算内で学びが多い
Mixup/CutMixのようなAugmentationは「多いほど良い」と考え常に全部盛りにする正則化を「品質向上のオプション」程度に軽く見てしまう強い正則化はエポック数など前提条件が揃って初めて効果が出る(Day119)。前提を無視すると単なる学習不足の原因になる
意思決定関数の出力を絶対的な正解として鵜呑みにするコードで出力された結果は「機械的で正しいはず」と思い込む関数はあくまで「たたき台」。クラス不均衡という追加リスクを人間側が読み取り、機械的な判定とは異なる選択をすることは十分にあり得る

🚀 次のステップ

  • 発展: recommend_cv_experiment_planに、Day116で学んだ「学習曲線の乖離幅」を入力として受け取り、乖離が大きいほど自動的にAugmentationレベルを引き上げる(basicrandaugmentmixup_cutmix)ロジックを追加してみましょう。タスク3のような判断を関数の中に組み込んで自動化する第一歩になります
  • 次回予告: Day 121 — 時系列ディープラーニング入門(LSTM)。CVブロック(Day114〜120)が完了したので、次は「16-18 時系列(LSTM・Prophet・Temporal Fusion Transformer)」ブロックへ進みます。GBDTベースの時系列特徴量(Day098-100)とは異なる、ニューラルネットワークならではの時系列の扱い方を学びます

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

1-5 DL基礎(PyTorch) 完了
6-10 NLP(後日)
11-15 CV 完了(本日総括)
16-18 時系列 ← 次回
19-20 マルチモーダル

Day114〜120の7日間でCNN基礎・転移学習・EfficientNet/timm・Augmentation応用・実験戦略までを一通り学び、CVブロックが完了した。次は時系列ブロック(16-18)へ進む。

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

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