머신러닝 모델은 학습 데이터에 잘 맞는 것만으로는 충분하지 않다.
중요한 질문은 이거다.
이 모델이 처음 보는 새 데이터에서도 잘 맞을까?
예를 들어 CS 문의 분류 모델을 만든다고 하자.
- 목표: 고객 문의 문장을 보고 문의 유형을 예측한다.
- 입력 예시: "배송이 너무 늦어요"
- 정답 예시: 배송 지연 문의
데이터가 10,000건 있다고 해서, 10,000건 전부로 학습하고 같은 10,000건으로 평가하면 안 된다.
그건 모델이 시험 문제를 미리 보고 공부한 뒤 같은 문제로 시험을 보는 것과 비슷하다.
모델이 진짜로 잘하는지 보려면, 학습에 쓰지 않은 데이터를 따로 남겨두고 평가해야 한다.
전체 데이터 10,000건
→ 학습용 데이터 8,000건
→ 검증용 데이터 1,000건
→ 테스트용 데이터 1,000건
데이터 분리의 목적은 단순히 데이터를 나누는 것이 아니다.
모델이 실제 운영 환경에서 만날 새 데이터에 얼마나 잘 대응하는지 추정하는 것
이 추정이 믿을 만하려면, 평가 데이터의 정보가 학습 과정에 들어가면 안 된다.
그 정보가 들어가는 순간 평가 점수는 실제보다 좋아 보인다.
이게 데이터 누수(data leakage)다.
핵심 용어
Train 데이터
모델이 실제로 학습하는 데이터.
모델은 train 데이터에서 패턴을 찾는다.
"배송", "도착", "택배"가 자주 나오면 배송 문의일 가능성이 높다.
"환불", "취소", "돈"이 자주 나오면 환불 문의일 가능성이 높다.
train 데이터는 모델의 파라미터를 바꾸는 데 직접 사용된다.
Validation 데이터
모델을 고르는 데 사용하는 데이터.
예를 들어 다음 중 어떤 모델이 좋은지 고를 때 validation 데이터를 쓴다.
모델 A: Logistic Regression
모델 B: Random Forest
모델 C: LightGBM
또는
KMeans K=10
KMeans K=30
KMeans K=70
validation 데이터는 학습에는 직접 쓰지 않지만, 모델 선택에 영향을 준다.
그래서 validation 데이터도 너무 많이 보면 거기에 맞춰질 수 있다.
validation 점수가 좋은 모델을 계속 고르다 보면
결국 validation 데이터에 과하게 맞춘 선택을 하게 된다.
Test 데이터
마지막에 딱 한 번 확인하는 최종 평가용 데이터.
test 데이터는 다음이 모두 끝난 뒤에만 봐야 한다.
모델 종류 선택
하이퍼파라미터 선택
전처리 방식 선택
특징 선택
분류 기준값(threshold) 선택
test 데이터를 보고 다시 모델을 고치면, 그 순간 test 데이터는 더 이상 순수한 test 데이터가 아니다.
test 점수 확인
→ 결과가 별로라서 모델 수정
→ 다시 test 점수 확인
이렇게 하면 test 데이터는 사실상 validation 데이터처럼 쓰인 것이다.
Train / Validation / Test 차이
| 구분 | 역할 | 모델이 직접 학습하는가 | 여러 번 봐도 되는가 | 비유 |
| Train | 패턴 학습 | 예 | 예 | 문제집 |
| Validation | 모델 선택 | 아니오 | 어느 정도 가능 | 중간고사 |
| Test | 최종 평가 | 아니오 | 원칙적으로 한 번 | 최종 시험 |
- Train: 모델을 만드는 데 사용
- Validation: 어떤 모델을 쓸지 고르는 데 사용
- Test: 최종 성능을 추정하는 데 사용

데이터 누수
정의
데이터 누수는 실제 예측 시점에는 알 수 없는 정보가 학습 또는 평가 과정에 들어가는 것이다.
또는 평가 데이터의 힌트가 모델 학습 과정에 새어 들어간 것이라고도 볼 수 있다.
누수가 생기면 평가 점수가 높아지지만, 그 점수를 믿을 수 없다.
모델이 진짜 패턴을 배운 것이 아니라, 평가 데이터나 미래 정보를 미리 본 상태로 문제를 푼 것이기 때문.
누수를 판단하는 기준
실제 운영에서 이 예측을 하는 순간, 이 정보를 알고 있었을까?
알고 있었다면 사용할 수 있다.
몰랐다면 사용하면 안 된다.
예를 들어 고객 이탈 예측을 한다고 하자.
- 목표: 이번 달 고객이 이탈할지 예측
- 예측 시점: 이번 달 1일
- 사용 가능한 정보:
- 지난달까지의 로그인 횟수
- 지난달까지의 결제 이력
- 지난달까지의 문의 기록
- 사용하면 안 되는 정보:
- 이번 달 20일에 실제로 탈퇴한 날짜
- 이탈 처리 완료 여부
- 해지 사유 코드
탈퇴일이나 해지 사유는 예측하고 싶은 결과가 일어난 뒤에 생기는 정보다.
이런 정보를 넣으면 모델은 미래를 맞힌 게 아니라 미래를 훔쳐본 것이다.
왜 누수가 위험한가
누수가 있으면 validation/test 점수가 실제보다 높아진다.
예를 들어 환불 문의 분류 모델에 이런 컬럼이 있다고 하자.
문의 내용: "상품을 반품하고 싶어요"
처리 상태: 환불 완료
정답 라벨: 환불 문의
처리 상태 = 환불 완료는 정답을 거의 직접 알려준다.
모델은 문의 문장을 이해하지 않아도 된다. (처리 상태가 환불 완료면 → 환불 문의)
이렇게 학습한 모델은 평가 데이터에서도 높은 점수를 낼 수 있다.
하지만 실제 운영에서는 문의가 들어온 순간 아직 처리 상태가 없기 때문에, 운영에 올리면 성능이 무너진다.
잘못된 의사결정을 만든다
누수가 있으면 이런 착각을 하게 된다.
이 모델은 정확도가 98%네.
운영에 바로 써도 되겠다.
하지만 실제로는 모델이 업무에 필요한 패턴을 배운 것이 아닐 수 있다.
- 진짜 배운 것: 고객 문장의 의미
- 누수로 배운 것: 나중에 생긴 처리 결과 컬럼
이 차이를 놓치면, 개발 단계에서는 좋아 보였던 모델이 운영에서 바로 실패한다.
대표적인 데이터 누수 유형
전처리 누수
전처리 누수는 전체 데이터에 전처리기를 먼저 fit한 뒤 train/test를 나누거나, validation/test 정보가 전처리 과정에 들어가는 경우다.
전처리기는 단순 변환처럼 보이지만, fit 단계에서 데이터를 보고 통계를 학습한다.
예를 들어 StandardScaler는 평균과 표준편차를 계산한다.
- fit: 평균과 표준편차 계산
- transform: 계산된 평균과 표준편차로 값을 변환
TF-IDF도 마찬가지다.
- fit: 전체 단어 목록과 IDF 계산
- transform: 문서를 TF-IDF 벡터로 변환
나쁜 방식
전체 데이터로 TF-IDF fit
→ 전체 데이터를 벡터화
→ train/test split
→ 모델 학습 및 평가
문제는 test 문서가 이미 TF-IDF의 vocabulary와 IDF 계산에 참여했다는 점이다.
즉, test 데이터의 단어 분포가 학습 과정에 들어갔다.
# 나쁜 예: 전체 데이터로 먼저 fit
vectorizer = TfidfVectorizer()
X_all = vectorizer.fit_transform(texts)
X_train, X_test, y_train, y_test = train_test_split(X_all, y)
좋은 방식
train/test split 먼저 수행
→ train 데이터로만 TF-IDF fit
→ train 데이터 transform
→ test 데이터는 transform만 수행
핵심은 이거다.
fit은 train에만 한다.
test에는 transform만 한다.
# 좋은 예: split 먼저, fit은 train에만
X_train_text, X_test_text, y_train, y_test = train_test_split(texts, y)
vectorizer = TfidfVectorizer()
X_train = vectorizer.fit_transform(X_train_text)
X_test = vectorizer.transform(X_test_text)
Target Leakage
Target leakage는 정답을 직간접적으로 알려주는 컬럼이 입력 특징(feature)에 들어가는 경우다.
예를 들어 환불 여부를 예측하는데 이런 컬럼이 있다고 하자.
환불 처리일
환불 완료 여부
환불 담당자 ID
환불 사유 코드
이 컬럼들은 환불이 발생한 뒤에 생긴다.
예측 시점에는 알 수 없다.
그런데 이런 컬럼을 입력 특징에 넣으면 모델은 너무 쉽게 맞힌다.
예시
예를 들어 문의 접수 시점에 이 문의가 환불 문의인지 예측한다고 하자.
- 사용 가능한 정보:
- 문의 제목
- 문의 내용
- 접수 시간
- 고객 등급
- 주문 상품 정보
- 사용하면 안 되는 정보:
- 처리 완료 상태
- 환불 승인 여부
- 환불 처리일
- 최종 상담 분류 코드
최종 상담 분류 코드는 정답 라벨과 거의 같다.
그걸 입력 특징으로 넣으면 모델이 “학습”하는 것이 아니라 답안지를 보는 것이다.
시간 누수
간 누수는 미래 정보를 과거 예측에 사용하는 경우다.
시계열 데이터나 운영 로그 데이터에서 자주 발생한다.
예를 들어 7월 주문 취소를 예측하는데 8월 행동 데이터가 들어가면 안 된다.
랜덤 분리가 위험한 경우
데이터에 시간 순서가 있을 때 무작위로 train/test를 나누면 미래 데이터가 train에 들어갈 수 있다.
1월 데이터
2월 데이터
3월 데이터
4월 데이터
5월 데이터
6월 데이터
무작위 분리를 하면 이렇게 될 수 있다.
Train: 1월, 3월, 5월, 6월
Test: 2월, 4월
그러면 모델은 5월, 6월의 패턴을 학습한 뒤 2월, 4월을 평가한다.
실제 운영에서는 과거를 예측할 때 미래를 알 수 없다.
좋은 방식
시간 순서가 중요하면 과거로 학습하고 미래로 평가해야 한다.
Train: 1월~4월
Validation: 5월
Test: 6월
또는 rolling validation을 쓸 수 있다.
Fold 1: 1~3월 학습 → 4월 평가
Fold 2: 1~4월 학습 → 5월 평가
Fold 3: 1~5월 학습 → 6월 평가

그룹 누수
룹 누수는 같은 사용자, 같은 상품, 같은 문서, 같은 환자처럼 서로 강하게 연결된 샘플이 train과 test에 동시에 들어가는 경우다.
모델은 새로운 대상을 맞힌 것이 아니라, 이미 본 대상의 다른 조각을 맞힌 것일 수 있다.
예시1: 사용자 데이터
한 사용자가 여러 개의 샘플을 만든다고 하자.
user_001의 1월 행동
user_001의 2월 행동
user_001의 3월 행동
랜덤 split을 하면 이렇게 될 수 있다.
Train: user_001의 1월, 2월 행동
Test: user_001의 3월 행동
모델은 user_001의 특성을 이미 봤다.
그래서 test 점수가 좋아 보일 수 있다.
하지만 실제 운영 목표가 “처음 보는 사용자”에 대한 예측이라면 이 평가는 과하게 낙관적이다.
예시 2: RAG 문서 청크
긴 문서를 여러 청크로 잘랐다고 하자.
문서 A - chunk 1
문서 A - chunk 2
문서 A - chunk 3
chunk 단위로 랜덤 split을 하면 같은 문서의 일부가 train과 test에 동시에 들어갈 수 있다.
Train: 문서 A chunk 1, chunk 2
Test: 문서 A chunk 3
이 경우 모델이나 검색 시스템은 완전히 새로운 문서를 만난 것이 아니다.
문서 단위로 나눠야 한다.
Train: 문서 A, B, C
Test: 문서 D, E
중복 누수
중복 또는 거의 같은 데이터가 train과 test에 같이 들어가는 경우다.
예를 들어 같은 문의가 약간만 바뀌어 여러 번 저장되어 있을 수 있다.
"배송이 너무 늦어요"
"배송이 너무 늦습니다"
"배송 너무 늦어요"
이 중 하나가 train에 있고 하나가 test에 있으면, 모델이 진짜 일반화한 것처럼 보이지만 사실상 같은 문제를 다시 푼 것에 가깝다.
중복 제거 또는 그룹화가 필요하다.
분리 전략 선택하기
기본 랜덤 분리
가장 단순한 방식. 전체 데이터에서 무작위로 train/validation/test를 나눈다.
사용 가능한 경우:
각 샘플이 서로 독립적이다.
시간 순서가 중요하지 않다.
같은 사용자/상품/문서가 반복되지 않는다.
클래스 불균형이 심하지 않다.
Stratified Split
분류 문제에서 클래스 비율을 유지하면서 나누는 방식.
예를 들어 전체 데이터가 이렇게 생겼다고 하자.
정상 문의: 9,000건
환불 문의: 1,000건
그냥 랜덤 split을 하면 작은 데이터에서는 특정 클래스가 한쪽에 치우칠 수 있다.
stratified split은 train/test에 클래스 비율이 비슷하게 유지되도록 나눈다.
Train: 정상 7,200건 / 환불 800건
Test: 정상 1,800건 / 환불 200건
사용 가능한 경우:
분류 문제다.
클래스 불균형이 있다.
시간/그룹 조건보다 클래스 비율 유지가 더 중요하다.
Group Split
같은 그룹이 train과 test에 동시에 들어가지 않게 나누는 방식.
그룹 예시:
사용자 ID
상품 ID
회사 ID
문서 ID
환자 ID
계약 ID
사용 가능한 경우:
같은 대상에서 여러 샘플이 만들어진다.
새로운 대상에 대한 일반화 성능을 보고 싶다.
Time-based Split
시간 순서대로 나누는 방식.
사용 가능한 경우:
운영에서 과거 데이터로 미래를 예측한다.
데이터 분포가 시간에 따라 변한다.
월별/일별 로그 데이터다.
시간 기반 split에서는 test 데이터가 train 데이터보다 미래여야 한다.
Train: 과거
Validation: 그 다음 기간
Test: 가장 미래 기간
Cross Validation
왜 쓰는가
데이터가 적으면 한 번의 train/test split만으로 성능을 판단하기 어렵다.
운 좋게 쉬운 데이터가 test에 들어가면 점수가 높게 나오고,
운 나쁘게 어려운 데이터가 test에 들어가면 점수가 낮게 나온다.
Cross Validation은 데이터를 여러 번 나눠 평가한다.
Fold 1: A/B/C/D로 학습 → E로 평가
Fold 2: A/B/C/E로 학습 → D로 평가
Fold 3: A/B/D/E로 학습 → C로 평가
Fold 4: A/C/D/E로 학습 → B로 평가
Fold 5: B/C/D/E로 학습 → A로 평가
그리고 점수를 평균낸다. 최종 점수 = 여러 fold 점수의 평균
Cross Validation에서도 누수는 생긴다
Cross Validation을 쓴다고 자동으로 안전해지는 것은 아니다.
전처리기를 전체 데이터에 먼저 fit한 뒤 K-Fold를 돌리면 누수가 생긴다.
- 나쁜 흐름
- 전체 데이터로 TF-IDF fit → K-Fold split → 각 fold 평가
- 문제: 각 validation fold가 이미 vocabulary와 IDF 계산에 참여했다.
- 좋은 흐름
- Fold마다 train fold로만 TF-IDF fit → validation fold에는 transform만 적용
- scikit-learn에서는 Pipeline을 사용하면 이런 실수를 줄일 수 있다.
- Pipeline 안에 넣으면 각 fold마다 train fold에서만 `fit`이 수행된다.
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
pipe = Pipeline([
("tfidf", TfidfVectorizer()),
("clf", LogisticRegression(max_iter=1000))
])
scores = cross_val_score(pipe, texts, labels, cv=5)
실무에서 자주 하는 실수
test 점수를 보고 threshold를 고침
test에서 recall이 낮네.
분류 기준값(threshold)을 0.5에서 0.3으로 낮추자.
다시 test 평가하자.
이렇게 하면 test 데이터를 모델 선택에 사용한 것.
분류 기준값은 validation 데이터에서 정해야 한다.
전체 데이터로 결측치 평균을 채움
전체 데이터 평균 나이 = 34.7
모든 결측치를 34.7로 채움
그다음 train/test split
test 데이터의 통계가 train 전처리에 들어갔다.
좋은 방식:
- train 평균으로 결측치 채움
- test는 train 평균을 그대로 사용해서 채움
미래에 생기는 컬럼을 feature로 사용
예측 시점 이후에 생기는 정보는 쓰면 안 된다.
환불 완료일
상담 처리 결과
배송 완료 여부
탈퇴 사유
최종 승인 상태
이런 컬럼은 모델 점수를 쉽게 올리지만, 운영에서는 사용할 수 없다.
같은 문서의 청크를 랜덤으로 나눔
RAG 평가에서 자주 생긴다.
같은 PDF의 chunk 일부는 train
같은 PDF의 다른 chunk는 test
이 경우 문서 단위 일반화 평가가 아니다.
문서 단위로 분리해야 한다.
분리 전략 선택 순서
실무에서는 아래 순서로 생각하면 된다.
- 시간 순서가 중요한가?
→ time-based split 우선 - 같은 사용자/상품/문서/환자가 여러 샘플을 만드는가?
→ group split 우선 - 분류 문제이고 클래스 불균형이 있는가?
→ stratified split 고려 - 데이터가 적은가?
→ cross validation 고려 - 전처리기가 있는가?
→ 반드시 train 또는 train fold 안에서만 fit
여러 조건이 동시에 있으면 더 강한 제약을 우선한다.
예를 들어 시간 순서와 사용자 그룹이 둘 다 중요하다면, 단순 랜덤 split은 거의 항상 부적절하다.
정리
- 데이터 분리는 모델이 처음 보는 데이터에서도 잘하는지 확인하기 위한 장치다.
- 데이터 누수는 평가 데이터나 미래 정보가 학습 과정에 섞여 점수를 가짜로 좋게 만드는 문제다.
- 가장 중요한 규칙
- fit은 train에만.
- test는 마지막에 한 번만.
- 실제 예측 시점에 모르는 정보는 입력 특징으로 쓰지 말 것.
GitHub 댓글