返回列表

8th Place Solution for S5E10: Predict Road Accident Risk

676. Playground Series - Season 5, Episode 10 | playground-series-s5e10

开始: 2025-10-01 结束: 2025-10-31 交通流量与路况 数据算法赛
S5E10 第 8 名解决方案:预测道路事故风险

S5E10 第 8 名解决方案:预测道路事故风险

副标题:集成 stacking 结合基于残差的 XGB、LGBM、HistGBM 和 TabM 模型,以及 Ridge 和神经网络元学习器。

作者: Matt graham (mooseml)
排名: 第 8 名
发布日期: 2025-11-01

概述

1. 基础模型

我没有训练数百个模型并进行爬山搜索,而是专注于较小数量的高质量模型集合,并使用它们的 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 和测试集预测结果。


2. 元特征与剪枝

我将所有基础 OOF(根据版本不同为 22-48 列)堆叠成一个元矩阵,并应用了以下步骤:

  • 过滤: 丢弃方差接近零或 OOF RMSE ≫ 中位数的模型(初步清理)。
  • 去重: 消除 |ρ| > 0.9995 的列,保留较强的一个。

经过第一阶段剪枝后,通过 Greedy NNLS 和 LassoCV 选择最终用于 stacking 的模型子集,这能识别出最具互补性的 OOF 特征。

这往往将特征集从 ~40 列减少到 4–6 列。


3. 元学习与集成

阶段 模型 / 技术 描述
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。

4. 评估与结果

候选提交 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

代码亮点

Stacking 前剪枝弱 OOF 特征

# 每列诊断
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()

在剪枝后的特征集上调整 Ridge 元学习器

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 比赛!

同比赛其他方案