676. Playground Series - Season 5, Episode 10 | playground-series-s5e10
副标题:集成 stacking 结合基于残差的 XGB、LGBM、HistGBM 和 TabM 模型,以及 Ridge 和神经网络元学习器。
我没有训练数百个模型并进行爬山搜索,而是专注于较小数量的高质量模型集合,并使用它们的 OOF(Out Of Fold)预测作为元特征:
| 模型类型 | 变体 / 备注 |
|---|---|
| XGBoost | 多个经过 Optuna 调优的 @cdeotte 的 “基于残差的 Boosting" (notebook) 版本。每个变体在折叠次数(5–55 折)和参数先验上有所不同。 |
| TabM | @masayakawamata 的 TabM 的三个变体 (discussion),其中两个被转换为预测合成数据生成器上的残差。 |
| HistGBM / LightGBM | 一个 HistGradientBoostingRegressor 和两个多样化的 LightGBM 基线。 |
| 神经网络 | 一个小型 Keras MLP,加上一个神经网络集成(平均多个种子/架构)。 |
| AutoGluon | 我自己版本的 @aliffaagnur 的 AutoGluon 适配器 (notebook)。 |
这些模型使用 K 折交叉验证(5–55 折) 进行训练,并保存了 OOF 和测试集预测结果。
我将所有基础 OOF(根据版本不同为 22-48 列)堆叠成一个元矩阵,并应用了以下步骤:
经过第一阶段剪枝后,通过 Greedy NNLS 和 LassoCV 选择最终用于 stacking 的模型子集,这能识别出最具互补性的 OOF 特征。
这往往将特征集从 ~40 列减少到 4–6 列。
| 阶段 | 模型 / 技术 | 描述 |
|---|---|---|
| 1 | RidgeCV 扫描 | 调整 α ∈ {0.01 … 10};产生了最佳单模型 CV (0.05579)。 |
| 2 | MetaNN | 小型 BN → Dense(32,GELU) → Dropout → Dense(1),AdamW + CosineRestarts,Huber 损失;7 折分层 CV × 3 种子。 |
| 3 | 残差 MetaNN | 在 (y − ridge OOF) 上训练,并加回 ridge 以捕捉非线性残留。 |
| 4 | Greedy NNLS & LassoCV 选择 | 识别最小模型子集和正 blending 权重。 |
| 5 | 最终融合 | • {MetaNN, Ridge} 的 NNLS • [ridge, MetaNN] 上的 2 阶段 Ridge。 |
| 候选提交 | CV (OOF RMSE) | LB 分数 |
|---|---|---|
| Ridge | 0.05579 | 0.05564 (第 8 名) |
| 两阶段 Ridge (Ridge + MetaNN) | 0.05579 | 0.05563 (本来会更好!) |
| NNLS 融合 | 0.05579 | ≈ 0.05564 |
| Greedy / Lasso 子集 | 0.05579 – 0.05580 | ≈ 0.05565 |
# 每列诊断
def rmse(a,b):
return mean_squared_error(a, b, squared=False)
stats = []
for c in X_meta.columns:
p = X_meta[c].values
stats.append((c, rmse(y, p), np.std(p), np.corrcoef(p, y)[0,1]))
stats_df = (pd.DataFrame(stats, columns=["col","oof_rmse","std","corr_y"]).sort_values("oof_rmse"))
# 过滤
bad_cut = stats_df.oof_rmse.median() + 0.02
bad_cols = set(stats_df.loc[(stats_df["std"]<1e-6) | (stats_df["oof_rmse"]>bad_cut), "col"])
keep = [c for c in X_meta.columns if c not in bad_cols]
# 通过相关性删除近似重复项
C = X_meta[keep].corr().values
drop_dupes, thr = set(), 0.9995
for i in range(len(keep)):
if keep[i] in drop_dupes:
continue
for j in range(i+1, len(keep)):
if keep[j] in drop_dupes:
continue
if abs(C[i,j]) > thr:
rm_i = stats_df.loc[stats_df.col==keep[i], "oof_rmse"].iloc[0]
rm_j = stats_df.loc[stats_df.col==keep[j], "oof_rmse"].iloc[0]
drop_dupes.add(keep[j] if rm_j >= rm_i else keep[i])
sel_cols0 = [c for c in keep if c not in drop_dupes]
X_USED, X_USED_TEST = X_meta[sel_cols0].copy(), X_meta_test[sel_cols0].copy()
def cv_ridge_preds(X, y, X_test, alpha, n_splits=7, seed=42, collect_coefs=False):
kf = KFold(n_splits=n_splits, shuffle=True, random_state=seed)
oof = np.zeros(len(y)); test_pred = np.zeros(len(X_test))
coefs = []
for tr, va in kf.split(X):
m = Ridge(alpha=alpha, fit_intercept=True, random_state=seed)
m.fit(X.iloc[tr], y.iloc[tr])
oof[va] = m.predict(X.iloc[va])
test_pred += m.predict(X_test) / n_splits
if collect_coefs: coefs.append(m.coef_)
return rmse(y, oof), oof, test_pred, (np.vstack(coefs) if collect_coefs else None)
best_rmse = 1e9
for a in [0.01, 0.03, 0.1, 0.3, 1.0, 3.0, 10.0]:
r, oof_r, pred_r, coef_arr = cv_ridge_preds(X_USED, y, X_USED_TEST, a, collect_coefs=True)
if r < best_rmse:
best_rmse, best_alpha = r, a
best_oof_ridge, best_pred_ridge, best_coef_arr = oof_r, pred_r, coef_arr
感谢大家参与这场精彩的 playground 比赛!