교차검증과 하이퍼파라미터 튜닝

교차검증과 하이퍼파라미터 튜닝

개요

교차검증은 모델의 일반화 성능을 더 정확하게 평가하고, 하이퍼파라미터 튜닝은 최적의 모델 설정을 찾는 과정입니다.


1. 교차검증 (Cross-Validation)

1.1 K-Fold 교차검증

import numpy as np
from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

# 데이터 로드
iris = load_iris()
X, y = iris.data, iris.target

# 모델 생성
model = LogisticRegression(max_iter=1000)

# K-Fold 교차검증
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

print("K-Fold 교차검증 (K=5)")
print(f"각 폴드 점수: {scores}")
print(f"평균 정확도: {scores.mean():.4f}")
print(f"표준편차: {scores.std():.4f}")
print(f"95% 신뢰구간: {scores.mean():.4f} (+/- {scores.std() * 2:.4f})")

1.2 Stratified K-Fold

from sklearn.model_selection import StratifiedKFold

# 클래스 비율 유지
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')

print("\nStratified K-Fold")
print(f"평균 정확도: {scores.mean():.4f}")

# 각 폴드의 클래스 분포 확인
print("\n각 폴드의 클래스 분포:")
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), 1):
    train_classes = np.bincount(y[train_idx])
    val_classes = np.bincount(y[val_idx])
    print(f"  Fold {fold}: Train={train_classes}, Val={val_classes}")

1.3 다양한 교차검증 방법

from sklearn.model_selection import (
    LeaveOneOut,
    LeavePOut,
    ShuffleSplit,
    RepeatedKFold,
    RepeatedStratifiedKFold
)

# Leave-One-Out (LOO)
loo = LeaveOneOut()
print(f"LOO 분할 수: {loo.get_n_splits(X)}")  # 데이터 수와 동일

# Shuffle Split (랜덤 분할)
ss = ShuffleSplit(n_splits=5, test_size=0.2, random_state=42)
scores = cross_val_score(model, X, y, cv=ss)
print(f"\nShuffle Split 평균: {scores.mean():.4f}")

# Repeated K-Fold (반복)
rkf = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42)
scores = cross_val_score(model, X, y, cv=rkf)
print(f"Repeated K-Fold 평균: {scores.mean():.4f}")
print(f"Repeated K-Fold 총 분할 수: {len(scores)}")  # 5 * 10 = 50

1.4 시계열 교차검증

from sklearn.model_selection import TimeSeriesSplit

# 시계열 데이터용 (과거 → 미래 예측)
tscv = TimeSeriesSplit(n_splits=5)

print("Time Series Split:")
for fold, (train_idx, test_idx) in enumerate(tscv.split(X), 1):
    print(f"  Fold {fold}: Train=[{train_idx[0]}:{train_idx[-1]}], Test=[{test_idx[0]}:{test_idx[-1]}]")

2. cross_val_score vs cross_validate

from sklearn.model_selection import cross_validate

# 여러 지표 동시 평가
scoring = ['accuracy', 'precision_weighted', 'recall_weighted', 'f1_weighted']

cv_results = cross_validate(
    model, X, y,
    cv=5,
    scoring=scoring,
    return_train_score=True
)

print("cross_validate 결과:")
for metric in scoring:
    train_key = f'train_{metric}'
    test_key = f'test_{metric}'
    print(f"\n{metric}:")
    print(f"  Train: {cv_results[train_key].mean():.4f} (+/- {cv_results[train_key].std():.4f})")
    print(f"  Test:  {cv_results[test_key].mean():.4f} (+/- {cv_results[test_key].std():.4f})")

# 학습 시간 정보
print(f"\n평균 학습 시간: {cv_results['fit_time'].mean():.4f}초")
print(f"평균 예측 시간: {cv_results['score_time'].mean():.4f}초")

3. 하이퍼파라미터 튜닝

from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler

# 데이터 준비
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target

# 스케일링
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 하이퍼파라미터 그리드
param_grid = {
    'C': [0.1, 1, 10, 100],
    'gamma': [1, 0.1, 0.01, 0.001],
    'kernel': ['rbf', 'linear']
}

# Grid Search
grid_search = GridSearchCV(
    SVC(),
    param_grid,
    cv=5,
    scoring='accuracy',
    verbose=1,
    n_jobs=-1  # 모든 CPU 사용
)

grid_search.fit(X_scaled, y)

print("\nGrid Search 결과:")
print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최적 점수: {grid_search.best_score_:.4f}")

# 모든 결과 확인
import pandas as pd
results = pd.DataFrame(grid_search.cv_results_)
print(f"\n상위 5개 조합:")
print(results.nsmallest(5, 'rank_test_score')[['params', 'mean_test_score', 'std_test_score', 'rank_test_score']])
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform, randint

# 하이퍼파라미터 분포
param_distributions = {
    'C': uniform(0.1, 100),  # 0.1 ~ 100.1 균등 분포
    'gamma': uniform(0.001, 1),
    'kernel': ['rbf', 'linear', 'poly']
}

# Randomized Search
random_search = RandomizedSearchCV(
    SVC(),
    param_distributions,
    n_iter=50,  # 50개 조합 시도
    cv=5,
    scoring='accuracy',
    random_state=42,
    n_jobs=-1
)

random_search.fit(X_scaled, y)

print("Randomized Search 결과:")
print(f"최적 파라미터: {random_search.best_params_}")
print(f"최적 점수: {random_search.best_score_:.4f}")
"""
Grid Search:
- 장점: 모든 조합 탐색, 최적해 보장 (그리드 내에서)
- 단점: 조합 수가 기하급수적으로 증가

Randomized Search:
- 장점: 계산 효율적, 연속 분포 탐색 가능
- 단점: 최적해 보장 없음

선택 기준:
- 파라미터 수 적고 범위 명확 → Grid Search
- 파라미터 수 많거나 범위 불확실 → Randomized Search
"""

4. 고급 튜닝 기법

from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingGridSearchCV

# 자원을 점진적으로 할당하며 탐색
halving_search = HalvingGridSearchCV(
    SVC(),
    param_grid,
    cv=5,
    factor=3,  # 각 라운드에서 후보 1/3로 축소
    resource='n_samples',
    random_state=42
)

halving_search.fit(X_scaled, y)

print("Halving Grid Search 결과:")
print(f"최적 파라미터: {halving_search.best_params_}")
print(f"최적 점수: {halving_search.best_score_:.4f}")

4.2 Bayesian Optimization (Optuna)

# pip install optuna

import optuna
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier

def objective(trial):
    # 하이퍼파라미터 제안
    n_estimators = trial.suggest_int('n_estimators', 10, 200)
    max_depth = trial.suggest_int('max_depth', 2, 32)
    min_samples_split = trial.suggest_int('min_samples_split', 2, 20)
    min_samples_leaf = trial.suggest_int('min_samples_leaf', 1, 10)

    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        min_samples_split=min_samples_split,
        min_samples_leaf=min_samples_leaf,
        random_state=42
    )

    scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
    return scores.mean()

# 최적화 실행
# study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)

# print(f"최적 파라미터: {study.best_params}")
# print(f"최적 점수: {study.best_value:.4f}")

5. 중첩 교차검증 (Nested CV)

from sklearn.model_selection import cross_val_score, GridSearchCV

# 외부 루프: 모델 평가
# 내부 루프: 하이퍼파라미터 튜닝

# 내부 CV (하이퍼파라미터 튜닝)
param_grid = {'C': [0.1, 1, 10], 'gamma': [0.1, 0.01]}
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
grid_search = GridSearchCV(SVC(), param_grid, cv=inner_cv, scoring='accuracy')

# 외부 CV (모델 평가)
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
nested_scores = cross_val_score(grid_search, X_scaled, y, cv=outer_cv, scoring='accuracy')

