📚 背景知識(読んでから問題へ)
アンサンブル(混ぜる)の考え方。 同じくらい上手で、間違え方が違う予想屋を2人雇い、平均を取ると、1人より当たる。逆に、2人が同じ間違え方をするなら平均を取っても変わらない。混ぜて得をする条件は「個々の精度」より「間違え方の違い(残差の相関の低さ)」である。Kaggle の上位解法で GBDT・NN・線形モデルを混ぜるのはこのため。
今日の3人の予想屋:
| モデル | 得意・不得意 | 今日使う特徴量 |
|---|---|---|
| HistGB(既定値、決定7) | 曲がった関係も拾える。割り算・掛け算の形は木を何本も重ねて近似する | 決定6の9列 |
| Ridge(線形回帰+縮小) | 「足し算の形」しか表せない。目的変数が rate × Duration のような掛け算なら、対数を取ると足し算になる(log(a×b)=log a+log b) | 9列、+ log_Dur、+ log 4列 |
| k近傍(15近傍の平均) | 似た行の平均を取る。木と違い、特徴量の「距離」で決まる | log_Dur・Heart_Rate・Weight・Age・Sex(標準化) |
Day164 からの持ち越し。 Day164 で log_Dur は木のモデルでは差がちょうど0だった(単調変換は木の分割点を変えない)。ただしその後ろに「線形モデルでは効くはず」という予想を置いたまま測っていない。今日測る。
今日の問い:
log_Durは Ridge に効くか。効くなら、Ridge は HistGB に追いつくか- 3モデルを混ぜると HistGB 単独より良くなるか。重みを OOF(Out-Of-Fold: 分割で学習に使わなかった行の予測)で決めたとき、見えた改善と本当の改善はどれだけずれるか
- 混ぜる重みは分割を変えても安定するか
用語整理:
| 用語 | 意味 |
|---|---|
| OOF | 5分割で、各行を「その行を学習に使っていない」モデルで予測した値。これで train 全体の点数が出る |
| 新分割R2 | 分割seed 1〜3 の5分割×3回、15個の fold 差から平均と SE を出したもの(Day164 から。重みは分割seed 0 で決めたものを固定して使う) |
| 真の効果 | test 6万行で、分割seed 1〜3 の予測を平均したものどうしの RMSLE の差(行で対にしてデルタ法で SE、Day164・165 と同じ) |
| 楽観 | 真の差 − 見えた差(×10⁻⁴)。正なら見かけの改善の方が大きい |
| 提出する形 | 5分割で作った5モデルの test 予測の平均(Day163 の設計)。OOF の予測は1モデルずつの予測で、提出する形とは違う |
縮小シミュレータは Day163〜165 と同じ(目的変数は Keytel et al. 2005, J Sports Sci 23(3):289–297 の心拍数からの消費カロリー式 × 運動時間)。ワールド0〜3 は Day164 のワールド0〜3 と同じ乱数のデータ(元15k・train 3万・test 6万)。数値は仮定の上の実測。
❓ 問題
タスクA(予想)— コードを書く前に紙に書く
log_Durを足すと Ridge の RMSLE はどれだけ下がるか。log 4列(log_Dur・log_HR・log_W・log_Keytel)まで足した Ridge は HistGB に勝てるか- 3モデルを OOF の重みで混ぜたとき、HistGB 単独に対する改善は、Day164 の特徴量1本(−0.00051)・Day165 のチューニング(−0.0002〜−0.0006)と比べて大きいか小さいか
- 重みは Ridge と k近傍のどちらに多く付くか
- OOF の重みで見えた改善は、test で見た本当の改善より大きいか。大きいなら原因は「重みを同じデータで決めたこと」だけか
タスクB(コーディング)
- Day165 のシミュレータで、HistGB・Ridge(3種の特徴量)・k近傍の OOF と test 予測を、分割seed 0〜3 で作る(
day166.py) - 表1(単独の点数)、表2(log 特徴量の効果)、表3(混ぜる改善の見かけと本当)、表4(重みの安定性)、表5(行を分けて重みの楽観を測る)を出す(
analyze166.py) - 補助: 3モデルの残差相関、1モデルで予測した test と 5モデル平均の test の差(
diag166.py)
タスクC(決定)
CV 設計書に「決定8: モデルを混ぜるか、どう混ぜるか」を足す。採らなかった案と、前提が変わったときの判断も書く。
💡 ヒント
ヒント1(方向性)
- 混ぜて得をするかは「個々の点数」と「間違え方の似かた」の両方で決まる。点数の悪いモデルでも、間違え方が違えば混ぜる価値がある
- 見えた改善の「甘さ」には少なくとも2つの出どころがある。重みを決めたデータで測ること、そして測っている予測の形と提出する予測の形が違うこと
ヒント2(アプローチ)
- 重みは非負・合計1に制限し、0.05 刻みの全探索で RMSLE を最小にする(モデル3つなら231通り。Breiman 1996 の stacked regressions も非負制約を置く)
- 行を半分に割って、半分で重みを決め、もう半分で測れば、「同じデータで決めた楽観」だけが分かる
- test を fold ごとに1モデルで予測した値を保存しておくと、「1モデル」と「5モデル平均」を比べられる
ヒント3(コード骨格)
コードを開く(7 行)
oof = np.zeros((4, n_models, N_TRAIN)); tp = np.zeros((4, n_models, N_TEST)) for s in range(4): # 分割seed 0〜3 for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=s).split(train)): for j, name in enumerate(MODELS): pv, pt = fit_predict(name, tr_df, y_log, [train.iloc[va], test]) oof[s, j, va] = pv; tp[s, j] += pt / 5 # tp = 5モデル平均 w = best_w(y, oof[0, ids], grid(3)) # 重みは分割seed 0 の OOF だけで決める
✅ 模範解答
タスクA(予想の模範と、実測との照合)
| # | 予想の模範 | 実測 |
|---|---|---|
| 1 | log_Dur で 0.03〜0.05 下がる。log 4列でさらに下がるが HistGB には届かない | 当たり。ridge_raw 0.2295 → +log_Dur 0.1798 → +log 4列 0.1655(ワールド0 test)。log_Dur 1列で −0.0497〜−0.0531(4ワールド、zT −55〜−57)、4列で −0.0640〜−0.0668。HistGB は 0.1555 で、4列の Ridge でも +0.0100 負ける |
| 2 | 0.0001〜0.0005 程度で、特徴量1本と同じ桁かそれ以下 | 当たり。ただし「見える」改善と「本当の」改善で割れた。OOF の最適重み(hgb 0.85〜0.9・knn 0.10〜0.15)で見えた改善は −0.00022〜−0.00034、test の本当の改善は −0.00021〜+0.00010(4ワールドの平均 −0.00006)。ワールド1 では本当は悪化(+0.97×10⁻⁴、zT +1.28) |
| 3 | Ridge の方が多く付く(log で線形化したので) | 外れ。 Ridge の重みは 20/20 回の重み決定すべてで 0。k近傍が 0.10〜0.15。Ridge は HistGB と残差の相関が 0.934〜0.939 と高く、k近傍は 0.885〜0.889 と低い。点数の悪い Ridge は「HistGB と同じ間違え方をする悪い版」だった |
| 4 | 大きい。原因は同じデータで重みを決めたこと | 半分当たり。 見かけ −0.00028 に対し本当は −0.00006(見かけの方が大きい)。ただし重みだけの楽観は 4ワールド平均 +0.29×10⁻⁴ しかない(表5)。大きいのは、OOF が測る「1モデル」と提出する「5モデル平均」の差で、混ぜる改善が 1モデルでは −2.4〜−4.3 のものが 5モデル平均では −0.4〜−2.3(×10⁻⁴、約 2×10⁻⁴ 縮む)になる |
タスクB(完全なコード)
day166.py(モデル作成。ワールド番号を引数に、4並行で各 67〜69秒):
コードを開く(112 行)
"""Day166: Playground S5E5 縮小シミュレータで「性質の違うモデルを混ぜる」を測る。 モデル3つ(HistGB / Ridge+log特徴量 / k近傍)を、分割seed 0〜3 の5分割で OOF 予測し、 (1) Ridge に log_Dur を足すと効くか (2) 混ぜる重みを OOF で決めたときの楽観 (3) 重みの安定性 を数える。 使い方: OMP_NUM_THREADS=2 python day166.py <world> -> raw_w<world>.npz """ import sys, time import numpy as np, pandas as pd from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.linear_model import Ridge from sklearn.neighbors import KNeighborsRegressor from sklearn.preprocessing import StandardScaler from sklearn.model_selection import KFold T0 = time.time() W = int(sys.argv[1]) N_ORIG, N_TRAIN, N_TEST = 15000, 30000, 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-165 と同じ 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, 0.05, 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-165 と同じ 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, 0.03, n))).round(), 1, None) return s def add_feats(d): d = d.copy() d["Dur_x_HR"] = d.Duration * d.Heart_Rate # 決定6 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 # 決定6 d["log_Dur"] = np.log(d.Duration) d["log_HR"] = np.log(d.Heart_Rate) d["log_W"] = np.log(d.Weight) d["log_Keytel"] = np.log(d.Keytel_kcal) return d BASE = ["Sex", "Age", "Height", "Weight", "Duration", "Heart_Rate", "Body_Temp", "Dur_x_HR", "Keytel_kcal"] SETS = {"hgb": BASE, "ridge_raw": BASE, "ridge_logDur": BASE + ["log_Dur"], # 1列だけ足す(Day164 の問いを線形モデルで) "ridge_log": BASE + ["log_Dur", "log_HR", "log_W", "log_Keytel"], "knn": ["log_Dur", "Heart_Rate", "Weight", "Age", "Sex"]} MODELS = list(SETS) def make(name): if name == "hgb": # 決定7の既定値、モデル側の乱数なし return HistGradientBoostingRegressor(early_stopping=False, random_state=0, learning_rate=0.1, max_iter=300, max_leaf_nodes=31, min_samples_leaf=20) if name.startswith("ridge"): return Ridge(alpha=1.0) return KNeighborsRegressor(n_neighbors=15, algorithm="kd_tree") def fit_predict(name, tr_df, ytr_log, preds_dfs): cols = SETS[name] if name == "hgb": m = make(name).fit(tr_df[cols], ytr_log) return [m.predict(d[cols]) for d in preds_dfs] sc = StandardScaler().fit(tr_df[cols]) m = make(name).fit(sc.transform(tr_df[cols]), ytr_log) return [m.predict(sc.transform(d[cols])) for d in preds_dfs] rng = np.random.default_rng(2000 + W) # Day164/165 のワールド W と同じデータ orig = make_original(N_ORIG, rng) pool = synth_from(orig, N_TRAIN + N_TEST, rng) orig, pool = add_feats(orig), add_feats(pool) train, test = pool.iloc[:N_TRAIN].reset_index(drop=True), pool.iloc[N_TRAIN:].reset_index(drop=True) oof = np.zeros((4, len(MODELS), N_TRAIN)) # [分割seed, モデル, 行] すべて log1p 空間 tp = np.zeros((4, len(MODELS), N_TEST)) tpf = np.zeros((5, len(MODELS), N_TEST), dtype=np.float32) # 分割seed1 の fold ごとの test 予測(1モデルずつ) for s in range(4): for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=s).split(train)): tr_df = pd.concat([train.iloc[tr], orig], ignore_index=True) y = np.log1p(tr_df.Calories.values) for j, name in enumerate(MODELS): pv, pt = fit_predict(name, tr_df, y, [train.iloc[va], test]) oof[s, j, va] = pv tp[s, j] += pt / 5 if s == 1: tpf[k, j] = pt print(f"world {W} split {s} done {time.time()-T0:.0f}s", flush=True) np.savez_compressed(f"raw_w{W}.npz", oof=oof, tp=tp, tpf=tpf, ytr=train.Calories.values, yt=test.Calories.values, models=np.array(MODELS)) print(f"world {W} wall {time.time()-T0:.0f}s")
analyze166.py(集計):
コードを開く(110 行)
"""Day166 集計: raw_w*.npz を読み、表1〜5 を出す。使い方: python analyze166.py""" import glob, itertools import numpy as np, pandas as pd from sklearn.model_selection import KFold pd.set_option("display.width", 250) files = sorted(glob.glob("raw_w*.npz")) M = ["hgb", "ridge_raw", "ridge_logDur", "ridge_log", "knn"] ix = {m: i for i, m in enumerate(M)} def sle(y, lp): return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2 def rmsle(y, lp): return np.sqrt(sle(y, lp).mean()) def folds(y, lp, s): return np.array([np.sqrt(sle(y[va], lp[va]).mean()) for _, va in KFold(5, shuffle=True, random_state=s).split(y)]) def grid(k, step=0.05): n = int(round(1 / step)) return [np.array(c) * step for c in itertools.product(range(n + 1), repeat=k) if sum(c) == n] def best_w(y, P, G): # P: (k, rows) sc = [rmsle(y, np.tensordot(w, P, axes=1)) for w in G] return G[int(np.argmin(sc))] def compare(y, yt, oof, tp, a, b): """a が b より良いか(a-b、負=a が良い)。新分割R2(分割seed1〜3の15 fold差)と test の真の効果""" d15 = np.concatenate([folds(y, oof[s, a], s) - folds(y, oof[s, b], s) for s in (1, 2, 3)]) dF, seF = d15.mean(), d15.std(ddof=1) / np.sqrt(15) pa, pb = tp[1:, a].mean(0), tp[1:, b].mean(0) ea, eb = sle(yt, pa), sle(yt, pb) dT = np.sqrt(ea.mean()) - np.sqrt(eb.mean()) seT = (ea - eb).std(ddof=1) / np.sqrt(len(yt)) / (2 * np.sqrt(eb.mean())) return dF, seF, dT, seT G3, G2 = grid(3), grid(2) t1, t2, t3, t4, t5 = [], [], [], [], [] for f in files: w = int(f[-5]) z = np.load(f); oof, tp, y, yt = z["oof"], z["tp"], z["ytr"], z["yt"] # 表1 単独モデルの点数 for m in M: t1.append(dict(world=w, model=m, oof_s0=rmsle(y, oof[0, ix[m]]), test=rmsle(yt, tp[1:, ix[m]].mean(0)))) # 表2 線形モデルへの log 特徴量 for a, b in [("ridge_logDur", "ridge_raw"), ("ridge_log", "ridge_raw"), ("ridge_log", "ridge_logDur")]: dF, seF, dT, seT = compare(y, yt, oof, tp, ix[a], ix[b]) t2.append(dict(world=w, pair=f"{a} - {b}", dF=dF * 1e4, zF=dF / seF, dT=dT * 1e4, zT=dT / seT)) # 表3 混ぜる: 分割seed0 の OOF で重みを決め、新分割R2 と test で測る for tag, names, G in [("hgb+ridge_log", ["hgb", "ridge_log"], G2), ("hgb+ridge_log+knn", ["hgb", "ridge_log", "knn"], G3)]: ids = [ix[n] for n in names] wts = best_w(y, oof[0, ids], G) for label, wv in [("OOF最適", wts), ("等分", np.ones(len(ids)) / len(ids))]: blend_oof = np.tensordot(wv, oof[:, ids], axes=([0], [1])) # (4, rows) blend_tp = np.tensordot(wv, tp[:, ids], axes=([0], [1])) oo, tt = oof.copy(), tp.copy() oo[:, 0] = blend_oof; tt[:, 0] = blend_tp # 0番目(hgb)を混ぜた予測で置換 dS = rmsle(y, blend_oof[0]) - rmsle(y, oof[0, 0]) fs0 = folds(y, blend_oof[0], 0) - folds(y, oof[0, 0], 0) seS = fs0.std(ddof=1) / np.sqrt(5) # 比較相手の hgb 単独は元の配列から d15 = np.concatenate([folds(y, blend_oof[s], s) - folds(y, oof[s, 0], s) for s in (1, 2, 3)]) dF, seF = d15.mean(), d15.std(ddof=1) / np.sqrt(15) ea, eb = sle(yt, blend_tp[1:].mean(0)), sle(yt, tp[1:, 0].mean(0)) dT = np.sqrt(ea.mean()) - np.sqrt(eb.mean()) seT = (ea - eb).std(ddof=1) / np.sqrt(len(yt)) / (2 * np.sqrt(eb.mean())) t3.append(dict(world=w, mix=tag, weights=label + str(np.round(wv, 2).tolist()), dS=dS * 1e4, zS=dS / seS, dF=dF * 1e4, zF=dF / seF, dT=dT * 1e4, zT=dT / seT, opt_S=(dT - dS) * 1e4, opt_F=(dT - dF) * 1e4)) # 表4 重みの安定性: 分割seed 0〜3 それぞれで決めた重み、test で決めた重み ids = [ix["hgb"], ix["ridge_log"], ix["knn"]] for s in range(4): t4.append(dict(world=w, source=f"OOF split{s}", **dict(zip(["hgb", "ridge_log", "knn"], best_w(y, oof[s, ids], G3))))) t4.append(dict(world=w, source="test(正解)", **dict(zip(["hgb", "ridge_log", "knn"], best_w(yt, tp[1:, ids].mean(0), G3))))) # 表5 重みを決める行と測る行を分ける: 行の半分で重みを決め、残り半分で測る(OOF split0、20回) rng = np.random.default_rng(w) gaps = [] for _ in range(20): perm = rng.permutation(len(y)); A, B = perm[: len(y) // 2], perm[len(y) // 2:] P = oof[0, ids] wv = best_w(y[A], P[:, A], G3) gA = rmsle(y[A], np.tensordot(wv, P[:, A], axes=1)) - rmsle(y[A], P[0, A]) gB = rmsle(y[B], np.tensordot(wv, P[:, B], axes=1)) - rmsle(y[B], P[0, B]) gaps.append((gA, gB)) gaps = np.array(gaps) t5.append(dict(world=w, 決めた半分=gaps[:, 0].mean() * 1e4, 測った半分=gaps[:, 1].mean() * 1e4, 楽観=(gaps[:, 1] - gaps[:, 0]).mean() * 1e4, 楽観SE=(gaps[:, 1] - gaps[:, 0]).std(ddof=1) / np.sqrt(20) * 1e4)) T1 = pd.DataFrame(t1).pivot(index="world", columns="model", values=["oof_s0", "test"]) print("=== 表1 単独モデルの RMSLE(oof_s0 = 分割seed0のOOF、test = 6万行)==="); print(T1.round(5).to_string()) print("\n=== 表2 log特徴量は線形モデルに効くか(差 ×1e-4、負 = 前者が良い)==="); print(pd.DataFrame(t2).round(2).to_string(index=False)) print("\n=== 表3 混ぜる: hgb 単独との差 ×1e-4(負 = 混ぜた方が良い)、opt = 真の差 − 見えた差 ==="); T3 = pd.DataFrame(t3); print(T3.round(2).to_string(index=False)) print("\n=== 表4 重みの安定性 ==="); print(pd.DataFrame(t4).round(2).to_string(index=False)) print("\n=== 表5 行を分けて重みを測る(hgb単独との差 ×1e-4、20回平均)==="); print(pd.DataFrame(t5).round(2).to_string(index=False)) T2, T4 = pd.DataFrame(t2), pd.DataFrame(t4) assert (T2[T2.pair == "ridge_logDur - ridge_raw"].zT < -10).all(), "log_Dur 1列で Ridge は全ワールドで大きく良くなる" assert (T1["test"]["ridge_log"] > T1["test"]["hgb"]).all(), "log 4列の Ridge でも HistGB には全ワールドで負ける" assert (T4[T4.source.str.startswith("OOF")].ridge_log == 0).all(), "OOF で決めた重みで Ridge は 16/16 回すべて 0" print("all asserts passed")
diag166.py(補助。残差相関と、1モデル対 5モデル平均):
コードを開く(36 行)
"""Day166 補助: (1) 3モデルの残差相関 (2) test 予測を 5モデル平均(1分割) と 15モデル平均(3分割) で作ったときの混ぜる効果""" import glob import numpy as np M = ["hgb", "ridge_raw", "ridge_logDur", "ridge_log", "knn"]; ix = {m: i for i, m in enumerate(M)} def sle(y, lp): return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2 def rm(y, lp): return np.sqrt(sle(y, lp).mean()) print("world | 残差相関 hgb-ridge_log / hgb-knn / ridge_log-knn (OOF split0, log空間) | test RMSLE hgb: 1分割(5モデル平均) 3分割(15モデル平均) | 混ぜる(0.9/0/0.1)との差 ×1e-4: OOF split0, test 1分割, test 3分割") for f in sorted(glob.glob("raw_w*.npz")): z = np.load(f); oof, tp, y, yt = z["oof"], z["tp"], z["ytr"], z["yt"] ly = np.log1p(y); lyt = np.log1p(yt) r = {m: oof[0, ix[m]] - ly for m in ["hgb", "ridge_log", "knn"]} c = lambda a, b: np.corrcoef(r[a], r[b])[0, 1] h1, h3 = rm(yt, tp[1, 0]), rm(yt, tp[1:, 0].mean(0)) w = np.array([0.9, 0, 0.1]); ids = [0, 3, 4] bl = lambda P: np.tensordot(w, P[ids], axes=1) d_oof = rm(y, bl(oof[0])) - rm(y, oof[0, 0]) d_t1 = rm(yt, bl(tp[1])) - rm(yt, tp[1, 0]) d_t3 = rm(yt, bl(tp[1:].mean(0))) - rm(yt, tp[1:, 0].mean(0)) print(f"{f[-5]} | {c('hgb','ridge_log'):.3f} / {c('hgb','knn'):.3f} / {c('ridge_log','knn'):.3f} | {h1:.5f} {h3:.5f} | {d_oof*1e4:+.2f} {d_t1*1e4:+.2f} {d_t3*1e4:+.2f}") print("\n--- 1モデル(fold単独) と 5モデル平均 の test 差(分割seed1、混ぜる 0.9/0/0.1 − hgb、×1e-4)---") for f in sorted(glob.glob("raw_w*.npz")): z = np.load(f); tpf, tp, yt = z["tpf"].astype(np.float64), z["tp"], z["yt"] w = np.array([0.9, 0, 0.1]); ids = [0, 3, 4] single = [rm(yt, np.tensordot(w, tpf[k][ids], axes=1)) - rm(yt, tpf[k, 0]) for k in range(5)] avg5 = rm(yt, np.tensordot(w, tp[1][ids], axes=1)) - rm(yt, tp[1, 0]) h_single = np.mean([rm(yt, tpf[k, 0]) for k in range(5)]); h_avg = rm(yt, tp[1, 0]) print(f"world {f[-5]}: 1モデル平均 {np.mean(single)*1e4:+.2f} (範囲 {min(single)*1e4:+.2f}〜{max(single)*1e4:+.2f}) | 5モデル平均 {avg5*1e4:+.2f} | hgb 単独 test RMSLE 1モデル {h_single:.5f} 5モデル平均 {h_avg:.5f}") print("\n--- 固定重み 0.9/0/0.1 の本当の効果(test、分割seed1〜3 平均、hgb 単独との差)---") for f in sorted(glob.glob("raw_w*.npz")): z = np.load(f); tp, yt = z["tp"], z["yt"] w = np.array([0.9, 0, 0.1]); ids = [0, 3, 4] ea, eb = sle(yt, np.tensordot(w, tp[1:][:, ids].mean(0), axes=1)), sle(yt, tp[1:, 0].mean(0)) d = np.sqrt(ea.mean()) - np.sqrt(eb.mean()); se = (ea - eb).std(ddof=1) / np.sqrt(len(yt)) / (2 * np.sqrt(eb.mean())) print(f"world {f[-5]}: dT {d*1e4:+.2f}e-4 zT {d/se:+.2f}")
実行結果(実測)
# ============ 実行環境: macOS (M1, メモリ8GB)・Python 3.13.12・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6(新規 uv venv)
# 4ワールドを別プロセスで並行(各 OMP_NUM_THREADS=2)。壁時計 66〜69秒(初回は 84〜87秒)。fit 数 = 4分割seed × 5 fold × 5モデル = 100 / ワールド
# 2回回して oof・tp が完全一致(最大差 0.0)。2回目は fold ごとの test 予測 tpf を足して保存 ============
=== 表1 単独モデルの RMSLE(oof_s0 = 分割seed0のOOF、test = 6万行)===
oof_s0 test
model hgb knn ridge_log ridge_logDur ridge_raw hgb knn ridge_log ridge_logDur ridge_raw
world
0 0.15830 0.17251 0.16713 0.18146 0.22884 0.15550 0.16922 0.16549 0.17983 0.22951
1 0.15773 0.17186 0.16730 0.18134 0.23287 0.15529 0.17050 0.16574 0.18115 0.23123
2 0.15943 0.17523 0.16882 0.18214 0.23426 0.15673 0.17047 0.16632 0.18002 0.23317
3 0.15780 0.17330 0.16804 0.18250 0.23375 0.15777 0.17354 0.16852 0.18198 0.23399
=== 表2 log特徴量は線形モデルに効くか(差 ×1e-4、負 = 前者が良い)===
world pair dF zF dT zT
0 ridge_logDur - ridge_raw -473.71 -78.81 -496.77 -55.53
0 ridge_log - ridge_raw -617.03 -119.52 -640.16 -69.40
0 ridge_log - ridge_logDur -143.33 -63.67 -143.39 -38.40
1 ridge_logDur - ridge_raw -515.34 -67.92 -500.78 -55.47
1 ridge_log - ridge_raw -655.60 -73.52 -654.92 -70.88
1 ridge_log - ridge_logDur -140.26 -58.86 -154.13 -41.05
2 ridge_logDur - ridge_raw -521.33 -60.22 -531.43 -57.14
2 ridge_log - ridge_raw -654.60 -90.47 -668.47 -69.98
2 ridge_log - ridge_logDur -133.27 -43.51 -137.04 -39.02
3 ridge_logDur - ridge_raw -512.48 -50.14 -520.07 -55.50
3 ridge_log - ridge_raw -657.15 -59.44 -654.70 -67.28
3 ridge_log - ridge_logDur -144.66 -48.08 -134.63 -35.48
=== 表3 混ぜる: hgb 単独との差 ×1e-4(負 = 混ぜた方が良い)、opt = 真の差 − 見えた差 ===
world mix weights dS zS dF zF dT zT opt_S opt_F
0 hgb+ridge_log OOF最適[0.95, 0.05] -0.35 -1.11 -0.62 -3.17 1.08 5.38 1.43 1.70
0 hgb+ridge_log 等分[0.5, 0.5] 19.55 7.20 17.37 10.74 29.97 14.18 10.42 12.59
0 hgb+ridge_log+knn OOF最適[0.9, 0.0, 0.1] -2.96 -3.70 -3.46 -7.22 -1.24 -2.57 1.71 2.22
0 hgb+ridge_log+knn 等分[0.33, 0.33, 0.33] 26.80 6.98 23.54 10.72 35.33 12.37 8.52 11.79
1 hgb+ridge_log OOF最適[1.0, 0.0] 0.00 NaN 0.00 NaN 0.00 NaN 0.00 0.00
1 hgb+ridge_log 等分[0.5, 0.5] 23.58 9.21 21.72 10.70 31.36 13.94 7.78 9.64
1 hgb+ridge_log+knn OOF最適[0.85, 0.0, 0.15] -3.37 -4.92 -3.48 -7.59 0.97 1.28 4.34 4.45
1 hgb+ridge_log+knn 等分[0.33, 0.33, 0.33] 28.88 10.71 27.95 13.24 40.52 13.25 11.65 12.57
2 hgb+ridge_log OOF最適[0.95, 0.05] -0.34 -0.70 -0.34 -1.88 0.66 3.12 1.00 1.00
2 hgb+ridge_log 等分[0.5, 0.5] 21.15 4.74 21.31 11.41 26.79 12.47 5.64 5.48
2 hgb+ridge_log+knn OOF最適[0.9, 0.0, 0.1] -2.22 -2.25 -1.88 -3.73 -2.09 -4.06 0.13 -0.21
2 hgb+ridge_log+knn 等分[0.33, 0.33, 0.33] 32.67 5.73 33.64 12.30 32.30 11.04 -0.36 -1.33
3 hgb+ridge_log OOF最適[0.95, 0.05] -0.09 -0.37 -0.27 -1.10 0.83 3.63 0.92 1.09
3 hgb+ridge_log 等分[0.5, 0.5] 24.42 10.59 23.37 10.71 30.52 13.13 6.10 7.15
3 hgb+ridge_log+knn OOF最適[0.9, 0.0, 0.1] -2.44 -3.48 -2.68 -6.65 -0.22 -0.42 2.22 2.46
3 hgb+ridge_log+knn 等分[0.33, 0.33, 0.33] 33.99 9.83 32.95 13.58 42.05 13.44 8.06 9.09
=== 表4 重みの安定性 ===
world source hgb ridge_log knn
0 OOF split0 0.90 0.0 0.10
0 OOF split1 0.85 0.0 0.15
0 OOF split2 0.85 0.0 0.15
0 OOF split3 0.85 0.0 0.15
0 test(正解) 0.90 0.0 0.10
1 OOF split0 0.85 0.0 0.15
1 OOF split1 0.90 0.0 0.10
1 OOF split2 0.85 0.0 0.15
1 OOF split3 0.85 0.0 0.15
1 test(正解) 0.95 0.0 0.05
2 OOF split0 0.90 0.0 0.10
2 OOF split1 0.90 0.0 0.10
2 OOF split2 0.90 0.0 0.10
2 OOF split3 0.90 0.0 0.10
2 test(正解) 0.90 0.0 0.10
3 OOF split0 0.90 0.0 0.10
3 OOF split1 0.90 0.0 0.10
3 OOF split2 0.90 0.0 0.10
3 OOF split3 0.90 0.0 0.10
3 test(正解) 0.95 0.0 0.05
=== 表5 行を分けて重みを測る(hgb単独との差 ×1e-4、20回平均)===
world 決めた半分 測った半分 楽観 楽観SE
0 -3.54 -2.19 1.35 0.69
1 -3.26 -3.30 -0.04 0.54
2 -2.21 -2.01 0.20 0.30
3 -2.17 -2.54 -0.37 0.29
all asserts passed
world | 残差相関 hgb-ridge_log / hgb-knn / ridge_log-knn (OOF split0, log空間) | test RMSLE hgb: 1分割(5モデル平均) 3分割(15モデル平均) | 混ぜる(0.9/0/0.1)との差 ×1e-4: OOF split0, test 1分割, test 3分割
0 | 0.939 / 0.889 / 0.904 | 0.15560 0.15550 | -2.96 -1.37 -1.24
1 | 0.939 / 0.888 / 0.904 | 0.15542 0.15529 | -3.25 -0.43 -0.26
2 | 0.937 / 0.886 / 0.907 | 0.15689 0.15673 | -2.22 -2.31 -2.09
3 | 0.934 / 0.885 / 0.905 | 0.15787 0.15777 | -2.44 -0.44 -0.22
--- 1モデル(fold単独) と 5モデル平均 の test 差(分割seed1、混ぜる 0.9/0/0.1 − hgb、×1e-4)---
world 0: 1モデル平均 -3.50 (範囲 -4.17〜-2.87) | 5モデル平均 -1.37 | hgb 単独 test RMSLE 1モデル 0.15694 5モデル平均 0.15560
world 1: 1モデル平均 -2.39 (範囲 -2.62〜-2.02) | 5モデル平均 -0.43 | hgb 単独 test RMSLE 1モデル 0.15669 5モデル平均 0.15542
world 2: 1モデル平均 -4.30 (範囲 -4.57〜-4.09) | 5モデル平均 -2.31 | hgb 単独 test RMSLE 1モデル 0.15816 5モデル平均 0.15689
world 3: 1モデル平均 -2.62 (範囲 -3.18〜-1.86) | 5モデル平均 -0.44 | hgb 単独 test RMSLE 1モデル 0.15924 5モデル平均 0.15787
--- 固定重み 0.9/0/0.1 の本当の効果(test、分割seed1〜3 平均、hgb 単独との差)---
world 0: dT -1.24e-4 zT -2.57
world 1: dT -0.26e-4 zT -0.51
world 2: dT -2.09e-4 zT -4.06
world 3: dT -0.22e-4 zT -0.42
読み方: dS は分割seed 0 の OOF、dF は新分割R2、dT は test で見た「混ぜた − HistGB単独」(×10⁻⁴、負なら改善)。opt_S・opt_F は真の差 − 見えた差(正なら見かけの方が大きい)。「OOF最適」の重みは分割seed 0 の OOF だけで決めた。hgb+ridge_log のワールド1 は重みが hgb 1.0 になり、混ぜていない(差 0、z は 0÷0 で NaN)。
📊 結果の可視化
図1 log_Dur は線形モデルに桁違いに効く。それでも HistGB には届かない
ridge_raw → +log_Dur で 497(×10⁻⁴、zT −55)縮み、log 4列でさらに 144。HistGB では log_Dur の差がちょうど0だった(Day164)。
図2 混ぜる改善は、測る「形」で大きさが変わる
青(OOF)と紫(test の1モデル)は同じ大きさ。黄(提出の形、5モデル平均)だけ約 2×10⁻⁴ 短い。重みは 0.9/0/0.1 で固定、test は分割seed 1 の5モデル。
🧭 決定 — 混ぜ方と CV 設計書「決定8」
タスクC(決定)
(1) 手順: モデルは HistGB を土台にし、k近傍を重み 0.10 で固定して混ぜる。Ridge は混ぜない。重みは OOF で細かく決めず、丸めた値で固定する。混ぜる効果は「OOF の改善 − 0.0002」と見積もる。
| 案 | 判定 | 理由(今日の数字) |
|---|---|---|
| 等分(1/3ずつ)で混ぜる | 採らない | 4ワールドで hgb 単独より +0.0032〜+0.0042 悪化(zT +11〜+13)。点数の悪いモデルを同じ重みで混ぜた |
| OOF 最適重み(hgb 0.85〜0.9, ridge 0, knn 0.10〜0.15)をそのまま使う | 採らない | 見かけ −0.00028 に対し本当は −0.00006。ワールド1 は 0.85/0/0.15 で本当は +0.97×10⁻⁴ 悪化。OOF の重みは test の最適(hgb 0.90〜0.95)より knn に 0.05〜0.10 多く振れた(表4、ワールド1・3) |
| Ridge を混ぜる | 採らない | 重みは 16/16 回すべて 0(OOF 4分割seed × 4ワールド)。log 4列で単独 0.1655 まで来ても HistGB と残差相関 0.934〜0.939 で、混ぜる情報が無い |
| k近傍を 0.10 固定で混ぜる | 採る | 0.9/0/0.1 の本当の差は 4ワールドとも負(−1.24・−0.26・−2.09・−0.22 ×10⁻⁴)。ただし小さく、zT(0.9/0/0.1 固定)は −2.57・−0.51・−4.06・−0.42 で、−2 を下回ったのは2ワールドだけ。費用は k近傍1回の学習で、悪化しない側に倒れた |
| HistGB 単独で出す | 次点 | 混ぜる本当の改善は平均 −0.00006 で、公開 LB の揺れ(Day163 の実物換算 約 0.0009、未照合)の 1/10 以下。混ぜなくても失うものは測れない大きさ。k近傍を採ったのは「4/4 で悪化しなかった」という、4ワールドだけの根拠 |
| stacking(メタモデルで重みを学習)・NN を混ぜる | 今回は採らない | 非線形に混ぜる自由度が増えると楽観が増える(Day165 の探索と同じ構造)。今日は重み2個の線形混合までしか測っていない |
注意(今日の比較の混ざり): 表3の「新分割R2(dF)」は重みを決めた分割seed 0 と同じ train 3万行で測っているため、行に由来する楽観は残る。表5(行を半分に割る)で重みだけの楽観は 4ワールド平均 +0.29×10⁻⁴(SE 0.3〜0.7)と小さかったが、ワールド0 だけ +1.35±0.69 で 2SE に近い。
(2) CV 設計書に書き足す「決定8: モデルを混ぜるか」(S5E5 用)
| # | 決定 | 採った案 | 採らなかった案と理由 | 前提が変わったら |
|---|---|---|---|---|
| 8 | モデルを混ぜるか、どう混ぜるか | HistGB(決定6・7 の設定)の5分割平均に、k近傍(log_Dur・Heart_Rate・Weight・Age・Sex、15近傍)を重み 0.10 で混ぜる。Ridge は混ぜない。重みは丸めて固定。報告する改善幅は「OOF の改善 − 0.0002」(実測の縮み 1.96〜2.18×10⁻⁴、4ワールド) | 等分混合(+0.003 悪化)、OOF 最適重み(knn 過多でワールド1 は悪化)、Ridge を混ぜる(重み 0)、stacking・NN(楽観の自由度が増える) | 提出を5分割平均でなく1モデルにする場合は OOF の改善がそのまま近い(1モデルの test では −2.4〜−4.3 ×10⁻⁴)ので、OOF 最適重みを使ってよい。GBDT 側に bagging・列抽出などの乱数があるモデルなら、5モデル平均の縮みは別に測る。Ridge は線形モデルが特徴量の足し算で強い問題(目的変数が加法的に分解できる)では重みが付く可能性がある。この問題では木が Duration × Heart_Rate の形を拾えるため 0 |
🪜 Step-by-Step
Step 1: 4つの「特徴量の組」を用意する
HistGB と ridge_raw は同じ決定6の9列。ridge_logDur は 9列+log_Dur の1列だけ足す(Day164 の「log_Dur は効くか」を線形モデルで隔離して測るため)。ridge_log は 9列+log 4列。k近傍は距離で効くので、似た意味の列を重ねず5列に絞り、log_Dur を使う(Duration の間隔は短い所ほど意味が大きい)。k近傍の列の選び方は測っていない(扱わなかった範囲)。
Step 2: 学習は log1p 空間に統一する
目的変数 log1p(Calories) に3モデルとも回帰する。RMSLE がこの空間の二乗誤差の平均の平方根そのものなので、混ぜる計算(log 空間の重み付き平均)と評価が噛み合う。Ridge と k近傍は標準化してから学習した(Ridge の縮小の強さを列の単位で変えないため)。
Step 3: 予想を作るときの「形」を2通り保存する
各 fold で、(a) 検証用行の予測(OOF)、(b) test 6万行の予測を作る。(b) は fold ごとに足して5で割り、5モデル平均として保存する(Day163 からの提出の形)。さらに分割seed 1 だけは fold ごとの予測も保存した(tpf、1モデルの予測)。今日の主発見はこの2つの比較から出た。
Step 4: log_Dur は線形モデルでは巨大に効く(表2)
ridge_raw 0.2295 → ridge_logDur 0.1798(ワールド0 test)。1列で −0.0497。4ワールドとも zT は −55〜−57。HistGB では同じ列が差ちょうど0だった(Day164)ので、特徴量の価値はモデルの種類で決まり、Day164 の「log_Dur は無意味」は木に限った結論だった。log 4列で 0.1655 まで来るが、HistGB の 0.1555 には届かない。
Step 5: 混ぜる重みを決める(表4)
0.05 刻みで RMSLE を最小にする重みを全探索した。Ridge の重みは 16/16 回で 0。理由は残差相関(log 空間、分割seed 0 の OOF)で、hgb と ridge_log は 0.934〜0.939、hgb と knn は 0.885〜0.889。点数も ridge_log(OOF 0.167〜0.169)は knn(0.172〜0.175)より良いのに、knn の方が HistGB と違う間違え方をした。
Step 6: 見えた改善と本当の改善(表3)
OOF の重みで混ぜた3モデルは、OOF で hgb 単独に −2.22〜−3.37 ×10⁻⁴(zS −2.25〜−4.92)。同じ重みで test を測ると −2.09〜+0.97 ×10⁻⁴。平均すると見かけ −2.75、本当 −0.65。
Step 7: 楽観の出どころを2つに分ける(表5 と 診断)
- 重みを同じデータで決めた楽観: 行を半分で割る実験(表5)で 4ワールド平均 +0.29 ×10⁻⁴。小さい
- OOF が測る形と提出する形の違い: 同じ重み 0.9/0/0.1 で test を測ると、fold ごとの1モデルでは −2.39〜−4.30、5モデル平均では −0.43〜−2.31。縮み 1.96〜2.18 ×10⁻⁴ が4ワールドでほぼ一定。HistGB 単独の test RMSLE も、1モデル 0.1567〜0.1592 から 5モデル平均 0.1554〜0.1579 に下がる。5モデルを平均すると、k近傍を混ぜて取りたかった「分散を減らす効果」を HistGB 自身がすでに取ってしまう(仮説。分割seed 1 の5モデルから15モデルに増やしても混ぜる改善は −1.37→−1.24 などほぼ動かず、分散の残りは小さいことと整合する)
- 採否の判定で何が起きたか: Day164・165 の R2(新分割の fold 差)は、OOF 同士の比較なので「1モデルの差」を測る。今日は R2 で zF −3〜−7 と全ワールド採用になるが、真の効果は zT −2 を下回ったのが 2/4 ワールドだけ。R2 が偽採用を出したのは、行の問題でなく測る対象の形が違うから
Step 8: 決定を設計書に足す
タスクCの表。決定6(特徴量)→ 決定7(チューニング)→ 決定8(混ぜる)の順で、S5E5 の手順は「特徴量を決める → 既定値を含めて探す → 5分割平均で提出する形を決める」になる。
🧮 数学・統計の補足(文系向け)
なぜ対数を取ると線形モデルが効くのか。 消費カロリーは「1分あたりの消費量 × 運動時間」の掛け算に近い。掛け算は線グラフの形で足し算に直せない(時間が2倍で結果が2倍=比例)。対数を取ると log(掛け算)=log(1分あたり)+log(時間) と足し算になり、「足し算しか知らない」Ridge でも表せる。木のモデルは「この値より大きいか」で分けるだけなので、値を対数に引き直しても大小の順は変わらず、分け方は同じ(Day164 の差0)。
残差相関。 各モデルが「どれだけ・どちらに間違えたか」を並べて、2つのモデルで連動するか見る数。1に近いと「同じ間違え方」で、混ぜても平均が元と同じになる。0.93 は高く、0.89 はやや低い。Ridge の相関が高いのは、同じ特徴量(決定6の列)を使い、同じ「滑らかな関係」を学習するから、と考えられる(因果は未検証)。
1モデルと5モデル平均の違い。 5人の予想屋の平均は、1人の予想より誤差が小さい(ランダムな間違いが打ち消し合う)。HistGB は5分割でデータの見え方が少しずつ違う5モデルを作るので、平均を取るだけで1モデルより良くなる。k近傍を混ぜる効果の一部は同じ「ランダムな間違いの打ち消し」なので、平均した後には残りが少ない。例え: 運転が荒い友人1人の評価を直すのに別の友人の意見を足すと効くが、すでに5人の友人の意見を平均した後では足す効果が薄い。
非負の重みと合計1。 重みに負を許すと、「あるモデルの逆を足して間違いを打ち消す」ような不安定な混ぜ方が最適に見え、新しいデータで外れやすい(Breiman 1996, Machine Learning 24(1):49–64, stacked regressions が非負制約を勧める理由。書誌は記憶による、未照合)。
🏆 Kaggleでの実践的な使い方
- 特徴量の価値はモデルごとに測る。 木で効かなかった特徴量(log_Dur)が NN や線形モデルでは桁違いに効く(今日 −0.05)。GBDT 用に作った特徴量セットを NN にそのまま渡すと損をする。逆に GBDT 向けの交互作用(掛け算)は NN が自力で学ぶことも多い
- 混ぜる候補は「点数」でなく「残差相関」で選ぶ。 今日は点数の良い Ridge(0.1655)より悪い k近傍(0.169)の方が重みを得た。Kaggle の上位解法で GBDT と NN を混ぜるのは、点数が近く、間違え方が違うため(一般に言われる傾向。今日の測定は GBDT 対 k近傍・Ridge のみ)
- 混ぜる重みは粗く固定する。 今日の OOF 重みは test の最適より knn に 0.05〜0.10 多く振れた(ワールド1・3)。0.05 刻みの全探索でも、分割seed 間で hgb 0.85〜0.90 に揺れた(表4)。重みの細かい違いは 1×10⁻⁴ 未満の差しか生まない
- OOF の改善を、提出する形の改善と取り違えない。 5分割のモデルを平均して提出するなら、OOF で見えた混ぜる改善は約 0.0002 過大になる(今日の4ワールドで 1.96〜2.18×10⁻⁴)。Playground の「公開 LB で混ぜるか決める」でも、公開 LB の揺れ(約 0.0009、未照合)より改善が小さいので、どちらにしても決め手にならない
- 混ぜる前に、同じ特徴量・同じ分割・同じ提出の形で測る。 今日のずれは「測る形」を揃えると消えた。Day164・165 の R2 が一致した場面(モデルが1つ)と、今日の R2 が偽採用を出した場面(混ぜる)の違いは、ここにある
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| log_Dur は無意味な特徴量だ | Day164 で差が0だった | 木に対してだけ。Ridge に1列足すと −0.0497(4ワールド zT −55〜−57) |
| 点数の良いモデルほど混ぜる価値がある | 強いモデル同士を混ぜる直感 | Ridge(0.1655)は k近傍(0.169)より良いのに重み 0。決めるのは残差相関(0.934〜0.939 対 0.885〜0.889) |
| 重みは OOF で細かく最適化すれば良い | 「最適」の言葉に引かれる | 分割seed ごとに 0.85〜0.90 で揺れ、test の最適より knn が 0.05〜0.10 多く出た(ワールド1・3)。丸めて固定する |
| OOF で見えた混ぜる改善がそのまま提出の改善になる | OOF と提出が同じ予測だと思う | OOF は1モデルの予測、提出は5モデル平均。混ぜる改善は −2.4〜−4.3 から −0.4〜−2.3(×10⁻⁴)に縮んだ |
| R2 が採用と言えば混ぜて良い | Day164・165 で R2 が真と一致した | 今日の R2 は 4/4 採用、真の効果で採用に足りたのは 2/4。R2 は OOF(1モデル)の差を測るので、提出する形が違うと外れる |
| 等分で混ぜておけば無難 | 平均は安全に見える | 点数の悪いモデルが入ると +0.003 悪化(4ワールドとも zT +11〜+13) |
🚧 扱わなかった範囲と理由
- ワールド数: 4ワールド、混ぜる判定は 3モデル混合で4件。「縮み 0.0002」は 4件の 1.96〜2.18 から出した値で、データや提出の形が変われば変わる。言えるのは今日の4件の中身まで
- 縮みの原因の特定: 「5モデル平均が分散を取ってしまう」は仮説である。分割seed 1 の5モデルと3分割の15モデルでの比較(−1.37→−1.24 など)は整合するだけで、1モデルでの訓練データ量の違い(OOF 側は train の 80%+元データ、提出側も同じ 80%+元データだが test の行は別)との分離はしていない。分けるには、学習に使う行の割合を変えた1モデルで同じ測定をする必要がある(次回候補)
- モデルの種類: Ridge は alpha=1 固定、k近傍は k=15・5列固定で、チューニングしていない。k近傍の列選択(Day164 の log_Dur を入れた効果)は未測定。NN・LightGBM・CatBoost・stacking のメタモデルは測っていない(Day165 のチューニングの楽観を混ぜる側にも持ち込むことになるため)
- 重み探索の粗さ: 0.05 刻みの全探索、非負・合計1。刻みを細かくした場合や、
scipy.optimizeの NNLS との差は未測定 - log 空間で混ぜる前提: 予測を log1p 空間で重み付き平均した。元の空間(カロリーの値)で混ぜる場合との差は未測定。Day163〜165 の「5モデル平均」は元の空間の平均だったが、今日の truth は log 空間の平均で作った(差は小さいと仮定、未測定)
- Nadeau & Bengio 補正・実データ(S5E5)・Day162 の選択バイアス仮説: Day164〜165 から持ち越し(実データは Kaggle のログインとルール同意が必要)
- 文献との照合: Wolpert 1992(Neural Networks 5(2):241–259, stacked generalization)、Breiman 1996(上記)、Caruana et al. 2004(ICML, ensemble selection)は存在を記憶しているが、今日は原文を開いていない。今日の結論は文献でなく測定による
🚀 次のステップ
- 発展: 「OOF が1モデル、提出が5モデル平均」のずれを、学習データ量を変えた1モデル(50%・80%)で測り、縮みが学習データ量で変わるかを見る。k近傍の列から log_Dur を外したときの混ぜる改善を測る
- 次回予告: Day167 — Playground Series 実戦投入⑤(最終提出の選び方。決定6〜8 で作った提出の形を、公開 LB(約 20%)と private(約 80%)が割れる状況でどう2本選ぶか。Day163 の公開 LB の揺れとシミュレーションで、公開 LB 最良を選ぶ・CV 最良を選ぶ・混ぜた1本と単独1本を選ぶ、の3方針を shake-up の大きさで比べる)
Phase 6 の学習マップ
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: