683. NFL Big Data Bowl 2026 - Prediction | nfl-big-data-bowl-2026-prediction
副标题:数据增强技巧 + 论文启发的 Transformer 架构 + 自定义训练策略 + 集成学习
我们的解决方案通过优先考虑物理信息驱动的归纳偏置(physics-informed inductive biases)而非暴力模型扩展,取得了 0.519 的公共 LB 分数。我们成功复现并调整了 STTRE(带相对嵌入的时空 Transformer) 架构,该架构最初是为多变量时间序列预测提出的 [1],现将其应用于 NFL 球员追踪领域。
我们的关键贡献在于一种新颖的手性增强(Chiral Augmentation)策略,它迫使模型学习场地对称性,显著减少了过拟合。虽然我们的模型在基于动量的轨迹预测上表现出色,但误差分析揭示了其在处理“急转弯(sharp cuts)”方面的根本局限性,这表明需要引入比赛结果分类。
我们改进的核心在于通过利用足球场的几何对称性来降低问题的维度。
我们将所有比赛标准化为从左到右移动。这有效地将预测空间减半。模型不再需要分别学习向左移动和向右移动的模式,从而可以专注于相对运动动力学。
def unify_left_direction(df: pd.DataFrame) -> pd.DataFrame:
if 'play_direction' not in df.columns: return df
df = df.copy(); right = df['play_direction'].eq('right')
# 反转向右移动比赛的 X 和 Y
if 'x' in df.columns: df.loc[right, 'x'] = FIELD_LENGTH - df.loc[right, 'x']
if 'y' in df.columns: df.loc[right, 'y'] = FIELD_WIDTH - df.loc[right, 'y']
# 调整角度
for col in ('dir','o'):
if col in df.columns:
numeric_col = pd.to_numeric(df.loc[right, col], errors='coerce')
transformed_angles = (numeric_col + 180.0) % 360.0
df.loc[right, col] = transformed_angles
if df[col].isnull().any(): df[col] = df[col].fillna(0.0)
# 调整球落点
if 'ball_land_x' in df.columns: df.loc[right, 'ball_land_x'] = FIELD_LENGTH - df.loc[right, 'ball_land_x']
if 'ball_land_y' in df.columns: df.loc[right, 'ball_land_y'] = FIELD_WIDTH - df.loc[right, 'ball_land_y']
return df
注意:此标准化逻辑由我们团队在竞赛早期开源,并被社区采用。
美式足球场沿长轴具有手性对称性(镜像对称)。跑向左侧边线的路线在物理上等同于跑向右侧边线的镜像路线。 为了迫使模型学习这种不变性,我们在训练期间实施了动态的随机 Y 轴翻转增强。
实现逻辑:
在我们的 SpatioTemporalData 加载器中,每个 epoch 以 $p=0.5$ 的概率反转 Y 轴逻辑:
# 在 __getitem__ 内部
# --- [开始] 1. 随机 Y 轴翻转 (手性增强) ---
if self.is_train and np.random.rand() < 0.5:
# 复制以避免修改缓存数据
seq_np = seq_np.copy()
# 1. 翻转坐标 'y' (y_new = FIELD_WIDTH - y)
if 'y' in self.feature_to_idx:
y_idx = self.feature_to_idx['y']
seq_np[..., y_idx] = FIELD_WIDTH - seq_np[..., y_idx]
# 2. 翻转矢量分量 (val_new = -val)
# 包括 velocity_y, acceleration_y, jerk_y 等
if self.flip_indices_invert:
seq_np[..., self.flip_indices_invert] = -seq_np[..., self.flip_indices_invert]
# 3. 翻转角度 (val_new = -val, 然后包裹)
# 包括 dir, o
if self.flip_indices_angular:
seq_np[..., self.flip_indices_angular] = wrap_angle_deg(-seq_np[..., self.flip_indices_angular])
# 4. 翻转目标轨迹
target_dy_np = -target_dy_np
该策略防止了模型记忆场地特定侧的“热点”,并显著提高了对未见比赛的泛化能力。
鉴于 STTRE 架构的限制(设计用于多变量时间序列而非图结构)以及训练数据中球员数量/角色/位置的不一致性,我们必须对包含在上下文窗口中的球员进行选择。
我们采用硬编码的选择逻辑来挑选4 个最相关的实体(MAX_SUBPLAY_SIZE = 4):
def find_relevant_entities_v3_PositionFiltered(play_df: pd.DataFrame, target_nid: int, cfg: Config) -> list:
# 1. 始终包含目标球员和传球手
entities, ids_to_exclude, ... = _get_common_entities(play_df, target_nid, cfg)
# 2. 寻找最近的对手 (优先考虑 DBs)
if not valid_opponent_ids.empty:
# ... 过滤 DBs ...
nearest_opp_nids = opponent_dist.nsmallest(cfg.N_OPPONENTS).index.tolist()
entities.extend(nearest_opp_nids)
# 3. 寻找最近的队友
# ...
return _finalize_entities(entities, ...)[:cfg.MAX_SUBPLAY_SIZE]
我们复现了论文 "STTRE: A Spatio-Temporal Transformer with Relative Embeddings" [1] 中的架构,并针对 NFL 场景进行了调整。
我们没有使用标准的 Transformer,而是利用三分支编码器来解耦依赖关系:
我们使用辅助速度损失(Auxiliary Velocity Loss) alongside 标准位置损失进行训练。
标准 MSE 损失通常会导致预测“懒惰”,滞后于真实轨迹。通过惩罚一阶导数(速度)的误差,我们迫使模型尊重物理连续性和动量。
为了榨取每一分性能,我们采用了稳健的推理流程:
std=0.02)以平滑模型方差。尽管我们的表现强劲,但可视化我们最差的预测揭示了一个明显的局限性:惯性偏差(Inertia Bias)。我们的模型过度依赖惯性/动量,预测的轨迹在前几帧保留了动量的方向。
如下图所示,我们的模型(红色)难以处理急转弯或"J 型转弯”,经常沿着原始速度矢量过度预测轨迹。模型假设球员将继续其动量(惯性),未能预见突然的制动或方向变化。这是一种过拟合,因为我们的训练专注于最小化轨迹的均方误差,而忽略了困难样本。
为什么球员会做出这些急转弯?它们几乎总是对传球结果的反应。 竞赛描述明确暗示了这一点:
“深远传球是美国体育的皇冠明珠。当球在空中时,任何事情都可能发生,比如达阵、拦截或争抢接球。这些比赛结果的不确定性和重要性有助于让观众保持紧张。”
我们将追踪数据视为确定性序列,但轨迹本质上是有条件的:
未来改进: 我们错过了构建多任务学习框架的机会。更好的方法是:
通过忽略这种结果不确定性,我们的模型本质上学习了接球和拦截之间的“平均”路径——这条路径在物理上并不存在于任何场景中。解决这个分类任务可能是突破 0.480 金牌壁垒的关键。
坐标的暴力平均通常会导致物理上不可能的轨迹(例如,如果模型 A 预测左转,模型 B 预测右转,简单的平均值预测球员直行但速度减半)。 为了解决这个问题,我们实施了一个异构集成,结合了三种不同的架构,并通过极矢量策略进行聚合。
多样性是我们集成成功的关键。我们结合了:
nv50)。擅长长期依赖和空间互动。标准集成平均 $x$ 和 $y$ 坐标:$\bar{x} = \sum w_i x_i$。这通常会导致能量损失——平均轨迹的速度低于任何单个模型。 我们实施了极向量平均。我们将预测分解为大小(速度)和方向(单位矢量)并分别对它们进行平均。
这确保了如果两个模型在方向上不一致,集成本质上是对角度进行“投票”,但保持了球员的动能(速度)。
实现:
def polar_vector_ensemble(preds_list, weights_list):
"""
物理感知集成:
分别平均速度 (大小) 和方向 (单位矢量)
以防止融合过程中的动能损失。
"""
magnitudes = []
unit_vectors = []
for p in preds_list:
# 计算速度
mag = np.linalg.norm(p, axis=-1, keepdims=True)
# 计算单位方向矢量
mask = mag > 1e-6
unit = np.zeros_like(p)
unit[mask[:,0]] = p[mask[:,0]] / mag[mask[:,0]]
magnitudes.append(mag)
unit_vectors.append(unit)
# 1. 速度的加权平均 (保持动量)
avg_mag = np.zeros_like(magnitudes[0])
for mag, w in zip(magnitudes, weights_list):
avg_mag += mag * w
# 2. 方向的矢量平均
avg_dir_vec = np.zeros_like(unit_vectors[0])
for vec, w in zip(unit_vectors, weights_list):
avg_dir_vec += vec * w
# 3. 重构速度矢量
avg_dir_norm = np.linalg.norm(avg_dir_vec, axis=-1, keepdims=True)
final_unit_vec = avg_dir_vec / (avg_dir_norm + 1e-6)
return final_unit_vec * avg_mag
我们观察到 RNN (GRU) 在长预测视野 ($t > 30$) 上迅速退化,而 Transformer 保持一致性。为了利用这一点,我们对 GRU 组件使用了线性衰减加权。
这允许集成利用 RNN 卓越的“即时反射”来处理近期未来,同时依赖 Transformer 的“战略规划”来处理长期。
# 动态加权逻辑
decay_curve = np.linspace(W_GRU_INIT, 0.0, DECAY_STEPS)
w_gru_vec = np.concatenate([decay_curve, np.zeros(...)])
参考文献:
[1] Deihim, A., Alonso, E., & Apostolopoulou, D. (2023). STTRE: A Spatio-Temporal Transformer with Relative Embeddings for multivariate time series forecasting. Neural Networks, 168, 549-559. DOI: 10.1016/j.neunet.2023.09.039
感谢 Kaggle 团队和 NFL 举办这场具有挑战性的竞赛并提供高质量的遥测数据!我们在这次竞赛中学到了很多!感谢这篇论文的作者启发了我们的模型架构。感谢 GPT 复现架构,感谢 Gemini 完成所有代码繁重工作、头脑风暴、实验分析,最重要的是,提供情感支持。
我们已开源了完整的训练笔记本。