print("중첩 교차검증 결과:")
print(f"각 외부 폴드 점수: {nested_scores}")
print(f"평균 점수: {nested_scores.mean():.4f} (+/- {nested_scores.std():.4f})")

# 비교: 일반 CV vs 중첩 CV
grid_search.fit(X_scaled, y)
print(f"\n일반 CV 최적 점수: {grid_search.best_score_:.4f}")
print(f"중첩 CV 평균 점수: {nested_scores.mean():.4f}")
# 중첩 CV가 더 현실적인 일반화 성능 추정

6. 파이프라인과 함께 사용

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

# 파이프라인 정의
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('svm', SVC())
])

# 파라미터 이름: step__parameter
param_grid = {
    'svm__C': [0.1, 1, 10],
    'svm__gamma': [0.1, 0.01, 0.001],
    'svm__kernel': ['rbf', 'linear']
}

grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy')
grid_search.fit(X, y)

print("파이프라인 Grid Search 결과:")
print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최적 점수: {grid_search.best_score_:.4f}")

7. 실전 팁

7.1 스코어링 함수

from sklearn.metrics import make_scorer, f1_score, mean_squared_error

# 내장 스코어링
# 분류: 'accuracy', 'precision', 'recall', 'f1', 'roc_auc'
# 회귀: 'r2', 'neg_mean_squared_error', 'neg_mean_absolute_error'

# 커스텀 스코어링 함수
def custom_score(y_true, y_pred):
    return f1_score(y_true, y_pred, average='weighted')

custom_scorer = make_scorer(custom_score)

scores = cross_val_score(model, X, y, cv=5, scoring=custom_scorer)
print(f"커스텀 스코어: {scores.mean():.4f}")

7.2 조기 종료 콜백

# Optuna에서 조기 종료
# import optuna

# def objective(trial):
#     # ...
#     for epoch in range(100):
#         accuracy = train_epoch()
#         trial.report(accuracy, epoch)
#         if trial.should_prune():
#             raise optuna.TrialPruned()
#     return accuracy

# study = optuna.create_study(direction='maximize',
#                            pruner=optuna.pruners.MedianPruner())

7.3 결과 저장

import joblib
import json

# 최적 모델 저장
best_model = grid_search.best_estimator_
joblib.dump(best_model, 'best_model.pkl')

# 결과 저장
results = {
    'best_params': grid_search.best_params_,
    'best_score': grid_search.best_score_,
    'cv_results': {k: v.tolist() if isinstance(v, np.ndarray) else v
                   for k, v in grid_search.cv_results_.items()}
}

with open('tuning_results.json', 'w') as f:
    json.dump(results, f, indent=2)

연습 문제

문제 1: K-Fold 교차검증

Iris 데이터로 10-Fold 교차검증을 수행하세요.

from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

iris = load_iris()
model = LogisticRegression(max_iter=1000)

# 풀이
scores = cross_val_score(model, iris.data, iris.target, cv=10)
print(f"평균 정확도: {scores.mean():.4f} (+/- {scores.std():.4f})")

로지스틱 회귀의 C 파라미터를 튜닝하세요.

from sklearn.model_selection import GridSearchCV

param_grid = {'C': [0.01, 0.1, 1, 10, 100]}

# 풀이
grid = GridSearchCV(LogisticRegression(max_iter=1000), param_grid, cv=5)
grid.fit(iris.data, iris.target)
print(f"최적 C: {grid.best_params_['C']}")
print(f"최적 점수: {grid.best_score_:.4f}")

요약

기법 용도 특징
K-Fold 모델 평가 데이터를 K개로 분할
Stratified K-Fold 불균형 데이터 클래스 비율 유지
Time Series Split 시계열 시간 순서 유지
Grid Search 파라미터 튜닝 모든 조합 탐색
Randomized Search 파라미터 튜닝 랜덤 샘플링
Nested CV 신뢰성 높은 평가 튜닝과 평가 분리
to navigate between lessons