返回列表

8th Place Solution for the Child Mind Institute - Detect Sleep States Competition

586. Child Mind Institute - Detect Sleep States | child-mind-institute-detect-sleep-states

开始: 2023-09-05 结束: 2023-12-05 健康管理与公共卫生 数据算法赛

儿童心智研究所 - 检测睡眠状态竞赛第8名解决方案

感谢CMI和Kaggle主办这场有趣的竞赛,也感谢其他选手的辛勤付出,共同提升了排行榜成绩,改进了事件检测方法的质量。我获得了我的第一枚奖牌 🦾,并从中学习了很多。

竞赛背景

竞赛概述:https://www.kaggle.com/competitions/child-mind-institute-detect-sleep-states/overview
数据:https://www.kaggle.com/competitions/child-mind-institute-detect-sleep-states/data

方案概览

我的解决方案的核心思想是尽可能减少预处理/后处理,尝试以端到端的方式检测睡眠/清醒事件。这是因为我观察到不同的后处理方法在不同折上的mAP表现不一致(有些提升,有些下降),这可能是由于标签的不一致性造成的。

我的流水线包含两种模型,一种用于检测事件的准确位置(称为“Regressor”),另一种用于检测事件在一天内发生的概率密度(称为“DensityNet”)。

详细方案

Regressor

这是一个简单的一维U-Net,仅使用局部信息和anglez特征来检测事件发生的位置。这一思路受Faster-RCNN以及后续如YOLO等边界框RPN回归方法的启发。由于我们处理的是1D数据且事件之间间隔明显,因此每步只需预测两个值(入睡、醒来)。

训练

训练时选择一个固定的超参数“width”。数据加载器会随机打乱并加载训练series_id的入睡和醒来事件,以及时间序列区间 $ [\text{事件} - \text{width}, \text{事件} + \text{width}] $。模型优化的目标如下:

  • 每个框对应一个时间步(5秒)
  • 彩色框表示事件的真实位置
  • 即模型预测当前步与事件位置的相对位置

由于数据存在噪声,我发现Huber损失效果最佳,类似于Fast-RCNN中的平滑L1回归损失,因此对异常值不敏感。

推理

推理时,Regressor网络将在整个时间序列上运行,预测每步的相对(入睡、醒来)值,从而得到感兴趣的位置。将以感兴趣位置为中心、标准差为12的高斯核进行累加。

  • 第一行:模型预测的相对位置
  • 第二行:时间步
  • 蓝色:当前累加迭代
  • 绿色:当前迭代的感兴趣位置
  • 图表:累加得分

将有两个累加得分,一个用于入睡,一个用于醒来。得分的峰值即为入睡和醒来事件的可能位置。我使用了最简单的峰值检测方法:

locations = np.argwhere((score[1:-1] > score[:-2]) & (score[1:-1] > score[2:])).flatten() + 1

额外的NMS后处理步骤,确保预测位置之间至少相隔6分钟。

模型结构

Regressor是一个简单的一维U-Net,输入通道为1,输出通道为2,采用一维ResNet作为骨干网络。隐藏通道数依次为2、2、4、8、16、32、32,每个池化操作之间包含2个ResNet块。我没有使用SE模块,并采用BatchNorm1D而非InstanceNorm/GroupNorm/LayerNorm,以使网络对全局变化不敏感。

集成

我在整个训练数据集上训练了3个模型,宽度分别为120、180、240(即10、15、20分钟)。在得到位置之前,将3个模型的得分进行平均。

局部位置预测质量

为了验证该模型的性能,我计算了每个事件前后120步(10分钟)内得分最大值的累积分布函数(CDF)(5折交叉验证),结果如下:

  • x轴:百分位数
  • y轴:距离(步数)
  • 使用Huber损失的模型表现最佳

约85%的argmax预测落在实际事件3分钟以内。由于在事件周围的240步区间内可能存在多个峰值,我们预期最小距离的误差会更小:

  • 相同的x、y轴
  • 事件与所有预测峰值之间的最小距离
  • 使用Huber损失的模型
  • ["Huber", "Gaussian", "Laplace"]是用于重构得分的核形状

