Predicting Irrigation Need 比赛回顾

Playground Series - Season 6 Episode 4

写于 2026.05.08

我的 Kaggle 主页与我的 GitHub 主页

前言

这场 Playground 月赛是我的第一场正式 Kaggle 比赛。尽管名次不高,不过学到了不少知识,还想出了一些自己觉得很不错的 idea,所以也还算满意。所以打算这里记录下我的解法与值得学习的 solution,也将这篇笔记作为 Kaggle 复盘系列的开端。

本次比赛与 @Yoican 组队完成。

我的解法

Piredicting Irrigation Need GitHub 仓库

成绩

951/4316, Private Score 0.97192/0.98236

思路

由于是第一次参赛,本次比赛主要以是照搬经典方法为主,也就是“特征工程 \rightarrow 模型训练 \rightarrow 结果拟合 \rightarrow 阈值微调 \rightarrow 最终结果”的 pipeline。更准确一点,主要是模仿 Masaya Kawamata 在2026年2月月赛 Predicting Heart Disease 中的 solution

比赛特点

这次比赛以平衡准确率(Balanced Accuracy,BAC)为评定分数的标准。而在我们获得的训练集中,预测为 High 的数据远远少于 Medium 和 Low。这意味着对 High 的评定成为了极难的部分,既需要保证精确率(Precision),又需要保证召回率(Recall)。因此,如果按一贯的思路来做,模型很可能为求稳定主动把处于 High 和 Medium 模糊地带间的样本判定为概率上更大的 Medium。但这一次,这种做法将受到 BAC 的严格惩罚。

另外,比赛数据由另外的大模型生成。赛事方不仅给出了大模型生成的数据,还给出了原始数据。这有助于我们探索从原始数据到大模型生成数据这一步发生了什么。

数据分析

由于这次既有原始数据(约一万条)又有大模型生成数据(六十三万条),一个想法就是将原始数据合并到生成数据中进行训练。不过,由于生成模型的影响,不能确定两者的特征是否相似,于是我自制了一个简单的数据分析器来探索列与列之间的关系。

我主要进行了针对单列的单变量分析和列与列之间的双变量分析。

先讲单变量分析的结论。在原始数据中,除了灌溉需求列(结果列)中 High 明显少于 Medium 和 Low 以外,对于其他各列来说,没有特别多或特别少的类别。而生成的数据也继承了这一特点。

双变量分析比较出人意料。我主要使用了卡方检验和逻辑斯蒂回归来对列间独立性进行显著性检验。在原始数据中,大部分列与结果列之间的相关性都不高(p 值大于 0.05),只有其中的四五列相关性较大(p 值小于 0.01);然而,在生成数据中,几乎所有列与结果列之间的相关性都极大(p 值小于 0.001),甚至有一些列的p值小于 103010^{-30} 量级。而且,有一个在原始数据中与结果相关性较大的列在生成数据中甚至p值大于 0.05,变成了相关性不显著。

这意味着生成模型注入了大量的伪相关。可以说,从原始数据到生成数据的过程中发生了严重的数据分布偏移。综上,我认为不应该进行训练集合并。

第一稿

特征工程

算上原始特征,我使用了 9 种特征工程,而这 9 种特种工程也一直保留到最后。

它们分别是:

  1. raw:原始特征
  2. agri:农业领域特征
  3. interact:交互特征
  4. kitchen_sink:综合特征(农业特征 + 交互特征)
  5. te_smooth:平滑目标编码特征
  6. te:目标编码特征
  7. woe:证据权重编码特征
  8. qbin:分位数分箱特征
  9. eqbin:等宽分箱特征

其中农业领域特征来源于查询到的论文公式。不过,或许是由于生成模型的偏移,它并没有起到特别好的效果。

基座模型

第一稿中的基座模型主要使用了 LightGBM, XGBoost, CatBoost 和 HistGradientBoosting。

模型训练

为避免偶然误差,每个组合使用 3 个随机种子。

结果拟合

按交叉验证得到的 BAC 排序,找出最优的 4-12 个模型,然后对这四个模型进行拟合。

采用 4 种拟合方法,并用 K 折交叉验证选出最优的拟合方法。这 4 种方法分别是逻辑斯蒂回归、对数逻辑斯蒂回归、可变加权法和排序平均法。

阈值优化

为最大化 BAC,在拟合出结果后再通过调整 offset 来修正预测结果,类似最小二乘法中 y^=Ax+b\hat{y} = A x + b 里的常数项截距 bb

结果

Public Score 0.97186

第一稿的聚合策略卓有成效,第一发就超过了先前的单模型预测分数。

第二稿

修改

部分特征工程×\times基座模型的预测模型本身效果就很好时,容易出现三个随机种子分数都很高,于是全部入选最终拟合列表中的情况。为解决这个问题,我决定把相同组合里三个不同的 seed 先合并到一起,产生一共 36 个预测结果,再根据 BAC 选出 4-12 个。

本次修改未提交。

第三稿

修改

  1. 本着“人多力量大”原则,我决定将基座模型数量增加至 9。
  2. 加入了 Optuna 调参以求更高的单模型预测精度

基座模型增加

增加至以下 9 种模型:

  • LightGBM
  • XGBoost
  • CatBoost
  • HistGradientBoosting
  • RealMLP
  • TabM
  • ExtraTrees
  • Logistic Regression
  • RandomForest

Optuna 调参

租用云服务器进行调参,用一张 RTX 4090 跑了近 50 个小时,消耗一百大洋。

出乎意料的结果

这次提交在 Public Score 上的分数反而降低了:0.97186 \rightarrow 0.96401。降低了约 0.8% 的预测准确率。这已经不能用偶然误差来形容了,是绝对的性能下降。

查验与反思

但是,为什么?明明只是增加了 5 个模型,并给每个模型都微调了参数。即使不说应该分数上升,再不济也不应该下降才对。发生了什么?

是微调参数导致过拟合了吗?我认为不是。其一,我的模型结构并不复杂,调参时我也限制了搜索范围;其二,根据先前的数据分析,这次的生成数据里有非常非常多的伪特征,模型比一般情况要更加拟合才对呢!

于是我查验了最终入选的拟合模型,并从中找到了原因。这次入选的模型中大部分都是随机森林。随机森林的单模型 BAC 很高,9 个随机森林几乎全部排在 BAC 榜单前 20。但是,不知道是随机森林本身不适合聚合还是因为相似模型过多导致无法总结多样化特征,这次入选的模型在阈值调整后的结构却并不理想。在第一稿中,调整阈值使模型从 0.95+ 升到了 0.972。而在第三稿中,尽管初始 CV 有 0.964,阈值调整却只调整到 CV=0.967,Public Score 中的成绩更是只有 0.964。

第四稿

修改

为解决这个问题,我认为应当把模型的多样性特征考虑入选择列表。于是,我制定了一种多层筛选法:

  1. 先根据 BAC 排序,选出 81 个结果中最强的 20 个
  2. 让这 20 个选出的模型进行无加权民主投票,得到一个民主预测结果
  3. 将每个模型的预测结果与民主预测结果进行比较,将偏离程度视作多样性
  4. 给 BAC 和多样性各乘上一个权值,然后加和作为排序分数
  5. 选取前 4-12 个模型

结果

它让我的 Public Score 从 0.96401 升到了 0.96537。尽管比上一稿强一些,这显然还远远不是一个优秀的算法。现在看来,这是因为我对“多样性”的理解有偏差。

多样性不是“偏离度”。事实上,即使是两个预测成功率相同的模型,也可能因为诸如某一种预测 High 更准而另一种预测 Low 更准的原因而产生多样性。妄图用一个数据来测量多样性是不可能的。

另外,我还妄图让“愚蠢的大多数”做筛选指导。应该相信群体的力量,但最好不要指望它们的智慧。群体固然能提供多样性,但是一旦将它们作为筛选标杆,无论怎么调整权值,总会出现这样的问题:

  • 高 BAC 模型不仅基础 BAC 高,“多样性”指数也因为偏离大众而增高,反而无法起到限制随机森林的作用
  • 低预测率模型可能与大众非常不同,但是因为吃到了多样性红利,反而名次很高。而它本质上没有什么洞见——傻子是不会提供什么洞见的!

第五稿

洞见

回顾整场比赛,我们发现它其实可以缩减为一个简单的问题:我应该怎么获得一个最优的 BAC?

再看我们的流程。我们现在有 81 个预测结果。然后我们打算用一种什么方式把这 81 个预测结果聚合起来。再然后我们调一下阈值。最后产生一个结果。就这些。我只需要在有限的时间内完成这个任务就行了。

前面做的一切理应都是为了这个目标服务。从一堆个结果里选 4-12 个是为了缩短 meta-ensemble 的调参时间。把各个随机种子的结果聚合起来是为了避免同类型聚合导致多样性不足,进而导致调整阈值无法提高分数。把多样性纳入分数也只是期望多样性增强能使调整阈值后的 BAC 提升。

但是现在我们做的,却似乎又并非完全以最终 BAC 为唯一目标。我们是层层筛选优中选优了没错,但我们选的其实是“最终 BAC 可能高”的预测结果组合,而不是“最终 BAC 肯定高”的组合。

