📚 背景知識(読んでから問題へ)
Day174 のおさらい。 小得ワールド(雑音 0.12/0.08)で、葉の数と抜き出しを 2×2 で分けた。
| 抜き出しなし | 抜き出し 0.7 | |
|---|---|---|
| 葉 31 | lgb_det(基準 0) | b7 +5.79 |
| 葉 63 | leaf63 −12.72 | leaf63_b7 −2.47 |
効いていたのは葉の数だけで、余裕 m の調整(0.1〜0.2)より候補の中身(12.72)が約 60 倍効いた。そこで決定15 を置いた。
lgb_det に固定しない。乱数なしの候補(葉 31・63 など)を補正CVで比べ、良い方を基準にする。そのあと決定14(基準より補正CVが 2×10⁻⁴ 以上良い乱数ありの候補だけに乗り換える)を使う△ の理由は2つ。(1) 低雑音・中雑音に leaf63 が無かった。(2) 低雑音で leaf63_b7 は +5.44 と悪かったので、「低雑音では葉を増やすと悪くなる」かもしれず、そうなら決定15 は「正しく選び分けられるか」が問われる。今日はこの2つを測る。
用語整理(Day174 と同じものは省略):
| 用語 | 意味 |
|---|---|
| 乱数なしの候補(DET3) | lgb_det(葉31)・leaf63・leaf127。bagging も列抽出もしないので model seed に依らず同じ予測。seed 組 s0・s1 で同じ値を使う |
| 全14本 | Day172 の11本(乱数あり9本+hgb_default+lgb_det)+leaf63+leaf127。小得にある b7 は低雑音・中雑音に無いので外した |
| 損(regret) | 選んだ候補の真の RMSLE − 全14本の中の真の最良(×10⁻⁴)。真は 6万行の truth で5 fold モデル平均の予測を採点したもの |
| 補正CV | 決定13: √(CV² − A_test)。A_test は test 行の fold 間の予測のばらつき(ラベル不要)。1モデルの OOF と5モデル平均の提出の差を引く |
| 最悪の超過損 | 3種類の世界それぞれで「その世界で一番良い方針」との差を取り、3つのうち最大。どの世界か分からないときの備え |
数値はすべて縮小シミュレータの上の実測。実データ(S5E5)で同じ大きさになるとは限らない。
データ・保存物の構造(スキーマ)
| 名前 | 形・型 | 意味・作り方 | 使う場所 |
|---|---|---|---|
| Sex … Body_Temp, Dur_x_HR, Keytel_kcal | int / float(9列) | Day163〜174 と同じ合成(ワールドの乱数 3000+番号)。Calories の雑音だけ 0.05/0.03(低)・0.12/0.08(小得)・0.15/0.10(中) | 全候補 |
| raw175_w*.npz(low/mid/sm) | oof (3,30000)・evk (3,5,80000) float32 | 今日作った乱数なし3本(lgb_det・leaf63・leaf127)。40 ワールド × 3本 × 5 fold = 600 fit | 基準選び・再現テスト |
| raw172 / raw173 / raw174 | oof (11〜13, seed, 30000)・evk (…, 5, 80000) | Day172〜174 の保存物。乱数ありの候補はここから読む(seed 組 s0・s1) | 決定14 の乗り換え先 |
| C / Ca / T5 | float(RMSLE) | 生の CV(1モデルの OOF)/補正CV √(C²−A_test)/真(6万行 truth で5 fold 平均を採点) | 表1〜5 |
| regret | float(×10⁻⁴) | 選んだ1本の T5 − 全14本の中の最良 T5 | 表3・表5 |
評価指標(RMSLE)と、生の CV の偏り
RMSLE は log1p 空間の RMSE(小さいほど良い)。生の CV は1モデルの OOF で採点するが、提出は5モデルの平均なので、1本ごとの予測がばらつく候補(葉が多い木)ほど不利に見える。下の図は、葉 63・127 で生の CV(赤)が真の差より何だけ悪く見せたかと、補正CV(緑)で残った偏り。
❓ 問題
タスクA(予想)— コードを書く前に
次の3つを数字で予想して書く(外れてよい。外れ方が今日の学び)。
- 低雑音ワールドで
leaf63・leaf127はlgb_detより良いか悪いか(Day172 でleaf63_b7は +5.44) - 生の CV(補正なし)は、葉を増やした候補を良く見せるか悪く見せるか。その大きさは葉の数とともにどう動くか
- 決定15 を入れたあと、決定14 の余裕 m(0〜6)で損は変わるか
タスクB(コーディング)
day174.pyの生成部(ワールドの乱数 3000+番号・雑音だけ引数)をそのまま使い、乱数なしの3本だけを作るday175.pyを書く。今日のlgb_detが保存物(raw172/173/174)のlgb_detと完全一致することを、OOF と fold 別の test 予測の両方で確かめる(決定9)。小得ではleaf63も raw174 と照合できる- 低雑音(0.05/0.03)16、中雑音(0.15/0.10)12、小得(0.12/0.08)12 を回す
- 集計
analyze175.pyで、(a) 真の差、(b) 基準選びの的中と損、(c) 方針ごとの損と最悪の超過損、(d) ワールド対の差を出す
タスクC(分析)
- 生の CV と補正CV は、葉の数を増やした候補の差をそれぞれどれだけずらすか(残りの偏り)
- 低雑音で決定15 の損 0.88 はどこから来るか。「常に
leaf63」(0.38)とどちらを採るべきか - 決定14・15 の印をどう付け替えるか。決定14 の m はこの先も要るか
💡 ヒント
ヒント1(方向性)
新しく fit するのは乱数なしの3本だけでよい。乱数ありの候補の予測は保存してあり、同じワールドを同じ乱数で作れば、行の順番もラベルも一致する(ytr・yev を np.array_equal で確かめる)。
ヒント2(アプローチ)
生の CV の偏りは「1モデルの OOF」と「5モデル平均の提出」の差(Day171)。葉が多いほど1本ごとの予測がばらつくので、5本平均の得は大きくなるはず。A_test(ラベル不要)を候補ごとに見れば、偏りの大きさを先に予想できる。
ヒント3(コード骨格)
DET = [("lgb_det", {}), ("leaf63", dict(num_leaves=63)), ("leaf127", dict(num_leaves=127))] for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=0).split(train)): ... for j, (nm, over) in enumerate(DET): m = lgb.LGBMRegressor(**{**LG, **over}, random_state=k).fit(X, y) # 乱数なしなので seed は形だけ oof[j, va] = m.predict(XV); evk[j, k] = m.predict(XE) # 決定15: 基準 = 乱数なし3本の補正CV最良 → 乱数あり9本+hgb の補正CV最良が m 以上良ければ乗り換え base = h.Ca[DET3].idxmin(); b = h.Ca[RAND].idxmin() pick = b if (h.Ca[base] - h.Ca[b]) * 1e4 > m else base
✅ 模範解答
タスクA(予想の模範と、実測との照合)
| 予想 | 模範の予想(書いた時点) | 実測 | 当たり外れ |
|---|---|---|---|
| 1. 低雑音で葉を増やすと | 悪くなる(leaf63_b7 +5.44 から) | leaf63 −3.67±0.34(16/16 で良い)、leaf127 −3.50±0.50(15/16) | 外れ。+5.44 は抜き出し 0.7 の害(Day174 の b7 +5.79)で、葉の数は低雑音でも効いた |
| 2. 生の CV の偏り | 葉が多いほど悪く見せる(5本平均の得を数えないため) | 生の偏り leaf63 +5.69〜+6.24、leaf127 +13.96〜+16.58。補正後は −0.86〜+2.04 | 方向は当たり。大きさは葉 63 で約 6、127 で約 15 と、葉の数にほぼ比例して増えた |
| 3. 決定15 のあと m で損は変わるか | 少し変わる | m=0〜6 で損が完全に同じ(3世界とも)。乗り換えが1回も起きなかった | 外れ。基準が leaf63/leaf127 になると、乱数ありの候補(葉31か leaf63_b7)はどれも補正CVで基準に届かない |
タスクB(完全なコード)
day175.py(生成部は day174.py と1文字も変えていない。変えたのは候補の一覧と保存形式だけ):
"""Day175: Day172〜174 と同じ生成器(seed 3000+W、雑音だけ引数)で、乱数なしの LightGBM 3本(lgb_det=葉31・leaf63・leaf127)を作る。 lgb_det は保存物(raw172/173/174)と一致するか確かめる用(決定9)。leaf63 は小得では raw174 とも照合できる。 使い方: OMP_NUM_THREADS=1 python day175.py <world> <noise_orig> <noise_syn> -> raw175_w<world>.npz """ import sys, time, hashlib import numpy as np, pandas as pd import lightgbm as lgb from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.model_selection import KFold T0 = time.time() W = int(sys.argv[1]) NO, NS = float(sys.argv[2]), float(sys.argv[3]) N_ORIG, N_TRAIN, N_TEST, N_TRUTH = 15000, 30000, 20000, 60000 def keytel_rate(sex, hr, w, age): m = (-55.0969 + 0.6309 * hr + 0.1988 * w + 0.2017 * age) / 4.184 f = (-20.4022 + 0.4472 * hr - 0.1263 * w + 0.074 * age) / 4.184 return np.where(sex == 1, m, f) def make_original(n, rng): # Day163-172 と同じ(雑音 NO だけ引数化) sex = rng.integers(0, 2, n); age = rng.integers(20, 80, n) h = np.where(sex == 1, rng.normal(175, 7, n), rng.normal(162, 6.5, n)).round() w = (rng.normal(24.5, 2.5, n) * (h / 100) ** 2).round() dur = rng.integers(1, 31, n).astype(float) hr = np.clip(80 + 1.2 * dur + rng.normal(0, 5, n), 67, 128).round() temp = (37 + 0.35 * dur ** 0.5 + rng.normal(0, 0.2, n)).round(1) rate = np.clip(keytel_rate(sex, hr, w, age), 0.3, None) cal = np.clip((rate * dur * np.exp(rng.normal(0, NO, n))).round(), 1, None) return pd.DataFrame(dict(Sex=sex, Age=age, Height=h, Weight=w, Duration=dur, Heart_Rate=hr, Body_Temp=temp, Calories=cal)) def synth_from(orig, n, rng): # Day163-172 と同じ(雑音 NS だけ引数化) s = orig.sample(n, replace=True, random_state=int(rng.integers(1 << 30))).reset_index(drop=True) s["Age"] = np.clip((s.Age + rng.normal(0, 1.5, n)).round(), 20, 79) s["Height"] = (s.Height + rng.normal(0, 2, n)).round() s["Weight"] = (s.Weight + rng.normal(0, 2, n)).round() s["Duration"] = np.clip((s.Duration + rng.normal(0, 1.0, n)).round(), 1, 30) s["Heart_Rate"] = (s.Heart_Rate + rng.normal(0, 2, n)).round() s["Body_Temp"] = (s.Body_Temp + rng.normal(0, 0.1, n)).round(1) s["Calories"] = np.clip((s.Calories * np.exp(rng.normal(0, NS, n))).round(), 1, None) return s def add_feats(d): d = d.copy() d["Dur_x_HR"] = d.Duration * d.Heart_Rate d["Keytel_kcal"] = np.clip(keytel_rate(d.Sex.values, d.Heart_Rate.values, d.Weight.values, d.Age.values), 0.3, None) * d.Duration d["log_Dur"] = np.log(d.Duration) return d RAW = ["Sex", "Age", "Height", "Weight", "Duration", "Heart_Rate", "Body_Temp"] BASE = RAW + ["Dur_x_HR", "Keytel_kcal"] # 決定6 LG = dict(objective="regression", learning_rate=0.1, n_estimators=300, num_leaves=31, min_child_samples=20, n_jobs=1, verbose=-1) # Day175: 乱数なしの候補だけを作る(seed 1組で十分)。lgb_det は保存物との一致確認用 DET = [("lgb_det", {}), ("leaf63", dict(num_leaves=63)), ("leaf127", dict(num_leaves=127))] names = [c[0] for c in DET] rng = np.random.default_rng(3000 + W) orig = make_original(N_ORIG, rng) pool = synth_from(orig, N_TRAIN + N_TEST + N_TRUTH, rng) orig, pool = add_feats(orig), add_feats(pool) train = pool.iloc[:N_TRAIN].reset_index(drop=True) evalset = pool.iloc[N_TRAIN:].reset_index(drop=True) XE = evalset[BASE].values oof = np.zeros((len(names), N_TRAIN)) evk = np.zeros((len(names), 5, N_TEST + N_TRUTH), np.float32) for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=0).split(train)): tr_df = pd.concat([train.iloc[tr], orig], ignore_index=True) X, y, XV = tr_df[BASE].values, np.log1p(tr_df.Calories.values), train.iloc[va][BASE].values for j, (nm, over) in enumerate(DET): m = lgb.LGBMRegressor(**{**LG, **over}, random_state=1000 * 0 + k).fit(X, y) oof[j, va] = m.predict(XV); evk[j, k] = m.predict(XE) print(f"world {W} fold {k} {time.time()-T0:.0f}s", flush=True) assert names == ["lgb_det", "leaf63", "leaf127"] # 決定9: 名前と行の対応 np.savez_compressed(f"raw175_w{W}.npz", oof=oof, evk=evk, ytr=train.Calories.values, yev=evalset.Calories.values, names=np.array(names)) print(f"world {W} wall {time.time()-T0:.0f}s")
analyze175.py:
"""Day175 集計: 決定15(基準を乱数なしの候補 lgb_det/leaf63/leaf127 から補正CVで選び、その後に決定14 の余裕 m で乱数ありへ乗り換え)を 低雑音(raw172 w0..15)・小得(raw174 sm w0..11)・中雑音(raw173 mid w0..11)で測る。乱数なしの3本は今日の raw175。 使い方: python analyze175.py <d172> <d173mid> <d174sm> (カレントに low/ mid/ sm/ の raw175 がある前提) """ import sys, glob import numpy as np, pandas as pd N = 20000 MS = [0, 1, 2, 3, 4, 6] POOL11 = ["hgb_default", "lgb_det", "bag08", "bag05", "ff05", "bag05_ff05", "bag03_ff05", "leaf63_b7", "leaf15_b7", "lr05_it600_b7", "et"] DET3 = ["lgb_det", "leaf63", "leaf127"] KINDS = ("低雑音", "小得", "中雑音") SRC = {"低雑音": (sys.argv[1], "raw172", "low"), "中雑音": (sys.argv[2], "raw173", "mid"), "小得": (sys.argv[3], "raw174", "sm")} def sle(y, lp): return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2 rows, rep = [], [] for kind, (d, pre, sub) in SRC.items(): for f in sorted(glob.glob(f"{d}/{pre}_w*.npz"), 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 n = np.load(f"{sub}/raw175_w{w}.npz"); O2, E2, n2 = n["oof"], n["evk"].astype(np.float64), list(n["names"]) assert n2 == DET3 and np.array_equal(n["ytr"], ytr) and np.array_equal(n["yev"], yev) j = names.index("lgb_det") r = dict(kind=kind, world=w, lgb_oof=np.abs(O2[0] - O[j, 0]).max(), lgb_ev=np.abs(E2[0] - E[j, 0]).max()) if "leaf63" in names: j2 = names.index("leaf63"); r.update(l63_oof=np.abs(O2[1] - O[j2, 0]).max(), l63_ev=np.abs(E2[1] - E[j2, 0]).max()) rep.append(r) ytru = yev[N:] for s in (0, 1): for j, nm in enumerate(names[:11]): 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=nm, C=np.sqrt(c), Ca=np.sqrt(c - a), T5=np.sqrt(t5))) for j, nm in enumerate(DET3[1:], start=1): c = sle(ytr, O2[j]).mean(); a = E2[j, :, :N].var(axis=0).mean(); t5 = sle(ytru, E2[j].mean(0)[N:]).mean() rows.append(dict(kind=kind, world=w, st=f"s{s}", cand=nm, C=np.sqrt(c), Ca=np.sqrt(c - a), T5=np.sqrt(t5))) D = pd.DataFrame(rows); RP = pd.DataFrame(rep) print("=== 表0 再現(決定9): 今日の lgb_det/leaf63 と保存物の最大絶対差 ===") print(RP.groupby("kind")[[c for c in RP.columns if c not in ("kind", "world")]].max().to_string()) P = D.pivot_table(index=["kind", "world", "st"], columns="cand", values="T5") R = (P.sub(P["lgb_det"], axis=0) * 1e4) Q = D.pivot_table(index=["kind", "world", "st"], columns="cand", values="Ca") RC = (Q.sub(Q["lgb_det"], axis=0) * 1e4) PC = D.pivot_table(index=["kind", "world", "st"], columns="cand", values="C") RR = (PC.sub(PC["lgb_det"], axis=0) * 1e4) print("\n=== 表1 真の差 T5−T5(lgb_det)(×1e-4、ワールド平均±SE)/良い割合 ===") for kind in KINDS: g = R.xs(kind).groupby("world").mean() out = [f"{c} {g[c].mean():+.2f}±{g[c].std(ddof=1)/np.sqrt(len(g)):.2f} ({(g[c]<0).mean():.2f})" for c in ["leaf63", "leaf127", "leaf63_b7", "hgb_default"]] print(f"[{kind}] " + " ".join(out)) print("\n=== 表1b 乱数なし3本の 生CV差・補正CV差・真の差(×1e-4、ワールド平均)とずれのSD ===") for kind in KINDS: x = [kind] for c in ["leaf63", "leaf127"]: g = pd.DataFrame(dict(raw=RR.xs(kind)[c], cor=RC.xs(kind)[c], tru=R.xs(kind)[c])).xs("s0", level="st") x.append(f"{c}: 生 {g.raw.mean():+.2f} 補正 {g.cor.mean():+.2f} 真 {g.tru.mean():+.2f} ずれSD {(g.cor-g.tru).std(ddof=1):.2f} 生ずれSD {(g.raw-g.tru).std(ddof=1):.2f}") print(" ".join(x)) print("\n=== 表2 基準選び(乱数なし3本の中で補正CV最良を選ぶ): 的中と損(×1e-4)===") for kind in KINDS: x = D[(D.kind == kind) & (D.st == "s0") & D.cand.isin(DET3)] hit, reg, picks, best = [], [], [], [] for w, h in x.groupby("world"): h = h.set_index("cand"); p = h.Ca.idxmin(); b = h.T5.idxmin() hit.append(p == b); reg.append((h.T5[p] - h.T5[b]) * 1e4); picks.append(p); best.append(b) print(f"[{kind}] 的中 {sum(hit)}/{len(hit)} 損 {np.mean(reg):.2f} 最大 {np.max(reg):.2f} 選んだ {pd.Series(picks).value_counts().to_dict()} 真の最良 {pd.Series(best).value_counts().to_dict()}") rg = pd.Series(reg); print(f" 生CVで選ぶと: ", end="") hit2, reg2 = [], [] for w, h in x.groupby("world"): h = h.set_index("cand"); p = h.C.idxmin(); b = h.T5.idxmin(); hit2.append(p == b); reg2.append((h.T5[p] - h.T5[b]) * 1e4) print(f"的中 {sum(hit2)}/{len(hit2)} 損 {np.mean(reg2):.2f}") ALL = POOL11 + ["leaf63", "leaf127"] RAND = [c for c in POOL11 if c != "lgb_det"] def policy(h, name, m): if name == "常にlgb_det": return "lgb_det" if name == "常にleaf63": return "leaf63" if name == "全14本の補正CV最良": return h.Ca.idxmin() if name == "決定15の基準だけ": return h.Ca[DET3].idxmin() base = "lgb_det" if name == "決定14" else h.Ca[DET3].idxmin() b = h.Ca[RAND].idxmin() return b if (h.Ca[base] - h.Ca[b]) * 1e4 > m else base rows = [] for (kind, w, st), h in D.groupby(["kind", "world", "st"]): h = h.set_index("cand"); best = h.T5[ALL].min() for name in ["決定14", "決定15", "決定15の基準だけ", "全14本の補正CV最良", "常にlgb_det", "常にleaf63"]: for m in (MS if name in ("決定14", "決定15") else [None]): p = policy(h, name, m) rows.append(dict(kind=kind, world=w, st=st, pol=name if m is None else f"{name} m={m}", pick=p, regret=(h.T5[p] - best) * 1e4)) G = pd.DataFrame(rows) print("\n=== 表3 方針ごとの損(全14本の真の最良との差、×1e-4、ワールド平均±SE)と3世界の最悪の超過損 ===") L = G.groupby(["pol", "kind"]).regret.mean().unstack("kind")[list(KINDS)] SEt = G.groupby(["pol", "kind", "world"]).regret.mean().groupby(["pol", "kind"]).agg(lambda s: s.std(ddof=1) / np.sqrt(len(s))).unstack("kind")[list(KINDS)] X = L - L.min() T = L.round(2).astype(str) + "±" + SEt.round(2).astype(str) T["最悪の超過"] = X.max(axis=1).round(2) print(T.sort_values("最悪の超過").to_string()) print("\n=== 表4 選ばれた候補(m=2)と真の最良 ===") for kind in KINDS: for pol in ["決定14 m=2", "決定15 m=2"]: x = G[(G.kind == kind) & (G.pol == pol)] print(f"[{kind}] {pol}: {x.pick.value_counts().to_dict()}") bc = D[(D.kind == kind) & D.cand.isin(ALL)].loc[lambda d: d.groupby(['world', 'st']).T5.idxmin()].cand.value_counts().to_dict() print(f"[{kind}] 真の最良: {bc}") print("\n=== 表5 ワールド対の差 決定15 m=2 − 決定14 m=2(×1e-4)===") for kind in KINDS: W = G[G.kind == kind].groupby(["world", "pol"]).regret.mean().unstack() for a, b in [("決定15 m=2", "決定14 m=2"), ("決定15 m=2", "常にleaf63"), ("決定15 m=2", "決定15の基準だけ"), ("決定15 m=0", "決定15 m=2")]: d = W[a] - W[b]; print(f"[{kind}] {a} − {b}: {d.mean():+.2f}±{d.std(ddof=1)/np.sqrt(len(d)):.2f}") G.to_pickle("pol175.pkl"); D.to_pickle("dec175.pkl")
extra175.py(残りの偏り・葉 63 対 127・単純な方を優先する版):
"""Day175 追加: 乱数なし3本の補正CVの残りの偏り(補正差−真の差)と、低雑音の leaf63 対 leaf127 の真の差、A_test の大きさ""" import numpy as np, pandas as pd D = pd.read_pickle("dec175.pkl"); D = D[D.st == "s0"] for k in ("低雑音", "小得", "中雑音"): P = {v: D[D.kind == k].pivot(index="world", columns="cand", values=v) for v in ("C", "Ca", "T5")} for c in ("leaf63", "leaf127"): e = ((P["Ca"][c] - P["Ca"]["lgb_det"]) - (P["T5"][c] - P["T5"]["lgb_det"])) * 1e4 r = ((P["C"][c] - P["C"]["lgb_det"]) - (P["T5"][c] - P["T5"]["lgb_det"])) * 1e4 print(f"[{k}] {c}: 補正の残り偏り {e.mean():+.2f}±{e.std(ddof=1)/np.sqrt(len(e)):.2f} 生の偏り {r.mean():+.2f}±{r.std(ddof=1)/np.sqrt(len(r)):.2f}") a = {c: ((P["C"][c] ** 2 - P["Ca"][c] ** 2) * 1e4).mean() for c in ("lgb_det", "leaf63", "leaf127")} print(f"[{k}] A_test(×1e-4, 二乗の単位)", {c: round(v, 3) for c, v in a.items()}) d = (P["T5"]["leaf127"] - P["T5"]["leaf63"]) * 1e4; dc = (P["Ca"]["leaf127"] - P["Ca"]["leaf63"]) * 1e4 print(f"[{k}] 真 leaf127−leaf63 {d.mean():+.2f}±{d.std(ddof=1)/np.sqrt(len(d)):.2f} 補正CV {dc.mean():+.2f} 符号一致 {(np.sign(d)==np.sign(dc)).sum()}/{len(d)} 相関 {np.corrcoef(d, dc)[0,1]:.2f}") print("\n--- 単純な方を優先する版(葉 31→63→127 の順に、補正CVが m' 以上良いときだけ大きい方へ進む): 全14本の最良との損(×1e-4、s0/s1 平均)---") D = pd.read_pickle("dec175.pkl") rows = [] for (k, w, st), h in D.groupby(["kind", "world", "st"]): h = h.set_index("cand"); best = h.T5.drop(["b7"], errors="ignore").min() for mp in [0, 1, 2, 3, 4]: cur = "lgb_det" for nx in ("leaf63", "leaf127"): if (h.Ca[cur] - h.Ca[nx]) * 1e4 > mp: cur = nx rows.append(dict(kind=k, world=w, mp=mp, regret=(h.T5[cur] - best) * 1e4, pick=cur)) R = pd.DataFrame(rows) L = R.groupby(["mp", "kind"]).regret.mean().unstack()[["低雑音", "小得", "中雑音"]] L["最悪の超過(決定15 m'=0 の 0.88/0/0 を含む表での)"] = (L - L.min()).max(axis=1) print(L.round(2).to_string()) print(R[R.kind == "低雑音"].groupby("mp").pick.value_counts().unstack().fillna(0).astype(int).to_string())
実行(run.sh は Day174 と同じく DYLD_LIBRARY_PATH に scikit-learn 同梱の libomp を入れ、day175.py <w> <雑音元> <雑音合成> を出力先ディレクトリで呼ぶだけ):
# 40 ワールドを 4 並行で作る(低雑音 16・中雑音 12・小得 12) (for w in $(seq 0 15); do echo "$w 0.05 0.03 low"; done; for w in $(seq 0 11); do echo "$w 0.15 0.10 mid"; echo "$w 0.12 0.08 sm"; done) | xargs -P 4 -L 1 sh -c './run.sh $0 $1 $2 $(pwd)/$3' # 保存物(d172・d173/mid・d174/sm)と今日の raw175 を合わせて集計する python analyze175.py <d172> <d173/mid> <d174/sm> # 残りの偏り・葉 63 対 127・単純な方を優先する版 python extra175.py
実行結果(実測)
# ============ 実行環境: macOS (M1, メモリ8GB)・Day172 と同じ uv venv(Python 3.13.12・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6・lightgbm 4.7.0)
# 40 ワールド × 5 fold × 乱数なし3本 = 600 fit。OMP_NUM_THREADS=1・n_jobs=1
# 壁時計: 4 並行。最初の一括実行が 600 秒の時間上限で止められ(32 ワールド完了・残り8は未着手)、残り8を 84 秒で回した。合計 約 684 秒、1ワールド 47〜82 秒
# 集計 analyze175.py 11.9 秒
=== 表0 再現(決定9): 今日の lgb_det/leaf63 と保存物の最大絶対差 ===
lgb_oof lgb_ev l63_oof l63_ev
kind
中雑音 0.0 0.0 NaN NaN
低雑音 0.0 0.0 NaN NaN
小得 0.0 0.0 0.0 0.0
=== 表1 真の差 T5−T5(lgb_det)(×1e-4、ワールド平均±SE)/良い割合 ===
[低雑音] leaf63 -3.67±0.34 (1.00) leaf127 -3.50±0.50 (0.94) leaf63_b7 +5.44±0.33 (0.00) hgb_default +0.35±0.23 (0.44)
[小得] leaf63 -12.72±0.29 (1.00) leaf127 -22.96±0.44 (1.00) leaf63_b7 -2.47±0.37 (0.92) hgb_default -0.07±0.18 (0.67)
[中雑音] leaf63 -16.47±0.55 (1.00) leaf127 -31.74±0.54 (1.00) leaf63_b7 -6.51±0.45 (1.00) hgb_default -0.11±0.28 (0.50)
=== 表1b 乱数なし3本の 生CV差・補正CV差・真の差(×1e-4、ワールド平均)とずれのSD ===
低雑音 leaf63: 生 +2.02 補正 -3.34 真 -3.67 ずれSD 4.29 生ずれSD 4.36 leaf127: 生 +10.46 補正 -1.46 真 -3.50 ずれSD 5.14 生ずれSD 5.25
小得 leaf63: 生 -6.74 補正 -13.23 真 -12.72 ずれSD 3.54 生ずれSD 3.58 leaf127: 生 -7.05 補正 -21.76 真 -22.96 ずれSD 5.00 生ずれSD 5.10
中雑音 leaf63: 生 -10.23 補正 -17.32 真 -16.47 ずれSD 3.17 生ずれSD 3.17 leaf127: 生 -15.16 補正 -31.74 真 -31.74 ずれSD 6.00 生ずれSD 6.09
=== 表2 基準選び(乱数なし3本の中で補正CV最良を選ぶ): 的中と損(×1e-4)===
[低雑音] 的中 5/16 損 0.88 最大 4.53 選んだ {'leaf63': 13, 'leaf127': 2, 'lgb_det': 1} 真の最良 {'leaf127': 8, 'leaf63': 8}
生CVで選ぶと: 的中 1/16 損 3.37
[小得] 的中 12/12 損 0.00 最大 0.00 選んだ {'leaf127': 12} 真の最良 {'leaf127': 12}
生CVで選ぶと: 的中 7/12 損 4.28
[中雑音] 的中 12/12 損 0.00 最大 0.00 選んだ {'leaf127': 12} 真の最良 {'leaf127': 12}
生CVで選ぶと: 的中 11/12 損 1.31
=== 表3 方針ごとの損(全14本の真の最良との差、×1e-4、ワールド平均±SE)と3世界の最悪の超過損 ===
kind 低雑音 小得 中雑音 最悪の超過
pol
全14本の補正CV最良 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15 m=0 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15 m=1 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15 m=2 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15 m=3 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15 m=4 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15 m=6 0.88±0.31 0.0±0.0 0.0±0.0 0.50
決定15の基準だけ 0.88±0.31 0.0±0.0 0.0±0.0 0.50
常にleaf63 0.38±0.14 10.24±0.34 15.28±0.34 15.28
決定14 m=0 4.85±0.65 21.02±0.63 25.24±0.5 25.24
決定14 m=1 4.4±0.36 21.08±0.61 25.24±0.5 25.24
決定14 m=2 4.24±0.32 21.14±0.64 25.24±0.5 25.24
決定14 m=3 4.2±0.32 21.25±0.64 25.76±0.67 25.76
決定14 m=4 4.09±0.36 21.45±0.68 25.76±0.67 25.76
決定14 m=6 4.05±0.37 22.73±0.4 26.96±0.94 26.96
常にlgb_det 4.05±0.37 22.96±0.44 31.74±0.54 31.74
=== 表4 選ばれた候補(m=2)と真の最良 ===
[低雑音] 決定14 m=2: {'lgb_det': 25, 'hgb_default': 4, 'bag08': 3}
[低雑音] 決定15 m=2: {'leaf63': 26, 'leaf127': 4, 'lgb_det': 2}
[低雑音] 真の最良: {'leaf127': 16, 'leaf63': 16}
[小得] 決定14 m=2: {'leaf63_b7': 14, 'lgb_det': 9, 'bag08': 1}
[小得] 決定15 m=2: {'leaf127': 24}
[小得] 真の最良: {'leaf127': 24}
[中雑音] 決定14 m=2: {'leaf63_b7': 24}
[中雑音] 決定15 m=2: {'leaf127': 24}
[中雑音] 真の最良: {'leaf127': 24}
=== 表5 ワールド対の差 決定15 m=2 − 決定14 m=2(×1e-4)===
[低雑音] 決定15 m=2 − 決定14 m=2: -3.36±0.43
[低雑音] 決定15 m=2 − 常にleaf63: +0.50±0.32
[低雑音] 決定15 m=2 − 決定15の基準だけ: +0.00±0.00
[低雑音] 決定15 m=0 − 決定15 m=2: +0.00±0.00
[小得] 決定15 m=2 − 決定14 m=2: -21.14±0.64
[小得] 決定15 m=2 − 常にleaf63: -10.24±0.34
[小得] 決定15 m=2 − 決定15の基準だけ: +0.00±0.00
[小得] 決定15 m=0 − 決定15 m=2: +0.00±0.00
[中雑音] 決定15 m=2 − 決定14 m=2: -25.24±0.50
[中雑音] 決定15 m=2 − 常にleaf63: -15.28±0.34
[中雑音] 決定15 m=2 − 決定15の基準だけ: +0.00±0.00
[中雑音] 決定15 m=0 − 決定15 m=2: +0.00±0.00
# ---- extra175.py ----
[低雑音] leaf63: 補正の残り偏り +0.33±1.07 生の偏り +5.69±1.09
[低雑音] leaf127: 補正の残り偏り +2.04±1.28 生の偏り +13.96±1.31
[低雑音] A_test(×1e-4, 二乗の単位) {'lgb_det': 3.957, 'leaf63': 5.632, 'leaf127': 7.713}
[低雑音] 真 leaf127−leaf63 +0.17±0.33 補正CV +1.87 符号一致 5/16 相関 -0.23
[小得] leaf63: 補正の残り偏り -0.51±1.02 生の偏り +5.99±1.03
[小得] leaf127: 補正の残り偏り +1.20±1.44 生の偏り +15.91±1.47
[小得] A_test(×1e-4, 二乗の単位) {'lgb_det': 4.955, 'leaf63': 7.523, 'leaf127': 10.779}
[小得] 真 leaf127−leaf63 -10.24±0.34 補正CV -8.53 符号一致 12/12 相関 -0.04
[中雑音] leaf63: 補正の残り偏り -0.86±0.91 生の偏り +6.24±0.92
[中雑音] leaf127: 補正の残り偏り -0.00±1.73 生の偏り +16.58±1.76
[中雑音] A_test(×1e-4, 二乗の単位) {'lgb_det': 5.874, 'leaf63': 9.021, 'leaf127': 13.218}
[中雑音] 真 leaf127−leaf63 -15.28±0.34 補正CV -14.42 符号一致 12/12 相関 -0.17
--- 単純な方を優先する版(葉 31→63→127 の順に、補正CVが m' 以上良いときだけ大きい方へ進む): 全14本の最良との損(×1e-4、s0/s1 平均)---
kind 低雑音 小得 中雑音 最悪の超過(決定15 m'=0 の 0.88/0/0 を含む表での)
mp
0 0.88 0.0 0.0 0.00
1 1.43 0.0 0.0 0.55
2 1.26 0.0 0.0 0.38
3 2.41 0.0 0.0 1.53
4 3.25 0.0 0.0 2.37
pick leaf127 leaf63 lgb_det
mp
0 4 26 2
1 4 20 8
2 2 22 8
3 4 12 16
4 4 6 22
タスクC(分析の模範)
- 生の CV は葉の多い候補を大きく悪く見せ、補正CVはそれをほぼ消す。 生の偏りは
leaf63で +5.7〜+6.2、leaf127で +14.0〜+16.6(3世界、どれも SE 1 前後)。A_test も葉 31→63→127 で約 1.4 倍・2 倍に増え、偏りの増え方と合う。補正後の残りはleaf63−0.86〜+0.33、leaf127−0.00〜+2.04 で、どれも 2SE 未満。生の CV で基準を選ぶと低雑音で的中 1/16・損 3.37(補正CV は 5/16・0.88)。低雑音の生の CV はleaf127を +10.46 と「悪い」と言うが、真は −3.50 で良い - 低雑音の損 0.88 は、
leaf63とleaf127の引き分けを補正CVが見分けられないことから来る。 真の差leaf127−leaf63は +0.17±0.33(真の最良は 16 対 16 で半々)。補正CVの差は +1.87 で、ワールドごとの真の差との相関は −0.23、符号一致 5/16。つまり差が 2 を切ると補正CV は当てずっぽう。それでも損は小さい(引き分けなのでどちらを選んでも近い)。0.88 の内訳は、lgb_detを選んだ2件(各 4.53)が 0.28、leaf63を選んだ26件(平均 0.47)が 0.38、leaf127を選んだ4件(平均 1.70)が 0.21。「常にleaf63」は低雑音で 0.38 だが、小得 10.24・中雑音 15.28 でleaf127を逃す。ワールド対で決定15 − 常にleaf63は低雑音 +0.50±0.32(1.6SE、有意ではない)、小得 −10.24、中雑音 −15.28。決定15 を採る - 決定15 の印を △→○。決定14 は「休眠」に。 3世界の最悪の超過損は決定15 が 0.50、決定14(基準
lgb_det固定)が 25.24、常にlgb_detが 31.74。決定15 のあと、m を 0〜6 のどれにしても損は小数2桁まで同じ。今日の乱数ありの候補はすべて葉31以下かleaf63_b7(抜き出しで悪化)なので、強い基準を超えられない。決定14 は「乱数ありの候補が基準と同じ構造で作られたとき」にだけ意味を持つ。今の候補一覧では効かないが、消すのではなく「乱数ありの候補を基準の構造(葉 127)に合わせて作り直したら測り直す」と条件を付けて残す
タスクD(決定)
| # | 決定 | 採った案 | 採らなかった案と理由 | 前提が変わったら |
|---|---|---|---|---|
| 15(改訂、印 △→○) | 基準の選び方 | 乱数なしの候補(葉 31・63・127)を補正CVで比べ、最良を基準にする。比べるのに余裕は付けない(m'=0) | (a) 常に leaf63(低雑音で 0.38 と一番良いが、小得・中雑音で 10〜15 の損。最悪の超過 15.28)。(b) 単純な方を優先し m' 以上良いときだけ葉を増やす(m'=2 で低雑音 1.26、m'=0 の 0.88 より悪い。lgb_det に留まる件が 2→8 に増える)。(c) 生の CV で選ぶ(低雑音で的中 1/16・損 3.37) | 葉 127 は小得・中雑音でまだ良くなり続けている(−22.96・−31.74)。上限(255・511)と min_child_samples を足して、どこで止まるかを測る。差が 2 未満の候補どうしは補正CVで見分けられないので、候補を細かく刻んでも得は無い |
| 14(据え置き、印 ○→「休眠」) | 基準からの乗り換え(m=2) | 文面はそのまま。ただし決定15 の基準のあとでは今日の3世界で1回も発動しなかった(m=0〜6 で損が同じ) | (a) 削除(乱数ありの候補を葉 127 で作り直すと、また効く場面が出るかもしれない)。(b) m を下げる(発動しないので根拠が無い) | 乱数ありの候補を基準の構造(葉 63/127)で作り直したら、m を測り直す |
| 13(補正CV で比べる) | 印 ◎ のまま | 乱数なしの葉 63・127 でも生の偏り +6・+15 をほぼ消した(残り 2SE 未満) | — | 葉 255 以上で A_test がさらに大きくなったとき、残りの偏り(leaf127 低雑音で +2.04±1.28)が育たないかを見る |
設計書の印の更新:
| 決定 | 前 | 後 | 根拠 |
|---|---|---|---|
| 13(補正CV) | ◎ | ◎ | 葉 63・127 の生の偏り +5.7〜+16.6 を、補正後 −0.86〜+2.04(2SE 未満)に |
| 14(乗り換え m=2) | ○ | 休眠 | 決定15 のあと m=0〜6 で損が同じ(乗り換え0回) |
| 15(基準を乱数なし候補から選ぶ) | △ | ○ | 3世界の最悪の超過損 0.50(決定14 25.24、常にleaf63 15.28)。◎ にしないのは、葉 127 が上限でない(まだ良くなる)ことと、実データを測っていないため |
📊 結果の可視化
1枚目: 3世界での最悪の超過損。決定15 が 0.50 で、基準を lgb_det に固定した決定14(25.24)の約50分の1 → 決定15 の印を △→○。2枚目: 葉を増やした候補の真の差。低雑音でも良く、Day174 の「低雑音では悪いかも」は外れ。3枚目: 世界ごとの損。決定15 は小得・中雑音で 0、低雑音でだけ常に leaf63 に 0.50 負ける(1.6SE、有意でない)。
🪜 Step-by-Step 解説
Step 1: 作るものを最小にする
乱数ありの候補は Day172〜174 で作って保存してある。同じワールドは同じ乱数(3000+番号)・同じ雑音で作り直せるので、新しく作るのは乱数なしの3本だけでよい。乱数なしなら seed 組も1つで済む。これで 600 fit・約 11 分に収まった(全部作り直すと 40 ワールド × 14 本 × 2 seed で約 1 時間半の見込み)。
Step 2: 先に再現を確かめる
今日の lgb_det と保存物の lgb_det を、OOF と fold 別の test 予測の両方で比べた。3世界とも最大差 0.0。小得では今日の leaf63 と Day174 の leaf63 も 0.0。ラベル ytr・yev も np.array_equal で一致を assert している。これが崩れていたら、保存物と今日の予測を混ぜた表はすべて意味を失う。
Step 3: 真の差を先に見る
方針を比べる前に、候補そのものの真の差を見た。leaf127 は小得 −22.96・中雑音 −31.74 と、leaf63 よりさらに大きく良い。低雑音でも leaf63 −3.67・leaf127 −3.50 で lgb_det より良い。Day174 で置いた「低雑音では葉を増やすと悪いかも」は外れで、Day172 の leaf63_b7 +5.44 は抜き出しの害だった。
Step 4: 偏りを生と補正で分ける
生の CV は葉の多い候補を最大 +16.6 悪く見せていた(Day171 の「1モデル対5モデル平均」と同じ仕組みで、葉が多いほど1本ごとのばらつきが大きく、平均の得が大きい)。補正CV でこれはほぼ消える。生の CV で基準を選んでいたら、低雑音で leaf127 を「悪い」と判断していた。
Step 5: 方針の損を3世界で並べる
決定15 は小得・中雑音で損 0(24/24 で leaf127 を選んだ)、低雑音で 0.88。決定14(基準 lgb_det 固定)は 4.24・21.14・25.24。決定15 のあと m を動かしても損は1つも変わらなかった。
Step 6: 引き分けの扱いを確かめる
低雑音の 0.88 を減らせるか、「単純な方を優先し、m' 以上良いときだけ葉を増やす」版を試した。m'=2 で 1.26、m'=4 で 3.25 と、どれも m'=0 より悪い。余裕を付けると、引き分けの leaf63/leaf127 の間だけでなく、本当に悪い lgb_det に留まる件が増えるため。引き分けは損が小さいので放っておき、本当に大きい差だけを確実に取るのが今日の結論。
📐 数学・統計の補足(文系向け)
なぜ葉が多いほど生の CV が悪く見えるか(写真の例え)。 手ぶれのある写真を1枚だけ見る(OOF=1モデル)のと、5枚重ねて平均した写真を見る(提出=5モデル平均)のとでは、5枚重ねた方がくっきりする。葉の多い木は1枚ごとの手ぶれ(予測のばらつき)が大きいので、重ねたときの改善も大きい。生の CV は「1枚の写真」で採点するので、手ぶれの大きい候補ほど不利に見える。補正CV は、5枚の写真がどれだけ互いにずれているか(A_test)を測って、その分を差し引く。今日の A_test は葉 31 を 1 とすると、葉 63 で約 1.4〜1.5、葉 127 で約 1.9〜2.3 倍で、生の偏り(約 6 → 約 15)の増え方とそろう。
「差が 2 を切ると当てずっぽう」の意味。 補正CV の差は、真の差に「ワールドごとのずれ」(SD 3〜6)が足されたもの。真の差が 0.17 しかないと、ずれの方がずっと大きいので、どちらが良く見えるかは偶然で決まる(相関 −0.23)。真の差が 10 あれば(小得・中雑音の leaf127 対 leaf63)、ずれに埋もれず 12/12 当たる。見分けられる差の大きさには下限がある。その下限より小さい差を選び分けようとしても得は出ないが、損も小さい。
🏆 Kaggleでの実践的な使い方
- 基準モデルの葉の数(木の大きさ)は、乱数の無い2〜3本を並べて補正CVで決める。 既定の
num_leaves=31は多くのテーブルコンペで小さすぎることがある。今日の縮小シミュレータでは、雑音の大きさに関係なく 63 以上が良かった - 生の CV で木の大きさを比べない。 1モデルの OOF は大きい木を不利に見せる(今日は最大 +16.6)。提出が fold モデルの平均なら、比べる指標もそれに合わせる(決定13)。手早く済ませるなら、test 予測の fold 間分散を候補ごとに出しておくだけでよい(ラベル不要)
- 候補が強くなると「選び方の細かい調整」は効かなくなる。 今日は基準を強くしただけで、余裕 m の調整(Day173〜174 で 3 日かけた)が意味を失った。調整を詰める前に、候補一覧の一番弱いところ(今は葉の上限)を先に見る
- 引き分けは放っておく。 CV の差が SE と同じくらいの候補どうしを選び分けようと手を足しても(今日の m' 版)、本当に悪い候補に留まる件が増えて損が増えた
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 雑音が小さいデータでは大きい木は過学習で悪い | 「複雑なモデルは過学習」という一般論 | 今日の低雑音でも leaf63 は 16/16 で良い。悪く見えたのは抜き出し 0.7 の害と、生の CV の偏り |
| 生の CV が悪いなら大きい木はやめる | CV をそのまま信じる | 生の CV は低雑音で leaf127 を +10.46 悪く見せたが、真は −3.50。fold 平均で提出するなら補正CVで比べる |
| 余裕 m を付けた方が安全 | 外れの損だけを見る | 基準選びに余裕 m' を付けると、低雑音で lgb_det に留まる件が 2→8 に増え、損 0.88→1.26 |
| 補正CV は小さい差も見分けられる | 偏りを消した=精度も上がったと考える | 偏り(平均)は消えても、ワールドごとのずれ(SD 3〜6)は残る。真の差 0.17 は符号一致 5/16 |
| 決定14 が発動しないのはバグ | 何も起きないことを失敗と読む | 基準が強くなり、乱数ありの候補がどれも届かなくなった結果。表3 で m=0〜6 の損が同じことで確かめた |
🧭 扱わなかった範囲と理由
- 葉 255・511 と
min_child_samples:leaf127が小得・中雑音でまだ良くなり続けており、上限が分かっていない。今日は決定15 を3世界で確かめるのが目的で、候補を広げると「選び方」と「候補の中身」が混ざるため分けた。次回の最優先 - 乱数ありの候補を葉 127 で作り直すこと: 決定14 が休眠したのは候補一覧のせいか、仕組みのせいかは未分離。作り直すと 40 ワールド × 9 本 × 2 seed で 1 時間以上かかる
- 候補数 K と選択の上振れ: 今日の基準選びは3本からなので上振れは小さい。K 依存は持ち越し
- 公開LB と 2 本目の枠、avg3(3 seed 平均)での損: 測っていない
- 実データ(S5E5): 縮小シミュレータの上の数字のみ。S5E5 のルール確認・公開割合・敵対的AUC は Day171 で利用者の作業として切り出したまま
- 持ち越し: 系統別の m(
et)、OOF 側と test 側の seed の揺れの分離、noKeytel・raw7の補正後のずれ、補正CV と入れ子CV の比較、A_test の推定誤差、Day170 のハイブリッド・狭い10本の出し直し、SE 個数差と分割の新しさ(Day165)、Day162 の選択バイアス、真の効果ちょうど0の偽採用率、Nadeau & Bengio・Krogh & Vedelsby 原典(今日も記憶による)
🚀 次のステップ
- 発展: 乱数なしの葉 127・255・511 ×
min_child_samples20/50 を3世界に足し、補正CVの残りの偏り(leaf127低雑音 +2.04)が葉とともに育つか、どこで真の改善が止まるかを測る - 発展: 乱数ありの候補を葉 127 の上で作り直し(bagging 0.8・列抽出 0.7)、決定14 が再び効く場面があるかを確かめる
- 次回予告: Day 176 — Playground Series 実戦投入⑪(木の大きさの上限: 葉 127/255/511 ×
min_child_samples20/50 を乱数なしで作り、決定15 の候補一覧をどこまで広げると得が止まるか、補正CV の残りの偏りが育つかを測る)。月替わりテーマ3「画像(CV)系コンペへの応用」は 10/25 から
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: