658. Jane Street Real-Time Market Data Forecasting | jane-street-real-time-market-data-forecasting
首先,感谢主办方举办这场精彩的比赛!这是一个罕见且令人兴奋的机会,可以将深度学习应用于表格数据,并测试模型如何在实时条件下适应新数据,模拟真实世界的环境。我非常享受参与和学习的过程。也要感谢所有参与公开讨论的参与者——我从你们身上学到了很多!@victorshlepov, @lihaorocky, @johnpayne0, @shiyili
我使用了带有两个折叠(fold)的时间序列 CV。验证集大小设置为 200 天,与公共数据集一致。它与公共 LB 分数相关性很好。此外,第一个折叠的模型在最后 200 天上进行了测试,中间有 200 天的间隔,以模拟私有数据集场景。
我使用了从 date_id = 700 开始的数据,因为此时 time_id 的数量稳定在 968。我尝试过使用整个数据集,但没有带来分数提升。
应用了简单的标准化和用零填充 NaN。其他方法没有提供改进。
我使用了所有原始特征,除了三个分类特征(特征 09-11)。我还选择了 16 个与目标高度相关的特征,并创建了两组附加特征:
date_id 和 time_id 的平均值。time_id 的滚动平均值和标准差。此外,我添加了 time_id 作为特征。
添加这些特征使 CV 提高了约 +0.002。
时间序列 GRU,序列长度为一天。最终确定了两种略有不同的架构:
第二个模型在 CV 上比第一个模型效果更好(+0.001),但第一个模型仍然对集成有贡献,所以我保留了它。
MLP、时间序列 transformers、跨符号注意力和嵌入对我没用。
我使用了 4 个响应器作为辅助目标:responder_7 和 responder_8,以及两个计算出来的目标:
df = df.with_columns(
(
pl.col("responder_8")
+ pl.col("responder_8").shift(-4).over("symbol_id")
).fill_null(0.0).alias("responder_9"),
(
pl.col("responder_6")
+ pl.col("responder_6").shift(-20).over("symbol_id")
+ pl.col("responder_6").shift(-40).over("symbol_id")
).fill_null(0.0).alias("responder_10"),
)
这些分别是基本目标超过 8 天和 60 天的近似滚动平均值。正如 @johnpayne0 在此讨论中详细描述的那样,responder_6 是某个变量的 20 天滚动平均值,而 responder_7 和 responder_8 是同一变量的 120 天和 4 天滚动平均值,添加了一些噪声。给定 N 天滚动平均值,我们可以轻松计算 N*K 天滚动平均值。
每个辅助目标使用单独的基础模型。这些模型的预测然后通过线性层传递以产生最终目标输出 responder_6。
使用每个响应器的损失之和(加权零均值 R²)来训练模型。
添加辅助目标使 CV 和 LB 分数提高了约 +0.001。
模型使用一天的批量大小训练,学习率为 0.0005。
对于提交,我在直到最后一个 date_id 的数据上训练模型,使用的 epoch 数量等于 CV 上的平均最佳 epoch 数。
我在 3 个种子上运行了两个模型,并对这 6 个模型的预测进行了简单的未加权平均。这导致 LB 分数为 0.0112(相比之下最佳单一模型 LB 为 0.0105)。
在推理期间,当带有目标的新数据可用时,我执行一次前向传递以更新模型权重,学习率为 0.0003。这种方法显著提高了模型在 CV 上的性能(+0.008)。有趣的是,对于 MLP 模型,没有在线学习的分数高于 GRU,但有了在线学习后则较低。
更新仅使用 responder_6 损失执行,不使用辅助目标。
更新应用于提交期间提供的整个数据集,包括 is_scored = False 的行。
我还考虑过在直到私有数据集开始的数据上执行完整的在线重新训练。这是有意义的,因为训练数据和私有数据集之间存在显著差距。然而,重新训练模型需要将训练过程分布在多个推理步骤中,因为日期之间的一分钟时间限制不足以完成。我认为这将是可行的,但我决定不花时间在上面,尽管我的测试表明它可以提供 +0.001 的分数改进。尽管如此,我发现令人惊讶的是,代替完整的模型重新训练,执行近一年的每日更新就足够了,模型继续表现良好。
推理速度至关重要,所以我花了大量时间优化代码,特别是数据处理和滚动特征的计算。
对于我的最终提交,运行一个推理步骤(time_id)需要 0.06 秒,其中 0.02 秒用于数据处理。每个 date_id 更新模型权重需要 3.6 秒。
我使用了 PyTorch,但由于据说 TensorFlow 更快,我尝试切换过去。然而,经过几天的实验,我无法获得更好的性能,所以我决定坚持使用 PyTorch。
由于 RAM 要求,我从 Google Colab 切换到了 vast.ai,对这个决定非常满意。我在本地编写代码,享受 VSCode 的所有好处,然后运行脚本将代码推送到 GitHub,在服务器上拉取并远程执行脚本。
我还使用了 WandB 来监控实验,这帮助我跟踪分数,并在出现问题时轻松回滚到旧版本的代码。
为了调试我的提交 notebook 并估计提交时间,我使用了 @shiyili 的合成数据集。
| CV 折叠 0 | CV 折叠 1v | 折叠 1 带 200 天间隔 | CV 平均 | |
|---|---|---|---|---|
| GRU 1 无辅助目标和在线学习 | 0.0161 | 0.0062 | 0.0011 | 0.0112 |
| GRU 1 无辅助目标 | 0.0235 | 0.0148 | 0.0136 | 0.0190 |
| GRU 1 | 0.0249 | 0.0153 | 0.0147 | 0.0201 |
| GRU 2 | 0.0262 | 0.0166 | 0.0161 | 0.0214 |
| GRU 1 + GRU 2 | 0.0268 | 0.0169 | 0.0163 | 0.0218 |
| GRU 1 3 种子 | 0.0258 | 0.0164 | 0.0152 | 0.0211 |
| GRU 2 3 种子 | 0.0267 | 0.0175 | 0.0163 | 0.0221 |
| GRU 1 + GRU 2 3 种子 | 0.0270 | 0.0175 | 0.0162 | 0.0222 |
折叠 0: date_id 从 1298 到 1498。
折叠 1: date_id 从 1499 到 1698。