那么要选“最终 BAC 肯定高”的组合应该怎么办呢?唯一的解法显而易见:

多试。

修改

小改动:新增了一个拟合方法:6 列原始特征+岭回归

我完全推翻了先前的拟合逻辑。新的拟合逻辑如下:

  1. 首先对照 BAC 榜单从上到下依次选取 20 个模型,且限制相同基座模型的数量。
  2. 将前 20 名的模型作为图的节点,每次选取两个节点进行结果拟合和调整阈值,最终 BAC 作为边的权值。
  3. 当这张 20 个节点 380 条边的图建成后,每条边用均差除以方差做归一化。
  4. 选取平均边权最大的子图,进行结果拟合,调整阈值,再计算 CV BAC。
  5. 禁忌算法搜索附近的子图,进行结果拟合,调整阈值,再计算 CV BAC。

我认为这个思路是一个很棒的主意。

  • 20 个节点的筛选方法既确保每个模型都足够聪明,又使得选取名单多样化。
  • 两两边权计算是因为要求多样性本质是追求模型能力互补,而用实战成绩检验能力互补程度最合适不过。
  • 边权归一化可以体现,在大环境中,哪些组合是副作用,哪些组合有正收益。
  • 选取子图可以找到理论互补度最好的几个模型。
  • 禁忌算法则是基于子图附近的图肯定也不差的想法,只求不遗漏附近在调整阈值后可能更优的子图。

结果

Public Score 升至 0.97192。调整阈值前后的 CV 从 0.964 升至 0.973。另外,50 步禁忌搜索里调整阈值后最优的结果总是在 15 步内,这同样印证了这套拟合算法的可靠性。

平均边权最优子图带来的思考

我自豪于这套方法,不止是因为它帮我回升了很多分数,更是因为我认为这是一套可推广的、具有普适性的通用方法。从一方面来说,这套方法在时间上很实惠——先通过收缩节点减少计算时长,然后用一一建边的方式快速建立互补度初印象,再通过归一化后跑最优子图配合禁忌算法在可接受的时间内得到足够好的结果。从另一方面来说,这套方法很稳定。对于多组去掉某些基座模型家族后进行的测试,它总可以稳定地找到调整阈值后 BAC 提升极大的优质互补组合。

第六稿

去除了拟合后 BAC 不高的两个元拟合算法(可变加权法和排序平均法)。

加入了由于强制工程化与考虑不周没有加上去的一些建树参数(如 class weighs 等)。由于时间不足,没有再次进行调参。

最后一次提交 Public Score 为 0.97101,Private Score 为 0.97312,可惜未入选 Final Submission。

别人的优秀 solution

问题建模

混淆矩阵与二步二叉树

根据混淆矩阵,模型几乎从不混淆 Low 和 High。于是先把 Low 和 Rest 分开,再从 Rest 里分出 Medium 和 High。

分步分类器

先分出 Middle 与两侧,再分两侧;作为多样性拼图放入 stacker。

特征工程

Ordered Target Encoding + 4x Shuffle

Digit extraction \rightarrow 4 份不同排列的 leave-one-out expanding TE \rightarrow 行拼接(4N行)

KBinsDiscretizer 三重分箱

对每个数值列生成 uniform/quantile/kmeans 各 80 桶离散特征。

n-gram TE + Category Twins

  • 多类别列拼接为词组后 TE,捕捉多列组合一阶非线性
  • 数值列分箱 → 与原始类别列交叉 → TE
  • 全局 count encoder
  • 总计获得 600 多个特征,单模型用 30-350 个

集成

集成器多于模型

在 3.3% High + BA 指标下,同一组 OOF 用不同 ensembler 的波动 > 加一个新模型。

9 模型 \rightarrow 11 个 L1 ensembler(Ridge/HC/LogReg/MLP/LGB/DiffEvol/RankAvg/AutoGluon/…)\rightarrow 4 个 L2 ensembler(HC/LogReg/DiffEvol/TopK)\rightarrow average(L2s) 最佳

多 ensembler 提供对 High 类预测的多种"赌注",L2 平均后消除单 ensembler 偏差。

GPU PyTorch Multinomial LogReg Stacker

配合:CrossEntropyLoss + class_weight=‘balanced’ + Adam weight_decay(只权重不bias)。

用 GPU 批量 stack 数百 OOF。

Ridge meta + 额外特征做 meta

Meta ensemble model 的输入除了用 class probabilities,还加入:

作为特征。

策略

故意加入弱模型帮助 stacker

不过滤 OOF,全丢给神经 stacker