返回列表

8th Place Solution

545. IceCube - Neutrinos in Deep Ice | icecube-neutrinos-in-deep-ice

开始: 2023-01-19 结束: 2023-04-19 物理与天文 数据算法赛
Waird团队第8名解决方案 - IceCube中微子探测竞赛

Waird团队第8名解决方案

竞赛: IceCube中微子探测竞赛
排名: 第8名
发布时间: 2023年4月24日

首先,我要感谢组织者和工作人员举办了如此精彩的比赛。也感谢我所有的队友!@anjum48@allvor@remekkinas@wrrosa

祝贺@anjum48!晋升GM!

我们的代码将在此处提供。

Datasaurus 部分

预处理

原始数据

这个数据集非常大,如果每个epoch都重复进行pandas操作会浪费大量CPU周期(polars似乎还不能与PyTorch数据加载器的多工作进程一起使用)。为了解决这个问题,我创建了PyTorch Geometric的`Data`对象用于每个事件,并将它们保存为`.pt`文件,可以在训练期间加载。使用32个线程创建这些文件大约需要8小时,占用约1TB空间。

这样做的问题是1TB空间分散在1.3亿多个小文件中。Linux分区有有限的"索引节点"或`inodes`,即特定文件的索引。由于这些文件非常小,我在2TB驱动器空间用完之前就遇到了inode限制。作为解决方法,我不得不将这些文件分散到两个驱动器上,所以如果有人想复现此方法或运行我的代码,请注意这一点。

更高效的方法可能是存储已预批处理的`.pt`文件,这样需要的文件更少,但你会失去每个epoch打乱数据的能力,这可能对这么多数据来说影响不大。我没有尝试GraphNet团队建议的sqlite方法。

特征

在GNN的上下文中,每个DOM被视为一个节点。每个节点具有以下11个特征:

  • X: sensor_geometry.csv中的X位置 / 500
  • Y: sensor_geometry.csv中的Y位置 / 500
  • Z: sensor_geometry.csv中的Z位置 / 500
  • T: (batch_[n].parquet中的时间 - 1e4) / 3e3
  • Charge: log10(batch_[n].parquet中的电荷) / 3.0
  • QE: 见下文
  • Aux: (False = -0.5, True = 0.5)
  • 散射长度: 见下文
  • 到前一次命中的距离: 见下文
  • 自前一次命中的时间差: 见下文
  • 散射标志: (False = -0.5, True = 0.5)。见下文

许多归一化方法来自GraphNet作为起点,但我改变了时间的尺度,因为时间在这里是一个非常重要的特征。

对于具有大量命中的事件,为防止OOM错误,我采样了256次命中。这会使过程略微非确定性。

量子效率

QE是DOM中光电倍增管的量子效率。DeepCore DOM的QE比常规DOM高35%(本文图1paper),因此QE everywhere设为1,DeepCore中较低的50个DOM设为1.35。最终的QE特征使用(QE - 1.25) / 0.25进行缩放。

散射长度

散射和吸收长度对于表征冰的透明度差异非常重要。此数据发布在本文第31页paper。使用1920米的数据深度,使得z = (深度 - 1920) / 500。使用`scipy.interpolate.interp1d`将数据重采样到z值。我发现经过`RobustScaler`处理后,散射和吸收数据几乎相同,因此我只使用了散射长度。

前一次命中特征

两种主要事件类型是径迹事件和级联事件。查看一些出色的可视化工具,例如edguy99的,我想到如果节点能了解最近前一次命中的位置和时间,可能有助于模型区分这两组。为计算此特征,对每个事件将命中按时间排序,计算所有命中的两两距离,屏蔽未来的命中,并计算到最近前一次命中的距离d。使用(d - 0.5) / 0.5进行缩放。前一次命中的时间差也使用相同方法计算,并使用(t - 0.1) / 0.1缩放。

散射标志

我尝试创建一个标志来识别命中是来自径迹直接产生,还是某些次级散射,灵感来自本文第2.1节paper。添加此标志的一个副作用是训练更加稳定。标志生成如下:

  1. 识别电荷最大的命中
  2. 从该DOM位置,计算到所有其他命中的距离和时间差
  3. 如果传播该距离所需时间 > 冰中的光速,则假设光子是散射结果

验证

我使用了90%-10%的训练-验证分割,由于数据集规模,没有使用交叉验证。分割通过创建log10(n_hits)的10个bin,然后使用`StratifiedKFold`进行10次分割完成。

模型

我直接使用GraphNet的`DirectionReconstructionWithKappa`任务,意味着嵌入(例如128的形状)将被投影到4维(x, y, z, kappa)

