返回列表

1st place solution for the LEAP competition

617. LEAP - Atmospheric Physics using AI (ClimSim) | leap-atmospheric-physics-ai-climsim

开始: 2024-04-18 结束: 2024-07-15 气象预报 数据算法赛
LEAP 竞赛第一名解决方案

LEAP 竞赛第一名解决方案

作者: greySnow (shlomoron)

发布时间: 2024-07-30

竞赛: LEAP Atmospheric Physics AI Climsim

这次竞赛是一次惊人的体验,也是一个极好的沙盒,可以深入测试各种技术和架构的想法与实验。感谢 Kaggle 提供这个 fantastic 平台,感谢 Colab 提供便宜的 TPU 供我训练,感谢 Google 作为 Kaggle 和 Colab 的所有者以及为我们提供 Tensorflow。当我想到这一点时,我在 TPU (Google) 上使用 tensorflow 框架 (Google) 在 Colab (Google) 上训练,用于举办在 Kaggle (Google) 上的竞赛。从头到尾都是 Google。你们太棒了,我向你们致敬。

我也想向竞赛主持人 @jerrylin96 表达衷心的感谢。你为我们提供这场精彩竞赛的努力,以及即使在面对意外的泄露 (LEAK) 问题时也坚定不移地保持竞赛正轨的承诺,确实值得称赞。感谢你的奉献和辛勤工作。

最后,感谢 Kaggle 社区,尤其是论坛和代码部分所有活跃和乐于助人的人们。你们让体验变得如此美好。我爱你们。

这次竞赛很奇怪。我构建了最好的模型,但有比我更好的 Kagglers——那些找到泄露点并成功利用它们的人。我没有。我还有很多要学的。我不会假装我对事情变成这样感到不开心,但最好的 Kagglers 值得尊重。你们拥有我的尊重。

我知道作为第一名,考虑到我的解决方案与下一个方案之间的显著分数差距,会有很多眼睛盯着我,以确保我没有利用任何泄露。因此,我付出了很多努力提供一个完整的 Kaggle pipeline,从从 HF 下载数据到 TFRecords 编码、训练、推理和提交,并提供了一个训练示例, resulting in a single model 0.79081/0.78811 public/private LB。通过一步步遵循我的 pipeline,你也可以从头开始构建数据集并训练你自己的 ~0.79+ public LB 模型,确保没有在训练集中隐藏泄露的伪标签,也没有在推理中进行恶意的测试集逆向工程。所有这一切只需复制并运行一系列 Kaggle notebooks。诚然,有很多 notebooks——如果你打算自己下载并编码所有数据到 TFRecords,超过 100 个 notebooks——但鉴于这次竞赛中海量的数据,这是不可避免的。请参阅 我的 GitHub 获取完整 pipeline 和额外细节。

背景部分

业务背景
数据背景

1. 方法概述

1.1. 模型

模型架构 summary_graphics

用一个词概括:Squeezeformer。这对于关注过去一年 Kaggle 上一些类似竞赛的人来说并不奇怪,特别是 ASLFRRibonanza。我使用了我在 Ribonanza 第二名解决方案 中看到的相同修改版 Squeezeformer 块,由 @hoyso48 提供。我记得做的唯一更改是删除了 1Dconv 块中的第一个 LayerNorm 并添加了一个 ECA 层。然而,可能还有更多我忘记的微小更改。我的 Tensorflow 实现是由 hoyso48 的 PyTorch 实现 guided 的,所以再次(连续第 3 次竞赛),我感谢他。

我使用了 12 块模型,维度为 256/384/512。在 Squeezeformer 块之前,我有一个线性 dense 层,后跟 LayerNorm,作为从输入数据到模型维度的编码器。在 Squeezeformer 块之后,我有一个预测头,由 swish dense 后跟 GLUMlp 块(swiGLU 后跟 linear dense),两者头维度取决于模型为 1024/2048。更多细节请查看我的代码。

起初,我使用了 dropout 层,当训练数据 order 为 1M 样本时帮助很大。然而,在我看到论坛上关于 dropout 不必要的评论后,当我扩展到 ~40M-80M 样本时再次实验,确实在集成时是不必要的(尽管它仍然允许单模型获得更高分数,但代价是两倍或三倍的 epochs)。由于移除它允许更快的训练,我也选择完全放弃 dropout。优化器是 AdamW,带有 half-cosine decay scheduler,最大 LR 1e-3,weight decay = 4*LR。

1.2. 损失函数

