返回列表

3rd Place Solution | KNN Imputation + LightGBM/XGBoost Ensemble

554. Playground Series - Season 3, Episode 15 | playground-series-s3e15

开始: 2023-05-16 结束: 2023-05-29 预测性维护 数据算法赛

第三名解决方案 | KNN插补 + LightGBM/XGBoost集成

作者:Alex D'Ippolito

排名:第3名

发布时间:2023-05-31

得票数:9票

首先,我要衷心感谢Kaggle举办这些 playground 竞赛,以及社区中每一位分享宝贵知识的朋友。在过去的几个月里,通过参加这些竞赛,我学到了太多太多!

以下是我的方法总结,完整代码 notebook 可在此处找到。

数据预处理

最初的步骤涉及大量手动工作,通过检查数据寻找模式来解决那些较容易填补的缺失值。这填充了 D_e、D_h 和 length 列中的大部分数值。以下是针对作者 'Inasaka' 行的部分代码示例:

data.loc[(data['author'] == 'Inasaka') & ((data['D_h'] == 3.0) | (data['length'] == 100.0)) & (data['D_e'].isnull()), 'D_e'] = 3

data.loc[(data['author'] == 'Inasaka') & ((data['D_e'] == 3.0) | (data['length'] == 100.0)) & (data['D_h'].isnull()), 'D_h'] = 3

data.loc[(data['author'] == 'Inasaka') & ((data['D_e'] == 3.0) | (data['D_h'] == 3.0)) & (data['length'].isnull()), 'length'] = 100

我的下一步是使用K近邻插补器填补 pressure、mass flux 列中剩余的缺失值,以及 D_e、D_h 和 length 中尚未填补的部分。感谢 @validmodel这篇帖子,其中列出了多种不同的插补技术。最终我选择了 sklearn 的 KNN 插补器来完成剩余数值的填充。我尝试了多个不同的 n_neighbors 参数值,发现在 87 到 89 范围内时,我的模型 CV 分数最佳。

缺失的分类列则通过更多手动代码进行插补,例如:

data.loc[(data['author'] == 'Inasaka') & ((data['D_h'] == 3.0) | (data['length'] == 100.0)) & (data['D_e'].isnull()), 'D_e'] = 3

data.loc[(data['author'] == 'Inasaka') & ((data['D_e'] == 3.0) | (data['length'] == 100.0)) & (data['D_h'].isnull()), 'D_h'] = 3

data.loc[(data['author'] == 'Inasaka') & ((data['D_e'] == 3.0) | (data['D_h'] == 3.0)) & (data['length'].isnull()), 'length'] = 100

这个方法表现相当不错,我得到了可靠的 CV 分数,但公开排行榜分数并不是最强的(后面会详细解释)。

然而,在比赛接近尾声时,@shalfey 发表了这篇优秀帖子。我将所有这些代码置于预处理的最前端,它显著改善了我的分数,可以说如果没有它,我不可能取得如此高的排名。

模型构建与交叉验证

首先,我构建了一个基本的未调优 XGBoost 模型来获得基线 CV 分数。当结果约为 0.07305 时,我非常惊讶。这个分数远优于当时公开排行榜的第一名分数(约 0.0743)。这让我感到一阵惶恐 😬。

这个基本模型不仅可能让我获得第一名,还可能以巨大优势获胜,这是极不可能的。尽管如此,我还是继续推进,对模型进行调优,生成测试预测并提交。最终公开分数为 0.07555。

CV 分数与公开排行榜之间存在差距,可能的原因有两个:

  1. 我的 CV 流程存在缺陷(我强烈怀疑预处理插补策略导致了目标泄露)
  2. 用于公开排行榜的 20% 数据包含了一个异常数据段,我的模型在这部分表现不佳,但在整个测试集上表现会更接近 CV 分数

