046 회귀에서의 교차 검증 전략
키워드: 교차 검증, cross validation, 시계열 분할
개요
회귀 문제에서 교차 검증은 모델의 일반화 성능을 평가하는 핵심 기법입니다. 특히 시계열 특성이 있는 데이터에서는 특별한 분할 전략이 필요합니다. 이 글에서는 다양한 교차 검증 전략을 알아봅니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
flaml[automl], scikit-learn, numpy
pip install flaml[automl] scikit-learn numpy matplotlib
기본 K-Fold 교차 검증
K-Fold 개념
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import KFold, cross_val_score
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import RandomForestRegressor
# 046 데이터 준비
data = fetch_california_housing()
X, y = data.data, data.target
# 046 K-Fold 시각화
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
print("5-Fold Cross Validation:")
for fold, (train_idx, val_idx) in enumerate(kfold.split(X), 1):
print(f" Fold {fold}: Train={len(train_idx)}, Val={len(val_idx)}")
FLAML에서 n_splits 설정
from flaml import AutoML
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
)
# 046 교차 검증 폴드 수 설정
automl = AutoML()
automl.fit(
X_train, y_train,
task="regression",
time_budget=60,
n_splits=5, # 5-Fold CV
metric="r2",
verbose=1
)
print(f"\n최적 모델: {automl.best_estimator}")
print(f"검증 R²: {1 - automl.best_loss:.4f}")
시계열 분할 (Time Series Split)
시간 순서가 중요한 경우
from sklearn.model_selection import TimeSeriesSplit
import pandas as pd
from datetime import datetime, timedelta
# 046 시계열 데이터 시뮬레이션
np.random.seed(42)
n_samples = 365
dates = [datetime(2023, 1, 1) + timedelta(days=i) for i in range(n_samples)]
values = np.cumsum(np.random.randn(n_samples)) + np.arange(n_samples) * 0.1
df = pd.DataFrame({'date': dates, 'value': values})
# 046 TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
print("Time Series Split:")
for fold, (train_idx, val_idx) in enumerate(tscv.split(df), 1):
train_start = df.iloc[train_idx[0]]['date'].strftime('%Y-%m-%d')
train_end = df.iloc[train_idx[-1]]['date'].strftime('%Y-%m-%d')
val_start = df.iloc[val_idx[0]]['date'].strftime('%Y-%m-%d')
val_end = df.iloc[val_idx[-1]]['date'].strftime('%Y-%m-%d')
print(f" Fold {fold}:")
print(f" Train: {train_start} ~ {train_end} ({len(train_idx)}개)")
print(f" Val: {val_start} ~ {val_end} ({len(val_idx)}개)")
시각화
fig, ax = plt.subplots(figsize=(14, 6))
tscv = TimeSeriesSplit(n_splits=5)
y_pos = 0
for fold, (train_idx, val_idx) in enumerate(tscv.split(df), 1):
ax.barh(y_pos, len(train_idx), left=0, color='blue', alpha=0.6, label='Train' if fold == 1 else '')
ax.barh(y_pos, len(val_idx), left=len(train_idx), color='orange', alpha=0.6, label='Validation' if fold == 1 else '')
y_pos += 1
ax.set_yticks(range(5))
ax.set_yticklabels([f'Fold {i+1}' for i in range(5)])
ax.set_xlabel('Sample Index')
ax.set_title('Time Series Split Visualization')
ax.legend()
plt.tight_layout()
plt.show()
FLAML에서 커스텀 분할 사용
split_type 설정
# 046 FLAML에서 시계열 분할
automl_ts = AutoML()
automl_ts.fit(
X_train, y_train,
task="regression",
time_budget=60,
split_type="time", # 시계열 분할
n_splits=5,
metric="mse",
verbose=1
)
print(f"시계열 분할 R²: {automl_ts.score(X_test, y_test):.4f}")
커스텀 분할 함수
from sklearn.model_selection import BaseCrossValidator
class BlockingTimeSeriesSplit(BaseCrossValidator):
"""블로킹 시계열 분할 (Gap 포함)"""
def __init__(self, n_splits=5, gap=0):
self.n_splits = n_splits
self.gap = gap
def get_n_splits(self, X=None, y=None, groups=None):
return self.n_splits
def split(self, X, y=None, groups=None):
n_samples = len(X)
fold_size = n_samples // (self.n_splits + 1)
for i in range(self.n_splits):
train_end = fold_size * (i + 1)
val_start = train_end + self.gap
val_end = val_start + fold_size
if val_end > n_samples:
break
train_idx = np.arange(train_end)
val_idx = np.arange(val_start, min(val_end, n_samples))
yield train_idx, val_idx
# 046 사용
btscv = BlockingTimeSeriesSplit(n_splits=4, gap=10)
print("\nBlocking Time Series Split (gap=10):")
for fold, (train_idx, val_idx) in enumerate(btscv.split(X), 1):
print(f" Fold {fold}: Train=[0:{train_idx[-1]}], Gap, Val=[{val_idx[0]}:{val_idx[-1]}]")
교차 검증 전략 비교
from sklearn.model_selection import (
KFold, RepeatedKFold, ShuffleSplit, TimeSeriesSplit
)
from sklearn.metrics import r2_score
# 046 다양한 분할 전략
strategies = {
'KFold (5)': KFold(n_splits=5, shuffle=True, random_state=42),
'KFold (10)': KFold(n_splits=10, shuffle=True, random_state=42),
'RepeatedKFold (5x3)': RepeatedKFold(n_splits=5, n_repeats=3, random_state=42),
'ShuffleSplit (5)': ShuffleSplit(n_splits=5, test_size=0.2, random_state=42),
'TimeSeriesSplit (5)': TimeSeriesSplit(n_splits=5)
}
# 046 비교
model = RandomForestRegressor(n_estimators=50, random_state=42)
print("교차 검증 전략 비교:")
print("-" * 50)
for name, cv in strategies.items():
scores = cross_val_score(model, X, y, cv=cv, scoring='r2')
print(f"{name}:")
print(f" 평균 R²: {scores.mean():.4f} (±{scores.std():.4f})")
그룹 기반 분할
GroupKFold
from sklearn.model_selection import GroupKFold
# 046 그룹 정보 (예: 지역별)
n_samples = len(X)
groups = np.random.choice(['A', 'B', 'C', 'D', 'E'], n_samples)
gkf = GroupKFold(n_splits=5)
print("\nGroup K-Fold (같은 그룹은 같은 폴드):")
for fold, (train_idx, val_idx) in enumerate(gkf.split(X, y, groups), 1):
train_groups = set(groups[train_idx])
val_groups = set(groups[val_idx])
print(f" Fold {fold}: Train groups={train_groups}, Val groups={val_groups}")
최적 폴드 수 찾기
def find_optimal_n_splits(X, y, model, max_splits=20):
"""최적 폴드 수 찾기"""
results = []
for n_splits in range(2, max_splits + 1):
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kf, scoring='r2')
results.append({
'n_splits': n_splits,
'mean_r2': scores.mean(),
'std_r2': scores.std(),
'stability': scores.mean() / (scores.std() + 1e-6)
})
return pd.DataFrame(results)
# 046 테스트
model = RandomForestRegressor(n_estimators=50, random_state=42)
results_df = find_optimal_n_splits(X[:2000], y[:2000], model, max_splits=15)
print("\n폴드 수별 성능:")
print(results_df.to_string(index=False))
# 046 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].errorbar(results_df['n_splits'], results_df['mean_r2'],
yerr=results_df['std_r2'], capsize=3)
axes[0].set_xlabel('Number of Folds')
axes[0].set_ylabel('R² Score')
axes[0].set_title('R² vs Number of Folds')
axes[1].plot(results_df['n_splits'], results_df['std_r2'], 'o-')
axes[1].set_xlabel('Number of Folds')
axes[1].set_ylabel('Standard Deviation')
axes[1].set_title('Variance vs Number of Folds')
plt.tight_layout()
plt.show()
교차 검증 선택 가이드
guide = {
'데이터 유형': ['일반 데이터', '시계열 데이터', '그룹 데이터', '작은 데이터셋'],
'권장 방법': ['KFold', 'TimeSeriesSplit', 'GroupKFold', 'RepeatedKFold'],
'n_splits': ['5-10', '3-5', '그룹 수', '5 (repeat 3-5)'],
'이유': [
'표준적인 방법',
'미래 데이터 누출 방지',
'그룹 독립성 유지',
'분산 감소'
]
}
print("\n교차 검증 선택 가이드:")
print(pd.DataFrame(guide).to_string(index=False))
정리
- K-Fold: 일반적인 데이터에 적합, n_splits=5 또는 10 권장
- TimeSeriesSplit: 시계열 데이터에 필수, 미래 데이터 누출 방지
- GroupKFold: 그룹이 있는 데이터 (예: 사용자별, 지역별)
- RepeatedKFold: 작은 데이터셋에서 분산 감소
- FLAML의
split_type="time"으로 시계열 분할 가능 - 폴드 수가 많을수록 안정적이지만 계산 비용 증가
다음 글 예고
다음 글에서는 잔차 분석과 모델 진단에 대해 알아보겠습니다. 회귀 모델의 가정을 검증하고 문제를 진단하는 방법을 다룹니다.
FLAML AutoML 마스터 시리즈 #046