我曾经读过,DL 模型最重要的部分是损失函数。当时我有点怀疑——当然,它很重要,但选择合适的损失并不复杂,对吧?比如说,如果我们的 metric 是 R-squared,最好的 loss 显然是...MSE?

1.2.1. 使用 MAE

它在各方面都优于 MSE——收敛更快,收敛性更好,更稳定。如果我需要为这次竞赛的高分选择一个“秘密”,除了 notorious 泄露外,就是这个。

1.2.2. 辅助损失

我们在训练集上有明确的时空数据,但在测试集上没有。辅助损失是这种情况下的常见做法。我在归一化的纬度/经度和日/年周期的 sin/cos 上使用了 MAE(如果不清楚,请查看我的代码)。

关于辅助损失的旁注——有些人推测,在竞赛的最后一周,使用明确的时空数据,即使是训练集的,也被视为禁止的泄露。所以,首先,这种使用被 主持人确认为合法,只要没有“黑客”测试集(可以通过使用辅助预测伪标签测试集来完成——我没有这样做)。其次,在最后一周,我训练了几个没有辅助时空损失的模型,我的无时空 6 模型集成获得了 0.79355/0.79092。所以我无论如何都赢了;辅助时空损失是不必要的,甚至可能没有帮助(因为我的获胜集成,虽然分数略高,但也有 13 个模型)。

1.2.3. 置信度头 (Confidence Head)

Ribonanza 第三名解决方案 的某个模型获得了可疑的好分数。我不会解释为什么可疑;你需要知道 Ribonanza 的细节作为背景。无论如何,这个可疑模型有两个独特之处——一个奇怪的架构和一个置信度头。我还需要更多关于所述奇怪架构的实验,但对于置信度头,在这次竞赛中尝试很容易,而且出乎意料地有效。想法很简单——我也预测每个目标的损失。我也对置信度损失使用了 MAE。我有一个没有置信度头训练的模型,获得了 LB 0.78945/0.78631,所以我想没有它我也能赢。话说回来,具有相同规格但带有置信度头的模型是我最好的模型,LB 0.79159/0.78869。所以,是的。出乎意料地有效。是的,这第二个模型本身就可以在这次竞赛中赢得第一名(0.78869 私有,虽然公开不是第一)。

1.2.4 掩码损失 (Masked Loss)

我掩码掉了提交中为零的目标,或者那些我们使用 ptend trick 的目标 (ptend_q0002_2-ptend_q0002_26)。

对于 LEAP 的新手——请阅读 这篇帖子 了解 ptend trick 背景。

关于损失函数的最终想法:好吧,它可能是深度学习模型最重要的部分。哈哈。

1.3. 数据准备

1.3.1 高分辨率数据

你可能已经注意到我使用了高分辨率数据。是的,它有帮助。虽然我没有它也能赢——我仅有的低分辨率数据 5 模型集成获得了 LB 0.79299/0.78951。但高分辨率确实有帮助,尽管有时需要特殊的软裁剪处理,正如你将看到的。此外,使用高分辨率训练不太稳定。因此,我通常使用更大的 batch sizes(1024/2048 相比仅低分辨率模型的 512)。我以 2:1 低:高 的比例混合高分辨率数据。低于这个比例,性能较弱;高于这个比例,增益相对于所需的额外训练时间来说很小。

1.3.2 多种数据表示

多种数据表示是我从 ASLFR 第一名解决方案 中学到的技巧。不过多细节,ASLFR 的情况是数据可以用两种不同方式归一化。我记得尝试了两种方式,找出哪种更好并坚持使用。然后竞赛结束了,猜猜怎么着?第一名以两种可能的方式归一化,连接了两种表示(中间有一些额外步骤;阅读他们的总结获取完整细节)并发送给模型。

首先,让我分离分布在 60 个高度层上的特征,我称之为 X_col,以及所有层相同的特征,我称之为 X_col_not。

对于 X_col_not,我只使用了一种表示:简单的归一化 (x-mean)/std。

对于 X_col,我使用了三种表示。第一种与 X_col_not 相同,我在每个层上用其自己的 mean/std 归一化每个特征。即,对于 state_t,那么我们有 (state_t_1-mean(state_t_1))/std(state_t_1), (state_t_2-mean(state_t_2))/std(state_t_2) 等。在我的代码中,我称这种表示为 x_col_not_norm (对于 x_col_not) 和 x_col_norm (对于 X_col)。

第二种表示通过所有层的总 mean 和 std 归一化每个特征。对于 state_t,我们有 (state_t_1-mean(state_t))/std(state_t), (state_t_2-mean(state_t))/std(state_t) 等。在我的代码中,我称这种表示为 x_total_norm。

最后,第三种表示是:

x_col_norm_log = tf.where((x_col_norm-x_col_norm_min+1)>=1, tf.math.log(x_col_norm-x_col_norm_min+1),
                                    -tf.math.log(1+1-(x_col_norm-x_col_norm_min+1)))

这种试图用文字解释的事情永远不如直接看代码清楚。

在你看了代码并理解后,你可能会想为什么不使用:

x_col_norm_log = tf.math.log(x_col_norm-x_col_norm_min+1)

为什么 tf.cond 有所有额外的步骤?看,我有个问题。我只用 Kaggle 数据计算了 x_col_norm_min,然后我将代码扩展到所有 HF 数据(其值低于 Kaggle 数据 x_col_norm_min),但不想改变归一化常数,因为这会破坏推理 pipeline。然后,我将不得不为旧模型和新模型使用不同的 pipeline。是的,有时我有点懒。为此自豪。当我也包括高分辨率数据时,这变成了一个极好的选择。

1.3.3 风 (Wind)

$$wind = \sqrt{(state_u)^2+(state_v)^2}$$

这很有意义,我想在模型中至少包含一个“物理上合理”的东西(傻我,是的)。它没有真正帮助但也没有伤害模型,所以它留下了。我只对 WIND 使用了第一种归一化,其中:
mean(wind) = mean(mean(state_u), mean(state_v))
并且:
std(wind) = sum(std(state_u), std(state_v))

总之,特征维度是:
9[col_features]*60[levels]*3[representations]+60[wind_levels]+16[not_col features] = 1696

1.3.4 特征软裁剪 1

归一化后,数据有一些极端值 (~±3000)。这个问题仅存在于第一种表示。模型实际上很容易处理它,但我更喜欢安全起见。所以,对于 x_col_norm 和 WIND,我应用了以下软裁剪:

cutoff = 30
square_cutoff = cutoff**0.5
x_col_norm = tf.where(x_col_norm>cutoff, x_col_norm**0.5+cutoff-square_cutoff, x_col_norm)
x_col_norm = tf.where(x_col_norm<-cutoff, -tf.math.abs(x_col_norm)**0.5-cutoff+square_cutoff, x_col_norm)

1.3.5 特征软裁剪 2

除了第一个软裁剪外,我应用了第二个软裁剪来处理来自高分辨率集的极端值。我在所有表示上应用了裁剪,包括 WIND,并且在我应用了第一个软裁剪 (1.3.3) 对于相关特征之后:

cutoff_2 = 86.0
log_cutoff = tf.math.log(cutoff_2)
x_col_norm = tf.where(x_col_norm>cutoff_2, tf.math.log(x_col_norm)+cutoff_2-log_cutoff, x_col_norm)
x_col_norm = tf.where(x_col_norm<-cutoff_2, -tf.math.log(-x_col_norm)-cutoff_2+log_cutoff, x_col_norm)

我选择了 cutoff_2 以便软裁剪仅影响高分辨率数据。

1.3.6 目标软裁剪

我只对高分辨率目标做了这个;如果每个目标相对于低分辨率对应目标 min/max 太极端,则进行软裁剪(这与 1.3.4/1.3.5 不同,软裁剪范围对于每个目标不同)。代码中:

rescale_factor = 1.1
if x['res'] == 0:
    y_norm = tf.where(y_normnorm_y_max*rescale_factor, norm_y_max*rescale_factor+tf.math.log(1+y_norm-norm_y_max*rescale_factor), y_norm)

1.4 后处理

1.4.1. 下 cast 和 上 cast

在 FP64 和 FP32 之间移动时应特别小心。我用 FP64 的原始值编码了我的 TFRecords。在我处理数据后(见 1.3. 除了 1.3.5 出于某种原因在 casting 之后发生),值在传输到模型之前被下 cast 为 FP32。然后我将预测上 cast 为 FP64,之后才应用去归一化以获得提交的值:

preds = preds + mean_y.reshape(1,-1)*stds
preds[:, np.where(stds_new == 0)] = 0
preds = preds/np.where(stds>0, stds, 1)

1.4.2. 坏目标的均值

这很简单:

metrics = np.asarray([sklearn.metrics.r2_score(val_labels[:, i], preds[:, i]) for i in range(368)])
for i in range(len(metrics)):
    if metrics[i]<0:
        preds[:,i] = 0

实际上,这最终是不必要的,因为唯一的坏目标是那些在提交中归零或在 ptend trick 范围内的目标(见下一点,1.4.3)。

1.4.3. Ptend trick

显然。如果你是 LEAP 的新手,看 这里获取细节

