교차검증과 부트스트랩으로 모델 일반화 성능 검증하기
K-fold, LOOCV, 부트스트래핑을 활용해 모델 성능의 불확실성을 추정하고 과적합과 데이터 누수를 줄이는 평가 방법을 다룬다.
2026-08-14 · 최초 발행 2024-04-29
일반화 성능은 분할 방식에서 갈린다
학습 데이터에서 높은 점수를 얻은 모델이 새로운 데이터에서도 같은 성능을 낸다는 보장은 없다. 모델 평가는 데이터 분할 방식, 성능 지표, 불확실성 추정, 과적합 억제 절차를 함께 설계해야 한다.
교차검증과 부트스트래핑은 각각 성능을 반복적으로 확인하고 그 추정치의 분포를 살피는 방법이다. 여기에 전처리와 하이퍼파라미터 탐색까지 평가 절차 안에 넣어야 데이터 누수나 낙관 편향을 피할 수 있다.
교차검증과 재표본화의 역할
K-fold 교차검증은 데이터를 K개 폴드로 나누고, 각 폴드를 차례로 검증 데이터로 사용해 K회 학습·평가하는 방식이다. 각 평가 결과의 평균으로 성능을 산출하며, 편향과 분산 사이의 균형을 맞추는 표준 절차로 쓰인다.
LOOCV(Leave-One-Out Cross-Validation)는 매 반복마다 샘플 1개만 검증에 쓰고 나머지를 학습에 사용한다. 학습 데이터 활용률이 높아 편향은 매우 낮지만, 분산과 계산비용이 크다.
부트스트래핑은 원본 데이터에서 복원추출로 여러 재표본을 만들고 통계량의 분포를 구하는 방식이다. 정확도나 AUC 같은 성능 지표의 분포와 신뢰구간을 추정할 때 활용할 수 있으며, 비모수적 불확실성 추정에 적합하다.
과적합은 학습 데이터에만 특화된 패턴을 익혀 일반화 성능이 떨어지는 현상이다. 규제·정규화, 조기 종료, 데이터 증강, 드롭아웃, 앙상블과 적절한 검증 절차가 이를 완화한다. 하이퍼파라미터 탐색까지 포함하는 경우에는 중첩 교차검증도 고려 대상이다.
데이터 특성에 맞춰 검증 경계를 정한다
Hold-out보다 K-fold는 데이터를 더 효율적으로 쓰고 성능 추정을 안정화하는 데 유리하다. 클래스 분포가 불균형하면 Stratified K-fold로 분포를 보존한다.
시계열 데이터는 시간 순서를 지켜야 한다. TimeSeriesSplit이나 walk-forward 검증을 사용하고, 미래 관측치가 학습 과정에 들어가지 않도록 막아야 한다.
같은 사용자, 환자, 장비처럼 그룹 또는 개체 단위의 상관이 있다면 GroupKFold가 필요하다. 동일 그룹이 학습과 검증 데이터에 동시에 포함되면 평가 결과가 과도하게 좋아질 수 있다.
전처리도 검증 경계 안에 있어야 한다. 스케일링, 특성 선택, 인코딩은 반드시 파이프라인 안에서 fold별로 fit해야 하며, 파이프라인 바깥에서 미리 fit하면 데이터 누수가 발생한다.
점수와 변동성을 함께 본다
분류 문제에서는 ROC AUC, PR AUC, F1, Brier score를 사용할 수 있다. 회귀 문제에서는 RMSE, MAE, R2가 대표적이다. 평가를 시작하기 전에 문제 목적에 맞는 1차 지표를 정해 두는 편이 좋다.
부트스트랩은 평균 성능의 신뢰구간을 산출하는 데 쓸 수 있고, 교차검증 점수의 분포는 평가 결과의 분산을 보여준다. 모델 선택은 단일 점수만으로 끝내기보다 성능 추정의 흔들림까지 함께 확인하는 과정이다.
모델 자체에서는 L1/L2 규제, 드롭아웃, 데이터 증강, 조기 종료, 앙상블을 적용할 수 있다. 프로세스 차원에서는 중첩 교차검증으로 하이퍼파라미터 탐색의 낙관 편향을 줄이고, 반복 K-fold로 분산 완화를 시도한다.
LOOCV ≫ K-fold ≫ Hold-out 순으로 계산비용이 커진다. 병렬화(n_jobs), 캐싱, 조기 중단은 운영 비용을 줄이는 수단이 될 수 있다. 재현성을 위해서는 난수 시드, 패키지 버전, 데이터 스냅샷도 고정해야 한다.
방법별 특성과 선택 기준
| 방법 | 성능 추정 편향 | 분산 | 확장성(계산비용) | 안정성(재현성) | 운영 편의 |
|---|---|---|---|---|---|
| K-fold | 낮음 | 중간 | 중간 | 높음 | 높음 |
| LOOCV | 매우 낮음 | 높음 | 낮음 | 중간 | 낮음 |
| 부트스트래핑 | 중간 | 조절 가능(표본 수 의존) | 중간 | 높음 | 중간 |
LOOCV는 훈련 데이터 활용을 극대화하므로 편향이 낮다. 반면 폴드 간 상관과 계산량 때문에 분산과 비용이 커진다.
부트스트래핑은 신뢰구간 산출에 강점이 있다. 모델 선택의 단독 기준으로 사용하기보다 교차검증과 결합하는 방식이 적합하다.
평가 절차를 파이프라인으로 묶기
데이터 유형별 검증 설계
불균형 이진 분류에서는 Stratified K-fold와 PR AUC 최적화를 함께 적용할 수 있다. 클래스 가중치나 언더·오버샘플링은 파이프라인 내부에 배치하고, 내부 CV에서 하이퍼파라미터를 탐색한 뒤 외부 폴드 성능을 집계한다. 부트스트랩으로 PR AUC 95% 신뢰구간을 보고하고, 임계값은 비용 기반으로 튜닝한다.
시계열 수요예측에서는 TimeSeriesSplit과 walk-forward 검증을 사용한다. 피처 생성과 스케일링은 각 훈련 윈도우에 한정해 fit해야 한다. 잔차 부트스트랩으로 예측 구간(PI)을 산출할 수 있으며, 롤링 재학습 주기와 연산 비용의 트레이드오프도 관리 대상이다.
소규모 의료 데이터에서는 LOOCV로 데이터 활용을 극대화할 수 있다. 높은 분산은 반복 BOOTSTRAP으로 CI를 제시해 보완하고, GroupKFold로 환자 단위 누수를 차단한다. F1, balanced accuracy처럼 해석 가능한 지표도 함께 보고한다.
반복 검증이 남기는 운영상의 이점
반복 K-fold는 추정 표준오차(SE)를 낮추는 경향이 있다. 이론적 근사에서는 폴드 수 k와 반복 r이 증가할수록 SE가 대략 1/sqrt(k·r) 스케일로 완화된다.
부트스트랩 95% CI는 의사결정 리스크를 드러내고, 임계값 선택에서는 기대 손실 최소화에 기여한다. 데이터 누수를 막는 재현 가능한 파이프라인은 배포 후 성능 변동을 줄이고 모델 신뢰도를 높이는 기반이 된다.
중첩 교차검증과 부트스트랩 코드
전제조건: Python 3.10+, scikit-learn >= 1.3, numpy >= 1.24
데이터 전처리/특성 선택은 반드시 파이프라인 내부에 위치.
# Nested CV + Bootstrap CI 예제 (분류, ROC AUC)
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, GridSearchCV
from sklearn.metrics import roc_auc_score
X, y = load_breast_cancer(return_X_y=True)
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
pipe = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(solver="liblinear", max_iter=500))
])
param_grid = {
"clf__penalty": ["l1", "l2"],
"clf__C": [0.1, 1.0, 10.0]
}
outer_scores = []
probas = np.zeros_like(y, dtype=float)
for train_idx, test_idx in outer_cv.split(X, y):
X_tr, X_te = X[train_idx], X[test_idx]
y_tr, y_te = y[train_idx], y[test_idx]
gs = GridSearchCV(
pipe, param_grid=param_grid, cv=inner_cv,
scoring="roc_auc", n_jobs=-1
)
gs.fit(X_tr, y_tr)
p = gs.best_estimator_.predict_proba(X_te)[:, 1]
probas[test_idx] = p
outer_scores.append(roc_auc_score(y_te, p))
outer_scores = np.array(outer_scores)
print("Outer-CV AUC mean:", outer_scores.mean(), "std:", outer_scores.std(ddof=1))
# Bootstrap CI (fold 평균에 대한 CI)
rng = np.random.default_rng(42)
B = 2000
boot_means = []
for _ in range(B):
sample = rng.choice(outer_scores, size=len(outer_scores), replace=True)
boot_means.append(sample.mean())
lo, hi = np.percentile(boot_means, [2.5, 97.5])
print(f"Bootstrap 95% CI of AUC mean: [{lo:.4f}, {hi:.4f}]")
LOOCV는 소규모 데이터에서만 권장한다.
# LOOCV 예시(계산량 주의)
from sklearn.model_selection import LeaveOneOut, cross_val_score
loo = LeaveOneOut()
score = cross_val_score(pipe, X[:200], y[:200], cv=loo, scoring="roc_auc", n_jobs=-1)
print("LOOCV AUC mean (first 200 samples):", score.mean())
운영 시에는 GridSearchCV의 n_jobs=-1을 통한 병렬화를 사용할 수 있지만, 메모리 사용량을 모니터링해야 한다. 파이프라인 외부에서 전처리를 fit하지 않으며, 시계열에서는 TimeSeriesSplit을 사용하고 셔플하지 않는다.