CS/Python

파이썬으로 모델 검증/해석 찍먹하기

munsik22 2026. 8. 7. 17:35

🧩 교차 검증이란?

교차검증이 필요한 이유

교차 검증(Cross-Validation)은 데이터를 여러 번 나누어 학습과 평가를 반복하고, 그 결과의 평균을 내어 모델의 성능을 정확하게 확인하는 방법이다.

 

이전에 train/test 데이터를 분할할 때 단순하게 80% 훈련 + 20% 테스트 이런 식으로 나누었다. 하지만 데이터를 어떻게 나누느냐에 따라 정확도가 달라질 수 있다. 예를 들어 우연히 테스트 데이터에 쉬운 경기만 많이 포함되면 성능이 높게 나올 수 있다. 교차 검증은 데이터를 여러 번 나눠 학습/검증하여 성능을 더 안정적으로 평가하는 방법이다.

K-Fold Cross-Validation

K-겹 교차 검증(K-fold cross validation)은 전체 데이터를 K개의 부분으로 나눈 뒤, K-1개는 학습용으로 쓰고 나머지 1개는 검증용으로 사용하여 이 과정을 K번 반복하고 성능을 평균 내는 모델 평가 방법이다.

5-Fold 예시 [출처] AskPython

  1. 데이터를 먼저 섞은 뒤, 총 N개의 데이터를 겹치지 않는 K개 그룹으로 분할한다.
  2. 각 그룹이 번갈아 검증셋이 되고, 나머지는 훈련셋이 된다.
  3. K-1개의 훈련셋으로 데이터를 학습한 뒤, 남은 1개의 검증셋으로 검증한다.
  4. K개의 MSE를 평균 내서 테스트 오류를 추정한다.

Stratified K-Fold

계층적 K-겹 교차 검증(Stratified K-Fold)는 분류 모델의 성능을 정확하게 평가하기 위해, 원본 데이터의 레이블(클래스) 비율을 각 Fold에서도 동일하게 유지하도록 데이터를 나누는 교차 검증 방법이다

 

일반적인 K-Fold는 데이터를 무작위로 나누기 때문에 특정 폴드에 정답 데이터가 빠지는 문제가 생길 수 있다. 이러한 문제를 해결하기 위해 등장한 것이 Stratified K-Fold로, 전체 데이터이 정답 비율과 각 분할된 폴드의 정답 비율을 같게 만든다. 데이터의 특정 값이 너무 많거나 적을 때 사용하며, 주로 정답이 범주형인 분류 문제에 쓰인다.

from sklearn.model_selection import StratifiedKFold

cv = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=42
)

교차 검증 결과 확인 및 해석하기

여기서 만든 clf Pipeline을 그대로 사용한다고 가정한다.

from sklearn.model_selection import cross_validate

scoring = {
    "accuracy": "accuracy",
    "f1": "f1",
    "roc_auc": "roc_auc"
}

cv_results = cross_validate(
    estimator=clf,
    X=X_train,
    y=y_train,
    cv=cv,
    scoring=scoring,
    return_train_score=True,
    n_jobs=-1
)

# 교차 검증 결과 확인
for metric in ["accuracy", "f1", "roc_auc"]:
    scores = cv_results[f"test_{metric}"]

    print(f"\n[{metric}]")
    print("각 Fold 점수:", np.round(scores, 4))
    print(f"평균: {scores.mean():.4f}")
    print(f"표준편차: {scores.std():.4f}")
    
'''
[accuracy]
각 Fold 점수: [0.5299 0.5281 0.5336 0.5361 0.528 ]
평균: 0.5311
표준편차: 0.0032

[f1]
각 Fold 점수: [0.5374 0.539  0.5433 0.5508 0.5369]
평균: 0.5415
표준편차: 0.0052

[roc_auc]
각 Fold 점수: [0.5488 0.5377 0.543  0.5436 0.5366]
평균: 0.5420
표준편차: 0.0044
'''

 

평균 정확도가 0.5311이고 표준편차가 0.0032가 나왔다.

  • 훈련 데이터를 서로 다르게 나눠 5번 검증했을 때, 평균적으로 약 53.11% 정도의 정확도를 보였다.
  • 표준편차가 비교적 작아(< 0.05) 모델 성능이 비교적 안정적이라고 볼 수 있다.

Train 점수와 Validation 점수 비교하기

# Train 점수와 Validation 점수 비교
for metric in ["accuracy", "f1", "roc_auc"]:
    train_scores = cv_results[f"train_{metric}"]
    valid_scores = cv_results[f"test_{metric}"]

    print(f"\n[{metric}]")
    print(f"Train 평균: {train_scores.mean():.4f}")
    print(f"Validation 평균: {valid_scores.mean():.4f}")

'''
[accuracy]
Train 평균: 0.6361
Validation 평균: 0.5311

[f1]
Train 평균: 0.6445
Validation 평균: 0.5415

[roc_auc]
Train 평균: 0.6887
Validation 평균: 0.5420
'''

 

