返回列表

Private 24th | Public 23rd solution - Data augmentation

683. NFL Big Data Bowl 2026 - Prediction | nfl-big-data-bowl-2026-prediction

开始: 2025-09-25 结束: 2025-12-03 运动员表现 数据算法赛
私有榜第 24 | 公有榜第 23 名解决方案 - 数据增强

私有榜第 24 名 | 公有榜第 23 名解决方案 - 数据增强

作者:Yannan Chen (MASTER)
发布日期:2025-12-04
比赛:NFL Big Data Bowl 2026 Prediction

这是我参加过的最有趣的比赛之一。我在比赛过半时才加入,只剩下一个月的时间来解决这个问题,而且我对 ST-transformers 不是很熟悉。这是一个真正的挑战,但也令人无比兴奋。多亏了社区,我学到了很多。

我就直入主题,总结一下我的解决方案。

(我和大多数人一样,从公开 notebook 开始,所以我只会强调我做的不同之处以及我发现有帮助的地方。)


模型结构 - ST-transformer play-level 模型

我应用了从论文 A Spatio-temporal Transformer for 3D Human Motion Prediction https://arxiv.org/abs/2004.08692 中学到的类似结构,并稍作调整以适应本次比赛的数据结构。

  • 数据输入形状:(WINDOW, NUM_PLAYERS, FEATURES)
  • 数据输出形状:(HORIZON, NUM_PLAYERS, 2)

关键模型参数如下:

HIDDEN_DIM = 64
NUM_HEADS_TEMPORAL = 8 
NUM_HEADS_SPATIAL = 8
NUM_LAYERS = 4
DROPOUT = 0.05
DIM_FEEDFORWARD = HIDDEN_DIM*4

我还在空间注意力输出之上添加了一些边缘偏置 (edge biases) (sameteam_ij, distance_ij, closing_speed_ij),这似乎有帮助,但作用不大。


特征

'x', 'y', 'o', 'dir',
'player_height_feet', 'player_weight', 
's', 'velocity_x', 'velocity_y', 'accel_x', 'accel_y',
'ball_land_x', 'ball_land_y',
'dist_from_right', 'distance_to_goal', 'play_direction_right',
'ball_dx', 'ball_dy', 'distance_to_ball', 'ball_dir_x', 'ball_dir_y', 'ball_closing_speed',
'speed_change', 'dir_change', 'vel_x_change', 'vel_y_change',
'passer_dx', 'passer_dy', 'passer_distance', 'vel_to_passer_alignment', 
'receiver_dx', 'receiver_dy', 'receiver_distance', 'vel_to_receiver_alignment', 
'post_pass', # 0=传球前,1=传球后
'frame_to_pass',  # 传球前为负值 (倒计时到 0) - 用于虚拟传球前后切割增强
'input_frame_count', 'output_frame_count',  # 总帧数

我注意到,超过某一点后,添加更多特征不再能提高性能。最终,我移除了大多数花哨的特征,主要保留了与基本相对距离和速度相关的特征。我还保留了几个与帧计数及其与传球点关系相关的特征。由于我正在做类似于滑动窗口的数据增强(如下所述),我希望模型能够理解时间帧的整体上下文。

此外,player_roleplayer_position 被嵌入为 8 维特征,并添加到上述特征中。


数据增强 - 虚拟传球前后切割 (最重要)

我只使用了本次比赛数据部分中提供的 2023 年数据,排除不良 play 后仅包含 14108 个 plays。需要生成更多数据来帮助模型泛化。

原始数据:

原始数据示意图

增强方法 1(将传球切割点移至传球前):

增强方法 1 示意图

增强方法 2(将传球切割点移至传球后):

增强方法 2 示意图

增强方法 1 和 2 一起总共为我提供了大约 200k 个额外样本。

增强方法 1 更重要,因为它将所有角色的球员都加入到了输出目标中,这对于使模型更稳健非常有帮助。

增强后的数据随后与原始数据一起用于训练,但损失权重较低,为 0.5。


TemporalHuber 损失函数 (非常重要)

我从社区学到了 TemporalHuber 损失函数,这非常有帮助。非常感谢!我稍微调整了一下,使训练更加稳定:

err = pred - target
abs_err = torch.abs(err)
huber = torch.where(
        abs_err <= self.delta,
        0.5 * err * err,
        self.delta * (abs_err - 0.5 * self.delta)
)

if self.time_decay > 0:
    L = pred.size(-1)
    t = torch.arange(L, device=pred.device, dtype=pred.dtype)
    w = torch.exp(-self.time_decay * t).view(1, 1, L)
else:
    w = 1.0

masked_weighted_huber = huber * mask * w # ◀️
mask_sum = mask.sum(dim=(1, 2)) + 1e-8 # ◀️
main_loss = masked_weighted_huber.sum(dim=(1, 2)) / mask_sum  # ◀️

训练 - 验证策略

我按周分割数据。共有 18 周的数据,在此基础上我创建了 6 折:

FOLDS = [(3,7,18),(1,10,17),(4,5,8),(2,11,14),(6,13,16),(9,12,15)]

每折我使用两个随机种子,所以每次训练运行给我的是 12 个模型的平均 RMSE 分数。尽管如此,我的 CV 和 LB 并不完全一致。在 0.001 的水平上,CV 经常下降而 LB 上升。只有当至少有 0.01 的明显改善时,我才信任 CV。

我的直觉是更信任 LB,因为最好的 CV 分数通常出现在几十个 epoch 之后,这感觉更像是过拟合或随机的幸运命中。


最终提交

最终提交包含 across 4 次训练试验48 个模型,每次试验包含来自不同折和种子的 12 个模型,使用简单加权平均组合。4 次试验主要在输出 horizon 上有所不同。

同比赛其他方案