我相当确定问题是第一个。我花了很多时间改变工作流程,以消除任何可能的目标泄露。然而,我的 CV 分数仍然在 0.0732 左右。而且提交这些模型时,我的公开排行榜分数表现更差。这些结果开始让我相信原因二才是正确的。

基于此,我回归到了最初的工作流程:在交叉验证之前完成所有预处理和插补,而不是逐折进行。在这种情况下,需要在轻微的目标泄露(KNN 插补器利用了目标特征)与提高模型训练输入特征的数据质量/准确性之间进行权衡。

如果我在设置一个真实的交叉验证实验,正确的选择应该是逐折进行所有插补,并在必要时收集更多数据来改进结果。然而,在这种情况下,收集更多数据显然是不可能的。我只能使用竞赛/原始数据集,需要从中榨取每一点信息,以最大化 KNN 插补步骤的准确性,即使这样做会造成轻微的目标泄露。

利用原始数据集

参加这些 playground 系列竞赛的一个独特之处在于,所有竞赛都使用从原始数据集生成的合成数据。这总是引出一个问题:"我们该如何利用原始数据?" 基于我参加 playground 竞赛的经验,答案是:你必须找到方法来利用它。

我要感谢 @adaubas 在蓝莓产量竞赛期间的这篇帖子,它让我真正开始思考原始数据集应该如何被使用。我以前只是将其视为简单的额外数据,立即将其合并到训练集中。但我发现这不是最佳的处理方式。

这样做的问题是,它会在进行 CV 时改变 OOF 验证数据的分布。合成数据似乎总是比原始数据噪声更大。@sergiosaharovskiy这篇帖子很好地可视化了这一观察到的现象。在 OOF 验证数据中包含原始数据,会让你得到比使用模型预测纯合成测试数据时更好的 CV 分数。

我发现使用原始数据的最佳方式是将其与训练数据分开保存,然后在交叉验证中将整个原始数据合并回每一折。示例代码如下(X_original 和 y_original 为整个原始数据集):

kf = KFold(n_splits=10, random_state=8, shuffle=True)

for train_idx, val_idx in kf.split(X_tr, y_tr):
    X_t, X_val = X_tr.iloc[train_idx], X_tr.iloc[val_idx]
    y_t, y_val = y_tr.iloc[train_idx], y_tr.iloc[val_idx]
    
    X_train = pd.concat([X_t, X_original], ignore_index = True)
    y_train = pd.concat([y_t, y_original], ignore_index = True)

    model = LGBMRegressor()
    
    model.fit(X_train, y_train, eval_set=[(X_val, y_val)])
    
    y_pred = model.predict(X_val)
    
    score = mean_squared_error(y_val, y_pred, squared=False)

模型与集成

我最终调优了 4 个模型,并希望观察它们作为集成模型的表现。

模型 1: LightGBM,使用原始数据集中除 geometry 外的所有特征

模型 2: LightGBM,与模型 1 相同,但新增两个特征 'adiabatic_surface_area' 和 'surface_diameter_ratio'。感谢 @tetsutani 及其notebook提供的特征思路

模型 3: LightGBM,与模型 2 相同,但使用 PLSRegression 在 mass_flux、pressure 和 chf_exp 特征上新增了一个特征/成分。再次感谢 @adaubas这篇帖子让我了解了 PLS

模型 4: XGBoost,与模型 1 使用相同的特征

我原本计划进一步探索 @tetsutaninotebook,并尝试 @samuelcortinhashill climbing 方法。遗憾的是,我 simply ran out of time,我的集成最终只是 4 个模型的基本权重组合。

结论

这场比赛进一步巩固了我的一点认识:最重要的是建立一个你值得信赖的 CV 工作流。让你的 CV 分数指导决策,不要过分看重公开排行榜分数。

再次感谢社区的每一位成员,感谢你们的热情好客和乐于分享!我学到了太多!祝你们在未来的比赛中取得好成绩!

同比赛其他方案