huny.log

기술 포스트 · 통계·ML

Cross-validation 기초 — 진짜 모델 성능을 측정하는 자리

학습 정확도 95% / 운영 정확도 60%의 함정은 검증 분할이 잘못됐기 때문입니다. cross-validation은 같은 데이터를 여러 번 쪼개 학습·평가해 진짜 일반화 능력을 측정합니다. K-fold·시계열 CV·운영 적용까지, ML 기초의 마지막 자리.

“검증 정확도 80%였는데 운영에서 60%로 떨어졌어요.” 학습·검증·테스트 분할이 잘못됐을 가능성이 큽니다. 한 번 분할로는 운에 따라 검증 데이터가 운영 데이터와 너무 유사하거나 다를 수 있습니다. Cross-validation은 같은 데이터를 여러 번 쪼개 학습·평가하면서 진짜 일반화 능력을 측정합니다. ML 기초 시리즈의 마지막 자리.

1. 검증의 한 줄 정의

ML 모델 평가의 한 줄 원칙:

모델이 학습 시 본 적 없는 데이터에서 평가해야 진짜 성능.

학습 데이터에서의 정확도는 의미 없습니다 (overfitting 글 참조). 본 적 없는 데이터에 어떻게 일반화하는지가 진짜 성능.

가장 단순한 분할 — Train/Validation/Test:

분할비율역할
Train70%모델 학습
Validation15%하이퍼파라미터 조정·early stopping
Test15%최종 평가 (1번만 사용)

이 단순 분할의 한계 — 한 번의 분할로는 운에 따라 결과가 흔들림. 검증 데이터가 우연히 쉬우면 80%, 어려우면 60% 같이.

이 한계를 푸는 도구가 cross-validation.

5-fold cross-validation의 데이터 분할 다이어그램 — 5개 fold가 차례로 검증 자리
데이터를 5개로 쪼개고, 1개씩 차례로 검증·나머지 4개로 학습. 5번 반복하면 모든 데이터가 한 번씩 검증에 사용됨.

2. CV의 전제 — IID 가정과 마케팅 데이터에서 깨지는 이유

K-Fold CV를 쓰기 전에 확인해야 할 전제 조건이 있습니다: IID(Independent and Identically Distributed) 가정입니다.

  • Independent(독립): 데이터 포인트들이 서로 영향을 주지 않는다
  • Identically Distributed(동일 분포): 학습 데이터와 검증 데이터가 같은 분포에서 왔다

표준 K-Fold CV는 이 두 가정 위에 서 있습니다. 그런데 마케팅 데이터는 이 가정이 자주 깨집니다.

독립성 위반 — 유저 데이터

같은 유저가 1월·2월·3월 세 번 구매했다면, 이 세 행은 독립이 아닙니다. 유저의 취향·구매 습관이라는 공통 요인이 세 데이터를 연결하고 있어요. 표준 K-Fold로 분할하면 1월 구매가 학습에, 2월·3월이 검증에 들어갈 수 있습니다. 모델이 유저 패턴을 학습 데이터에서 “외우고” 검증에서 확인하는 셈이라 가짜 정확도가 나옵니다.

이게 인과추론의 SUTVA(Stable Unit Treatment Value Assumption) 위반과 정확히 같은 구조입니다. 유닛(유저) 간 독립성이 전제돼야 CV가 유의미한 평가를 하는데, 같은 유저의 데이터가 학습·검증에 동시에 있으면 유닛 내 spillover가 평가를 오염시킵니다.

해결책: Group K-Fold — 유저 ID를 그룹으로 지정해 같은 유저가 학습과 검증에 동시에 들어가지 않게.

동일 분포 위반 — 시계열 데이터

광고 클릭 패턴, 구매 행동은 시간이 지나면서 분포가 바뀝니다(distribution shift). 2024년 1월 데이터와 12월 데이터는 같은 분포에서 왔다고 보기 어렵습니다. 표준 K-Fold로 랜덤 분할하면 미래(12월) 정보가 학습에, 과거(1월) 검증에 들어가는 데이터 누수가 발생합니다.

해결책: Time Series CV — 항상 과거로만 학습하고 미래로 검증.

3. K-Fold Cross-Validation — 표준

K-fold CV의 흐름:

  1. 데이터를 K개로 균등 분할 (보통 K=5 또는 10)
  2. K번 반복:
    • 1개 fold를 검증, 나머지 K-1개로 학습
    • 검증 정확도 기록
  3. K개 정확도의 평균·표준편차

5-fold CV로 정확도 [82%, 79%, 81%, 80%, 78%] 나오면 평균 80% ± 1.6%. 이 분산이 모델의 진짜 신뢰도.

장점:

  • 모든 데이터가 한 번씩 검증에 사용
  • 평균·분산을 같이 보고 → 신뢰도 측정
  • 작은 데이터셋에 효과적

단점:

  • K번 학습 → 학습 시간 K배
  • 시계열 데이터는 IID 가정이 깨져서 부적합
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.ensemble import GradientBoostingClassifier
import numpy as np
# 기본 5-fold CV
model = GradientBoostingClassifier(random_state=42)
scores = cross_val_score(model, X, y, cv=5, scoring='roc_auc')
print(f"AUC: {scores.mean():.3f} ± {scores.std():.3f}")
print(f"Fold별: {np.round(scores, 3)}")
# 출력 예시:
# AUC: 0.812 ± 0.018
# Fold별: [0.831 0.796 0.823 0.802 0.810]
# 클래스 불균형(이탈 5%) — Stratified K-Fold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
stratified_scores = cross_val_score(model, X, y, cv=skf, scoring='roc_auc')
print(f"\nStratified AUC: {stratified_scores.mean():.3f} ± {stratified_scores.std():.3f}")

4. CV의 변형 — 자리별 도구

4-1. Stratified K-Fold

클래스 불균형 자리. 각 fold에 양성 비율을 같게 유지. 이탈률 5% 자리에서 fold마다 양성 비율이 흔들리지 않게.

4-2. Leave-One-Out CV

K = N (데이터 수). 한 데이터씩 검증. 매우 작은 데이터셋(N < 50)에 적합. 학습 N번이라 큰 데이터엔 비실용.

4-3. Time Series CV

시계열 데이터의 표준. 미래 데이터로 과거 검증하면 데이터 누수 — 시간 순서 유지하며 분할:

FoldTrainValidation
11-100주101-110주
21-110주111-120주
31-120주121-130주

매 fold마다 검증 직전까지의 데이터로 학습. 미래 정보 누수 차단.

4-4. Group K-Fold

같은 그룹의 데이터가 학습·검증에 동시에 들어가지 않게. 마케팅에서:

  • 같은 유저의 여러 세션 → 같은 fold
  • 같은 캠페인의 여러 임프레션 → 같은 fold

데이터 누수 방지.

CV 변형적합 자리
K-Fold일반 IID 데이터
Stratified K-Fold클래스 불균형
Time Series시계열
Group K-Fold동일 그룹 데이터
Leave-One-Out매우 작은 데이터

5. CV 변형별 코드 예시

5-1. Time Series CV — TimeSeriesSplit

from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np
# 시계열 CV — 항상 과거로 학습, 미래로 검증
tscv = TimeSeriesSplit(n_splits=5, gap=0)
model = GradientBoostingRegressor(random_state=42)
scores = cross_val_score(model, X_time, y_time, cv=tscv, scoring='neg_mean_absolute_error')
mae_scores = -scores # neg_MAE → 양수로 변환
print("Time Series CV MAE:")
for i, mae in enumerate(mae_scores, 1):
print(f" Fold {i}: {mae:.1f}")
print(f"평균 MAE: {mae_scores.mean():.1f} ± {mae_scores.std():.1f}")
# 출력 예시:
# Fold 1: 1823.4 ← 초기 데이터 → 훈련, 직후 기간 → 검증
# Fold 2: 1654.2
# Fold 3: 1791.8
# Fold 4: 1702.3
# Fold 5: 1889.1 ← 가장 최근 검증 기간 — 분포 drift 있으면 이 fold가 높아짐
# 평균 MAE: 1772.2 ± 87.4

gap 파라미터로 학습·검증 사이의 간격을 조정합니다. LTV 예측처럼 “6개월 뒤 LTV를 예측”이면 gap=26(주 단위)으로 설정해 데이터 누수를 완전히 차단합니다.

5-2. Group K-Fold — 유저 단위 분리

from sklearn.model_selection import GroupKFold, cross_val_score
import numpy as np
# user_ids: 각 행의 유저 ID (같은 유저 데이터를 같은 fold로)
gkf = GroupKFold(n_splits=5)
scores = cross_val_score(
model, X, y,
cv=gkf,
groups=user_ids, # ← 핵심: 유저 ID 기반 분할
scoring='roc_auc'
)
print(f"Group K-Fold AUC: {scores.mean():.3f} ± {scores.std():.3f}")
# 비교: 일반 K-Fold (유저 누수 있음)
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)
leaky_scores = cross_val_score(model, X, y, cv=kf, scoring='roc_auc')
print(f"일반 K-Fold AUC: {leaky_scores.mean():.3f} ± {leaky_scores.std():.3f}")
# 일반 K-Fold가 Group K-Fold보다 AUC가 높게 나오면
# → 유저 레벨 데이터 누수가 있었다는 신호

