657. Playground Series - Season 5, Episode 6 | playground-series-s5e6
首先,我要向 Kaggle 举办此次竞赛以及社区分享知识和创造有趣讨论表示巨大的感谢。这是我第一次使用 map@3 指标,我发现排名预测为解决方案提供了更多的创意空间。
祝贺大师本人 @cdeotte 再次赢得另一个 playground 竞赛,同时也祝贺 @masayakawamata, @mahoganybuttstrings, @hahahaj 和 @optimistix 与他展开了激烈的竞争。
CV=0.37799,使用 Optuna 调优,6 个固定折叠和样本权重提升。CV=0.383830。0.383918,私有 LB 得分为 0.38472。
每个新版本的爬山法都有更大集的 OOF,最后一个版本约有 350 个。
XGBoost 表现最好,顶级单个模型达到了 CV=0.37799。感谢 @ravi20076 在竞赛早期阶段 在此处 指出。找到的最佳参数如下:
Params = {
'max_depth': 17,
'min_child_weight' : 4,
'subsample' : 0.8932774942420912,
'colsample_bytree' : 0.40035993059700825,
'gamma' : 0.44089845615519774,
'reg_alpha' : 3.0189326628791378,
'reg_lambda' : 1.0463133005441632,
'max_delta_step': 5}
这是通过使用如下所述的针对第 2、3 和 4 名的样本权重提升 (sample weight bumping) 实现的。令人惊讶的是深度 17 是最优的,我将其归因于数据的合成性质。
两者都包含以增加多样性,但它们的表现通常不如 XGBoost。
我使用了 @paddykb 优秀 notebook 的修改版本:No Keras, No Loan。我能达到的最好结果是 CV=0.35097。
我尝试了将数值特征作为数值和类别两种情况,发现后者表现最好。
这是改编自 @siukeitin 的 What if you can only use logistic regression...。通过样本权重提升,我达到了 CV=0.37468。
我使用 @cdeotte 的 GPU 爬山法创建了 20 个不同版本。我最好的爬山集成包含以下 14 个 OOF 预测:
- oof_model-1-15-w11-xgboost_trial_3_map3_0.37799.npy
- oof_model-13-3-w10-logisticregression_trial_0_map3_0.37530.npy
- oof_model-4-3-w10-NN_trial_0_map3_0.35016.npy
- oof_model-5-5-w10-catboost_trial_1_map3_0.34875.npy
- oof_model-12-1-w10-xgboost_index_0_map3_0.36555.npy
- oof_model-1-15-w11-xgboost_trial_0_map3_0.37737.npy
- oof_model-4-2-w10-NN_trial_3_map3_0.34908.npy
- oof_model-5-5-w10-catboost_trial_2_map3_0.35196.npy
- oof_model-12-1-w10-xgboost_index_67_map3_0.36579.npy
- oof_model-1-7-w10-xgboost_trial_0_map3_0.37640.npy
- oof_model-13-1-w10-logisticsregression_trial_0_map3_0.37411.npy
- oof_model-12-1-w10-xgboost_index_39_map3_0.36198.npy
- oof_model-3-2-w10-lightgbm_trial_2_map3_0.35041.npy
- oof_model-5-3-w10-catboost_trial_2_map3_0.34606.npy
这导致了 CV=0.383830 和 Public LB=0.38268。正如 @cdeotte 多次指出的那样,关键在于模型的多样性,而不是高性能的单个模型。
旁注,我认为爬山法还有更多值得研究的地方(想法,未在此次竞赛中使用):
在此次竞赛期间,我注意到有时向爬山添加新的 OOF 会显著降低分数,这让我对该方法的潜在改进感到好奇。
使用 AutoGluon 对 9 个最好的爬山集成进行进一步集成,生成最终提交,CV=0.383918 和 Private LB=0.38472。
感谢 @ravaghi 在 这里 指出如何在 AutoGluon 中使用 map@3。
遵循公开 notebook 中的常见技术,我将原始数据的 sample_weight 设置为 4 倍,这提高了分数。
我分析了 OOF 预测,以确定正确类别被预测为第 2、3 或 4 名的实例。然后我使用增加的 sample_weight 重新训练模型,以鼓励模型将它们排名更高。提升一个位置的潜在收益为:
我尝试了对两个位置之间的概率差低于阈值(例如 0.02)的预测提升 sample_weight。这项技术为许多模型带来了约 0.001 的 CV 改进。我尝试了仅提升第 2 名预测,以及提升所有三个排名。我考虑过用 Optuna 自动化这个过程,但这将是未来 playground 竞赛的项目。
我对所有实验使用了固定的 6 折分层 K 折 (Stratified-K-Fold) 策略。为了确保一致性并防止阶段间的数据泄露,我在脚本中添加了一个验证步骤:
def verify_first_fold(val_idx):
# 硬编码的第一个验证折叠的前 10 个索引
val_idx_expected = np.array([1, 2, 5, 8, 9, 14, 16, 23, 27, 30])
if np.array_equal(val_idx[:10], val_idx_expected):
return
print("第一个折叠验证索引与预期值不匹配。")
print("预期:", val_idx_expected)
print("实际:", val_idx[:10])
raise ValueError("检测到折叠不匹配。停止执行。")
# 在训练循环中
for fold, (train_idx, val_idx) in enumerate(kf.split(X=train_df, y=...)):
if fold == 0:
verify_first_fold(val_idx)
这不是一个万无一失的方法,但它多次救我免于污染我的 OOF 预测。
def merge_optuna_studies(
source_storages: List[str],
output_storage: str,
min_trials: int = 10,
name_prefix: str = ""
) -> int:
"""将多个 Optuna 存储中的研究合并到一个中。"""
copied_studies = 0
for source_storage in source_storages:
try:
# 获取源存储中的所有研究摘要
study_summaries = optuna.study.get_all_study_summaries(storage=source_storage)
if len(study_summaries) == 0:
print(f"在 {source_storage} 中未找到研究。跳过。")
continue
print(f"在 {source_storage} 中找到 {len(study_summaries)} 个研究")
# 处理每个研究
for summary in study_summaries:
study_name = summary.study_name
n_trials = summary.n_trials
if n_trials < min_trials:
print(f"跳过研究 '{study_name}',只有 {n_trials} 次试验 (最少要求:{min_trials})")
continue
print(f"复制研究 '{study_name}',共 {n_trials} 次试验")
optuna.study.copy_study(from_study_name=study_name,
from_storage=source_storage,
to_storage=output_storage,
to_study_name=study_name)
copied_studies += 1
except Exception as e:
print(f"处理 {source_storage} 时出错:{str(e)}")
return copied_studies
WilcoxonPruner 来减少计算工作量,尽管这导致生成的 OOF 较少。设置如下:# 剪枝器设置
p_threshold = 0.08 # 剪枝的 p 值阈值
n_startup_steps = 2 # 开始剪枝前完成的试验次数
pruner = optuna.pruners.WilcoxonPruner(
p_threshold=p_threshold,
n_startup_steps=n_startup_steps
)
# 创建 Optuna 研究
study = optuna.create_study(
direction="maximize",
pruner=pruner
)
def objective(trial):
# ...
# 在 CV 循环内,向 Optuna 报告中间分数
# ... 在折叠上训练和评估 ...
trial.report(map3_score, step=fold)
# 如果试验没有希望则剪枝,除非在最后一个折叠。
if trial.should_prune() and fold < total_folds - 1:
raise optuna.TrialPruned()
return final_cv_score
此设置还将 CV 分数(中间值)填充到 Optuna Dashboard 中:

竞赛中期,我固定了最好的 XGBoost 参数,并系统地测试了约 200 个工程特征,计算每个特征的 map@3。一些示例:
{
"tested_features": {
"Potassium_binned": 0.36687777777820074,
"Humidity_x_Moisture_x_Potassium": 0.36101355555603537,
"Humidity_x_Potassium": 0.36245444444491326,
"Humidity_x_Moisture": 0.36123777777825505,
"log_Temparature": 0.3660744444448719,
"Temparature_x_Nitrogen_div_Humidity": 0.36036377777826734,
"sqrt_Phosphorous": 0.36733622222264173,
"Temparature_x_Humidity_div_Nitrogen": 0.3603313333338274
}
}
没有一个特征能改善无特征工程基线的结果。 这与其他竞争对手的发现一致。然而,由于增加的多样性,其中一些 OOF 仍被爬山算法选中。我知道 @cdeotte 建议添加所有特征然后逐个删除,但我缺乏计算资源来进行那种方法。由于我电脑上可用于爬山的 OOF 数量的内存限制,我不得不删除一些 OOF。
我花了一些时间尝试让基于 TabTransformer 库的网络工作,但只能达到 CV=0.31。我后来发现了 @omidbaghchehsaraei 的优秀 notebook TabTransformer,但没有时间整合他的工作。
分析几个 OOF 的每个类别 map@3 分数,我发现 DAP 和尿素与其他类别相比结果较差:
各类别 MAP@3 贡献:(最终 oof)
Class 14-25-14: MAP@3=0.432626, 贡献=0.066011 (114,436 样本)
Class 10-26-26: MAP@3=0.408642, 贡献=0.062052 (113,887 样本)
Class 17-17-17: MAP@3=0.426334, 贡献=0.063923 (112,453 样本)
Class 28-28: MAP@3=0.372506, 贡献=0.055209 (111,158 样本)
Class 20-20: MAP@3=0.369162, 贡献=0.054581 (110,889 样本)
Class DAP: MAP@3=0.341869, 贡献=0.043240 (94,860 样本)
Class Urea: MAP@3=0.315330, 贡献=0.038814 (92,317 样本)
我尝试训练一个单独的二分类模型来识别 DAP/尿素,并将其预测作为主模型的特征,但这并没有提高我的分数。我还尝试使用 sample_weight 专门提升 DAP/尿素,但这只导致 CV 变得更差。
这是我第一次尝试写解决方案总结。感谢 Kaggle 社区提供的所有经验教训,我期待下一个 playground 竞赛。Happy Kaggling!