📚 背景知識(読んでから問題へ)
Day173 のおさらい。 2 種類の世界で余裕 m を測った。
| 世界 | 雑音(元/合成) | 本当に良い候補 | m を上げると |
|---|---|---|---|
| 低雑音(Day172、16 ワールド) | 0.05 / 0.03 | ほぼ無し | 外れの損が減る(m=0: 1.12 → m≥5: 0.33) |
| 中雑音(Day173、12 ワールド) | 0.15 / 0.10 | leaf63_b7、得 6.51 | 見逃しの損が増える(m≤2: 0 → m=3: 0.53) |
2 つを混ぜる割合 p を知らないときの「最悪の超過損」が最小の m=2 を採った(0.18)。ただし弱点が2つ残った。
- 得が小さい良い候補への弱さを、仮想でしか見ていない。 Day173 の追加2 は「中雑音の 24 件の余裕と得を同じ量だけ下げる」という導出で、得 3 なら m=2 の損 0.65・見逃し 6/24 と出した。補正CV のずれの分布が得の大きさと無関係だと置いている
leaf63_b7がなぜ勝ったか分かっていない。 葉 63(表現力)と抜き出し 0.7(平均で丸める)のどちらが効いたか、候補が無いので分けられなかった
今日はこの2つを実測する。雑音を中間の 0.12/0.08 に置き、leaf63_b7 の得が 2〜3 になる世界を作る。分離のため、葉 63・抜き出しなし(leaf63、乱数なし) と 葉 31・抜き出し 0.7(b7) を候補の末尾に足す。m を決める集計は Day173 とそろえるため元の 11 本だけで行い、足した2本は分離(表6)と感度(表7)にだけ使う。
用語整理(Day173 と同じものは省略):
| 用語 | 意味 |
|---|---|
| 小得ワールド | 今日の 12 ワールド(雑音 0.12/0.08)。raw174_w*.npz。生成器・ワールドの乱数(3000+番号)・候補 0〜10 は Day172・173 と同じ |
| 2×2 の分離 | 葉 {31, 63} × 抜き出し {なし, 0.7} の 4 本: lgb_det・b7・leaf63・leaf63_b7。交互作用 = leaf63_b7 − leaf63 − b7(どれも lgb_det との差) |
| 選択の上振れ | 補正CV最良(lgb_det 以外の 10 本から最小を選んだもの)の余裕 adv − 真の得 gain。10 本から一番良く見えるものを選ぶので、見かけが真より良く出やすい(勝者の呪い) |
数値はすべて縮小シミュレータの上の実測。実データ(S5E5)で同じ大きさになるとは限らない。
データ・保存物の構造(スキーマ)
| 名前 | 形・型 | 意味・作り方 | 使う場所 |
|---|---|---|---|
| Sex … Body_Temp, Dur_x_HR, Keytel_kcal | int / float(9列) | Day163〜173 と同じ合成。Calories の雑音だけ 0.05/0.03(低)・0.12/0.08(小得)・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 ワールド(Day173 の保存物) | 見逃しの損 |
| raw174_w0..11.npz | oof (13,2,30000)・evk (13,2,5,80000) | 小得 12 ワールド(今日、1,440 fit)。候補 0〜10 は同じ、11=leaf63・12=b7 | 見逃し・2×2 の分離 |
| adv / gain / regret | float(×10⁻⁴) | adv=補正CV の余裕、gain=真の得、regret=選んだ1本の T5 − 候補中の最良 | 表2〜5 |
評価指標(RMSLE)と、今日の損の測り方
RMSLE は log1p 空間の RMSE。「損」は選んだ1本の RMSLE が候補中の最良からどれだけ悪いか(×10⁻⁴)。下の図は3種類の世界での m ごとの損。小得(青)は中雑音(金)と同じ向きに上がるが、傾きがゆるく、m=0.5〜2 の差は 0.14 しかない。
❓ 問題
タスクA(予想)— コードを書く前に
- 小得ワールドで m=2 の損は、Day173 の仮想(得 3 で 0.65、得 2 で 0.55)と比べてどうなるか
- 3 種類の世界(低雑音・小得・中雑音)の最悪の超過損で、最小になる m は 2 のままか
leaf63(葉 63・抜き出しなし)とb7(葉 31・抜き出し 0.7)は、lgb_detより良いか悪いか
タスクB(分析)
day173.pyの末尾に候補2本を足したday174.pyを作り、雑音 0.05/0.03 で候補 0〜10 が Day172 の予測と完全一致することを確かめる(決定9)- 小得(0.12/0.08)で 12 ワールド × model seed 2組を作り、低雑音・中雑音の保存物と合わせて m=0〜8 の損を出す。低雑音・中雑音の数字が Day173 と一致することも確かめる
- Day173 の追加2(仮想)の予測と、今日の実測を同じ得の大きさで並べる
- 2×2 で葉の数と抜き出しの効果を分ける
タスクC(戦略)
- 決定14 の m を変えるか。3 種類の世界の表から決め、理由を書け
- 表6・表7 の結果から、決定14 の「基準=
lgb_det」という置き方自体をどう直すか
💡 ヒント
ヒント1(方向性)
- 得が小さいと、余裕 adv の山(得 ± ずれ)が m の線に近づく。見逃しが増えるのは当然だが、ずれの SD も同じとは限らない
- 2×2 は「4 本の差」から主効果と交互作用を出すだけ。足し算で説明できない分が交互作用
ヒント2(アプローチ)
- 最悪の超過損: 期待損は混ぜる割合の一次式なので、最大は必ず「どれか1種類の世界だけ」の端で起きる → 各世界の超過損の最大を取ればよい
- 仮想と実測を比べるときは、仮想の得を実測の平均得に合わせる
ヒント3(コード骨格)
L = G.groupby(["kind", "m"]).regret.mean().unstack("kind") X = L - L.min() # 各世界の最良 m からの超過 X["最悪(3世界)"] = X[["低雑音", "小得", "中雑音"]].max(axis=1) S = R.xs("小得").groupby("world").mean() # R = T5 − T5(lgb_det) inter = S["leaf63_b7"] - S["leaf63"] - S["b7"]
✅ 模範解答
タスクA(予想の模範と、実測との照合)
| # | 予想の模範 | 実測 |
|---|---|---|
| 1 | 仮想どおり 0.5〜0.65 前後 | ほぼ当たり。 小得の平均得は 1.93(中央値 2.15)。m=2 の実測損 0.67(仮想 0.54)。ただし m=3 は実測 0.78・見逃し 7 に対し仮想 1.27・見逃し 14 で、仮想は大きい m の見逃しを2倍に見積もっていた(追加の表) |
| 2 | m を下げる方が良くなる(1 か 1.5) | 外れ。 3 世界の最悪の超過損は m=2.5 が 0.14、m=2 が 0.18、m=1 が 0.35。小得だけなら m=0.5 が最良(0.53)だが、m=2 との差は 0.14±0.07(ワールド対、表5)と小さく、低雑音での外れの損(m=1 で 0.35 超過)の方が重い |
| 3 | 両方とも少し良い | 半分外れ。 leaf63 は −12.72±0.29(12/12 ワールドで良い)、b7 は +5.79±0.27(全部悪い)。抜き出し 0.7 は葉 31 でも葉 63 でも悪くし、交互作用は +4.47±0.24。leaf63_b7 の −2.47 は「葉 63 の大きな得」から「抜き出しの損」を引いた残りだった |
タスクB(完全なコード)
day174.py は day173.py から次の差分だけを変えた(他は Day173 の模範解答と同じ):
1,4c1,4
< """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 と同じ予測になる)
---
> """Day174: Day173 と同じ生成器・同じ候補11本に、分離用の2本(leaf63=葉63・抜き出しなし、b7=葉31・抜き出し0.7)を末尾に足す。
> 今日の本番は雑音 0.12/0.08 の「小得ワールド」(leaf63_b7 の真の得が 2〜3×1e-4 になる狙い)。
> 使い方: OMP_NUM_THREADS=1 python day174.py <world> <noise_orig> <noise_syn> <n_seeds> -> raw174_w<world>.npz
> 候補 0..10 は day173.py と同じ順・同じ乱数(雑音 0.05/0.03 なら Day172 の予測と一致するはず)
72c72,74
< ("et", dict(extra_trees=True))]
---
> ("et", dict(extra_trees=True)),
> ("leaf63", dict(num_leaves=63)), # Day174 追加: 葉の数だけ(乱数なし)
> ("b7", dict(**B7))] # Day174 追加: 抜き出しだけ(葉31)
92a95,96
> if nm == "leaf63" and si > 0: # 乱数なしなので seed 0 の予測を写す(fit を節約)
> oof[j, si, va] = oof[j, 0, va]; evk[j, si, k] = evk[j, 0, k]; continue
98c102
< np.savez_compressed(f"raw173_w{W}.npz", oof=oof, evk=evk, ytr=train.Calories.values,
---
> np.savez_compressed(f"raw174_w{W}.npz", oof=oof, evk=evk, ytr=train.Calories.values,
repcheck.py(決定9 の再現テスト):
"""決定9 の再現テスト: day174.py(雑音 0.05/0.03・seed 1組)の候補 0..10 が Day172 の raw172_w0.npz の seed 0 と一致するか""" import sys, numpy as np, hashlib a = np.load(sys.argv[1]); b = np.load(sys.argv[2]) assert list(a["names"][:11]) == list(b["names"]) print("oof 最大差", np.abs(a["oof"][:11, 0] - b["oof"][:, 0]).max(), " evk 最大差", np.abs(a["evk"][:11, 0] - b["evk"][:, 0]).max(), " ytr 一致", (a["ytr"] == b["ytr"]).all(), " 追加2本", list(a["names"][11:]))
実行(run.sh は DYLD_LIBRARY_PATH に scikit-learn 同梱の libomp を入れ、day174.py <w> <雑音元> <雑音合成> <seed組数> を出力先ディレクトリで呼ぶだけ):
./run.sh 0 0.05 0.03 1 $PWD/rep # 再現テスト用のワールド0(seed 1組、193秒)
python repcheck.py rep/raw174_w0.npz <d172>/raw172_w0.npz # Day172 との一致を確かめる
seq 0 11 | xargs -P 4 -I{} ./run.sh {} 0.12 0.08 2 $PWD/sm # 小得 12 ワールドを 4 並行で作る(壁時計 1,156 秒)
python analyze174.py <d172> <d173/mid> sm # 表1〜7(低雑音・中雑音は保存物を再利用)
python extra174.py <d173>/guard173.pkl # Day173 追加2(仮想)と実測の照合
analyze174.py:
"""Day174 集計: 決定14(乱数なしの基準 lgb_det より補正CV が m×1e-4 以上良いときだけ乗り換える)の余裕 m を、 低雑音(Day172 raw172_w0..15、良い候補なし)・中雑音(Day173 raw173 w0..11、得 約6.5)・ 小得(今日 raw174 w0..11、雑音 0.12/0.08)の3種類の世界で測る。seed 組は s0・s1。 選ぶ候補の集まりは3種類とも Day173 と同じ11本(今日足した leaf63・b7 は表6 の分離と表7 の感度にだけ使う)。 使い方: python analyze174.py <d172のdir> <d173 midのdir> <d174 smのdir> """ import sys, glob import numpy as np, pandas as pd N = 20000 MS = [0, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8] POOL11 = ["hgb_default", "lgb_det", "bag08", "bag05", "ff05", "bag05_ff05", "bag03_ff05", "leaf63_b7", "leaf15_b7", "lr05_it600_b7", "et"] KINDS = ("低雑音", "小得", "中雑音") 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[:11] == POOL11 # 決定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", "中雑音"), load(sys.argv[3] + "/raw174_w*.npz", "小得")]) print({k: D[D.kind == k].world.nunique() for k in KINDS}) print("\n=== 表1 真の差 T5 − T5(lgb_det)(×1e-4、seed 組 s0/s1 平均、ワールド平均±SE)と、lgb_det より真に良かった割合(ワールド×seed組) ===") 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(s.notna().sum()):.2f}" if s.notna().any() else "-").T[list(KINDS)] for k in KINDS: t[k + " 良"] = (R.xs(k) < 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)") print((RC - R).groupby("kind").std(ddof=1).round(2).T[list(KINDS)].to_string()) def guard(D, pool): rows = [] for (kind, w, st), h in D[D.cand.isin(pool)].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 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=str(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, bestc=h.T5.idxmin())) return pd.DataFrame(rows) G = guard(D, POOL11) order = [str(m) for m in MS] + ["常にlgb_det"] print("\n=== 表2 余裕 m ごとの損(regret、×1e-4、ワールド平均±SE)と乗り換えの内訳 ===") for kind in KINDS: 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 KINDS: x = H[H.kind == kind] print(f"[{kind}] 選ばれた {x.b.value_counts().to_dict()} 真の最良 {x.bestc.value_counts().to_dict()} adv 中央値 {x.adv.median():.2f} gain 中央値 {x.gain.median():.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 各世界での超過損(その世界の最良 m との差)と、3世界の最悪の超過損(混ぜ方を知らないとき)===") L = G.groupby(["kind", "m"]).regret.mean().unstack("kind")[list(KINDS)].loc[order] X = L - L.min() X["最悪(低・中の2世界)"] = X[["低雑音", "中雑音"]].max(axis=1) X["最悪(3世界)"] = X[list(KINDS)].max(axis=1) print(L.round(2).add_prefix("損 ").join(X.round(2).add_prefix("超過 ")).to_string()) print("\n=== 表5 ワールド対の差(×1e-4、平均±SE)===") for kind in KINDS: W = G[G.kind == kind].groupby(["world", "m"]).regret.mean().unstack() for a, b in [("2", "0"), ("2", "1"), ("3", "2"), ("常にlgb_det", "2")]: d = W[a] - W[b]; print(f"[{kind}] m={a} − m={b}: {d.mean():+.2f}±{d.std(ddof=1)/np.sqrt(len(d)):.2f}") print("\n=== 表6 葉の数と抜き出しの分離(小得ワールドのみ、真の差 T5 − T5(lgb_det)、×1e-4、ワールド平均±SE)===") S = R.xs("小得").groupby("world").mean() for c in ["leaf63", "b7", "leaf63_b7"]: print(f" {c:10s} {S[c].mean():+.2f}±{S[c].std(ddof=1)/np.sqrt(len(S)):.2f} 良い割合 {(R.xs('小得')[c] < 0).mean():.2f}") inter = S["leaf63_b7"] - S["leaf63"] - S["b7"] print(f" 交互作用 leaf63_b7 − leaf63 − b7 = {inter.mean():+.2f}±{inter.std(ddof=1)/np.sqrt(len(S)):.2f}") ds = (RC - R).xs("小得") print(f" 補正CV ずれ SD: leaf63 {ds['leaf63'].std(ddof=1):.2f} b7 {ds['b7'].std(ddof=1):.2f}") print("\n=== 表7 感度: 小得ワールドで候補を13本(leaf63・b7 も選べる)にしたときの m ごとの損 ===") G13 = guard(D[D.kind == "小得"], POOL11 + ["leaf63", "b7"]) x = G13.groupby("m").regret.mean().loc[order] print(x.round(2).to_string()) print("選ばれた", G13[G13.m == "0"].b.value_counts().to_dict(), " 真の最良", G13[G13.m == "0"].bestc.value_counts().to_dict()) G.to_pickle("guard174.pkl"); D.to_pickle("dec174.pkl")
extra174.py:
"""Day174 追加: Day173 の追加2(中雑音の得を縮めた仮想)が、今日の小得ワールドの実測を当てたか。 仮想の得は今日の実測の平均得(選ばれた候補の gain 平均)に合わせる。使い方: python extra174.py <guard173.pkl>""" import sys, numpy as np, pandas as pd G = pd.read_pickle("guard174.pkl"); G3 = pd.read_pickle(sys.argv[1]) S = G[(G.kind == "小得") & (G.m == "0")] g = S.gain.mean(); print(f"小得ワールドの選ばれた候補の真の得 平均 {g:.2f}(中央値 {S.gain.median():.2f})、n={len(S)}") H = G3[(G3.kind == "中雑音") & (G3.m == "0")] d = H.gain.mean() - g; adv, gain = H.adv - d, H.gain - d rows = [] 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)) x = G[(G.kind == "小得") & (G.m == str(m))] rows.append(dict(m=m, 仮想_損=round(loss.mean(), 2), 仮想_見逃し=int((~sw & (gain > 0)).sum()), 仮想_誤り=int((sw & (gain <= 0)).sum()), 実測_損=round(x.regret.mean(), 2), 実測_見逃し=int(((~x.switch) & (x.gain > 0)).sum()), 実測_誤り=int(x.bad.sum()), n=len(x))) print(pd.DataFrame(rows).set_index("m").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 乱数あり 11本 × seed 2 + leaf63 1) = 1,440 fit。OMP_NUM_THREADS=1・n_jobs=1
# 壁時計: 4 並行で 1,156 秒(1ワールド 325〜467 秒、平均 374 秒)。再現テストを1本並行で回した(193 秒)
# 再現テスト(雑音 0.05/0.03・seed 1組・ワールド0):
oof 最大差 0.0 evk 最大差 0.0 ytr 一致 True 追加2本 ['leaf63', 'b7']
# ワールド0 の途中確認(本番を止めずに見た): leaf63_b7 −3.19/−2.15(seed組 s0/s1)、leaf63 −12.41 → 狙いの「得 2〜3」に入った
{'低雑音': 16, '小得': 12, '中雑音': 12}
=== 表1 真の差 T5 − T5(lgb_det)(×1e-4、seed 組 s0/s1 平均、ワールド平均±SE)と、lgb_det より真に良かった割合(ワールド×seed組) ===
kind 低雑音 小得 中雑音 低雑音 良 小得 良 中雑音 良
cand
b7 - +5.79±0.27 - 0.00 0.00 0.00
bag03_ff05 +15.52±0.46 +16.85±0.34 +17.49±0.34 0.00 0.00 0.00
bag05 +6.55±0.39 +6.07±0.38 +4.81±0.35 0.00 0.00 0.00
bag05_ff05 +11.06±0.28 +12.25±0.31 +12.13±0.28 0.00 0.00 0.00
bag08 +1.09±0.29 -0.13±0.21 -1.33±0.32 0.19 0.50 0.83
et +11.08±0.70 +14.64±0.74 +17.17±0.68 0.00 0.00 0.00
ff05 +4.50±0.34 +5.76±0.28 +6.94±0.32 0.00 0.00 0.00
hgb_default +0.35±0.23 -0.07±0.18 -0.11±0.28 0.44 0.67 0.50
leaf15_b7 +9.53±0.38 +15.02±0.38 +18.15±0.33 0.00 0.00 0.00
leaf63 - -12.72±0.29 - 0.00 1.00 0.00
leaf63_b7 +5.44±0.33 -2.47±0.37 -6.51±0.45 0.00 0.96 1.00
lgb_det +0.00±0.00 +0.00±0.00 +0.00±0.00 0.00 0.00 0.00
lr05_it600_b7 +4.45±0.35 +4.49±0.25 +4.48±0.27 0.00 0.00 0.00
補正CV の lgb_det との差 − 真の差 の SD(×1e-4)
kind 低雑音 小得 中雑音
cand
b7 NaN 2.28 NaN
bag03_ff05 4.20 3.54 4.29
bag05 3.29 3.09 3.33
bag05_ff05 3.55 2.03 3.23
bag08 3.16 2.39 2.81
et 5.61 5.54 5.94
ff05 3.08 2.92 2.61
hgb_default 2.97 2.42 2.17
leaf15_b7 3.53 2.31 2.79
leaf63 NaN 3.46 NaN
leaf63_b7 4.13 2.94 3.62
lgb_det 0.00 0.00 0.00
lr05_it600_b7 3.60 1.82 2.90
=== 表2 余裕 m ごとの損(regret、×1e-4、ワールド平均±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
2.5 0.47±0.14 6 1 5 0.15 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.55±0.26 23 20 3 0.10 0.00 0 24
0.5 0.53±0.26 22 20 2 0.07 0.00 0 24
1 0.61±0.26 20 18 2 0.07 0.08 2 24
1.5 0.63±0.27 18 16 2 0.07 0.10 4 24
2 0.67±0.28 15 14 1 0.03 0.18 6 24
2.5 0.67±0.28 15 14 1 0.03 0.18 6 24
3 0.78±0.31 14 13 1 0.03 0.30 7 24
4 0.98±0.35 12 11 1 0.03 0.49 9 24
5 1.77±0.29 5 5 0 0.00 1.32 15 24
6 2.26±0.38 2 2 0 0.00 1.80 18 24
8 2.36±0.37 1 1 0 0.00 1.90 19 24
常にlgb_det 2.49±0.35 0 0 0 0.00 2.03 20 24
[中雑音]
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
2.5 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} 真の最良 {np.str_('lgb_det'): 15, np.str_('hgb_default'): 12, np.str_('bag08'): 5} adv 中央値 0.11 gain 中央値 -0.87
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'): 19, np.str_('bag08'): 3, np.str_('hgb_default'): 2} 真の最良 {np.str_('leaf63_b7'): 23, np.str_('lgb_det'): 1} adv 中央値 3.84 gain 中央値 2.15
adv (-99,0]: n= 1 真に良い 0 gain 平均 -0.22
adv (0,1]: n= 3 真に良い 2 gain 平均 +0.49
adv (1,2]: n= 5 真に良い 4 gain 平均 +0.27
adv (2,3]: n= 1 真に良い 1 gain 平均 +2.75
adv (3,5]: n= 9 真に良い 8 gain 平均 +2.64
adv (5,99]: n= 5 真に良い 5 gain 平均 +3.45
[中雑音] 選ばれた {np.str_('leaf63_b7'): 24} 真の最良 {np.str_('leaf63_b7'): 24} adv 中央値 7.07 gain 中央値 6.49
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 各世界での超過損(その世界の最良 m との差)と、3世界の最悪の超過損(混ぜ方を知らないとき)===
kind 損 低雑音 損 小得 損 中雑音 超過 低雑音 超過 小得 超過 中雑音 超過 最悪(低・中の2世界) 超過 最悪(3世界)
m
0 1.12 0.55 0.00 0.79 0.02 0.00 0.79 0.79
0.5 1.09 0.53 0.00 0.77 0.00 0.00 0.77 0.77
1 0.68 0.61 0.00 0.35 0.08 0.00 0.35 0.35
1.5 0.65 0.63 0.00 0.33 0.10 0.00 0.33 0.33
2 0.51 0.67 0.00 0.18 0.14 0.00 0.18 0.18
2.5 0.47 0.67 0.00 0.14 0.14 0.00 0.14 0.14
3 0.47 0.78 0.53 0.14 0.25 0.53 0.53 0.53
4 0.36 0.98 0.53 0.03 0.45 0.53 0.53 0.53
5 0.33 1.77 1.27 0.00 1.24 1.27 1.27 1.27
6 0.33 2.26 1.72 0.00 1.73 1.72 1.72 1.73
8 0.33 2.36 4.12 0.00 1.83 4.12 4.12 4.12
常にlgb_det 0.33 2.49 6.51 0.00 1.96 6.51 6.51 6.51
=== 表5 ワールド対の差(×1e-4、平均±SE)===
[低雑音] m=2 − m=0: -0.61±0.43
[低雑音] m=2 − m=1: -0.17±0.07
[低雑音] m=3 − m=2: -0.04±0.04
[低雑音] m=常にlgb_det − m=2: -0.18±0.11
[小得] m=2 − m=0: +0.12±0.07
[小得] m=2 − m=1: +0.06±0.03
[小得] m=3 − m=2: +0.11±0.11
[小得] m=常にlgb_det − m=2: +1.82±0.47
[中雑音] m=2 − m=0: +0.00±0.00
[中雑音] m=2 − m=1: +0.00±0.00
[中雑音] m=3 − m=2: +0.53±0.36
[中雑音] m=常にlgb_det − m=2: +6.51±0.45
=== 表6 葉の数と抜き出しの分離(小得ワールドのみ、真の差 T5 − T5(lgb_det)、×1e-4、ワールド平均±SE)===
leaf63 -12.72±0.29 良い割合 1.00
b7 +5.79±0.27 良い割合 0.00
leaf63_b7 -2.47±0.37 良い割合 0.96
交互作用 leaf63_b7 − leaf63 − b7 = +4.47±0.24
補正CV ずれ SD: leaf63 3.46 b7 2.28
=== 表7 感度: 小得ワールドで候補を13本(leaf63・b7 も選べる)にしたときの m ごとの損 ===
m
0 0.00
0.5 0.00
1 0.00
1.5 0.00
2 0.00
2.5 0.00
3 0.00
4 0.00
5 0.00
6 0.00
8 0.95
常にlgb_det 12.72
選ばれた {np.str_('leaf63'): 24} 真の最良 {np.str_('leaf63'): 24}
# ---- extra174.py(Day173 追加2 の仮想 vs 今日の実測)----
小得ワールドの選ばれた候補の真の得 平均 1.93(中央値 2.15)、n=24
仮想_損 仮想_見逃し 仮想_誤り 実測_損 実測_見逃し 実測_誤り n
m
0 0.33 4 2 0.55 0 3 24
1 0.40 7 2 0.61 2 2 24
2 0.54 8 2 0.67 6 1 24
3 1.27 14 1 0.78 7 1 24
4 1.29 15 1 0.98 9 1 24
# ---- 選択の上振れ(adv − gain、×1e-4、補正CV最良を選んだ時点)----
低雑音 adv-gain 平均 1.43 SD 3.46
小得 adv-gain 平均 1.51 SD 1.98
中雑音 adv-gain 平均 1.23 SD 3.62
読み方:
- 表2 の低雑音・中雑音は Day173 の表2 と全行一致(m=0 低 1.12・m=3 中 0.53 など)。保存物の読み直しと集計が変わっていない証拠
- 表1: 小得では
leaf63_b7が −2.47±0.37(ワールド×seed 組の 96% で良い)。狙った「得 2〜3」に入った。bag08は −0.13 で五分五分 - 表2 [小得]: m を上げると見逃しが増える(m=2 で 6/24、m=3 で 7/24、m=5 で 15/24)。m=0 でも誤りは 3/24(損 0.10)だけ。最良は m=0.5 の 0.53、m=2 は 0.67
- 表3 [小得]: 補正CV最良が
leaf63_b7だったのは 19/24。残り 5 件はbag08・hgb_defaultで、真の得は ±0.5 程度。真の最良は 23/24 がleaf63_b7 - 表4: 3 世界の最悪の超過損は m=2.5 が 0.14、m=2 が 0.18。差 0.04 は低雑音ワールドの1件(余裕が 2〜2.5 の帯の誤り)だけで決まっている
- 表6: 葉の数の得 −12.72、抜き出しの損 +5.79、交互作用 +4.47。抜き出し 0.7 は、このデータでは単独でも組み合わせでも悪い
- 表7: 候補に
leaf63を入れると、補正CV は 24/24 回leaf63を選び、それが真の最良。m=6 まで損 0、常にlgb_detだと 12.72 の損 - 選択の上振れ: 3 種類の世界とも補正CV最良の余裕は真の得より平均 1.2〜1.5 大きい。m≈2 は、この上振れ(約 1.4)を相殺する線として働いている(後付けの解釈、事前に予想していない)
タスクC(戦略の模範)
- m=2 を据え置く。 3 世界の最悪の超過損で m=2(0.18)と m=2.5(0.14)の差は低雑音の1件で決まる差で、ワールド対の SE(0.04 前後)と同じ桁。下げる方向(m=1: 0.35、m=0.5: 0.77)はどちらも悪い。小得だけを見れば m=0.5 が最良だが、m=2 との差は 0.14±0.07 で、低雑音で増える外れの損(m=0.5 で +0.58)の方が大きい。Day173 の仮想が示した「得が小さい問題では m を下げる」は、実測では採るほどの差にならなかった
- 基準を固定しない。 表6・表7 で、m の調整で動く損(0.1〜0.2)より、候補に
leaf63があるかどうか(12.72)の方が約 60 倍大きい。基準lgb_det(葉 31)は小得ワールドでは表現力不足の候補で、それを基準にした「乗り換えの余裕」は、悪い基準を守る仕組みになっていた。直し方は、乱数なしの候補(葉の数などの構造の違い)を先に補正CV で比べて基準を決め、そのあとで乱数ありの候補との乗り換えに m=2 を使う。乱数なしの候補どうしは seed の揺れが無いので、決定13 の補正CV だけで比べられる(表7 で 24/24 正しい)
タスクD(決定)
| # | 決定 | 採った案 | 採らなかった案と理由 | 前提が変わったら |
|---|---|---|---|---|
| 14(据え置き) | 基準からの乗り換え | 基準より補正CV が 2×10⁻⁴ 以上良い乱数ありの候補だけを1本目に採る(Day173 と同じ)。3 世界での最悪の超過損 0.18 を根拠に添える | (a) m=2.5(最悪の超過損 0.14 で表上は最小だが、m=2 との差は低雑音の1件で決まる)。(b) m=0.5(小得だけなら最良 0.53、低雑音で 0.77 超過)。(c) 世界ごとに m を変える(実戦ではどの世界か分からない) | 低雑音のような「良い候補が無い」問題が多いと分かったら m=2.5〜3 も同じくらい良い。系統の違う候補(et は 3 世界ともずれの SD が最大 5.5〜5.9)が最良になる問題では、その候補だけ m を上げる(未測定) |
| 15(新設) | 基準の選び方 | 基準は lgb_det に固定しない。乱数なしの候補(今は葉 31・63)を補正CV で比べ、良い方を基準にする。そのあと決定14 で乱数ありの候補と比べる | (a) lgb_det 固定のまま(小得で 12.72 の損を見逃す)。(b) 乱数なし・ありを全部まとめて補正CV最良を採る(表7 では同じ結果だが、乱数ありは seed の揺れで余裕が上振れする=Day172 の外れ。乱数なしどうしなら揺れが無い)。(c) 葉の数を Optuna で探す(Day165 で探索の楽観を測った。候補2〜3本の比較で足りるうちは探索しない) | 低雑音で leaf63 が lgb_det より悪い(未測定、leaf63_b7 は +5.44)なら、決定15 は「どちらかを選ぶ」仕組みとして効くかを低雑音で確かめる必要がある。印は △ |
設計書の印の更新:
| 決定 | 前 | 後 | 根拠 |
|---|---|---|---|
| 13(補正CV で比べる) | ◎ | ◎ | 小得でも真の最良を 23/24 の世界で leaf63_b7 と当て、13 本にすると 24/24 で leaf63 |
| 14(乗り換えに余裕 m=2) | ○ | ○ | 3 世界での最悪の超過損 0.18(低雑音で決まる)。◎ にしないのは、系統の違う候補が良い世界と実データを測っていないため |
| 15(基準を乱数なし候補から選ぶ) | なし | △ | 小得ワールドだけの証拠(表6・表7)。低雑音・中雑音で leaf63 を作っていない |
📊 結果の可視化
上: 3種類の世界での最悪の超過損。m=2 は 0.18(m=2.5 は 0.14、差は低雑音の1件で決まる)→ 決定14 は据え置き。中: 2×2 の分離。効いていたのは葉の数だけ。下: m の調整で動く損と、候補を1本足して動く損の大きさの比較(約60倍)→ 決定15 新設。
🪜 Step-by-Step 解説
Step 1: 雑音の大きさを決める
Day173 のパイロットでは、leaf63_b7 の真の差が雑音 0.05/0.03 で +5.44、0.15/0.10 で −6.51。この2点を直線で結ぶと、差 −2.5 は雑音(元)約 0.116 の位置。合成側も同じ比(1.5 倍)で 0.12/0.08 にした。別のパイロットは回さず、本番のワールド0 が終わった時点で −3.19/−2.15 を確かめて続けた(外れていたら止めるつもりだった)。
Step 2: 候補を足しても再現が崩れないことを確かめる
足す2本は末尾に置いた。LightGBM の乱数は random_state=1000*s+k で候補ごとに独立なので、末尾に足しても候補 0〜10 の予測は変わらないはず。雑音 0.05/0.03 で回し、Day172 と fold 別予測まで最大差 0.0 を確認した。
Step 3: 3 種類の世界を同じ集計に通す
低雑音・中雑音は作り直さず保存物を読み直し、まず Day173 の表2 と全行一致することを確かめた。そのうえで小得を足した。こうすると、表の差は「世界の違い」だけから来ると言える。
Step 4: 仮想と実測を並べる
Day173 の追加2 を、今日の平均得 1.93 に合わせて計算し直して並べた。m≤2 では仮想と実測の損が 0.1〜0.2 の差で合ったが、m=3 以上では仮想の見逃しが実測の 2 倍。小得ワールドでは補正CV のずれの SD が中雑音より小さい(leaf63_b7 で 2.94 対 3.62)ので、余裕の山が狭く、m の線を越えにくかった。仮想は「ずれは同じ」という置き方の分だけ悲観的だった。
Step 5: 2×2 で分ける
lgb_det・b7・leaf63・leaf63_b7 の 4 本で、葉の数の効果(−12.72)、抜き出しの効果(+5.79)、交互作用(+4.47)を出した。Day173 で「雑音があるときは平均で丸める仕組みが効く」と推測したが、少なくとも小得ワールドでは逆で、効いていたのは葉の数だけだった。
Step 6: 決定を書き直す
m の調整で動く損(0.1〜0.2)と、候補を1本足すだけで動く損(12.72)を並べ、決定15 を新設した。証拠は小得ワールドだけなので印は △ にした。
📐 数学・統計の補足(文系向け)
2×2 の分け方(料理の例え)。 塩(葉の数)とレモン(抜き出し)を、入れる/入れないの 4 通りで味見する。
| 抜き出しなし | 抜き出し 0.7 | |
|---|---|---|
| 葉 31 | lgb_det(基準 0) | b7 +5.79 |
| 葉 63 | leaf63 −12.72 | leaf63_b7 −2.47 |
足し算だけなら両方入れると −12.72 + 5.79 = −6.93 のはず。実際は −2.47 なので、4.47 だけ「組み合わせると余計に悪い」。これが交互作用。塩を足して良くなった料理に、レモンをかけると単独のとき以上に台無しになった、という形。
選択の上振れ(勝者の呪い)。 10 本の候補の補正CV には、それぞれ ±3 くらいのずれがある。一番良く見える1本を選ぶと、「本当に良い」に加えて「たまたま良く見えた」分も一緒に選んでしまう。今日は 3 種類の世界とも、選んだ1本の余裕が真の得より平均 1.2〜1.5 大きかった。m=2 はこの上振れをほぼ打ち消す高さに当たる。候補を増やすほど上振れは大きくなるので、候補数が変わると m も変わるはず(K 依存は未測定)。
🏆 Kaggleでの実践的な使い方
- しきい値を詰める前に、候補の一覧を疑う。 今日、m の調整で動いた損は 0.1〜0.2、候補を1本足して動いた損は 12.72。Playground Series の上位解法でも、勝因は「何を候補に入れたか」(特徴量・モデルの系統)であることが多く、選び方の微調整ではない
- 基準モデルは一度だけ決めて終わりにしない。 基準(
lgb_det)が表現力不足だと、「乗り換えの余裕」は悪い基準を守る仕組みになる。葉の数・深さなど乱数の無い構造の違いを2〜3本並べて、補正CV で基準を選び直す - bagging・列抽出は「雑音が多ければ効く」とは限らない。 今日の小得ワールドでは、抜き出し 0.7 は葉 31 でも葉 63 でも悪かった(+5.79、交互作用 +4.47)。他人のノートブックの
subsample=0.7, colsample_bytree=0.7を既定値として持ち込まない - 「仮想で詰めた数字」は、実測で置き換える予定を書いておく。 Day173 の仮想は m≥3 の見逃しを 2 倍に見積もっていた
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 得が小さい問題では m を下げるべき | 見逃しの損だけを見る | 小得だけなら m=0.5 が最良だが、m=2 との差は 0.14±0.07。低雑音での外れの増加(m=0.5 で +0.58)の方が大きい |
Day173 で効いた leaf63_b7 は抜き出しのおかげ | 「雑音には平均で丸める」という一般論 | 小得では抜き出しは単独で +5.79、組み合わせでも +4.47 余計に悪い。効いたのは葉の数(−12.72) |
| 仮想の計算は実測の代わりになる | 同じ形の表が出るので区別しなくなる | 仮想は「ずれの分布は変わらない」と置いた分だけ悲観的で、m=3 の見逃しを 14 対 7 と 2 倍に見積もった |
| 基準モデルは固定しておく方が安全 | 基準を動かすと比べる軸がぶれると考える | 表現力不足の基準を固定すると、どの m でも良い候補を逃す(常に lgb_det で 12.72) |
| 補正CV最良の余裕はそのまま得の見積もりに使える | 補正CV で偏りを消したので「当たっている」と思う | 一番良く見えるものを選んだ時点で上振れが入る(平均 +1.2〜1.5)。補正CV は平均の偏りを消すが、選択の上振れは消さない |
🧭 扱わなかった範囲と理由
- 低雑音・中雑音での
leaf63・b7: 保存物に無い。作り直すと 28 ワールドで約 45 分かかるため、今日は小得だけで分離した。決定15 の印を △ にした理由。次回の最優先 - 葉の数の上限(127・255)と
min_child_samples: 葉 63 で −12.72 なら、さらに増やす方向も効く可能性がある。今日は 2×2 だけ - 候補数 K と選択の上振れの関係: 上振れ 1.2〜1.5 は候補 10 本のときの値。K を変えた測定は無い
- 系統別の m(
et):etのずれの SD は 3 世界とも最大(5.5〜5.9)だが、etが補正CV最良になったのは低雑音の1件だけ - 公開LB と 2 本目の枠、avg3(3 seed 平均)での損: 今日も測っていない(seed 2 組)
- 実データ(S5E5): 縮小シミュレータの上の数字のみ。S5E5 のルール確認・公開割合・敵対的AUC は Day171 で利用者の作業として切り出したまま
- 持ち越し(Day173 から): OOF 側と test 側の seed の揺れの分離、
noKeytel・raw7の補正後のずれ、補正CV と入れ子CV の比較、A_test の推定誤差、K > 13、Day170 のハイブリッド・狭い10本の出し直し、SE 個数差と分割の新しさ(Day165)、Day162 の選択バイアス、真の効果ちょうど0の偽採用率、Nadeau & Bengio・Krogh & Vedelsby 原典(今日も記憶による)
🚀 次のステップ
- 発展: 低雑音(16)・中雑音(12)ワールドに乱数なしの
leaf63・leaf127を足し、決定15(基準を乱数なしの候補から補正CV で選ぶ)を 3 種類の世界で測る。低雑音で葉を増やすと悪くなるなら、決定15 の「選ぶ」部分が本当に効くかが分かる - 発展: 候補数 K を 5/10/20 に変え、選択の上振れ(adv − gain)と最適な m が K とともにどう動くかを測る
- 次回予告: Day 175 — Playground Series 実戦投入⑩(決定15 を低雑音・小得・中雑音の 3 種類で確かめ、乱数なしの候補の葉の数 31/63/127 から基準を選ぶ損を測る)。月替わりテーマ3「画像(CV)系コンペへの応用」は 10/25 から
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: