📚 背景知識(読んでから問題へ)
Day172 のおさらい。 補正CV(決定13: √(CV² − A_test))は、乱数ありの LightGBM でも「平均の偏り」を消した。ところが候補を選ぶと、乱数なしの lgb_det を常に出す方が損が小さかった(0.33 対 0.75)。理由は、9本の乱数ありの候補がどれも本当に lgb_det より悪く、選んでも拾える得が無かったから。そこで決定14 を作った:
補正CV で見て、基準(lgb_det)より m×10⁻⁴ 以上良い候補があるときだけ乗り換える。m = 3(仮置き)
仮置きの理由。 m を大きくすると「悪い候補に乗り換える外れ」は減るが、「本当に良い候補を止めてしまう見逃し」は増える。Day172 の候補には本当に良い候補がいなかったので、見逃しの側は一度も測れていない。今日はその側を作る。
| 損の種類 | 起きる場面 | m を上げると |
|---|---|---|
| 外れの損 | 補正CV が「良い」と言ったが本当は基準より悪い候補に乗り換えた | 減る |
| 見逃しの損 | 本当に良い候補がいたのに、余裕が m に届かず基準のままにした | 増える |
作り方。 Day163〜172 と同じ生成器で、目的変数(消費カロリー)にかける雑音だけを大きくする。元データの雑音を 0.05 → 0.15、合成時の雑音を 0.03 → 0.10(どちらも log 空間の標準偏差)。特徴量の作り方・ワールドの乱数(3000+ワールド番号)・候補11本は Day172 と同じなので、「同じ人たちのデータで、測定の雑音だけが大きい」世界になる。雑音が大きいと、bagging や列抽出のように予測を平均で丸める設定が効きやすい、という予想に基づく。
用語整理(Day172 と同じものは省略):
| 用語 | 意味 |
|---|---|
| 低雑音ワールド | Day172 の 16 ワールド(雑音 0.05/0.03)。保存済みの予測 raw172_w*.npz を再利用。seed 組は s0・s1 だけ使う(今日と条件をそろえる) |
| 中雑音ワールド | 今日作った 12 ワールド(雑音 0.15/0.10)。raw173_w*.npz |
| 余裕(adv) | 補正CV で見た lgb_det − 補正CV最良(lgb_det 以外)。×10⁻⁴。正なら補正CV は乗り換えを勧めている |
| 真の得(gain) | 同じ2本の真の点数(truth 6万行、5 fold 平均の提出)の差 T5(lgb_det) − T5(候補)。正なら本当に良い |
| p | 実戦で「本当に良い候補がある問題」に当たる割合。今日は低雑音(無い)と中雑音(有る)を p : 1−p で混ぜて期待損を出す |
| 最悪の超過損 | p を 0〜1 のどこに置いても、その p での最良の m と比べて最大どれだけ損するか。p を知らないときの選び方の物差し |
数値はすべて縮小シミュレータの上の実測(仮定の上の数字)。実データ(S5E5)で同じ割合・同じ大きさになるとは限らない。
データ・保存物の構造(スキーマ)
| 名前 | 形・型 | 意味・作り方 | 使う場所 |
|---|---|---|---|
| Sex … Body_Temp, Dur_x_HR, Keytel_kcal | int / float(9列) | Day163〜172 と同じ合成。Calories の雑音だけ 0.05/0.03(低)→ 0.15/0.10(中) | 全候補 |
| raw172_w0..15.npz | oof (11,3,30000)・evk (11,3,5,80000) | 低雑音 16 ワールド(Day172 の保存物、作り直さない)。seed 組 s0・s1 だけ使う | 外れの損 |
| raw173_w0..11.npz | oof (11,2,30000)・evk (11,2,5,80000) | 中雑音 12 ワールド(今日、1,260 fit)。先頭2万行=test、残り6万行=truth | 見逃しの損 |
| adv | float(×10⁻⁴) | 補正CV(lgb_det) − 補正CV最良(lgb_det 以外)。ラベル不要の「余裕」 | 乗り換え判定 adv > m |
| gain | float(×10⁻⁴) | T5(lgb_det) − T5(候補)。truth 行の真の得(実戦では見えない) | 正しい/誤り・見逃しの判定 |
| regret | float(×10⁻⁴) | 選んだ候補の T5 − 候補中の T5 最良 | 方針の比較 |
評価指標(RMSLE)と、今日の損の測り方
RMSLE は log1p 空間の RMSE。今日の「損」は、選んだ1本の RMSLE が候補中の最良からどれだけ悪いか(×10⁻⁴)。余裕 m を上げると、低雑音ワールドでの外れの損は下がり、中雑音ワールドでの見逃しの損は上がる。下の図で、その2本の線が交差する付近が m の候補になる。
❓ 問題
タスクA(予想)— コードを書く前に
- 雑音を大きくした中雑音ワールドで、Day172 の候補11本のうち
lgb_detより本当に良くなるのはどれか。予想を1〜2本挙げ、理由を書け - 補正CV の「ずれ」(補正CV の差 − 真の差)の大きさは、雑音を大きくすると大きくなるか
- 余裕 m = 0, 1, 2, 3, 5 で、中雑音ワールドの損はどう並ぶか
タスクB(コーディング)
day172.pyの雑音2か所だけを引数にしたday173.pyを作り、雑音 0.05/0.03 で Day172 の予測と完全一致することを確かめる(決定9: 起動時の再現テスト)- 中雑音(0.15/0.10)で 12 ワールド × model seed 2組を作る
- 低雑音 16 ワールドと中雑音 12 ワールドで、m = 0〜8 の損・乗り換えの内訳(正しい/誤り)・外れの損・見逃しの損を出す(
analyze173.py)
タスクC(戦略)
- 本当に良い候補がある割合 p を知らないとき、m をいくつにするか。損の表から決め、理由を書け
- 良い候補の得が今日より小さい(例: 2〜3×10⁻⁴)問題では、選んだ m はどうなるか
タスクD(決定)
決定14 の m を改訂するか決め、設計書の印(◎○△✕)を付け直す。
💡 ヒント
ヒント1(方向性)
- 雑音が大きい = 1本の木が雑音に合わせて曲がりやすい。平均で丸める仕組み(行の抜き出し・列の抜き出し)が効く方向
- 補正CV のずれは「CV の当たり外れ」と「seed の手ぶれ」でできている。雑音の大きさとは別に、候補の系統ごとのくせが効く(Day172 の
et)
ヒント2(アプローチ)
- 外れの損は低雑音側で、見逃しの損は中雑音側で出る。片方の世界だけで m を決めると、もう片方を無視することになる
- p を知らないなら、p を 0〜1 で動かしたときの「最良の m からの最大の超過」を比べる(ミニマックス後悔)
ヒント3(コード骨格)
for (kind, w, st), h in D.groupby(["kind", "world", "st"]): h = h.set_index("cand"); b = h.Ca.drop("lgb_det").idxmin() adv = (h.Ca["lgb_det"] - h.Ca[b]) * 1e4 # 補正CV の余裕 gain = (h.T5["lgb_det"] - h.T5[b]) * 1e4 # 真の得 for m in MS: pick = b if adv > m else "lgb_det" regret = (h.T5[pick] - h.T5.min()) * 1e4 L = G.groupby(["kind", "m"]).regret.mean().unstack("kind") E = {p: (1 - p) * L["低雑音"] + p * L["中雑音"] for p in np.linspace(0, 1, 101)}
✅ 模範解答
タスクA(予想の模範と、実測との照合)
| # | 予想の模範 | 実測 |
|---|---|---|
| 1 | bagging の強い bag05・bag03_ff05 が良くなる | 外れ。 良くなったのは leaf63_b7(葉 63・行 0.7・列 0.7)で −6.51±0.45、12/12 ワールドで良い。bag08 も −1.33±0.32(ワールド×seed 組で 83%)。bag05 は +4.81、bag03_ff05 は +17.49 で悪いまま。「平均で丸める」は効いたが、効いたのは葉を増やして表現力を上げた上で軽く抜き出す組み合わせだった |
| 2 | 雑音が大きいほど補正CV のずれも大きくなる | 外れ。 ずれの SD は中雑音 2.2〜5.9、低雑音 3.0〜5.6 でほぼ同じ。et が両方で最大(5.9・5.6)。ずれは雑音の大きさより候補の系統で決まる |
| 3 | m を上げるほど見逃しで損が増える | 当たり。 m = 0〜2 は損 0(24/24 回とも leaf63_b7 に正しく乗り換え)。m = 3 で 2 回見逃し 0.53、m = 5 で 5 回 1.27、m = 8 で 15 回 4.12、常に lgb_det は 6.51 |
1が外れた理由(推測、未検証): 雑音を大きくしても、Keytel の式に沿った「時間 × 心拍」の強い構造は残る。葉 31 の lgb_det はその構造を表しきれず、葉 63 にすると表現力が足りるが雑音にも合わせやすくなる。そこを行・列の抜き出し 0.7 が抑えた、という読み。低雑音では leaf63_b7 は +5.44 で悪いので、「葉を増やす得」は雑音があるときだけ抜き出しと組み合わさって出ている。葉 63 だけ(抜き出しなし)の候補を置いていないので、どちらが効いたかは分けられていない。
タスクB(完全なコード)
day173.py は day172.py から次の差分だけを変えた(他は Day172 の模範解答のコードと同じ):
1,3c1,4
< """Day172: Day171 と同じワールドで、LightGBM の bagging・列抽出(model seed の揺れがある候補)を
< model seed 3通り × 5 fold で作り、fold 別の予測を平均せずに保存する。
< 使い方: OMP_NUM_THREADS=1 python day172.py <world> -> raw172_w<world>.npz
---
> """Day173: Day172 と同じ生成器・同じ候補11本で、目的変数の雑音だけを大きくしたワールドを作る(今日の本番は 0.15/0.10 の「中雑音」)。
> 狙い: bagging などの乱数ありの候補が、乱数なしの基準 lgb_det より本当に良くなる世界を作る。
> 使い方: OMP_NUM_THREADS=1 python day173.py <world> <noise_orig> <noise_syn> <n_seeds> -> raw173_w<world>.npz
> Day172 は noise_orig=0.05, noise_syn=0.03(このとき day172.py と同じ予測になる)
12a14
> NO, NS, NSEED = float(sys.argv[2]), float(sys.argv[3]), int(sys.argv[4])
14c16
< SEEDS = (0, 1, 2)
---
> SEEDS = tuple(range(NSEED))
23c25
< def make_original(n, rng): # Day163-171 と同じ
---
> def make_original(n, rng): # Day163-172 と同じ(雑音 NO だけ引数化)
31c33
< cal = np.clip((rate * dur * np.exp(rng.normal(0, 0.05, n))).round(), 1, None)
---
> cal = np.clip((rate * dur * np.exp(rng.normal(0, NO, n))).round(), 1, None)
36c38
< def synth_from(orig, n, rng): # Day163-171 と同じ
---
> def synth_from(orig, n, rng): # Day163-172 と同じ(雑音 NS だけ引数化)
44c46
< s["Calories"] = np.clip((s.Calories * np.exp(rng.normal(0, 0.03, n))).round(), 1, None)
---
> s["Calories"] = np.clip((s.Calories * np.exp(rng.normal(0, NS, n))).round(), 1, None)
96c98
< np.savez_compressed(f"raw172_w{W}.npz", oof=oof, evk=evk, ytr=train.Calories.values,
---
> np.savez_compressed(f"raw173_w{W}.npz", oof=oof, evk=evk, ytr=train.Calories.values,
再現確認: 雑音 0.05/0.03・seed 1組でワールド 0 を作り、Day172 の raw172_w0.npz の seed 0 と比べた → fold 別予測・OOF とも最大差 0.0(完全一致)。中雑音ワールド 0 は、パイロット(seed 1組)と本番(seed 2組)で lgb_det の OOF の sha256 先頭が同じ(1de1a259d41fe358)。
実行(run.sh は DYLD_LIBRARY_PATH に scikit-learn 同梱の libomp を入れて day173.py <w> 0.15 0.10 2 を呼ぶだけ。xargs 経由の sh -c では macOS が DYLD_ 変数を消すため、スクリプトの中で export した):
seq 0 11 | xargs -P 4 -n 1 ../run.sh # 中雑音 12 ワールドを 4 並行で作る(壁時計 1,984 秒) python analyze173.py <d172のディレクトリ> <midのディレクトリ> # 表1〜5 python extra173.py # 追加1・2
analyze173.py:
"""Day173 集計: 決定14(乱数なしの基準 lgb_det より補正CV が m×1e-4 以上良いときだけ乗り換える)の余裕 m を、 低雑音ワールド(Day172 の raw172_w0..15、本当に良い候補がほぼ無い)と 中雑音ワールド(raw173 mid w0..11、雑音 0.15/0.10、本当に良い候補がある)で測る。 seed 組は両方とも s0・s1(Day172 の s2 は使わない=条件をそろえる)。 使い方: python analyze173.py <d172のディレクトリ> <midのディレクトリ> """ import sys, glob import numpy as np, pandas as pd N = 20000 MS = [0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8] def sle(y, lp): return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2 def load(pattern, kind): rows = [] for f in sorted(glob.glob(pattern), key=lambda f: int(f.split("_w")[1].split(".")[0])): w = int(f.split("_w")[1].split(".")[0]) z = np.load(f); O, E, ytr, yev, names = z["oof"], z["evk"].astype(np.float64), z["ytr"], z["yev"], list(z["names"]) assert names[1] == "lgb_det" # 決定9: 名前と行の対応 ytru = yev[N:] for s in (0, 1): for j, n in enumerate(names): c = sle(ytr, O[j, s]).mean(); a = E[j, s, :, :N].var(axis=0).mean() t5 = sle(ytru, E[j, s].mean(0)[N:]).mean() rows.append(dict(kind=kind, world=w, st=f"s{s}", cand=n, C=np.sqrt(c), Ca=np.sqrt(c - a), T5=np.sqrt(t5))) return pd.DataFrame(rows) D = pd.concat([load(sys.argv[1] + "/raw172_w*.npz", "低雑音"), load(sys.argv[2] + "/raw173_w*.npz", "中雑音")]) print(D.groupby("kind").world.nunique().to_dict()) print("\n=== 表1 真の差 T5 − T5(lgb_det)(×1e-4、seed 組 s0/s1 平均、ワールド平均±SE)と、lgb_det より真に良かった割合 ===") P = D.pivot_table(index=["kind", "world", "st"], columns="cand", values="T5") R = P.sub(P["lgb_det"], axis=0) * 1e4 g = R.groupby(["kind", "world"]).mean() t = g.groupby("kind").agg(lambda s: f"{s.mean():+.2f}±{s.std(ddof=1)/np.sqrt(len(s)):.2f}").T t["低雑音 良い割合"] = (R.xs("低雑音") < 0).mean().round(2); t["中雑音 良い割合"] = (R.xs("中雑音") < 0).mean().round(2) print(t.to_string()) Q = D.pivot_table(index=["kind", "world", "st"], columns="cand", values="Ca") RC = Q.sub(Q["lgb_det"], axis=0) * 1e4 print("\n補正CV の lgb_det との差 − 真の差 の SD(×1e-4、候補ごと・ワールドと seed 組を通して)") print((RC - R).groupby("kind").std(ddof=1).round(2).T.to_string()) rows = [] for (kind, w, st), h in D.groupby(["kind", "world", "st"]): h = h.set_index("cand"); b = h.Ca.drop("lgb_det").idxmin() adv = (h.Ca["lgb_det"] - h.Ca[b]) * 1e4 # 補正CV で見た乗り換えの余裕 gain = (h.T5["lgb_det"] - h.T5[b]) * 1e4 # 乗り換えた場合の真の得(負なら害) best = h.T5.min() for m in MS + ["常にlgb_det"]: sw = (m != "常にlgb_det") and adv > m pick = b if sw else "lgb_det" rows.append(dict(kind=kind, world=w, st=st, m=m, regret=(h.T5[pick] - best) * 1e4, switch=sw, good=sw and gain > 0, bad=sw and gain <= 0, harm=-gain if (sw and gain <= 0) else 0.0, miss=gain if (not sw and adv > 0 and gain > 0) else 0.0, adv=adv, gain=gain, b=b)) G = pd.DataFrame(rows) G["m"] = G.m.astype(str) order = [str(m) for m in MS] + ["常にlgb_det"] print("\n=== 表2 余裕 m ごとの損(regret、×1e-4、ワールド×seed組 の平均±SE(ワールドで束ねた SE))と、乗り換えの内訳 ===") for kind in ("低雑音", "中雑音"): out = [] for m in order: x = G[(G.kind == kind) & (G.m == m)]; wv = x.groupby("world").regret.mean() out.append(dict(m=m, regret=f"{wv.mean():.2f}±{wv.std(ddof=1)/np.sqrt(len(wv)):.2f}", 乗り換え=int(x.switch.sum()), 正しい=int(x.good.sum()), 誤り=int(x.bad.sum()), 外れの損=round(x.harm.mean(), 2), 見逃しの損=round(x.miss.mean(), 2), 止めた良い候補=int(((x.miss > 0)).sum()), n=len(x))) print(f"[{kind}]"); print(pd.DataFrame(out).set_index("m").to_string()) print("\n=== 表3 余裕(adv)と真の得(gain):補正CV最良(lgb_det 以外)を選んだ時点の値 ===") H = G[G.m == "0"] for kind in ("低雑音", "中雑音"): x = H[H.kind == kind] print(f"[{kind}] 選ばれた候補 {x.b.value_counts().to_dict()} adv 中央値 {x.adv.median():.2f} gain 中央値 {x.gain.median():.2f} 相関 {np.corrcoef(x.adv, x.gain)[0,1]:.2f}") for lo, hi in [(-99, 0), (0, 1), (1, 2), (2, 3), (3, 5), (5, 99)]: y = x[(x.adv > lo) & (x.adv <= hi)] if len(y): print(f" adv ({lo},{hi}]: n={len(y):2d} 真に良い {int((y.gain>0).sum()):2d} gain 平均 {y.gain.mean():+.2f}") print("\n=== 表4 混ぜた世界: 中雑音(良い候補あり)の割合 p で、期待損 =(1−p)·低 + p·中 が最小になる m ===") L = G.groupby(["kind", "m"]).regret.mean().unstack("kind") for p in np.arange(0, 1.01, 0.1): e = (1 - p) * L["低雑音"] + p * L["中雑音"] print(f"p={p:.1f}: 最良 m={e.idxmin():>12s} 損 {e.min():.2f} | m=0 {e['0']:.2f} m=1 {e['1']:.2f} m=2 {e['2']:.2f} m=3 {e['3']:.2f} 常にlgb_det {e['常にlgb_det']:.2f}") d03 = L.loc["0"] - L.loc["3"] print(f"m=0 と m=3 が釣り合う p = {d03['低雑音']/(d03['低雑音']-d03['中雑音']):.2f}") # ワールドで対にした差(m=3 − m=0 など) print("\n=== 表5 ワールド対の差(×1e-4、平均±SE)===") for kind in ("低雑音", "中雑音"): W = G[G.kind == kind].groupby(["world", "m"]).regret.mean().unstack() for a, b in [("3", "0"), ("2", "0"), ("1", "0"), ("3", "1"), ("常にlgb_det", "1")]: d = W[a] - W[b]; print(f"[{kind}] m={a} − m={b}: {d.mean():+.2f}±{d.std(ddof=1)/np.sqrt(len(d)):.2f}") G.to_pickle("guard173.pkl"); D.to_pickle("dec173.pkl")
extra173.py(最悪の超過損と、良い候補の得を小さくした仮想):
"""Day173 追加: (1) 各 m の「最悪の超過損」(p を 0〜1 で動かしたとき、その p の最良 m との差の最大) (2) 中雑音で leaf63_b7 の真の得を縮めた仮想(得 g を 6.5 → 1〜6 にずらす = 補正CV の余裕も同じだけずらす)で、m ごとの見逃し 使い方: python extra173.py(analyze173.py の後) """ import numpy as np, pandas as pd G = pd.read_pickle("guard173.pkl") L = G.groupby(["kind", "m"]).regret.mean().unstack("kind") ps = np.linspace(0, 1, 101) E = pd.DataFrame({p: (1 - p) * L["低雑音"] + p * L["中雑音"] for p in ps}) ex = (E - E.min()).max(axis=1) print("=== 追加1 最悪の超過損(×1e-4)と、それが起きる p ===") print(pd.DataFrame(dict(最悪の超過損=ex.round(2), 起きるp=(E - E.min()).idxmax(axis=1).round(2))).loc[[str(m) for m in [0,0.5,1,1.5,2,3,4,5,6,8]]+["常にlgb_det"]].to_string()) # (2) 仮想: 中雑音で真の得と見かけの余裕を同じ量 d だけ小さくする(補正CVのずれの分布はそのまま) H = G[(G.kind == "中雑音") & (G.m == "0")] print("\n=== 追加2 良い候補の真の得を小さくした仮想(中雑音 24件、余裕も同じだけずらす): 見逃し件数と損 ===") rows = [] for g in [6.5, 5, 4, 3, 2, 1]: d = H.gain.mean() - g adv, gain = H.adv - d, H.gain - d for m in [0, 1, 2, 3, 4]: sw = adv > m loss = np.where(sw, np.where(gain > 0, 0, -gain), np.where(gain > 0, gain, 0)) rows.append(dict(得=g, m=m, 見逃し=int((~sw & (gain > 0)).sum()), 誤り=int((sw & (gain <= 0)).sum()), 損=round(loss.mean(), 2))) print(pd.DataFrame(rows).pivot(index="得", columns="m", values="損").to_string()) print(pd.DataFrame(rows).pivot(index="得", columns="m", values="見逃し").to_string())
実行結果(実測)
# ============ 実行環境: macOS (M1, メモリ8GB)・Python 3.13.12・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6・lightgbm 4.7.0(Day172 と同じ uv venv)
# 中雑音: 12ワールド × 5 fold × (HistGB 1 + LightGBM 10本 × seed 2) = 1,260 fit。OMP_NUM_THREADS=1・n_jobs=1
# 壁時計: 4 並行で 1,984 秒(1ワールド 554〜686 秒)。同時に仮想マシンとブラウザが CPU を使っていた(負荷平均 12〜18)
# 低雑音: Day172 の保存済み 16ワールドを再利用(seed 組 s0・s1 のみ)。新しい fit は無し
# パイロット(seed 1組、ワールド0 のみ、3 並行で各 634〜690 秒):
# 雑音 0.05/0.03 → Day172 と完全一致 / 0.15/0.10 → leaf63_b7 −6.75・bag08 −1.64 / 0.30/0.20 → leaf63_b7 −25.74・bag08 −6.85・bag05 −3.91
# 0.30/0.20 は得が大きすぎて「m を決める」問題にならないので、本番は 0.15/0.10 にした
{'中雑音': 12, '低雑音': 16}
=== 表1 真の差 T5 − T5(lgb_det)(×1e-4、seed 組 s0/s1 平均、ワールド平均±SE)と、lgb_det より真に良かった割合 ===
kind 中雑音 低雑音 低雑音 良い割合 中雑音 良い割合
cand
bag03_ff05 +17.49±0.34 +15.52±0.46 0.00 0.00
bag05 +4.81±0.35 +6.55±0.39 0.00 0.00
bag05_ff05 +12.13±0.28 +11.06±0.28 0.00 0.00
bag08 -1.33±0.32 +1.09±0.29 0.19 0.83
et +17.17±0.68 +11.08±0.70 0.00 0.00
ff05 +6.94±0.32 +4.50±0.34 0.00 0.00
hgb_default -0.11±0.28 +0.35±0.23 0.44 0.50
leaf15_b7 +18.15±0.33 +9.53±0.38 0.00 0.00
leaf63_b7 -6.51±0.45 +5.44±0.33 0.00 1.00
lgb_det +0.00±0.00 +0.00±0.00 0.00 0.00
lr05_it600_b7 +4.48±0.27 +4.45±0.35 0.00 0.00
補正CV の lgb_det との差 − 真の差 の SD(×1e-4、候補ごと・ワールドと seed 組を通して)
kind 中雑音 低雑音
cand
bag03_ff05 4.29 4.20
bag05 3.33 3.29
bag05_ff05 3.23 3.55
bag08 2.81 3.16
et 5.94 5.61
ff05 2.61 3.08
hgb_default 2.17 2.97
leaf15_b7 2.79 3.53
leaf63_b7 3.62 4.13
lgb_det 0.00 0.00
lr05_it600_b7 2.90 3.60
=== 表2 余裕 m ごとの損(regret、×1e-4、ワールド×seed組 の平均±SE(ワールドで束ねた SE))と、乗り換えの内訳 ===
[低雑音]
regret 乗り換え 正しい 誤り 外れの損 見逃しの損 止めた良い候補 n
m
0 1.12±0.42 16 2 14 0.81 0.00 0 32
0.5 1.09±0.42 15 2 13 0.78 0.00 0 32
1 0.68±0.16 13 2 11 0.36 0.00 0 32
1.5 0.65±0.16 12 2 10 0.34 0.00 0 32
2 0.51±0.14 7 1 6 0.19 0.01 1 32
3 0.47±0.14 6 1 5 0.15 0.01 1 32
4 0.36±0.12 3 1 2 0.04 0.01 1 32
5 0.33±0.11 0 0 0 0.00 0.01 2 32
6 0.33±0.11 0 0 0 0.00 0.01 2 32
8 0.33±0.11 0 0 0 0.00 0.01 2 32
常にlgb_det 0.33±0.11 0 0 0 0.00 0.01 2 32
[中雑音]
regret 乗り換え 正しい 誤り 外れの損 見逃しの損 止めた良い候補 n
m
0 0.00±0.00 24 24 0 0.0 0.00 0 24
0.5 0.00±0.00 24 24 0 0.0 0.00 0 24
1 0.00±0.00 24 24 0 0.0 0.00 0 24
1.5 0.00±0.00 24 24 0 0.0 0.00 0 24
2 0.00±0.00 24 24 0 0.0 0.00 0 24
3 0.53±0.36 22 22 0 0.0 0.53 2 24
4 0.53±0.36 22 22 0 0.0 0.53 2 24
5 1.27±0.56 19 19 0 0.0 1.27 5 24
6 1.72±0.64 17 17 0 0.0 1.72 7 24
8 4.12±0.99 9 9 0 0.0 4.12 15 24
常にlgb_det 6.51±0.45 0 0 0 0.0 6.51 24 24
=== 表3 余裕(adv)と真の得(gain):補正CV最良(lgb_det 以外)を選んだ時点の値 ===
[低雑音] 選ばれた候補 {np.str_('bag08'): 16, np.str_('hgb_default'): 15, np.str_('et'): 1} adv 中央値 0.11 gain 中央値 -0.87 相関 -0.06
adv (-99,0]: n=16 真に良い 6 gain 平均 -0.61
adv (0,1]: n= 3 真に良い 0 gain 平均 -4.74
adv (1,2]: n= 6 真に良い 1 gain 平均 -0.89
adv (2,3]: n= 1 真に良い 0 gain 平均 -1.24
adv (3,5]: n= 6 真に良い 1 gain 平均 -0.76
[中雑音] 選ばれた候補 {np.str_('leaf63_b7'): 24} adv 中央値 7.07 gain 中央値 6.49 相関 0.11
adv (2,3]: n= 2 真に良い 2 gain 平均 +6.31
adv (3,5]: n= 3 真に良い 3 gain 平均 +5.95
adv (5,99]: n=19 真に良い 19 gain 平均 +6.61
=== 表4 混ぜた世界: 中雑音(良い候補あり)の割合 p で、期待損 =(1−p)·低 + p·中 が最小になる m ===
p=0.0: 最良 m= 5 損 0.33 | m=0 1.12 m=1 0.68 m=2 0.51 m=3 0.47 常にlgb_det 0.33
p=0.1: 最良 m= 4 損 0.38 | m=0 1.01 m=1 0.61 m=2 0.46 m=3 0.48 常にlgb_det 0.94
p=0.2: 最良 m= 4 損 0.39 | m=0 0.90 m=1 0.54 m=2 0.41 m=3 0.48 常にlgb_det 1.56
p=0.3: 最良 m= 2 損 0.36 | m=0 0.78 m=1 0.47 m=2 0.36 m=3 0.49 常にlgb_det 2.18
p=0.4: 最良 m= 2 損 0.31 | m=0 0.67 m=1 0.41 m=2 0.31 m=3 0.49 常にlgb_det 2.80
p=0.5: 最良 m= 2 損 0.25 | m=0 0.56 m=1 0.34 m=2 0.25 m=3 0.50 常にlgb_det 3.42
p=0.6: 最良 m= 2 損 0.20 | m=0 0.45 m=1 0.27 m=2 0.20 m=3 0.50 常にlgb_det 4.03
p=0.7: 最良 m= 2 損 0.15 | m=0 0.34 m=1 0.20 m=2 0.15 m=3 0.51 常にlgb_det 4.65
p=0.8: 最良 m= 2 損 0.10 | m=0 0.22 m=1 0.14 m=2 0.10 m=3 0.51 常にlgb_det 5.27
p=0.9: 最良 m= 2 損 0.05 | m=0 0.11 m=1 0.07 m=2 0.05 m=3 0.52 常にlgb_det 5.89
p=1.0: 最良 m= 0 損 0.00 | m=0 0.00 m=1 0.00 m=2 0.00 m=3 0.53 常にlgb_det 6.51
m=0 と m=3 が釣り合う p = 0.55
=== 表5 ワールド対の差(×1e-4、平均±SE)===
[低雑音] m=3 − m=0: -0.65±0.42
[低雑音] m=2 − m=0: -0.61±0.43
[低雑音] m=1 − m=0: -0.44±0.38
[低雑音] m=3 − m=1: -0.21±0.07
[低雑音] m=常にlgb_det − m=1: -0.35±0.15
[中雑音] m=3 − m=0: +0.53±0.36
[中雑音] m=2 − m=0: +0.00±0.00
[中雑音] m=1 − m=0: +0.00±0.00
[中雑音] m=3 − m=1: +0.53±0.36
[中雑音] m=常にlgb_det − m=1: +6.51±0.45
=== 追加1 最悪の超過損(×1e-4)と、それが起きる p ===
最悪の超過損 起きるp
m
0 0.79 0.0
0.5 0.77 0.0
1 0.35 0.0
1.5 0.33 0.0
2 0.18 0.0
3 0.53 1.0
4 0.53 1.0
5 1.27 1.0
6 1.72 1.0
8 4.12 1.0
常にlgb_det 6.51 1.0
=== 追加2 良い候補の真の得を小さくした仮想(中雑音 24件、余裕も同じだけずらす): 見逃し件数と損 ===
m 0 1 2 3 4
得
1.0 0.37 0.44 0.90 0.88 0.86
2.0 0.34 0.40 0.55 1.31 1.33
3.0 0.23 0.49 0.65 0.88 1.93
4.0 0.00 0.32 0.66 0.90 1.21
5.0 0.00 0.00 0.40 0.83 1.15
6.5 0.00 0.00 0.00 0.53 0.53
m 0 1 2 3 4
得
1.0 5 6 11 11 12
2.0 4 6 8 14 15
3.0 2 4 6 8 15
4.0 0 2 4 6 8
5.0 0 0 2 4 6
6.5 0 0 0 2 2
読み方:
- 表1: 中雑音では
leaf63_b7が 12/12 ワールドで本当に良い(−6.51±0.45)。低雑音では同じ候補が +5.44 で悪い。同じ候補が、問題によって最良にも外れにもなる - 表1 下: 補正CV のずれの SD は雑音の大きさでほぼ変わらない(中 2.2〜5.9、低 3.0〜5.6)。だから m の決め方は「ずれの大きさ」ではなく「本当に良い候補がどれくらいの頻度・大きさで来るか」で決まる
- 表2: 低雑音の数字は Day172 の追加2 の s0・s1 の平均と一致(m=0: (0.75+1.49)/2 = 1.12、m=3: (0.57+0.37)/2 = 0.47)。中雑音では m ≤ 2 で損 0、m = 3 で見逃し 2/24
- 表3: 中雑音で補正CV が乗り換えを勧めた余裕は中央値 7.07、最小は 2〜3 の帯(2件、どちらも本当に良い)。この2件が m = 3 の見逃し
- 表4・追加1: p を知らないとき、m = 2 の最悪の超過損 0.18(p = 0、良い候補が無い世界で起きる)が最小。m = 3 は 0.53(p = 1)、常に
lgb_detは 6.51 - 追加2(仮想): 良い候補の得が 6.5 → 3 に縮むと、m = 2 の見逃しは 0 → 6/24、損 0.65。m = 0 なら 0.23。得が小さい問題では m = 2 でも見逃しが出る
タスクC(戦略の模範)
- m = 2。 p を知らないときの最悪の超過損が 0.18 で、0〜8 の中で最小。m = 3 は「良い候補がある世界」で 0.53 損し、m = 1 以下は「良い候補が無い世界」で 0.35〜0.79 損する。p が 0.3 以上なら m = 2 がそのまま最良、0.1〜0.2 なら m = 4 が最良だが m = 2 との差は 0.02〜0.08
- 得が小さい問題では m を下げる方が良いが、どこまで下げるかは今日のデータでは決められない。 追加2 の仮想では、得 3 のとき m = 0 で 0.23、m = 2 で 0.65。ただしこの仮想は「良い候補が本当に1本だけ・ずれの分布は中雑音と同じ」という置き方で、低雑音側の外れの損(m = 0 で 0.79 超過)と同時に下げる根拠にはならない。得が小さい良い候補を混ぜた第3の世界を作って測るまで、m = 2 を据え置く
タスクD(決定)
決定14 を改訂: 余裕 m を 3 → 2。
| # | 決定 | 採った案 | 採らなかった案と理由 | 前提が変わったら |
|---|---|---|---|---|
| 14(改訂) | 基準からの乗り換え | 乱数なしの基準(今は lgb_det)より補正CV が 2×10⁻⁴ 以上良い候補だけを1本目に採る。届かなければ基準を1本目、補正CV最良を2本目の候補にする。候補の系統ごとに補正CV のずれの SD を報告に添える(今日 2.2〜5.9) | (a) m = 3 のまま(良い候補がある世界で見逃し 2/24・損 0.53。最悪の超過損 0.53)。(b) m = 0(良い候補が無い世界で外れ 14/32・最悪の超過損 0.79)。(c) 常に基準(良い候補がある世界で 6.51 の損)。(d) p を見積もってから m を決める(実戦では p を測る手段が無い。ワールドは1つしかない)。(e) m を候補ごとのずれの SD の倍数で決める(et だけ SD が大きいので理屈は通るが、今日のデータで et が選ばれたのは低雑音の1件だけで、効果を測れない) | 良い候補の得が小さい問題(2〜3×10⁻⁴)が多い → 仮想では m = 0〜1 が良い。第3の世界で確かめてから下げる。et・NN など系統の違う候補が補正CV最良になる → その候補だけ m を上げる(SD 比で 5.6/3.3 ≈ 1.7 倍の m = 3.4 が目安、未測定) |
設計書の印の更新:
| 決定 | 前 | 後 | 根拠 |
|---|---|---|---|
| 13(補正CV で比べる) | ◎ | ◎ | 中雑音でも補正CV最良は 24/24 回 leaf63_b7=真の最良(表3) |
| 14(乗り換えに余裕) | △(m = 3 仮置き、見逃し未測定) | ○(m = 2、2種類の世界で最悪の超過損 0.18) | 表2・表4・追加1。◎にしないのは、得が小さい良い候補(2〜3)の世界と、系統の違う候補が良い世界を測っていないため |
| 12(最終2本) | 1本目=決定14 の結果、2本目=公開LB最良 | 変更なし | 今日は公開LB を測っていない |
📊 結果の可視化
上: 同じ leaf63_b7 が、低雑音では +5.44 で悪く、中雑音では −6.51 で 12/12 ワールド最良。下: 良い候補がある割合 p を知らないとき、m=2 の最悪の超過損 0.18 が最小(旧 m=3 は 0.53)。
🪜 Step-by-Step 解説
Step 1: 「見逃しが起きる世界」を作る方針を決める
Day172 の弱点は「本当に良い候補がいない」こと。候補を人為的に良くする(例: 真の値を少し混ぜる)と CV と真の関係が壊れるので、データの作り方(雑音)だけを変えて、同じ候補が自然に良くなる世界を探した。
Step 2: パイロットで雑音の大きさを決める
雑音 0.05/0.03(Day172 と同じ)・0.15/0.10・0.30/0.20 でワールド0 を1つずつ作った。0.05/0.03 は Day172 と完全一致(コードが変わっていない証拠)。0.30/0.20 は leaf63_b7 の得が 25.7 と大きすぎ、どの m でも見逃さない。0.15/0.10 は得 6.75 で、m = 3〜8 の範囲で見逃しが起きうる大きさだった。
Step 3: 本番を回す
中雑音 12 ワールド × seed 2組。低雑音は作り直さずに Day172 の保存物を使い、seed 組を s0・s1 にそろえた。M1 8GB で仮想マシンが動いていたため、Day172 より 1 ワールドあたりの時間が長かった(seed 2組で 554〜686 秒、Day172 は seed 3組で 282〜355 秒)。
Step 4: 損を2つに分ける(表2)
乗り換えた結果を「正しい/誤り」に分け、誤りの損(外れ)と、止めたせいで取り逃した得(見逃し)を別々に足した。低雑音は外れだけ、中雑音は見逃しだけが出る。m の効き方が正反対なのが表で見える。
Step 5: 混ぜて m を決める(表4・追加1)
実戦でどちらの世界に当たるかは分からない。p を 0〜1 で動かし、それぞれの p での最良の m からの超過を取り、その最大が最小になる m を選ぶ(ミニマックス後悔)。結果は m = 2(0.18)。
Step 6: 決定の弱いところを書いておく(追加2)
良い候補の得が小さいと m = 2 でも見逃しが出る(仮想)。ここは測っていないので印を ○ に留め、「前提が変わったら」に書いた。
📐 数学・統計の補足(文系向け)
2つの間違いの釣り合い。 煙探知機の感度と同じ。感度を上げると(m を小さく)トーストの煙でも鳴る(外れ)。下げると(m を大きく)本当の火事でも鳴らない(見逃し)。どこに置くのが良いかは、「火事がどれくらいの頻度で起きるか(p)」と「火事を見逃したときの損(得の大きさ)」で決まる。
p を知らないときの選び方(ミニマックス後悔)。 「後悔」=その状況で一番良い選び方をした場合との差。p がいくつでも後悔の最大が一番小さい選び方を採る。今日の表では:
| m | p = 0(良い候補なし)での後悔 | p = 1(良い候補あり)での後悔 | 最大 |
|---|---|---|---|
| 0 | 0.79 | 0 | 0.79 |
| 1 | 0.35 | 0 | 0.35 |
| 2 | 0.18 | 0 | 0.18 |
| 3 | 0.14 | 0.53 | 0.53 |
| 常に基準 | 0 | 6.51 | 6.51 |
(後悔は ×10⁻⁴。期待損は p の一次式なので、最大は必ず p = 0 か p = 1 の端で起きる)
なぜ m = 2 なのか、感覚的に。 補正CV のずれの SD はおよそ 3。良い候補の得は 6.5 なので、余裕の見え方はおよそ「6.5 ± 3」。m = 2 なら「6.5 − 1.5SD」より下でしか見逃さない。悪い候補(得は −1 前後)の見え方は「−1 ± 3」で、m = 2 を超えるのは「+1SD」より上のときだけ。2つの山のちょうど間に線を引いた形になっている。
🏆 Kaggleでの実践的な使い方
- 「CV が良いから乗り換える」には、乗り換えの余裕を決めておく。 今日の縮小シミュレータでは 2×10⁻⁴(RMSLE)。実データでは、自分の CV のずれの SD(fold を変える・seed を変える、で測れる部分)の約 0.6 倍を目安にし、その根拠を設計書に残す
- 同じ設定が、データの雑音で最良にも外れにもなる。
leaf63_b7は低雑音で +5.44、中雑音で −6.51。他人のノートブックで効いた設定を、自分の CV で確かめずに持ち込まない - 見逃しの損は外れの損より大きくなりうる。 今日は「常に基準」で 6.51 失い、m = 0 の外れの損 0.81 の8倍。保守的にしすぎるのも損になる
- 余裕は「基準と1本目の乗り換え」にだけ使う。 2本目の枠(決定12)は見逃しの保険として補正CV最良を入れる
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 余裕は大きいほど安全 | 外れの損だけを見る | 見逃しの損が増える。今日は m = 8 で 15/24 見逃し・損 4.12、常に基準で 6.51 |
| 1種類の世界で m を決めれば良い | 自分の手元の状況が全部だと思う | 低雑音だけなら m ≥ 5 が最良、中雑音だけなら m ≤ 2 が最良で逆向き。p を知らないなら両方の後悔を見る |
| 雑音が大きいと補正CV は当てにならなくなる | 「データが汚い=評価も汚い」と考える | ずれの SD は中 2.2〜5.9・低 3.0〜5.6 でほぼ同じ。今日の中雑音では補正CV最良が 24/24 回真の最良だった |
| bagging が効くなら抜き出し率は小さいほど良い | 「平均で丸める」を強めれば強めるほど良いと思う | 中雑音でも bag03_ff05 は +17.49、bag05 は +4.81 で悪い。効いたのは葉 63+抜き出し 0.7 |
| 仮想の計算(追加2)を実測として扱う | 表の形が同じなので区別しなくなる | 追加2 は「ずれの分布は同じまま得だけ小さくする」と置いた導出。得が小さい世界を作って測るまで、m を下げる根拠にしない |
🧭 扱わなかった範囲と理由
- 得が小さい良い候補(2〜3×10⁻⁴)の世界: 雑音を 0.15/0.10 と 0.30/0.20 の間で調整すれば作れるが、今日の計算時間(M1 8GB・他の負荷あり、1 ワールド約 10 分)では 12 ワールド増やすと約 35 分かかるため、仮想(追加2)に留めた。次に測るべき最重要項目
- 葉を増やす効果と抜き出しの効果の分離:
leaf63(抜き出しなし)の候補を置いていない。leaf63_b7が勝った理由は推測 - 系統別の m:
etのずれの SD が両方の世界で最大(5.6・5.9)だが、今日etが補正CV最良になったのは低雑音の1件だけで、系統別の m の効果は測れない - 公開LB と 2本目の枠: 今日は公開LB の再分割をしていない。決定12 の「2本目=公開LB最良」の得は Day172 から変わらず未測定
- seed 組の数: 中雑音は seed 2組(Day172 は 3組)。avg3(3 seed 平均の提出)での損は出していない
- p の実際の値: 実戦で「本当に良い乱数ありの候補がある問題」がどれくらいの割合かは分からない。今日の m = 2 は p を知らない前提の選び方
- 持ち越し(Day172 から): OOF 側と test 側の seed の揺れの分離、
noKeytel・raw7の補正後のずれ、補正CV と入れ子CV の比較、A_test の推定誤差、K > 13、Day170 のハイブリッド・狭い10本の出し直し、SE 個数差と分割の新しさ(Day165)、Day162 の選択バイアス、真の効果ちょうど0の偽採用率、Nadeau & Bengio・Krogh & Vedelsby 原典(今日も記憶による)
🚀 次のステップ
- 発展: 雑音 0.08〜0.12 の間で「良い候補の得が 2〜3」になる世界を作り、低雑音・中雑音と3種類で m を決め直す(追加2 の仮想を実測に置き換える)
- 発展:
leaf63(抜き出しなし)・leaf63_b5などを足し、中雑音で効いたのが葉の数か抜き出しかを分ける - 次回予告: Day 174 — Playground Series 実戦投入⑨(得が小さい良い候補の世界を作り、決定14 の m = 2 を3種類の世界で確かめる)。月替わりテーマ3「画像(CV)系コンペへの応用」は 10/25 から
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: