왜 이 글이 중요한가
낙상 감지 모델이 94.3% 정확도를 찍었습니다. README에도 쓰고 이력서에도 썼죠. 그런데 정직하게 다시 재보니 69.1%였습니다. 25%p 차이입니다.
이 글은 "데이터 누수(Data Leakage)란 무엇인가"를 추상적으로 설명하는 글이 아닙니다. 실제로 파이프라인 안에서 어떤 형태로 숨어 있는지, 숫자로 보여주는 회고입니다. 비디오, 오디오, 웨어러블 센서, 시계열 데이터 — 순차적(sequential) 데이터로 모델을 평가하고 있다면 같은 버그가 지금 당신 파이프라인에도 있을 확률이 꽤 높습니다.
근거자료: My Fall-Detection Model Scored 94%, and It Was Lying to Me
실무에서 ML 모델을 다룬다면 이 글에서 다루는 세 가지 함정은 반드시 짚고 넘어가야 합니다. 특히 국내 SI/스타트업 환경에서는 "일단 정확도 잘 나오면 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 간격으로 찍힌 두 프레임은 같은 사람, 같은 자세, 같은 조명 — 특징 벡터도 거의 동일합니다. 랜덤 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초짜리 비디오 하나가 양성 클래스 전체의 **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%p를 날린 경험에서 건진 것들입니다. 첫 번째 항목만 가져가도 본전은 뽑습니다.
- 샘플을 의존적으로 만드는 것으로 split을 그룹핑하세요. 비디오, 환자, 피험자, 세션, 디바이스. 랜덤 split으로 바꿨을 때 점수가 뛰면 높은 쪽이 거짓말입니다.
- 라벨을 실제 어노테이션과 대조하세요. 파일 구조를 믿지 말고, 양성 클래스 안에 뭐가 들었는지 직접 보세요.
- 사용자가 경험하는 단위로 이벤트를 측정하세요. 몇 건 잡았는지, 얼마나 빨리 알람이 갔는지, 아무 일 없을 때 얼마나 자주 울렸는지.
- 가장 두려운 실패 모드에 대한 홀드아웃 행동 테스트를 유지하세요. 제 경우는 "의도적으로 눕기 → 알람 울리면 안 됨"이었습니다.
- 수정 사항을 공개하세요. 믿을 수 있는 수정된 시스템이, 믿을 수 없는 인상적인 시스템보다 가치 있습니다.
실무 적용 조언
국내 환경에서 특히 주의할 부분이 있어요. 의료/케어 AI를 SI 프로젝트로 납품할 때 "정확도 94%"라는 숫자 하나로 계약이 진행되는 경우가 많은데, 그 숫자가 프레임 랜덤 split에서 나왔다면 실제 현장 성능은 절반일 수 있습니다. 발주처에 이벤트 단위 지표(감지율, 지연시간, 오탐률) 를 함께 제시하는 걸 권합니다. 이게 실제로 사용자가 체감하는 숫자니까요.
이 기술의 한계
GroupKFold도 만능은 아닙니다. 그룹 수가 적으면(예: 비디오 10개) fold 간 분산이 커져서 신뢰구간이 넓어집니다. 또 그룹핑 기준이 여러 개(환자 + 디바이스)라면 다중 그룹 split을 고려해야 하고, 이때는 GroupShuffleSplit이나 커스텀 splitter가 필요합니다. 그리고 이벤트 단위 평가는 라벨링 비용이 프레임 단위보다 훨씬 큽니다 — 예산에 반영하세요.
다음 단계 학습 방향
- 데이터 누수 탐지:
sklearn의check_cv와cross_val_score(groups=...)사용법을 익히세요. - 이벤트 기반 평가:
torchmetrics의 detection metric이나 자체 IoU 기반 이벤트 매칭을 구현해보세요. - 포즈 추정 신뢰도: MediaPipe나 YOLO-Pose의 confidence score를 특징으로 넣어 "포즈 추정 실패" 자체를 감지하는 것도 다음 단계입니다.