架构

我使用了以下3种架构。所有验证分数都应用了6x TTA:

GraphNet/DynEdge - Val = 0.98501
GPS - Val = 0.98945*
GravNet - Val = 0.98519

这3个模型的平均值得到了0.982的LB分数。

GraphNet/DynEdge模型几乎没有修改,只是改用了GELU激活。

GPS和GravNet使用了8个块,你可以在此处的代码中找到两种架构的确切结构。

*GPS是最强大的模型,但训练也最慢,属于transformer类型模型(我的机器上大约11小时/epoch)。我成功训练了一个达到0.98XX验证分数的模型,但太晚了未能包含在最终提交中。

损失函数

我使用VonMisesFisher3DLoss + (1 - CosineSimilarity)作为最终损失函数,因为余弦相似度是平均角度误差的一个很好的代理指标。对于CosineSimilarity,我将目标方位角和天顶角值转换为笛卡尔坐标。

这比分别对方位角(VMF2D)和天顶角(MSE)使用损失函数要好得多,这是我最初采用的方法。

增强

我将数据以string 35(DeepCore string)为中心,并围绕z轴以60度为步长旋转。这实际上并没有改善验证性能,但好处是使模型具有旋转不变性,从而可以利用探测器对称性并应用6倍测试时增强(TTA)。这通常能将分数提高0.002-0.003。

训练参数

  • AdamW优化器
  • Epochs = 6
  • Cosine调度(无warmup)
  • 学习率 = 0.0002
  • Batch size = 1024
  • Weight decay = 0.001 - 0.1(取决于模型)
  • FP16训练
  • 硬件:2x RTX 3090,128 GB RAM

最终提交

循环平均

对扰动的鲁棒性

待定

经验教训/无效的尝试

  • GraphNet DynEdge基线极其强大且难以改进 - 向团队致敬!它也是最快/高效的模型,我用于大多数实验
  • 更多数据 = 更好。但问题是,我发现基于1%或5%数据得出的实验结论在完整数据集上不再适用。这使得实验缓慢且昂贵
  • 批归一化使训练不稳定且没有显示改进
  • Lion优化器泛化效果不如AdamW
  • Weight decay对某些模型很重要。因此我假设改变Adam中的epsilon值会有影响,但我没有看到显著效果
  • 在我的实验中,GNN似乎受益于泄漏激活,例如GELU
  • 对于MPNN聚合,[min, max, mean, sum]似乎就足够了。添加更多没有显示出显著增益
  • 将所有时间重新对齐到每个事件的第一次命中时间会降低性能,可能是由于数据中的噪声/错误触发等
  • 径向最近邻在定义图边时并不比KNN更好
  • 仅使用1 - CosineSimilarity作为损失函数非常不稳定。添加VMF3D有很大帮助

代码

我的所有代码将很快在此提供:https://github.com/Anjum48/icecube-neutrinos-in-deep-ice

Isamu 部分

在团队合并之前,我创建了LSTM和GraphNet模型。团队合并后,我专注于GraphNet,因为Remek的LSTM模型优于我的。我使用graphnet(https://github.com/graphnet-team/graphnet)作为基线,并进行了多项改进以提高其准确性。以下是我进行的一些有效实验(有很多无效的尝试):

  • 随机采样(如果指定数据长度超过800,则从DB中随机采样)
  • 增加KNN层的最近邻数量(8->16)
  • 添加特征
    • x, y, z
    • 时间
    • 电荷
    • 辅助特征
    • 冰透明度特征
  • 使用kappa(sigma)的vonMisesFisher分布的两阶段模型
    • 训练第一阶段模型预测x, y, z, kappa
      • 从公共基线notebook的GraphNet权重重新开始
      • 使用了约1-250个batch
      • batch size 512
      • epoch 20
      • DirectionReconstructionWithKappa
    • 根据第一阶段kappa值将数据分为简单和困难部分
      • 使用第一阶段模型进行推理,并根据预测的kappa值将数据分为两组(简单部分和困难部分)
    • 训练针对简单和困难部分的专家模型并组合它们的预测
      • 使用了约250-350个batch
      • batch size 512
      • epoch 20
      • DirectionReconstructionWithKappa
  • TTA
    • 绕z轴180度旋转TTA
  • 损失
    • DirectionReconstructionWithKappa
  • 硬件:RTX 3090,64 GB RAM,8TB HDD,2TB SSD,Google Colab Pro

上述模型(第一阶段和第二阶段)的集成给出了public LB 0.995669,private LB 0.996550

Remek 部分 - LSTM

对于LSTM训练,我们采用了Robin Smits(@rsmits)提出的方法并进行了一些改进:

  • 我们增加了更多LSTM/GRU层(4层GRU/LSTM)- 实验表明多于或少于4层效果都不更好
  • 我们添加了冰透明度作为额外特征(同时使用了透明度和吸收两个特征)
  • 我们重新设计了训练循环,在所有batch上训练模型 - 可以在DS的不同部分上训练模型(从一个batch到所有DS在一个epoch内)
  • 我们验证了许多假设(其中一部分使用了weight and biases的Swipe工具):
    • 不同LSTM单元大小 - 最终196在我们的模型中效果最好
    • 不同bin大小 - 24是我们的最终选择
    • 不同特征数量和脉冲选择 - 最终我们采用策略:首先选择非辅助事件,然后添加随机辅助事件(这也给了我们分数提升 - 这是一种数据增强技术)
    • 不同模型架构 - 最终我们的分数融合使用了纯LSTM/GRU设置。我们测试了transformer架构,但似乎放弃得太早(第一次分数比纯LSTM差很多)
    • 不同优化器(AdamW, NAdam) - 最终选择是Adam
    • 不同调度器 - CosineDecay, OneCycle,但最终使用了下面描述的阶梯式LR调度

训练分为三个LR调度阶段:

  • 阶段1 - 训练基线模型(两个模型)- batch 4-330 (m1) 和 331-660 (m2),使用LR = 0.005,6-8个epoch,稀疏分类交叉熵损失。两个模型都在batch 1-3上进行验证。
  • 阶段2 - 微调模型m1和m2,使用LR/10 = 0.0005,3个epoch,稀疏分类交叉熵损失。
  • 阶段3 - 使用LR = 0.00025微调模型m1和m2,2个epoch,使用不同的损失函数 - 带标签平滑0.05的分类交叉熵
  • 然后我们取m1和m2模型中根据MAE指标最好的4个模型(共8个模型),使用SWA(随机权重平均)生成一个模型。我们简单地平均了模型权重。结果显示该模型与8个模型集成的性能相同,但显著降低了LSTM推理时间。单个模型分数(public LB)(无TTA):1.0024。

未能提升我们分数的方法:

  • LSTM/GRU层和线性头中的Dropout
  • Masking层或LSTM/GRU层后的GaussianNoise层
  • Adam + Lookahead优化器
  • 梯度累积模拟TPU大批次大小 - 我(Remek)在TF/Keras中正确实现此功能有问题(在Pytorch中很容易,似乎在TF/Keras中实现并不完全简单 - 我的日常选择是Pytorch)

额外工具:

  • Weights and biases用于两个任务 - 记录和监控训练过程,以及swipe超参数调整(LSTM单元数量、LR调度器、优化器和LR)

对于我的(Remek)实验,我最初使用ZbyHP Z4配备2xA5000,后来HP给我寄了一台ZbyHP Z8工作站,配备2x Intel Xeon CPU和Nvidia A6000 GPU。我个人可以说这帮助我建立了快速的实验流程。我能够非常快速地处理数据集文件。拥有A6000让我能够使用更大的batch size训练模型。感谢HP支持我的工作。

最后的话 - 我认为我们组建了一个伟大的团队 - 我们每个人都负责解决方案的一部分,我们讨论了很多,但没有"我的更好"这种想法。虽然这是我们第一次合作,但我感觉我们好像认识了一辈子。伟大的团队,伟大的成绩!感谢大家让我有机会向伟大的AI专家学习。

Alvor 部分 - 融合

我在本次竞赛中的主要贡献是开发了融合队友解决方案的方法。分析不同类型的解决方案(GraphNet、LSTM等)表明,它们在不同预测天顶角和方位角值(主要是天顶角)下的效率不同。因此,我将最初的"恒定权重"方法改为"分箱"方法。

该方法包括将预测的天顶角值分成10个等宽度的箱。因此,当组合两个解决方案时,我们总共得到100个箱。值10是可配置的,但实验表明它接近最优。

然后,在每个箱中找到天顶角的融合权重,并找到方位角的融合权重。预测天顶角的融合通过简单的线性组合完成。由于可能通过2π的过渡,方位角的融合稍微复杂一些。因此,首先计算两个解决方案中预测方位角之间的差异,以及第二个值相对于第一个值的方向。

权重拟合在大小接近测试数据的训练数据样本(5个batch,包含100万事件)上进行。

还测试了几种改进此融合方法的方法,特别是使用GBDT。

其中一种方法是尝试将事件分类为"简单"和"复杂"(受此kernel启发)。

实验表明,某些类型的神经网络更擅长处理简单事件,而其他类型更擅长处理复杂事件。如果我们能准确确定每个事件的类型,这将极大改善竞赛指标。构建的模型在事件分类方面的效率达到了公共kernel的水平(AUC 0.93),但这还不足以获得融合质量的显著提升。

还训练了几个分类模型(二元变量 - 哪个神经网络对给定事件的预测更好)和回归模型(目标变量 - 两个神经网络对给定事件的竞赛指标差异)。不幸的是,所有这些方法仅显示了结果的轻微改进(第四位小数),但它们非常容易过拟合。

后来,我的方法被Wojtek Rosa显著改进。因此,他的方法被用于最终提交,他在自己的部分详细描述了该方法。特别地,我想指出他用决策树模型构建融合箱的绝妙想法。

Wojtek 部分 - 更多融合

感谢竞赛主办方举办如此激动人心的IceCube竞赛!

也感谢我的队友们 - 再次祝贺他们的奖项、GM头衔和出色的解决方案。

我的部分是关于融合的。

我使用batch_ids 1-5进行评估和调整参数,后来仅将batch_ids 655+用于评估目的。

当我加入Remek&Alvor团队时,我们有出色的LSTM解决方案、公共Graphnet和令人惊叹的融合技术:

def alvor_blend(s):
   s['diff'] = np.abs(s['azimuth_2'] - s['azimuth_1'])
   s['direction'] = np.where(
   s['diff'] < np.pi,
   np.sign(s['azimuth_2'] - s['azimuth_1']),
   -np.sign(s['azimuth_2'] - s['azimuth_1'])
   )
   N = 10
   s['bin'] = (N*np.floor(N*s['zenith_1']/np.pi) + np.floor(N*s['zenith_2']/np.pi)).astype(int)

并最小化每个箱的分数,找到最佳的qu, alpha,例如:

s0['azimuth_pred'] = s0['azimuth_1'] + alpha * s0['diff'] * s0['direction']
s0['zenith_pred'] = (1-qu) * s0['zenith_1'] + qu * s0['zenith_2']

我意识到,与简单/公共向量权重集成相比,这种方法要好得多。

我尝试用更大的N值来提高分数,但这导致过拟合。

之后,我通过创建使用回归树的箱来改进分数,目标 = score_1 - score_2,特征:

cls = ['azimuth_1','zenith_1','azimuth_2','zenith_2','diff','direction', 'n_pulses','zenith_1_2','direction_x','direction_y','direction_z','direction_kappa',
       'zenith_3','azimuth_3','score_1_3','score_2_3']

其中direction_%来自Isamu提交,zenith_3来自Robert的1.183线性解决方案。我尝试了许多来自事件的直接特征,但决策树回归器的简单性给了我们(几乎)完全控制N箱的能力,用于调整allvors的参数qu和alpha:

regr_1 = DecisionTreeRegressor(max_depth=11, min_samples_leaf=500, min_samples_split=500)
s['bin_raw'] = regr_1.predict(s[cls])
s['bin'] = round(s['bin_raw'],5).astype(str)

使用决策树使我们的本地分数和LB都提升了0.0027。我尝试了其他融合方法,如MLP或LGBM,但没有成功。

后来,我还稍微修改了`adjusting`循环,以找到zenith_1和zenith_2的完整线性组合:

s0['azimuth_pred'] = s0['azimuth_1'] + alpha * s0['diff'] * s0['direction']
s0['zenith_pred'] = ru * s0['zenith_1'] + qu * s0['zenith_2']

找到ru和qu需要更多计算,但这给了我们额外的0.001提升。

我们的最终融合包括:
Isamu解决方案 .995 -> Remek LSTM 1.002 -> datasaurus Graphnet .982 -> Robert 1.183
本地分数 0.9774692 Public LB 0.975545, Private LB 0.97658

发现:

  • 我们发现了一种有趣的错误类型,"模型放弃"并预测天顶角接近0。
    我们的最终解决方案天顶角预测与分数的关系(x轴:预测天顶角,y轴:天顶角真实值):

天顶角预测 vs 分数

真实天顶角(x轴) vs 分数(y轴) - "错误三角形"可见:
真实天顶角 vs 分数

  • 我们发现LSTM分类的预测在bin中心异常准确,图表:zenith_pred_lstm*1000 (x轴),平均分数(y轴),天顶角边缘(红线)

LSTM分类预测准确性

同比赛其他方案