5-3. 하이퍼파라미터 튜닝 — 이중 CV (Nested CV)

CV는 모델 평가 + 하이퍼파라미터 튜닝의 두 자리에 들어옵니다. 두 자리를 분리해야 데이터 누수 방지.

단순 흐름 (위험)

  1. CV로 하이퍼파라미터 결정
  2. 같은 CV 결과로 모델 평가 보고

문제 — 하이퍼파라미터 자체가 검증 데이터에 맞춰 선택됐기 때문에 보고된 정확도가 부풀려짐.

이중 CV (Nested CV)

  • 바깥쪽 CV — 모델 평가
  • 안쪽 CV — 하이퍼파라미터 튜닝

각 바깥 fold 안에서 안쪽 CV로 best 하이퍼파라미터 결정 → 그 모델로 바깥 fold 검증. 데이터 누수 차단.

운영 부담은 K × K’ 학습이라 큼. 정확한 평가 필요한 자리(논문·중요 의사결정)에 적용.

from sklearn.model_selection import GridSearchCV, cross_val_score
# 안쪽 CV — 하이퍼파라미터 튜닝
inner = GridSearchCV(model, param_grid, cv=3)
# 바깥쪽 CV — 모델 평가
outer_scores = cross_val_score(inner, X, y, cv=5)
print(f'True performance: {outer_scores.mean():.3f}')

이게 본문에 박는 유일한 코드입니다. sklearn으로 nested CV 한 묶음.

6. CV 결과의 해석

6-1. 평균 정확도

전체적 모델 성능. 이 숫자만 보고 결정하지 말고 분산도.

6-2. 표준편차

분산이 크면 모델이 데이터 분할에 흔들림 — 일반화 약함. 분산이 작으면 안정적.

운영 표준:

  • 평균 80%, 표준편차 1% — 매우 안정 (좋음)
  • 평균 80%, 표준편차 5% — 약간 흔들림 (보통)
  • 평균 80%, 표준편차 10% — 흔들림 큼 (재검토)

6-3. fold 간 패턴

특정 fold만 정확도 매우 낮으면 — 그 fold의 데이터가 어렵거나 다른 분포. 데이터 검토.

6-4. 학습·검증 갭

각 fold의 학습 정확도와 검증 정확도 갭:

  • 작음 (5%p 이내) — 일반화 잘 됨
  • 큼 (15%p+) — overfitting

7. 마케팅 운영 자리별 CV 가이드

7-1. LTV 예측 — Group K-Fold (유저 단위)

LTV 예측 모델의 학습 데이터는 대부분 유저별 다중 시점 행동 로그입니다. 같은 유저의 1월·3월·6월 데이터가 각기 다른 fold에 분산되면, 모델이 “이 유저는 X를 샀다”는 유저 고유 패턴을 학습 fold에서 외우고 검증 fold에서 확인하는 꼴이 됩니다.

결과: CV AUC가 0.85인데 운영에서 0.71로 떨어지는 전형적인 패턴. 새 유저(학습 데이터에 없던 유저)에게 적용하면 모델이 아무런 유저 고유 패턴을 못 활용하기 때문.

올바른 설정: GroupKFold(n_splits=5)groups=user_id를 지정. 5개 fold가 5개 유저 집합으로 분리됩니다. CV AUC가 더 낮게 나오더라도 — 그게 운영 실제 성능에 더 가깝습니다.

추가로, LTV처럼 6개월 후 결과를 예측하는 경우라면 Group K-Fold에 시간 기반 정렬을 결합합니다. 학습 기간(예: 1-6월)과 검증 기간(7-12월)을 완전히 분리한 뒤, 검증 기간 내에서 Group K-Fold를 적용하는 방식입니다.

7-2. 광고 캠페인 효과 예측 — Time Series CV

“다음 달 이 캠페인의 ROAS가 얼마나 될까”를 예측하는 모델은 반드시 Time Series CV를 씁니다. 이유는 세 가지:

첫째, 미래 정보 누수: 랜덤 K-Fold를 쓰면 11월 데이터가 학습에, 3월 데이터가 검증에 들어갈 수 있습니다. 하지만 운영 예측은 항상 “현재까지의 데이터로 미래 예측”이므로 평가도 그 방향이어야 합니다.

둘째, 시즌 효과: 블랙프라이데이·설날 같은 시즌이 특정 기간에 집중되어 있습니다. 랜덤 분할하면 어떤 fold에는 시즌이 학습에, 다른 fold에는 검증에 들어가 불균등한 평가가 됩니다. 시간 순서 분할을 쓰면 “시즌 전 데이터로 시즌 중 예측”이라는 실제 운영 조건과 동일해집니다.

셋째, Distribution shift 감지: Time Series CV의 마지막 fold(가장 최근 기간) MAE가 앞 fold들보다 크게 높으면 → 데이터 분포가 시간이 지나면서 변했다는 신호입니다. 모델 재학습이나 feature 추가가 필요하다는 조기 경보입니다.

7-3. 이탈 분류 — Stratified K-Fold

이탈률 5% 데이터에서 일반 K-Fold를 쓰면 어떤 fold에는 양성(이탈)이 3%, 다른 fold에는 8%가 들어갈 수 있습니다. fold마다 클래스 불균형 정도가 달라지면 recall·precision이 fold별로 크게 흔들립니다.

Stratified K-Fold는 각 fold에 양성 비율을 동일하게(5%) 유지합니다. 결과: fold 간 AUC·recall 분산이 절반 이하로 줄어 CV 표준편차가 안정됩니다.

주의: 이탈 클래스가 너무 작아 fold당 양성이 10건 미만이면 CV 자체가 불안정합니다. 이 경우 오버샘플링(SMOTE) 후 CV를 쓰되, 반드시 각 fold의 학습 split에만 SMOTE를 적용해야 합니다. 검증 split에 SMOTE를 적용하면 평가가 오염됩니다.

7-4. CTR 예측 — Stratified + Group 결합

광고 소재별 CTR 예측은 가장 복잡한 CV 설정이 필요합니다. 두 가지 문제가 동시에 있기 때문입니다:

  • 같은 소재(creative)가 여러 날 노출 → 소재 단위 그룹 누수
  • 클릭/비클릭 비율이 99:1 수준으로 불균형

이 두 가지를 동시에 해결하는 표준: StratifiedGroupKFold (sklearn 1.1+).

from sklearn.model_selection import StratifiedGroupKFold
sgkf = StratifiedGroupKFold(n_splits=5)
# groups=creative_id (소재 단위 분리)
# y=clicked (클릭 여부로 계층화)
scores = cross_val_score(
model, X, y,
cv=sgkf,
groups=creative_ids,
scoring='average_precision' # 불균형 데이터엔 AP가 AUC보다 민감
)
print(f"AP: {scores.mean():.3f} ± {scores.std():.3f}")

8. CV가 깨질 때 — 흔한 함정 3가지

8-1. 시계열에 일반 K-Fold

미래 데이터로 과거 검증 → 데이터 누수. CV 정확도 매우 높지만 운영에서 떨어짐. 시계열은 반드시 Time Series CV.

8-2. 그룹 누수

같은 유저·같은 캠페인의 여러 데이터가 학습·검증에 동시에. 모델이 그룹의 패턴을 학습 데이터에서 외워서 검증에서 잘 맞춤 — 가짜 정확도. Group K-Fold로 차단.

8-3. 너무 작은 K

K=2면 학습·검증 데이터가 절반씩이라 분산이 매우 큼. 보통 K=5-10이 표준. 데이터 매우 작으면 LOOCV.

9. 마치며 — ML 기초 5편의 마지막

이 시리즈 5편 (회귀·분류·손실 함수·overfitting·평가 지표·CV)을 통해 머신러닝 기초 체력을 정리했습니다.

회귀·분류 두 가족. 손실 함수 + gradient descent로 학습. overfitting을 정규화로 방지. 평가 지표는 자리에 맞춰. CV로 진짜 성능 측정.

이 5가지가 huny.log의 모든 ML 글의 토대. 이 위에 시리즈 1·2·3의 도구들이 자연스럽게 얹힙니다. BG/NBD LTV·Conformal Prediction·Sequential testing 같은 도구들이 모두 회귀·분류·CV·평가 지표 위에서 작동.

다음 시리즈는 매체(마케팅 인프라) 기초 체력으로 넘어갑니다 — 광고 생태계 지도·RTB·어트리뷰션의 역사·KPI·데이터 흐름.

참고

통계·ML 카테고리의 다른 글

전체 보기 →