DensityNet

另一个网络(称为“DensityNet”)用于为每个事件分配得分。该网络必须判断哪一段清醒和睡眠阶段最有可能(选择一天中最长的一段),并遵守规定的30分钟长度和中断规则。因此需要更长的上下文信息。

为此,我在一维U-Net的最深层特征层中加入了Transformer编码器模块,以建模全局信息。由于每天最多只有一个事件,DensityNet将预测该时间窗口内入睡/醒来事件的概率密度。我使用了对称的ALiBi编码,使Transformer编码器块具有平移等变性。

训练

我发现,在比推理更大的区间上训练模型有助于包含更多全局上下文。然而,我的模型并不预测每步的入睡/醒来概率。受信号处理的启发,未知的入睡/醒来信号是感兴趣固定区间内的随机变量。因此,DensityNet在2天区间内与真实的入睡+醒来位置进行拟合。

这相当于具有12 * 60 * 24 * 2 = 34560个类别的交叉熵损失。由于标签存在噪声(并被裁剪到最近的分钟),目标概率分布采用拉普拉斯分布进行平滑。伪代码如下:

# (N, T, 2),     N = batch_size,    T = 34560
target_distribution = get_distribution(interval_min, interval_min + 34560, series_onset_lbls, series_wakeup_lbls)
pred_logits = model(time_series) # (N, T, 2)
loss = cross_entropy_loss(pred_logits.permute(0, 2, 1), target_distribution) # torch cross entropy acts on 2nd axis

由于手表摘下时数据会填充虚假区间,DensityNet还预测两个概率——整个区间内是否存在入睡或醒来事件。

  • 训练了三种模型
    • 仅使用anglez输入
    • 仅使用enmo输入
    • 使用anglez + 时间输入(时间以均匀分布[-30分钟, 30分钟]随机偏移以避免过拟合)
  • 随机翻转
  • 随机弹性形变

推理

该模型现在仅在2天区间的中心1天子区间上进行推理。为了给Regressor预测的事件分配得分,我们使用条件概率

$$p(t|\text{实际事件出现在Regressor预测结果中}) = \frac{p(t)}{\sum_{t' \in \text{Regressor预测结果}} p(t')}$$

其中p(t)是DensityNet预测的概率密度。考虑到虚假区间,最终得分为

$$\text{score} = q * p(t|\text{实际事件出现在Regressor预测结果中})$$

其中q是区间包含某个事件(入睡或醒来)的预测概率。

整个序列中事件的得分通过在整个序列上滑动预测窗口计算,并在每个预测窗口重叠时进行平均。条件概率可以通过将softmax限制在Regressor建议的位置的logits上来轻松计算。

后处理

预测平移

与其他团队类似,我将事件时间移至xx:xx:15、xx:xx:45,以提高mAP。注意,xx:xx:30也不理想,因为mAP评分中存在7.5分钟的窗口。

数据增强

我很惊讶没有太多顶尖方案使用这个技巧来提高mAP。显然,事件无法被精确标记到3分钟/1分钟的精度,稍微移动鼠标就会使标签偏移1分钟。以下是本地5折交叉验证的mAP分数(已排除不良序列和缺失标签的部分):


(未使用增强)


(使用增强,mAP提升约0.002)

  • 当容差大于3分钟时,性能相似
  • 在容差为1分钟和3分钟时有所提升

使用矩阵轮廓去除虚假数据

矩阵轮廓(Matrix profile)可以检测完全重复的模式。我将其作为额外的后处理步骤,以剔除位于虚假数据中的预测。

可能的改进方向

将矩阵轮廓结果作为输入

一些顶尖方案添加了二进制特征(1, 0)来指示该步是否位于虚假数据内,或仅在干净数据区间上训练。这相比我目前让模型预测窗口内是否存在事件的方法,应能提升模型性能。

利用每15分钟标注误差分布

许多顶尖方案利用了这种模式(每15分钟事件分布不均)。这表明将该模式作为模型输入或后处理步骤可能会提升性能。

同比赛其他方案