この記事が重要な理由
転倒検知モデルが94.3%の精度を記録しました。READMEにも書き、履歴書にも書きました。ところが正直に測り直すと69.1%でした。25ポイントの差です。
本記事は「データリークとは何か」を抽象的に解説するものではありません。実際にパイプラインの中でどのような形で潜んでいるのか、数字で示す回顧録です。動画、音声、ウェアラブルセンサー、時系列データ——逐次(sequential)データでモデルを評価しているなら、同じバグが今あなたのパイプラインにも存在する可能性はかなり高いです。
根拠資料: My Fall-Detection Model Scored 94%, and It Was Lying to Me
実務でMLモデルを扱うなら、本記事の3つの罠は必ず押さえておくべきです。特に日本の受託開発やPoCフェーズでは「とりあえず精度が出ていればOK」という空気が強く、こうしたリークが埋もれがちです。

罠1: ランダムsplitが生んだ幽霊精度
問題のコード
元のコードはこの一行でした。
from sklearn.model_selection import train_test_split
# フレーム単位でランダム分割 — これが問題の始まり
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
一見問題なさそうに見えます。しかしフレームは独立ではありません。 80ms間隔で撮影された2フレームは、同じ人物、同じ姿勢、同じ照明——特徴ベクトルもほぼ同一です。ランダムsplitをすると、ほぼ全てのテストフレームが学習セットに双子を持つことになります。
つまりこの評価は「見たことのない人物や部屋に汎化するか」を測っているのではなく、**「記憶力がどれだけ良いか」**を測っています。Random Forestは記憶力が良いので、高い数字が出ます。
解決策: GroupKFoldで動画単位に分割
from sklearn.model_selection import GroupKFold
import numpy as np
# 動画IDをグループとして、同じ動画が両側に入らないようにする
video_ids = np.array([f["video_id"] for f in features])
gkf = GroupKFold(n_splits=5)
for train_idx, test_idx in gkf.split(X, y, groups=video_ids):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# 学習・評価を実行
結果は69.1%。あるfoldでは31%まで落ちました。同じ特徴、同じモデル、splitを正直に変えただけです。
核心原則: サンプルを依存的にする何か(動画、患者、セッション、デバイス)でグループ化してください。ランダムsplitに変えたときスコアが跳ね上がるなら、高い方が嘘です。

罠2: フォルダ名から来たラベル
splitを監査した後、ラベルを監査し始めました。結果はさらに深刻でした。
学習ラベルがフォルダ名に由来していました。falls/フォルダにあれば、その動画の全フレームが「転倒」とラベリングされました。しかし実際の転倒は1〜2秒で、残りは歩く・座る・立つの日常動作です。
実際のアノテーションと照合した結果:
- 「転倒」ラベルのフレームのうち76%は普通に歩いている
- 実際の転倒は6%
- 残り18%はすでに床に倒れている状態
- 78秒の動画1本が陽性クラス全体の**44%**を占める(うち1.5秒だけが本物の転倒)
モデルは意図した問題ではなく、ラベルが説明するより簡単な問題を解いていました。データリークはラベルが間違っていることを教えてくれません。記憶力を測るだけですから。モデルは間違ったラベルも簡単に覚えます。
チェック: ラベルがフォルダ名、ファイルパス、その他の便宜的痕跡に由来するなら、splitと同じくらい疑うべきです。
罠3: フレーム精度はユーザー体験ではない
ラベルを再構築した後、フレーム単位の精度は0.97を超えました。しかしもうフレーム精度をヘッドラインとして信頼できませんでした。介護者はフレームを見ません。アラートが来るか来ないか、どちらかです。
そこで本番アラートロジックをそのままシミュレーションしました(フレームごとの確率に対する投票ウィンドウ)。結果:
- 126件中121件検出 (96.0%)
- 転倒開始からアラートまで中央値0.58秒
- 転倒なしの60本の動画のうち3〜7本で誤検知
全ての未検出の原因は同じでした: ポーズ推定器が転倒中に人物を見失う(家具による遮蔽、モーションブラー、低解像度)。ポーズ推定器が見えなければ、分類器の出番はありません。これはイベント単位で評価しなければ見えてきません。
ラベルポリシーが生むトレードオフ
| モデル | 転倒検出率 | 誤検知動画 | 意図的な横臥 |
|---|---|---|---|
| "person down"ラベル | 96.0% | 7/60 | アラート発生 |
| "fall motion"ラベル | 89.7% | 3/60 | 無音 |
どちらも間違いではありません。高齢者ケアでは床に倒れていればアラートが鳴るべきかもしれませんし、「転倒と横臥を区別できるか」が目標ならmotionモデルが勝ちます。挙動はアーキテクチャではなくラベルポリシーが決めます。 単一の精度指標はこのトレードオフを丸ごと隠します。

正直なMLシステムのためのチェックリスト
25ポイントを失った経験から得たものです。最初の項目だけでも元は取れます。
- サンプルを依存的にするものでsplitをグループ化する。 動画、患者、被験者、セッション、デバイス。ランダムsplitでスコアが跳ねるなら高い方が嘘。
- ラベルを実際のアノテーションと照合する。 ファイル構造を信じず、陽性クラスの中身を直接見る。
- ユーザーが体験する単位でイベントを測定する。 何件捕捉したか、どれだけ早くアラートが鳴ったか、何もないときにどれだけ頻繁に鳴ったか。
- 最も恐れる失敗モードに対するホールドアウト行動テストを維持する。 私の場合は「意図的な横臥 → アラートが鳴ってはいけない」でした。
- 修正を公開する。 信頼できる修正済みシステムは、信頼できない印象的なシステムより価値があります。
実務適用のアドバイス
日本の開発現場では、PoC段階で「精度94%」という数字だけで次フェーズに進んでしまうケースが少なくありません。ただしその数字がフレームのランダムsplitから出たものなら、実現場での性能は半分かもしれません。発注側にはイベント単位の指標(検出率、レイテンシ、誤検知率) を併記することを強く推奨します。これが実際にユーザーが体感する数字ですから。
この技術の限界
GroupKFoldも万能ではありません。グループ数が少ない場合(例: 動画10本)、fold間の分散が大きくなり信頼区間が広がります。またグループ化基準が複数(患者 + デバイス)なら多重グループsplitを検討する必要があり、GroupShuffleSplitやカスタムsplitterが必要です。そしてイベント単位評価はラベリングコストがフレーム単位よりはるかに大きくなります——予算に反映してください。
次のステップ学習方向
- データリーク検出:
sklearnのcheck_cvとcross_val_score(groups=...)の使い方を習得しましょう。 - イベントベース評価:
torchmetricsのdetection metricや、自前のIoUベースのイベントマッチングを実装してみましょう。 - ポーズ推定の信頼度: MediaPipeやYOLO-Poseのconfidence scoreを特徴量に組み込み、「ポーズ推定失敗」自体を検知するのも次のステップです。