Kaggle Child Mind Institute振り返り

Kaggle Child Mind Institute

手首の加速度計のデータから子供の入眠と覚醒を予測するKaggleコンペ “Child Mind Institute – Detect Sleep States“が2023年12月6日まで行われていました。このコンペで使用されていたデータの特徴、深層学習や決定木モデルなどの手法の概要、および上位入賞者の解法を紹介する中で、このコンペで得られた知見を振り返ることが本記事の目的です。

コンペ概要

本コンペのタスクは手首の加速度計のデータから子供の入眠と覚醒を予測することです。睡眠は健康にとって極めて重要であり、発育から認知機能まで、あらゆることに影響します。しかし、入眠・覚醒のタイミングをデータとして取得することが容易でないことから、睡眠に関する研究は困難であると言われています。そのため、データサイエンスによって、手首に装着した加速度計のデータから睡眠をより適切に分析できるようになれば、睡眠に関する大規模な研究を実施しやすくなると期待されます。

データセットは、277人のユーザーについて加速度計の1〜3ヶ月の5秒間隔の記録から構成され、2つのイベントタイプ、すなわち睡眠の開始であるonsetと、睡眠の終了であるwakeupのタイミングが注釈されていました。また、加速度計のデータから計算される値であるz-angle、ENMOの時系列データが提供されていました。

提出ファイルにはonsetとwakeupのタイミングに加えて、その予測の信頼値を表すscoreを含む必要があり、検出されたイベントの平均精度、タイムスタンプの誤差許容閾値の平均値、イベントクラスの平均値によって評価されました。

データセットについて

コンペ未参加の方・コンペ概要を忘れてしまった方の理解を助ける目的で、解法を説明する前にデータセットの中身を少し見ていこうと思います。不要な方は「上位解法の振り返り」の項までスキップしていただいて構いません。

まずは生の学習データをロードして表示してみます。

import numpy as np
import pandas as pd

#rawデータの読み込み
series = pd.read_parquet("/kaggle-child-mind-institute-detect-sleep-states/data/train_series.parquet")
events = pd.read_csv("/kaggle-child-mind-institute-detect-sleep-states/data/train_events.csv")

学習データはtrain_series.parquet (series)とtrain_events.csv (events)で構成されており、seriesは各ユーザーの加速度計の時系列データ、eventsは各ユーザごとのonsetまたはwakeupの発生タイミングをまとめたデータになっています。

series
events

各ユーザーごとの時系列データが一つの表データとしてまとめられているので、かなり大きいデータとなっていることがわかりますね。全てを可視化するとものすごい量になってしまうので、一人のユーザーの8月14~15日の期間のデータに絞って見ていきたいと思います。

#データの絞り込み
this_series = series[series['series_id']=='038441c925bb']
thie_events = events[events['series_id']=='038441c925bb']
#timestampを日付へ変換
this_series['timestamp'] = pd.to_datetime(this_series['timestamp'], format="%Y-%m-%dT%H:%M:%S%z")
this_series['month'] = this_series['timestamp'].dt.month
this_series['day'] = this_series['timestamp'].dt.day
this_series['hour'] = this_series['timestamp'].dt.hour
this_series = this_series[(this_series['month']==8) & (this_series['day'] <= 15)]

seriesをeventとmergeし、睡眠状態を1、覚醒状態を0として、正規化したz-angleとENMOと合わせて表示してみます。

this_series_with_event = this_series.merge(thie_events[['step', 'event']], how='left', on='step')
this_series_with_event['event'].replace({"onset":1, "wakeup":0})
this_series_with_event['event'] = this_series_with_event['event'].fillna(-1)

states = []
state = 0 #覚醒状態: 0
#覚醒状態を0、睡眠状態を1として値割り当て
for value in this_series_with_event['event'].values:
    if (value == -1) and (state == 0):
        states.append(0)
        state = 0 #覚醒状態
    elif (value == -1) and (state == 1):
        states.append(1)
        state = 1 #睡眠状態
    else:
        states.append(value)
        state = value
states = np.array(states)

this_series_with_event['event'] = states
#enmo、anglezの正規化
this_series_with_event['normed_enmo'] = (this_series_with_event['enmo'] - this_series_with_event['enmo'].min())/(this_series_with_event['enmo'].max() - this_series_with_event['enmo'].min())
this_series_with_event['normed_anglez'] = (this_series_with_event['anglez'] - this_series_with_event['anglez'].min())/(this_series_with_event['anglez'].max() - this_series_with_event['anglez'].min())
this_series_with_event.plot(x='step', y=['normed_enmo', 'normed_anglez', 'event'], figsize=(20, 5))

上の図において、青色の凡例がENMO、オレンジ色の凡例がz-angle、緑色の凡例がeventを表しています。この図を見てみると、このユーザーのこの時間のデータについては、睡眠状態と覚醒状態では目に見えてENMOやz-angleの値の特徴に違いがあることがわかりますね。また、数ヶ月のうちたった1日と少し分のデータであっても、20000step超とかなり長いこともわかります。コンペ参加者の多くは数ヵ月分のデータを4000~10000step程度に区切って学習や予測を行っていましたが、それでもデータ長が長く計算時間がかかってしまうこと、1step単位での正確な予測は難しいことなどから、データを1/2や1/4などにダウンサンプリングしていたようでした。それでは、上位参加者たちがこのような時系列データを用いてどのように予測モデルを構築していたのか、具体的な解法を見ていきたいと思います。

上位解法の振り返り

アプローチ

上位入賞者の多くはニューラルネットワークを用いたモデルを構築していました。具体的には、UNETやGRU、またはそれらを組み合わせたモデルを使用していたようです (引用: 1st2nd3rd4th5th6th7th8th9th10th)。一方、LightGBMなどの決定木モデルを解法の全体もしくは一部に組み込んでいた参加者も複数見受けられました (引用: 2nd3rd4th5th)。また、データの前処理や後処理にも各チームごとの工夫が見られました。

前処理

ほとんどの参加者が、時刻情報 (hour, min) 、ENMOやangle-z、その差分や集約特徴量などを特徴量として使用していました。また、ユーザーごとに数ヶ月分存在するデータをおおよそ1日ごとに分割し、さらにダウンサンプリングを行なっていました。

1st Solutionでは、SE moduleを用いてスケーリングを行なっていたようでした。

class SEScale(nn.Module):
   def __init__(self, ch: int, r: int) -> None:
       super().__init__()
       self.fc1 = nn.Linear(ch, r)
       self.fc2 = nn.Linear(r, ch)

   def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
       h = self.fc1(x)
       h = F.relu(h)
       h = self.fc2(h).sigmoid()
       return h * x

2nd Solutionでは、主にTransformerなどで利用されるposition encodingを用いて、daily stepを特徴量として使用していました (引用: 2nd)。

モデル

多くの参加者がニューラルネットモデルを採用していたという点は共通していましたが、そのモデル構造は様々でした。しかしその中でも、Segmentation Taskにおいてよく使用されるUnet構造は高い人気を誇っており、多くの上位入賞者が組み込んでいました (引用: 1st2nd3rd4th6th8th10th)。同じくGRUやLSTMなどの回帰型ネットワークも人気があり、これらはUnet構造の内部や最終層に組み込む解法が主流でした (引用: 1st3rd4th6th10th)

1st Solutionが利用していたUnet + GRUモデル (引用: CMSS GRUNET TRAIN)

また、音声合成モデルとして知られるWaveNetを使用しているチーム (引用: 4th7th9th)や、近年あらゆるタスクで高い性能を叩き出しているTransformerを採用しているチームもありました (引用: 4th9th)。

WaveNetのモデル構造 (引用: WaveNet)

テーブルコンペで高い人気を誇る決定木モデルである、LightGBMを使用しているチームもありました。2nd Solutionでは、1DCNNモデルが予測を出力した後の後処理として2種類のLightGBMモデルを採用していました。具体的には、1つ目のLightGBMモデルでは1DCNNの出力したスコアの振り直しを行い、2つ目のモデルではイベントが検出されたstepの周辺ステップにもイベントを割り振っていました。3rd Solution4th Solutionでは、GRU、UNETのNNモデルに加えてLightGBMをアンサンブルとして用いていました。5th Solutionでは、ルールベースに基づいて予測値の候補を生成した後、2つのLightGBMモデルによって予測値の補正とスコアリングを行なっていました。

後処理

適切な後処理もスコアを高めるのに貢献したようです。1st Solutionは特に独創的かつ効果的な後処理を採用していました。具体的には、評価計算の許容範囲 (予測がk stepずれていても正解と判断して誤差を計算し、k = 12、36、…、360として誤差の平均をとる) に注目して予測されたステップのスコアを再計算するという手法をとっていました。4th Solution5th Solutionでは、一定周期のstepの予測だけを残し、それと近すぎるstepの予測を除去して残したstepのスコアを増やすことを繰り返して予測を生成していました。

まとめ

今回のコンペではデータの前処理、モデル選定、後処理など工夫できるポイントが多くあり、アプローチのバリエーションが豊富であったことが印象的でした。既存のコンペや論文などを効率的にサーベイし、このコンペに有効な解法を見出す、リサーチ力及び発想力が問われるコンペであったように思います。

参考文献