返回列表

[33rd solution] Transformer+Augmentation Tricks

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

开始: 2025-09-25 结束: 2025-12-03 运动员表现 数据算法赛
[第 33 名方案] Transformer+ 增强技巧

[第 33 名方案] Transformer+ 增强技巧

副标题:数据增强技巧 + 论文启发的 Transformer 架构 + 自定义训练策略 + 集成学习

作者: xiaosuan (及团队成员)

发布日期: 2025-12-04

竞赛排名: 33

手性不变性与 STTRE:一种物理信息驱动的方法

1. 执行摘要

我们的解决方案通过优先考虑物理信息驱动的归纳偏置(physics-informed inductive biases)而非暴力模型扩展,取得了 0.519 的公共 LB 分数。我们成功复现并调整了 STTRE(带相对嵌入的时空 Transformer) 架构,该架构最初是为多变量时间序列预测提出的 [1],现将其应用于 NFL 球员追踪领域。

我们的关键贡献在于一种新颖的手性增强(Chiral Augmentation)策略,它迫使模型学习场地对称性,显著减少了过拟合。虽然我们的模型在基于动量的轨迹预测上表现出色,但误差分析揭示了其在处理“急转弯(sharp cuts)”方面的根本局限性,这表明需要引入比赛结果分类。

2. 数据工程:对称性与手性

我们改进的核心在于通过利用足球场的几何对称性来降低问题的维度。

2.1 统一比赛方向(空间减半)

我们将所有比赛标准化为从左到右移动。这有效地将预测空间减半。模型不再需要分别学习向左移动和向右移动的模式,从而可以专注于相对运动动力学。

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

注意:此标准化逻辑由我们团队在竞赛早期开源,并被社区采用。

2.2 随机 Y 轴翻转:强制手性不变性(关键创新)

美式足球场沿长轴具有手性对称性(镜像对称)。跑向左侧边线的路线在物理上等同于跑向右侧边线的镜像路线。 为了迫使模型学习这种不变性,我们在训练期间实施了动态的随机 Y 轴翻转增强。

实现逻辑: 在我们的 SpatioTemporalData 加载器中,每个 epoch 以 $p=0.5$ 的概率反转 Y 轴逻辑:

$$y' = W - y$$ $$v_y', a_y' = -v_y, -a_y$$ $$\theta' = -\theta \quad (\text{对于方向 } o \text{ 和 } dir)$$
# 在 __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

该策略防止了模型记忆场地特定侧的“热点”,并显著提高了对未见比赛的泛化能力。

2.3 上下文选择

鉴于 STTRE 架构的限制(设计用于多变量时间序列而非图结构)以及训练数据中球员数量/角色/位置的不一致性,我们必须对包含在上下文窗口中的球员进行选择。 我们采用硬编码的选择逻辑来挑选4 个最相关的实体MAX_SUBPLAY_SIZE = 4):

  1. 目标球员(始终包含)
  2. 传球手(四分卫)
  3. 最近的队友
  4. 最近的防守球员(特别优先考虑防守后卫 DBs)
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]

3. 模型架构:带门控融合的 STTRE

我们复现了论文 "STTRE: A Spatio-Temporal Transformer with Relative Embeddings" [1] 中的架构,并针对 NFL 场景进行了调整。

模型架构

我们没有使用标准的 Transformer,而是利用三分支编码器来解耦依赖关系:

  1. 时间分支:捕捉单个球员的动量(使用最后一帧池化)。
  2. 空间分支:捕捉球员之间的互动(使用时间上的平均池化)。
  3. 时空分支:捕捉复杂的演化互动。

关键架构特征:

  • 相对角色偏置(Relative Role Bias):我们用相对角色偏置替换了标准嵌入。注意力机制明确编码了查询和键之间的关系(例如,队友对队友防守者对目标),使模型能够“理解”覆盖方案。
  • Sigmoid 门控融合:受近期 LLM 研究(如 Qwen)的启发,我们使用Sigmoid 门控网络动态融合空间和时间分支。这充当非线性滤波器,允许模型在动量是主导因素时忽略嘈杂的空间互动。

4. 训练策略

我们使用辅助速度损失(Auxiliary Velocity Loss) alongside 标准位置损失进行训练。

$$L = L_{pos} + \lambda \cdot L_{vel}$$

标准 MSE 损失通常会导致预测“懒惰”,滞后于真实轨迹。通过惩罚一阶导数(速度)的误差,我们迫使模型尊重物理连续性和动量。

