返回列表

17th Place Solution

665. CMI - Detect Behavior with Sensor Data | cmi-detect-behavior-with-sensor-data

开始: 2025-05-30 结束: 2025-09-02 健康管理与公共卫生 数据算法赛
第 17 名解决方案

第 17 名解决方案

作者: Anil Ozturk & Optimo
发布日期: 2025-09-03
竞赛排名: 17th Place

热烈祝贺所有获奖者,感谢组织者!不幸的是,我们未能利用许多团队发现的针对特定主体(subject-wise)的提升技巧。但很高兴看到我们获得了一个鲁棒(robust)的集成模型!我们的解决方案是两个 Pipeline 的平等融合:


1. Anil 的 Pipeline

1.1. 建模

Anil 模型架构
  • 模式 1:仅 IMU - 当 THM/ToF 传感器不可用时,使用加速度计和旋转特征
  • 模式 2:完整传感器套件 - 利用所有三种传感器模态(IMU + THM + ToF)

1.2. 特征

仅加速度计特征

  • 原始加速度: acc_x, acc_y, acc_z
  • 加加速度(加速度导数): acc_jerk_x, acc_jerk_y, acc_jerk_z

仅旋转特征

  • 6D 旋转矩阵差异: rot_6d_diff_col1_x/y/z, rot_6d_diff_col2_x/y/z
  • 绝对 6D 旋转差异: abs_rot_6d_diff_col1_x/y/z, abs_rot_6d_diff_col2_x/y/z
  • 帧间相对旋转: rot_rel_6d_col1_x/y/z, rot_rel_6d_col2_x/y/z, abs_rot_rel_6d_col1_x/y/z, abs_rot_rel_6d_col2_x/y/z

ACC-ROT 融合特征

  • 重力估计: gravity_est_x/y/z, gravity_est_jerk_x/y/z
  • 线性加速度: linear_acc_x/y/z, abs_linear_acc_x/y/z
  • 加加速度特征: jerk_x/y/z
  • 主体对齐特征: acc_tangential/lateral/vertical, jerk_tangential/lateral/vertical
  • 倾斜计算: tilt, tilt_diff

1.3. 架构

Anil 模型架构层

NaN 处理层

  • IMU/THM:用于缺失值的可学习嵌入向量
  • ToF:广播到所有缺失像素的单个可调参数

IMU 分支

  • 两个带有 SE 的残差 CNN 块
  • 卷积核大小:3 和 5
  • 输出:每个时间步 128 个特征
  • 特定模式 Dropout:0.3 (模式 1), 0.1 (模式 2)

THM 分支

  • 两个带有 SE 的残差 CNN 块
  • 输出:每个时间步 64 个特征

ToF 分支

  • 空间注意力
  • 深度可分离卷积
  • 残差连接
  • 多尺度特征提取
  • 输入:(5, 8, 8) 每时间步 → 输出:128 个特征

三模态注意力

  • 跨模态注意力 介于 IMU, ToF 和 THM 之间
  • 双向
  • 残差连接

BiLSTM + 注意力

  • 128 隐藏单元,双向(总共 256)
  • 用于序列级任务的注意力池化
  • Dropout: 0.4

多任务辅助学习

  • 手势 (主要): 20 类
  • 方向: 4 类
  • 惯用手: 2 类
  • 行为 (每时间步): 4 类

2. Optimo 的 Pipeline

我将分享我的最终 Pipeline 看起来是什么样,并尝试解释我是如何做到的。最后我做了很多消融实验,表明一个相当简单的解决方案也可以具有竞争力。

2.1 数据标准化

这次竞赛的数据集相当小,因此似乎重要的是以更标准化的方式表示每个序列。

  • ToF 对齐:由于我使用 2D CNN 进行 ToF 特征提取(见后续架构),感觉重要的是让传感器间的像素对应。因此按照设备方案,我将 ToF 传感器 3 和 5 旋转 90°。
  • 左撇子对齐:我做了完全相同的事情,正如 @tatamikenn这里 描述的那样,但他的绘图能力远比我好,所以我就不在这里竞争了!变换包括改变正确 acc 轴和 rot 轴的符号,以及切换传感器 3 和 5(thm 和 tof)以及翻转 tof 图像。想法是,当右撇子将手带到头部/身体时,加速度是从右到左,而左撇子则相反。
  • 上下颠倒对齐:当我查看我的 OOF 分数时,很明显 "SUBJ_045235", "SUBJ_019262" 出了些问题。经过大量数据可视化,我得出结论,他们佩戴设备是上下颠倒的。当应用相应的变换(切换传感器,否定某些 acc 和 rot 轴)后,这两个主体的分数恢复正常。为了确保我的变换是正确的,我训练了一个“上下颠倒检测器”:随机对所有序列应用变换并预测是否使用了它。模型在这方面非常出色,并正确指出这两个主体确实是上下颠倒的。问题是你没有像“惯用手”那样的信息来知道某人佩戴设备是否正确。所以我尝试了一个提交,首先使用检测器,如果需要则在预测前应用变换:得到了相似的分数(那时还是两位数 LB 时代)。我想“测试集是干净的,每个人都以正确的方式佩戴设备”,所以我只是翻转了这两个主体,然后继续改进我的 CV。大错误:一些私有主体确实佩戴设备上下颠倒,检测器效果很好(LB: 863 - PLB: 857)。我应该以正确的方式去做:不要在自行更改数据后允许我的 CV 提高,我的解决方案应该包括检测器以提高 CV,我应该相信它!
  • 序列长度:所有序列向右裁剪/填充到 96(64 或 128 也得到相似结果)

2.2 数据增强

我做了很多即时增强:

  • 感知对齐的 MIXUP:在对齐手势开始后随机平均序列,这样平均更有意义
  • 随机旋转:观看 Youtube 上女士表演手势的视频时,你可以看到设备并不总是佩戴得非常完美。所以我向四元数添加随机小旋转 (+-30°) 绕 Z 轴。
  • 随机拉伸
  • 随机忽略序列开头的一些点

2.3 损失函数

我觉得使用人口统计数据不太舒服,因为主体太少,对训练没有意义。尽管如此,我试图通过强迫模型预测尽可能多的事物(不同的头)来 incorporate 尽可能多的知识:

  • 18 种手势
  • 二元目标
  • 方向
  • 位置:手势自定义混合为 4 个位置(腿 - 脸 - 颈 - 前)
  • 动作:手势自定义混合为 5 个动作(拉头发 - 捏 - 抓挠 - 写字 - 其他)
  • 是否是儿童

位置、动作和儿童可能是无用的。

2.4 特征工程

我做了很少的特征工程:

  • 加速度:原始加速度,时间差分 1, acc_mal_jerk, 序列中的归一化位置 (0 到 1)
  • 四元数:原始四元数,旋转角和角速度 (x, y, z) 及距离,角速度范数和 10 窗口滚动均值
  • thm:原始 thm
  • rof:原始 tof

2.5 架构

我最终得到了一个相对简单的架构,我只使用了一个单一模型来学习忽略缺失信息。我没有为仅 IMU 和完整传感器设置特定模型:我只是在训练期间随机掩蔽 thm 和 tof 特征。

首先在时间级别编码信息

  • 独立的 conv1D 用于 acc, quats 和 thm 进行时间级别编码(卷积核 3-5-7),每个具有 N_OUT 特征
  • 2D CNN 具有 5 个通道,将 5x8x8 图像转换为 N_OUT(不共享时间序列信息)

然后每个流通过一个 gating 单元,决定每个传感器流的重要性:

class GatingUnit_v2(nn.Module):
    def __init__(self, input_dim, hidden_dim=64):
        super().__init__()

        self.pool = nn.AdaptiveAvgPool1d(1)
        self.gater = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1),
            nn.Sigmoid()
        )
        
    def forward(self, x):
        # x: (batch, feat_dim, seq_len)
        pooled_x = self.pool(x).squeeze(-1)  # (batch, feat_dim)
        gate = self.gater(pooled_x).unsqueeze(-1)  # (batch, 1, 1)
        gated_x = gate * x
        return gated_x, gate

然后我通过求和组合所有特征,这迫使模型为每个数据流共享相同的表示:

numerator = accel_features + orient_features + temp_features + tof_features
denominator = gate_accel + gate_orient + gate_temp + gate_tof

combined_feats = numerator / (denominator + 1e-6)

然后我有一个注意力池化机制,从时间序列转到序列表示,然后是基本的 MLP 头。

2.6 CV 分数

遵循上述 Pipeline,我可以达到平均 CV 85.8(仅 IMU 82.9 - 全部 88.7),并正确翻转了上下颠倒的主体。
集成几个具有不同训练参数的模型可以达到 86.4 CV。

我的 LB 从未超过 86.6 使用我自己的模型,我从未能够过拟合公共 LB,不知道为什么。

简单地合并一个 86.5 的 Anil 提交和我自己的一个 86.5 单模型产生了 LB 87.6,所以我想这里方法的多样性很重要!

2.7 一些错误

过去一个月我未能改进我的 Pipeline 的主要原因是因为固执和 CV 蒸馏。如果使用模型的 OOF 从头训练模型,遵循相同的 CV 方案,我可以轻松达到 CV 0.88(LB 显著下降)。我花了将近一个月才接受这只是纯粹的泄漏。如果我随时间衰减 OOF 标签的重要性,CV 会下降但 LB 会上升。然后我像一个疯子一样尝试了 50 种 shades 的泄漏来降低我的 CV 并提高我的 LB。

我应该:

  • 接受使用 OOF 进行蒸馏会产生强大的泄漏
  • 计算我的 CV 时不允许任意将两个主体上下颠倒(我可能会尝试将我的上下颠倒检测器作为后期提交,但我怀疑 PLB 会有巨大改进)
  • 可能利用 LB 来利用所有可用信息,就像一个真正的 Kaggle 冠军应该做的那样

无论如何!感谢阅读至此,再次祝贺获奖者,祝 Kaggle 愉快!

同比赛其他方案