1.5 验证

我的本地验证包括两个从低分辨率数据集中随机选择的验证集(所有模型相同样本),每个大小为 100K 样本。此外,我从高分辨率数据集中随机选择了 100K 样本,从低分辨率训练集中选择了 100K 样本(即,最后一个验证集是在我也训练的样本上,以更好地判断过拟合)。总之,我有四个验证集。

当我在 ~1M 样本上训练时,本地验证与 public leaderboard 分数高度相关,但当我扩展到所有数据时,相关性较低(可能是因为当我使用完整训练集时,它包括在时间上非常接近本地验证样本且具有相同纬度/经度的样本,所以它甚至开始在验证集上过拟合,而 public 测试集包括与训练集中存在的年份完全不同的样本)。例如,我可以将我的本地验证分数推到 ~0.8,但在 public LB 上,它将获得 ~0.785,并且分数低于一个达到 say 0.795 但训练 epochs 较少的模型。所以,对于在所有 HF 低分辨率数据上训练的最终模型(以及那些我也在高分辨率数据上训练的模型),我针对 public LB 验证以掌握良好的过拟合范围,集成验证直接针对 public LB 完成。因此,我的集成略微过拟合到 public LB,尽管我采取行动减少所述过拟合(相等混合权重,如果我判断它们足够成功并基于使用的参数预期它们会好,则包括“不太成功”的模型)。如果听起来有点像黑魔法——是的,它是!深度学习有时是科学,有时是艺术。

2. 提交细节

2.1 集成

我的获胜集成包括 13 个模型,每个都有点不同(见我的 GitHub 完整细节,'The steps to reproduce my solution' 第 5 点)。最好的模型 (11) 是 LB 0.79159/0.78869,最差的 (2) 是 LB 0.78795/0.78388。两者在 public 和 private LB 上都是最好/最差。完整集成是 LB 0.79410/0.79123。此外,我的仅低分辨率数据 5 模型集成有 LB 0.79299/0.78951,我的无时空辅助损失 6 模型集成有 0.79355/0.79092。当你阅读我的解决方案时,你可能试图找出让我获得第一名的“秘密酱汁”,但真的是组合起了作用。我使用的每一种技术,我认为没有它我仍然可以获得第一名。

2.2 有用的技术

这是我在上面深入编写的方法的简短总结:Squeezeformer, 宽 GLUMlp 预测头,无 dropout, MAE, 辅助时空损失,置信度头,掩码损失,多种数据表示,高分辨率数据,特征和目标软裁剪以及仔细的下 cast/上 cast。

2.3 什么没起作用

各种模型架构(纯 transformer, 其他 1Dconv/transformer 组合,Unet, dropout, 更小模型,更大模型,其他优化器),简而言之,许多次优超参数。Log-normalization(即 log(x),不是我的 log(1+x) 表示处理不同问题)。MSE, MSE/MAE 各种组合,加权损失函数(查看我的 Ribonanza 解决方案获取细节),层/特征掩码,预测年份作为额外辅助损失,可能还有其他我不记得的不是很重要的事情。

2.4 硬件

我在 Kaggle 和 Colab TPU 上训练,在 Kaggle P100 GPU 上推理。计算方面,我的实验和训练在 Colab 计算单元上至少花费 200 美元,总共可能少于 300 美元。如果与你“免费”的个人机器相比听起来很多,请考虑使用 RTX4090 的电费...

2.5 bonus: 置信度是关键

既然我已经有了置信度预测, simply 因为置信度头改进了模型,我出于好奇更进一步,检查我是否可以通过丢弃“低置信度”样本获得更高分数,以及我可以提高多少。这是一个图表(仅针对一个模型,不是完整集成),显示 R-squared 作为在我丢弃最“低置信度”样本后剩余样本分数的函数。

置信度是关键图表

正如你所见,通过丢弃 ~0.1 的样本,我已经可以获得 ~0.83+ 分数。这并不非常 surprising:如果你检查数据,你会看到某些月份和位置分数低得多,所以更高分数可以通过丢弃 said 月份/位置简单实现。然而,通过置信度丢弃可能更精确和高效。进一步研究可能很有趣,特别是鉴于这个特定问题,因为我们可以用模型预测替换模拟器计算,然后将低置信度样本发送回模拟器。

作为旁注,我也尝试了按置信度集成(即,给低置信度预测较低权重),但我的初步研究没有显示 promising 结果。虽然它是初步的——我直到竞赛最后一周的周日才做到这一点,要么是更深入地研究它,要么是观看 Euro 2024 决赛...(恭喜西班牙!)

同比赛其他方案