5. 后处理与 TTA

为了榨取每一分性能,我们采用了稳健的推理流程:

  • TTA(测试时增强):我们在原始序列及其Y 轴翻转版本上运行推理,并对结果取平均(翻转后还原)。
  • 噪声运行:我们执行多次前向传播,注入轻微的高斯噪声(std=0.02)以平滑模型方差。
  • 外推:对于序列末尾的任何缺失帧,我们使用基于最后已知速度矢量的物理线性外推,以防止“跳变”归零。

6. 误差分析与潜在改进

尽管我们的表现强劲,但可视化我们最差的预测揭示了一个明显的局限性:惯性偏差(Inertia Bias)。我们的模型过度依赖惯性/动量,预测的轨迹在前几帧保留了动量的方向

J 型转弯和切向

6.1“急转弯”问题

如下图所示,我们的模型(红色)难以处理急转弯或"J 型转弯”,经常沿着原始速度矢量过度预测轨迹。模型假设球员将继续其动量(惯性),未能预见突然的制动或方向变化。这是一种过拟合,因为我们的训练专注于最小化轨迹的均方误差,而忽略了困难样本。

6.2 错失的机会:比赛结果分类

为什么球员会做出这些急转弯?它们几乎总是对传球结果的反应。 竞赛描述明确暗示了这一点:

“深远传球是美国体育的皇冠明珠。当球在空中时,任何事情都可能发生,比如达阵、拦截或争抢接球。这些比赛结果的不确定性和重要性有助于让观众保持紧张。”

我们将追踪数据视为确定性序列,但轨迹本质上是有条件的

  • 如果接球成功:接球手保持速度/方向以得分。
  • 如果拦截:接球手停止、转身并擒抱(急转弯)。
  • 如果不完整:接球手减速或脱离路线。

未来改进: 我们错过了构建多任务学习框架的机会。更好的方法是:

  1. 分类结果:训练一个头来预测 $P(\text{接球})$, $P(\text{拦截})$, $P(\text{不完整})$。
  2. 条件轨迹:使用这些概率来调节轨迹解码器。

通过忽略这种结果不确定性,我们的模型本质上学习了接球和拦截之间的“平均”路径——这条路径在物理上并不存在于任何场景中。解决这个分类任务可能是突破 0.480 金牌壁垒的关键。

7. 集成策略:物理感知矢量聚合

坐标的暴力平均通常会导致物理上不可能的轨迹(例如,如果模型 A 预测左转,模型 B 预测右转,简单的平均值预测球员直行但速度减半)。 为了解决这个问题,我们实施了一个异构集成,结合了三种不同的架构,并通过极矢量策略进行聚合。

7.1 异构架构

多样性是我们集成成功的关键。我们结合了:

  1. STTRE (Transformer):我们的主模型(nv50)。擅长长期依赖和空间互动。
  2. ESG (GNN):演化子图图神经网络。它比全局注意力更好地捕捉局部空间互动(阻挡/擒抱)。
  3. Bi-GRU (RNN):双向 GRU 模型。虽然在长视野上较弱,但擅长捕捉即时动量和短期运动学。

7.2 极向量集成(关键创新)

标准集成平均 $x$ 和 $y$ 坐标:$\bar{x} = \sum w_i x_i$。这通常会导致能量损失——平均轨迹的速度低于任何单个模型。 我们实施了极向量平均。我们将预测分解为大小(速度)方向(单位矢量)并分别对它们进行平均。

$$\vec{v}_{final} = \left( \sum w_i \cdot \text{Speed}_i \right) \times \text{Norm}\left( \sum w_i \cdot \vec{u}_i \right)$$

这确保了如果两个模型在方向上不一致,集成本质上是对角度进行“投票”,但保持了球员的动能(速度)

实现:

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

7.3 动态时间衰减加权

我们观察到 RNN (GRU) 在长预测视野 ($t > 30$) 上迅速退化,而 Transformer 保持一致性。为了利用这一点,我们对 GRU 组件使用了线性衰减加权

  • 第 0 帧:GRU 权重 = 0.15
  • 第 30 帧:GRU 权重 = 0.00
  • 第 55 帧:GRU 权重 = 0.00

这允许集成利用 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 完成所有代码繁重工作、头脑风暴、实验分析,最重要的是,提供情感支持

我们已开源了完整的训练笔记本。

同比赛其他方案