dataforge.ai.kr
‹ 가이드 홈
개념 아티클 · 데이터 분할

AUC 0.987은 축하가 아니라 경고다 — 데이터 누수 이야기

기대보다 나쁜 모델은 사람을 속이지 않습니다. 사람을 속이는 것은 언제나 기대보다 좋은 모델입니다.

키워드: 데이터 누수 · 타깃 누수 · 데이터 분할 · Pipeline · AUC

축하합니다, AUC 0.987입니다

품질팀이 6개월 치 최종검사 기록 3,513행을 넘겨줍니다. 로트 700개 분량이고 불량률은 29.0%입니다. "불량을 미리 예측할 수 있을까요?"라는 요청에, 배운 대로 합니다. 식별자(로트번호·검사일)를 빼고, 나머지 컬럼을 전부 특성으로 넣고, 8:2로 나누고, 랜덤포레스트를 돌립니다.

결과는 AUC 0.987, 정확도 96.7%. 첫 시도에 이런 숫자가 나왔습니다. 보고서를 쓰고 싶어집니다.

그런데 잠깐 멈춰야 합니다. AUC 0.987은 "불량인 부품과 정상인 부품을 무작위로 하나씩 뽑았을 때 98.7%의 확률로 불량 쪽에 더 높은 점수를 준다"는 뜻입니다. 사실상 완벽합니다. 그리고 제조 현장의 불량 예측에서 이런 숫자는 나오지 않습니다. 온도와 진동 몇 개로 불량을 99% 맞힐 수 있었다면 애초에 머신러닝이 필요 없었을 겁니다. 센서에 임계값만 걸면 됐겠지요.

너무 좋은 숫자는 축하할 일이 아니라 조사할 일입니다.

시험 문제를 미리 본 학생

이런 상황을 이해하는 가장 좋은 비유가 있습니다. 인턴이 자랑합니다. "이탈 예측 모델 만들었는데요, 정확도가 100%예요!" 팀장이 딱 한 마디 묻습니다. "그거 어떤 데이터로 채점한 거예요?" — "학습시킨 데이터로요."

모델은 암기할 수 있습니다. 학습에 쓴 데이터로 채점하면 외운 것을 확인할 뿐입니다. 시험 문제를 미리 보고 답을 외운 학생의 100점이 실력을 전혀 말해주지 않는 것과 같습니다. 그래서 데이터의 일부를 일부러 숨겨두고(테스트 세트), 숨긴 데이터로만 채점합니다.

데이터 누수(data leakage)는 이 원칙이 교묘하게 깨지는 것입니다 — 모델이 실전에서는 쓸 수 없는 정보를 학습 중에 훔쳐보는 것. 모의고사 문제지를 미리 본 학생은 모의고사에서 100점을 받고, 진짜 시험에서 무너집니다. 무너진 게 아니라 처음부터 그 실력이었던 겁니다.

누수된 모델의 무서운 점은 에러가 나지 않는다는 것입니다. 코드는 아름답게 돌아가고, 숫자는 훌륭하고, 보고서는 통과합니다. 현장에 배포되고 나서야 정체가 드러납니다. "검증에서 99% 나왔는데 실제로는 60%밖에 안 맞습니다" — 머신러닝 프로젝트가 실패하는 가장 흔한 이유 중 하나이고, 알고리즘을 바꿔도 하이퍼파라미터를 튜닝해도 고쳐지지 않습니다. 문제가 모델이 아니라 데이터를 다루는 방식에 있기 때문입니다.

범인 찾기 — 시간의 화살

AUC 0.987이 어디서 왔는지 뜯어봅니다. 특성 중요도를 뽑아보면 후속공정통보(0.455)재작업시간(0.331) 두 컬럼이 성능의 78.5%를 만들고 있습니다. 온도도 진동도 압력도 거의 기여하지 않습니다.

데이터 사전을 보면 정체가 드러납니다. '재작업시간'은 불량 판정된 부품을 손보는 데 걸린 시간이고, '후속공정통보'는 다음 공정에 "불량 나왔다"고 알린 기록입니다. 실제로 불량일 때 재작업시간 평균은 18.0분, 정상일 때는 0.2분. 재작업시간이 0보다 큰 행의 불량률은 81.0%입니다. 이 컬럼 하나로 거의 답이 나옵니다. 당연합니다 — 재작업은 불량이라서 하는 것이니까요. 원인과 결과를 거꾸로 넣은 겁니다.

타깃 누수를 잡아내는 질문은 딱 하나입니다. "이 값은 내가 예측하려는 시점에 이미 존재하는가?" 우리가 예측하려는 시점은 부품이 막 만들어진 순간입니다. 그 순간 온도·진동·압력은 존재하지만, 재작업시간은 불량 판정이 난 뒤에야 생깁니다. 이런 '사후 컬럼'은 어느 회사 데이터에나 있습니다. 이탈 예측인데 해지사유코드가 있고, 대출 부도 예측인데 추심담당자가 있는 식입니다. 업무 시스템은 원래 일이 끝난 뒤 결과를 기록하려고 만든 것이지, 예측하려고 만든 게 아니기 때문입니다.

💡 누수를 의심해야 하는 신호 세 가지 — ① 특성 하나(또는 둘)가 중요도를 압도한다 ② 그 특성 이름에 '결과'의 냄새가 난다(재작업·통보·사유·처리) ③ 성능이 도메인 상식보다 지나치게 좋다. 셋 중 둘 이상이면 거의 확실합니다. 컬럼 이름만으로 판단이 어려우면 현업 담당자에게 "이거 언제 입력되는 값이에요?"라고 물어보세요. 가장 값싼 누수 방지책입니다.

타깃 누수 제거: 0.987 → 0.888

범인을 찾았으니 걷어냅니다. 재작업시간과 후속공정통보를 빼고, 예측 시점에 진짜로 존재하는 값들 — 온도·진동·압력·두께편차·속도 — 만으로 다시 학습합니다. 결과는 AUC 0.987 → 0.888. 10%p 가까이 떨어졌습니다.

여기서 많은 사람이 이렇게 말합니다. "성능이 너무 떨어지는데... 재작업시간을 다시 넣으면 안 될까요?" 절대 안 됩니다. 그 컬럼은 예측 시점에 존재하지 않습니다. 모델을 공장에 배포했다고 상상해 보세요. 부품이 막 나왔고 예측을 하려는데, 재작업시간 칸에 무엇을 넣을까요? 아직 재작업을 안 했으니 전부 0을 넣게 되고, 학습 때 "재작업시간 0 = 대체로 정상"이라고 배운 모델은 전부 "정상"이라고 답합니다. AUC 0.987짜리 모델이 현장에서 아무것도 못 잡는 이유입니다. 성능이 낮아지는 게 아니라 작동 자체를 안 합니다.

그래서 성능이 떨어졌을 때의 올바른 반응은 실망이 아니라 안도입니다. 0.987은 애초에 우리 모델의 실력이 아니었습니다. 시험지를 미리 본 점수였습니다. 0.888은 정직한 방향으로 한 걸음 간 숫자입니다. 참고로 이 이야기의 원본 실험에서는 여기가 끝이 아닙니다 — 같은 로트의 행들이 학습과 테스트에 흩어져 들어간 그룹 누수까지 걷어내자 0.719로, 누수를 전부 제거하자 최종 0.638까지 내려갑니다. 내려갈 때마다 모델은 더 좋아지고 있었습니다. 올라가 있던 것이 거짓말이었기 때문입니다.

🤔 생각해볼 점 — 현업 데이터에서 첫 시도에 AUC 0.99가 나왔습니다. 기뻐해야 할까요? 그리고 상사가 "0.888보다 0.987이 보고서에 좋아 보이는데"라고 말한다면, 배포 후에 벌어질 일을 어떻게 설명하시겠습니까?

전처리는 분할 뒤에

누수는 컬럼에만 숨는 게 아니라 절차에도 숨습니다. 대표적인 것이 전처리 순서입니다. StandardScaler().fit(X)처럼 스케일러를 전체 데이터로 학습시키면, 그 평균과 표준편차 안에 테스트 세트의 정보가 들어갑니다. 테스트 세트는 '아직 오지 않은 미래'를 흉내 낸 것인데, 내일 올 고객의 평균을 오늘 알 수는 없습니다. 그 미래의 통계를 미리 쓰는 것은 반칙입니다.

스케일러 정도는 점수를 조금 부풀리는 데 그치지만, 특성 선택을 전체 데이터에서 하면 재앙이 됩니다. 원본 실험에서는 y와 아무 관련이 없는 난수 특성 300개를 만들고, 전체 데이터에서 y와 상관 높은 20개를 고른 뒤 나눠서 학습했습니다. 정보가 정확히 0인 데이터인데 AUC 0.6531이 나옵니다. 올바르게 학습 세트 안에서만 고르면 0.5099 — 동전 던지기 수준, 즉 정보가 0인 데이터에서 나와야 할 정직한 점수입니다. 그 0.15의 차이는 모델이 발견한 패턴이 아니라 우리가 테스트 정답을 보고 골라준 것입니다.

규칙은 간단합니다. 분할을 코드의 맨 위에 두고, 모든 전처리(스케일러·인코더·결측 대치·특성 선택·오버샘플링)는 Pipeline 안에 넣어서 학습 데이터로만 배우게 합니다. 그리고 가장 중요한 습관은 코드가 아니라 반사신경입니다 — 성능이 기대보다 좋으면, 축하하기 전에 의심한다.

이 글은 DataForge 교육 과정 원고에서 발췌·각색했습니다. 아래 시뮬레이터에서 데이터 분할과 모델 학습 과정을 직접 값을 바꿔가며 확인할 수 있습니다.

🤖 머신러닝 시뮬레이터 열기 →

함께 보면 좋은 가이드