훈련 데이터에서는 63~68% 정도를 맞췄지만, 검증 데이터에서는 53~54% 정도를 맞췄다. 약 10% 정도 차이가 나므로 정상이라고 하기에도, 과적합이라고 하기에도 애매하다.

최종 테스트 데이터 평가

교차 검증으로 모델 설정을 확인한 뒤에는 훈련 데이터 전체를 사용해서 모델을 학습한다.

# 최종 테스트 데이터 평가
clf.fit(X_train, y_train)

from sklearn.metrics import accuracy_score, roc_auc_score

y_pred = clf.predict(X_test)
y_prob = clf.predict_proba(X_test)[:, 1]

test_accuracy = accuracy_score(y_test, y_pred)
test_auc = roc_auc_score(y_test, y_prob)

print(f"최종 Test Accuracy: {test_accuracy:.4f}")
print(f"최종 Test ROC-AUC: {test_auc:.4f}")
# 최종 Test Accuracy: 0.5316
# 최종 Test ROC-AUC: 0.5409

하이퍼파라미터 찾기

LogisticRegression()에는 C라는 파라미터가 있다. C는 규제의 강도를 조절하며, C가 작을수록 규제가 강해져 모델이 단순해진다.

# 하이퍼파라미터 찾기
from sklearn.model_selection import GridSearchCV

param_grid = {
    "model__C": [0.01, 0.1, 1, 10, 100] # "단계이름__파라미터이름"
}

grid_search = GridSearchCV(
    estimator=clf,
    param_grid=param_grid,
    cv=cv,
    scoring="roc_auc",
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train, y_train)

print("최적 파라미터:", grid_search.best_params_)
print(f"교차 검증 최고 ROC-AUC: {grid_search.best_score_:.4f}")

# 최적 모델로 테스트 데이터 평가
best_model = grid_search.best_estimator_

y_pred = best_model.predict(X_test)
y_prob = best_model.predict_proba(X_test)[:, 1]

print("Test Accuracy:", accuracy_score(y_test, y_pred))
print("Test ROC-AUC:", roc_auc_score(y_test, y_prob))

'''
Fitting 5 folds for each of 5 candidates, totalling 25 fits
최적 파라미터: {'model__C': 0.1}
교차 검증 최고 ROC-AUC: 0.5446
Test Accuracy: 0.533210332103321
Test ROC-AUC: 0.5447767632563115
'''

🧩 모델 해석하기

로지스틱 회귀 계수로 모델 해석하기

w가 특성의 계수이다.

  • 계수가 양수: 팀1 승리 확률을 높이는 방향
  • 계수가 음수: 팀1 승리 확률을 낮추는 방향
  • 절댓값이 클수록 모델이 더 크게 반응하는 특성

우선 One-Hot Encoding 후 특성 이름을 가져온다.

best_model = grid_search.best_estimator_
preprocessor = best_model.named_steps["preprocessor"]
logistic_model = best_model.named_steps["model"]
feature_names = preprocessor.get_feature_names_out()
coefficients = logistic_model.coef_[0]
coef_df = pd.DataFrame({
    "feature": feature_names,
    "coefficient": coefficients
})
coef_df["abs_coefficient"] = coef_df["coefficient"].abs()

 

팀1 승리에 긍정적인 영향을 준 특성은 다음과 같다.

# 팀1 승리에 긍정적인 영향을 준 특성
top_positive = coef_df.sort_values(
    "coefficient",
    ascending=False
).head(15)
print(top_positive)

'''
                           feature  coefficient  abs_coefficient
1681      categorical__t1_ban1_222     0.468949         0.468949
2530      categorical__t2_ban2_432     0.450617         0.450617
47     categorical__t1_champ1id_50     0.406596         0.406596
604    categorical__t1_champ5id_56     0.399429         0.399429
178    categorical__t1_champ2id_40     0.395065         0.395065
2694       categorical__t2_ban4_21     0.382102         0.382102
228   categorical__t1_champ2id_106     0.377775         0.377775
1105    categorical__t2_champ4id_1     0.373558         0.373558
316    categorical__t1_champ3id_40     0.372024         0.372024
2387      categorical__t2_ban1_420     0.368459         0.368459
2617       categorical__t2_ban3_98     0.352809         0.352809
1834      categorical__t1_ban2_429     0.348614         0.348614
589    categorical__t1_champ5id_37     0.342789         0.342789
2870       categorical__t2_ban5_62     0.342428         0.342428
434    categorical__t1_champ4id_20     0.342226         0.342226
'''
  • categorical__t1_ban1_222: 팀1의 첫 번째 밴 카드로 ID 222가 등장했을 때, 다른 조건이 같다면 팀1 승리 확률이 높아지는 방향으로 모델이 학습했다.
  • categorical__t1_champ1id_50: 팀1의 첫 번째 픽으로 ID 50이 등장했을 때, 다른 조건이 같다면 팀1 승리 확률이 높아지는 방향으로 모델이 학습했다.

팀1 승리에 부정적인 영향을 준 특성은 다음과 같다.

# 팀1 승리에 부정적인 영향을 준 특성
top_negative = coef_df.sort_values(
    "coefficient",
    ascending=True
).head(15)
print(top_negative)

'''
                           feature  coefficient  abs_coefficient
151    categorical__t1_champ2id_13    -0.598514         0.598514
552   categorical__t1_champ4id_516    -0.496437         0.496437
414   categorical__t1_champ3id_516    -0.483014         0.483014
1624       categorical__t1_ban1_69    -0.461521         0.461521
617    categorical__t1_champ5id_74    -0.446998         0.446998
1031   categorical__t2_champ3id_74    -0.415994         0.415994
2874       categorical__t2_ban5_68    -0.403847         0.403847
1925      categorical__t1_ban3_102    -0.388269         0.388269
1613       categorical__t1_ban1_56    -0.386127         0.386127
530   categorical__t1_champ4id_164    -0.382047         0.382047
168    categorical__t1_champ2id_30    -0.381820         0.381820
892    categorical__t2_champ2id_72    -0.380050         0.380050
2217      categorical__t1_ban5_120    -0.376469         0.376469
278     categorical__t1_champ3id_2    -0.375515         0.375515
1965      categorical__t1_ban3_254    -0.371410         0.371410
'''
  • categorical__t1_champ2id_13: 팀1의 두 번째 픽으로 ID 92가 등장했을 때, 팀1 입장에서는 불리한 방향으로 학습되었다.
# 가장 영향력이 큰 계수 시각화
top_features = coef_df.nlargest(15, "abs_coefficient")

plt.figure(figsize=(10, 7))

sns.barplot(
    data=top_features,
    x="coefficient",
    y="feature",
    hue="coefficient",
    palette="coolwarm",
    legend=False
)

plt.axvline(0, color="black", linestyle="--")
plt.title("로지스틱 회귀의 영향력이 큰 특성 Top 15")
plt.xlabel("계수")
plt.ylabel("One-Hot Encoding 특성")
plt.tight_layout()
plt.show()

오른쪽 막대: 긍정적인 영향 / 왼쪽 막대: 부정적인 영향

Permutation Importance로 해석하기

계수 해석은 One-Hot Encoding된 개별 특성을 중심으로 본다 하지만 원래 컬럼 단위로 "어떤 입력 변수가 중요한가?"를 보고 싶다면 permutation_importance를 사용할 수 있다.

  1. 원래 모델 성능을 측정
  2. 특정 컬럼의 값을 무작위로 섞음
  3. 성능이 얼마나 떨어지는지 측정
  4. 성능이 많이 떨어질수록 중요한 변수라고 판단
# Permutation Importance로 해석하기
from sklearn.inspection import permutation_importance

perm_result = permutation_importance(
    estimator=best_model,
    X=X_test,
    y=y_test,
    scoring="roc_auc",
    n_repeats=10,
    random_state=42,
    n_jobs=-1
)

importance_df = pd.DataFrame({
    "feature": X_test.columns,
    "importance_mean": perm_result.importances_mean,
    "importance_std": perm_result.importances_std
})

importance_df = importance_df.sort_values(
    "importance_mean",
    ascending=False
)

importance_df.head(15)

top_importance = importance_df.head(15)

plt.figure(figsize=(10, 6))

sns.barplot(
    data=top_importance,
    x="importance_mean",
    y="feature",
    hue="feature",
    legend=False,
    palette="viridis"
)

plt.title("Permutation Importance Top 15")
plt.xlabel("ROC-AUC 감소량")
plt.ylabel("원본 특성")
plt.show()

importance_mean이 크다면 "해당 컬럼을 섞었더니 모델의 ROC-AUC가 크게 떨어졌다. 즉, 모델이 그 컬럼 정보를 예측에 유의미하게 사용하고 있다"는 뜻이다.

모델 해석 시 주의할 점

  • 계수가 인과관계를 의미하지 않는다: categorical__t1_champ1id_50의 계수가 높게 나왔지만, 그것이 "스웨인(ID 50)을 픽하면 반드시 승리한다"는 의미는 아니다.
  • 챔피언 슬롯의 의미를 확인해야 한다: t1_champ1id가 항상 탑라이너인지, 첫번째 픽을 하는 사람인지, 아니면 랜덤인지 확인해야 한다. (여기서는 '첫번째 픽을 하는 사람'으로 보고 해석했다.)
  • 챔피언 조합 시너지는 로지스틱 회귀로 잘 표현하지 못할 수 있다: "챔피언 A를 상대로 챔피언 B가 카운터다"와 같은 복잡한 상호작용은 신경망 모델 등을 사용해 학습해야 한다.

🧩 전체 검증 흐름 요약

  1. 전체 데이터를 train / test로 분리
  2. train 데이터만 사용하여 5-Fold 교차 검증
  3. 평균 Accuracy, F1, ROC-AUC 확인
  4. GridSearchCV로 최적 C 값 찾기
  5. 최적 모델을 train 전체 데이터로 학습
  6. test 데이터로 최종 성능 평가
  7. 계수와 Permutation Importance로 모델 해석