📚 背景知識(読んでから問題へ)
sqrt(p(1-p)/n) でサンプルサイズnに依存するため、テストデータのごく一部しか使わないPublic LBはCVよりノイズが大きい。この差をシミュレーションで再現し、「CV1つ+LB1つ」の定石ミックス戦略が本当に最適かを検証する。| キーワード | 直感的な説明 |
|---|---|
| 提出予算(submission budget) | コンペ期間中に使える提出回数の総量。探索・検証・最終選定への配分を計画すること |
| Public LB / Private LB | テストデータを分割し、一部は提出のたびに即時採点・公開(Public)、残りはコンペ終了後のみ採点・公開(Private)される仕組み |
| シェイクアップ(shake-up) | コンペ終了時にPublic LBとPrivate LBの順位が大きく入れ替わる現象。Public LBへの過学習が主因 |
| 最終提出2枠(final submissions) | コンペ終了までに選ぶ、Private LB成績に使われる最大2つの提出。選ばれた2つのうち良い方が採用される |
| 標準誤差 sqrt(p(1-p)/n) | 正解率をサンプルで測定するときの測定ノイズの目安。サンプルサイズnが小さいほどノイズが大きい |
🎯 問題
タスクA(戦略・記述)
30日間のコンペ、1日最大5回・合計150回の提出枠があるが実際に使い切る参加者は少なく、上位陣は合計30〜50回程度に留める傾向があるとする。以下の3フェーズに提出予算をどう配分するか計画し、「1日1提出ルール」の狙いを今日のノイズの話と関連づけて説明する。
タスクB(実装・シミュレーション)
| 項目 | 値 |
|---|---|
| 候補モデル数 | N_CAND = 30(30日コンペで試した候補モデル) |
| 本当の実力の分布 | true_skill ~ N(0.800, 0.010) |
| ノイズの標準偏差 | se(n) = sqrt(0.8 * 0.2 / n) |
| サンプルサイズ | CV実質8000件 / Private 4000件 / Public 1000件 |
| 比較する戦略 | CV一択 / Public LB一択 / 定石ミックス(CV1+LB1) / CV1枠のみ |
| 反復回数 | N_SIM = 20000 |
import numpy as np rng = np.random.default_rng(0) N_CAND = 30 N_SIM = 20000 TRUE_MEAN = 0.800 TRUE_STD = 0.010 def se(n, p=0.80): """サンプルサイズnでの正解率測定の標準誤差""" return np.sqrt(p * (1 - p) / n) N_CV_EFFECTIVE = 8000 N_PRIVATE = 4000 N_PUBLIC = 1000 CV_STD = se(N_CV_EFFECTIVE) PRIVATE_STD = se(N_PRIVATE) PUBLIC_STD = se(N_PUBLIC)
true_skillを生成し、CV/Public/Privateそれぞれ別ノイズを加えた3つの観測値を作るprivate_scoreを記録する💡 ヒント
タスクAは正解が1つに決まる問題ではない。「なぜその回数配分にするか」をコンペの進行段階の役割(広く探る/絞り込む/安全に選ぶ)と紐づけて説明できることが重要。タスクBは「同じtrue_skillから3種類のノイズ付き観測値を作り、その観測値だけを頼りに意思決定した場合どれだけ取りこぼすか」を測るシミュレーションで、CVは学習データ全体を使えるためノイズが小さく、Public LBはテストのごく一部しか使えないためノイズが大きいというサンプルサイズの非対称性を乱数の標準偏差の違いとして表現する。
se(n, p=0.80)関数はどの観測値のノイズにも共通して使う。N_CV_EFFECTIVE > N_PRIVATE > N_PUBLICの順にサンプルサイズが大きい(=ノイズが小さい)ことを先に確認するnp.argsort(-cv_score)[:2]のようにインデックスを取得すれば「上位2件」が得られる- 戦略C(定石ミックス)は「CV最高」と「Public LB最高」が同じモデルを指すことがある。その場合は2枠目をCV2位で埋める
- 「選ばれた2枠のうちprivate_scoreが高い方」=
private_score[picks].max()。Kaggleの「最終2つのうち良い方が採用される」ルールに対応 - 「本当に実力最良のモデル」は
np.argsort(-true_skill)[0]。これが最終2枠のインデックスに含まれるかをinで判定する
results = {"cv_cv": [], "lb_lb": [], "mixed": [], "single_cv": []}
best_possible, truebest_private = [], []
hit_truebest = {"cv_cv": 0, "lb_lb": 0, "mixed": 0}
for sim in range(N_SIM):
true_skill = rng.normal(TRUE_MEAN, TRUE_STD, N_CAND)
cv_score = true_skill + rng.normal(0, CV_STD, N_CAND)
public_score = true_skill + rng.normal(0, PUBLIC_STD, N_CAND)
private_score = true_skill + rng.normal(0, PRIVATE_STD, N_CAND)
order_true = np.argsort(-true_skill)
best_possible.append(private_score.max())
truebest_private.append(private_score[order_true[0]])
top2_cv = np.argsort(-cv_score)[:2]
results["cv_cv"].append(private_score[top2_cv].max())
if order_true[0] in top2_cv: hit_truebest["cv_cv"] += 1
top2_lb = np.argsort(-public_score)[:2]
results["lb_lb"].append(private_score[top2_lb].max())
if order_true[0] in top2_lb: hit_truebest["lb_lb"] += 1
best_cv_idx = np.argsort(-cv_score)[0]
best_lb_idx = np.argsort(-public_score)[0]
picks = [best_cv_idx, np.argsort(-cv_score)[1]] if best_cv_idx == best_lb_idx else [best_cv_idx, best_lb_idx]
results["mixed"].append(private_score[picks].max())
if order_true[0] in picks: hit_truebest["mixed"] += 1
results["single_cv"].append(private_score[best_cv_idx])
✅ 模範解答
# --- ノイズの大きさを確認 --- print(f"CV_STD={CV_STD:.5f} PRIVATE_STD={PRIVATE_STD:.5f} PUBLIC_STD={PUBLIC_STD:.5f}") # 実測: CV_STD=0.00447 PRIVATE_STD=0.00632 PUBLIC_STD=0.01265 # → Public LBのノイズはCVの約2.8倍、Privateの約2.0倍 # --- モンテカルロシミュレーション本体(ヒント3のループを実行) --- best_possible = np.array(best_possible) for k, arr in results.items(): arr = np.array(arr) regret = (best_possible - arr).mean() print(f"{k:10s} mean_private={arr.mean():.5f} regret={regret:.5f}") # ============ 実測結果 ============ # [参考] 神視点の理論上限 best_possible_private mean = 0.82423 # [参考] 真に実力最良のモデルを選んだ場合の private mean = 0.82048 # # cv_cv mean_private=0.82147 regret_vs_bestpossible=0.00276 # mixed mean_private=0.82064 regret_vs_bestpossible=0.00359 # single_cv mean_private=0.81866 regret_vs_bestpossible=0.00557 # lb_lb mean_private=0.81742 regret_vs_bestpossible=0.00681 # # cv_cv P(真に実力最良のモデルを最終2枠に含める)=0.774 # mixed P(真に実力最良のモデルを最終2枠に含める)=0.690 # lb_lb P(真に実力最良のモデルを最終2枠に含める)=0.401 # # cv_cv P(採用した最終提出が真の下位半分由来)=0.000 # mixed P(採用した最終提出が真の下位半分由来)=0.003 # lb_lb P(採用した最終提出が真の下位半分由来)=0.020
📊 結果の可視化
戦略ごとの「真に実力最良のモデルを最終2枠に含める確率」:
「採用した最終提出が真の下位半分由来だった」確率: CV一択 0.0% / 定石ミックス 0.3% / Public LB一択 2.0%(シェイクアップで沈む典型パターンに対応)
| 戦略 | 平均private | regret | 真ベストを含む確率 |
|---|---|---|---|
| 神視点(理論上限) | 0.82423 | 0(基準) | — |
| CV一択 | 0.82147 | 0.00276 | 77.4% |
| 定石ミックス | 0.82064 | 0.00359 | 69.0% |
| CV1枠のみ | 0.81866 | 0.00557 | — |
| Public LB一択 | 0.81742 | 0.00681 | 40.1% |
🪜 Step-by-Step 解説
1なぜ同じtrue_skillから3種類の観測値を作るのか
true_skill = rng.normal(TRUE_MEAN, TRUE_STD, N_CAND)
cv_score = true_skill + rng.normal(0, CV_STD, N_CAND)
public_score = true_skill + rng.normal(0, PUBLIC_STD, N_CAND)
private_score = true_skill + rng.normal(0, PRIVATE_STD, N_CAND)
2なぜCV一択がミックス戦略に勝つことがあるのか
# cv_cv regret_vs_bestpossible=0.00276
# mixed regret_vs_bestpossible=0.00359
# lb_lb regret_vs_bestpossible=0.00681
3なぜ「下位半分から選んでしまう確率」がCV一択でほぼ0なのか
# cv_cv P(下位半分由来)=0.000
# mixed P(下位半分由来)=0.003
# lb_lb P(下位半分由来)=0.020
🔁 追加検証(Public LBのサンプルサイズを大きくした場合)
「CV一択がミックスに勝つ」という結論は、Public LBのノイズがCVよりかなり大きいという前提に強く依存する。この前提を崩した場合(Public/Privateの分割比率を逆転させ、Publicの取り分を大きくしてノイズを減らす)に結論が変わるかを確認した。
# N_PUBLIC=3000, N_PRIVATE=2000 のケース
# CV_STD=0.00447 PRIVATE_STD=0.00894 PUBLIC_STD=0.00730
#
# cv_cv mean_private=0.82258
# mixed mean_private=0.82242
# lb_lb mean_private=0.82116
🧮 数学・統計の補足(文系向け)
選挙の当落を予想する世論調査で、10人にしか聞かなかった調査と、10,000人に聞いた調査があったら、どちらを信じるべきかは直感的に分かるはずです。10人の調査はたまたま偏った10人に当たっただけで結果が大きくブレる可能性があります。今日のPublic LB(1000件)とCV(実質8000件)の関係もこれと同じで、Public LBは「一部の人にしか聞いていない世論調査」、CVは「もっと多くの人に聞いた世論調査」に相当し、後者の方が本当の実力に近い数字を返します。
30枚の宝くじがあり、それぞれ当たりやすさ(true_skill)が少しずつ違うとします。中身は直接見えず、「うわさ話(CV)」と「近所の人の体験談(Public LB)」という間接情報しか得られません。うわさ話の方が多くの人から集めた情報なので信頼度が高く、体験談は少人数の話なので当てにならないことがある、という状況で最終的にどの2枚を買うかを決めるのが今日のシミュレーションです。regretは「中身が最初から見えていたら得られたはずの当たり」と「実際に選んだ2枚での当たり」の差、つまり情報不足によって取りこぼした分を表します。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☑ 時系列(Time Series)
📉 「CVを信じよ」は条件付きの経験則
Public LBのサンプルサイズが小さいほど強く成り立つ
コンペのDiscussionタブでPublic/Privateの分割比率が公開されていれば、その比率から「今回はどれくらいCVを信じるべきか」を数値的に見積もることができる。
🎫 最終提出2枠は「保険」の質を吟味する
LB側の情報が信頼できる場合にこそ機能する定石
「CV1つ+LB1つ」の定石は万能ではない。テストデータが小さいコンペや、CVとLBの乖離が大きいコンペでは、あえて「CV上位2つ」を選ぶ判断も選択肢に入れるべき。
🗓️ 提出予算は3フェーズで配分する
探索→検証→最終選定
序盤に手数を使いすぎると、終盤の最終選定に使える回数が減り、CVとPublic LBの乖離を落ち着いて検討する余裕がなくなる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| Public LBの順位が上がったら、モデルが本当に改善したと思う | 目に見える数字が上がるとつい信じたくなる | 今日の実測のようにPublic LBはCVよりノイズが大きい。小さな順位変動は測定誤差の範囲内であることが多い |
| 「CV1つ+LB1つ」の定石ミックスは常にCVだけより優れていると思い込む | 情報源を混ぜれば必ず安全になるという直感 | 今日の実測ではCV一択(regret 0.00276)がミックス(regret 0.00359)を上回った。混ぜる情報源自体のノイズが大きい場合、混ぜることが逆に不利になることがある |
| 提出回数は多いほど良い結果につながると思う | 「試行回数=改善の機会」という単純な発想 | 提出回数を使う「タイミング」(探索か検証か最終選定か)の方が回数そのものより重要。終盤にむやみに提出を重ねるとPublic LBへの過学習を招きやすい |
| CVスコアさえ良ければPrivate LBも必ず良いと思う | CVは「万能な予言者」だという期待 | 今日の実測でもCV一択のregretは0ではない(0.00276)。CVもサンプルサイズに応じたノイズを持つ推定値の一つであり、完璧な予言者ではない |
🚀 次のステップ
- 発展: N_CAND=30を10個や100個に変えた場合、また候補モデル間の実力差TRUE_STDを今日より小さく・大きくした場合に、CV一択とミックス戦略の優劣がどう変わるか試してみましょう
- 次回予告: Phase6テーマ4「コンペ戦略・Gold 3枚達成プラン」の2日目に入ります。今日はコンペ終了時点の「最終提出の選び方」を扱いましたが、次回はコンペ進行中の意思決定、CVスコアが伸び悩んだときに新しい特徴量を試すべきか、アンサンブルの微調整に留めるべきかという探索と活用のトレードオフを、限られた提出予算のもとでどう判断するかを扱う予定です
提出予算配分プラン(タスクA・例)
最終選定フェーズで初めてPublic LBを「保険」として使う。それ以外のフェーズはCVの安定した改善だけを判断材料にする。
Phase 6 の学習マップ(全20テーマ)
Day141で最終提出の選び方(CV vs Public LB)を実測。次回以降、コンペ進行中の探索/活用トレードオフや提出予算の実務判断に進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: