032 SHAP으로 모델 해석하기
키워드: SHAP, 모델 해석, XAI
개요
SHAP(SHapley Additive exPlanations)은 게임 이론의 Shapley 값을 기반으로 모델의 예측을 설명하는 강력한 도구입니다. 이 글에서는 FLAML 모델에 SHAP을 적용하여 예측을 해석하는 방법을 알아봅니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
flaml[automl], shap, scikit-learn
pip install flaml[automl] shap scikit-learn matplotlib
SHAP이란?
기본 개념
SHAP은 각 특성이 예측에 기여하는 정도를 계산합니다.
# 032 SHAP의 핵심 아이디어
# 032 예측값 = 기본값 + 특성1 기여 + 특성2 기여 + ... + 특성n 기여
# 032 예시
base_value = 0.5 # 평균 예측
shap_values = [0.1, -0.2, 0.15] # 각 특성의 기여
prediction = base_value + sum(shap_values) # 0.55
print(f"기본값: {base_value}")
print(f"SHAP 값: {shap_values}")
print(f"예측값: {prediction}")
특성 중요도 vs SHAP
| 항목 | 특성 중요도 | SHAP |
|---|---|---|
| 수준 | 전역(Global) | 전역 + 지역(Local) |
| 방향성 | 없음 | 양/음 방향 제공 |
| 해석 | 중요한 정도 | 예측에 대한 기여도 |
| 계산 | 빠름 | 상대적으로 느림 |
데이터 및 모델 준비
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from flaml import AutoML
# 032 데이터 준비
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 032 FLAML 학습 (트리 기반 모델)
automl = AutoML()
automl.fit(
X_train, y_train,
task="classification",
time_budget=60,
estimator_list=["lgbm", "xgboost"],
verbose=0
)
print(f"최적 모델: {automl.best_estimator}")
print(f"테스트 정확도: {automl.score(X_test, y_test):.4f}")
SHAP 값 계산
TreeExplainer 사용
import shap
# 032 SHAP 설명자 생성 (트리 모델용)
explainer = shap.TreeExplainer(automl.best_model)
# 032 SHAP 값 계산
shap_values = explainer.shap_values(X_test)
# 032 SHAP 값 구조 확인
print(f"SHAP 값 shape: {np.array(shap_values).shape}")
print(f"테스트 데이터 shape: {X_test.shape}")
# 032 이진 분류의 경우 shap_values가 리스트일 수 있음
if isinstance(shap_values, list):
# 클래스 1(양성)에 대한 SHAP 값 사용
shap_values_class1 = shap_values[1]
else:
shap_values_class1 = shap_values
전역 해석 (Global Interpretation)
Summary Plot
import matplotlib.pyplot as plt
# 032 Summary Plot - 전체 특성의 중요도와 영향 방향
plt.figure(figsize=(12, 8))
shap.summary_plot(shap_values_class1, X_test, show=False)
plt.title('SHAP Summary Plot')
plt.tight_layout()
plt.show()
Bar Plot (평균 절대 SHAP 값)
# 032 특성별 평균 |SHAP| 값
plt.figure(figsize=(10, 8))
shap.summary_plot(shap_values_class1, X_test, plot_type="bar", show=False)
plt.title('Mean |SHAP| Value')
plt.tight_layout()
plt.show()
# 032 수치로 확인
mean_abs_shap = np.abs(shap_values_class1).mean(axis=0)
shap_importance = pd.DataFrame({
'feature': X.columns,
'mean_abs_shap': mean_abs_shap
}).sort_values('mean_abs_shap', ascending=False)
print("SHAP 기반 특성 중요도 (상위 10개):")
print(shap_importance.head(10).to_string(index=False))
지역 해석 (Local Interpretation)
단일 예측 설명
# 032 특정 샘플 선택
sample_idx = 0
sample = X_test.iloc[[sample_idx]]
# 032 실제 예측
actual = y_test.iloc[sample_idx]
predicted = automl.predict(sample)[0]
prob = automl.predict_proba(sample)[0]
print(f"샘플 #{sample_idx}:")
print(f" 실제: {'양성' if actual == 1 else '음성'}")
print(f" 예측: {'양성' if predicted == 1 else '음성'}")
print(f" 확률: 음성={prob[0]:.4f}, 양성={prob[1]:.4f}")
Waterfall Plot
# 032 단일 예측에 대한 Waterfall Plot
shap.plots.waterfall(shap.Explanation(
values=shap_values_class1[sample_idx],
base_values=explainer.expected_value[1] if isinstance(explainer.expected_value, list) else explainer.expected_value,
data=X_test.iloc[sample_idx],
feature_names=X.columns.tolist()
), show=False)
plt.title(f'Sample #{sample_idx} Prediction Explanation')
plt.tight_layout()
plt.show()
Force Plot
# 032 Force Plot (단일 샘플)
shap.initjs() # JavaScript 초기화 (노트북 환경)
# 032 단일 샘플 Force Plot
if isinstance(explainer.expected_value, list):
base_val = explainer.expected_value[1]
else:
base_val = explainer.expected_value
shap.force_plot(
base_val,
shap_values_class1[sample_idx],
X_test.iloc[sample_idx],
feature_names=X.columns.tolist(),
matplotlib=True
)
plt.title(f'Force Plot - Sample #{sample_idx}')
plt.tight_layout()
plt.show()
특성별 영향 분석
Dependence Plot
# 032 가장 중요한 특성의 dependence plot
top_feature = shap_importance.iloc[0]['feature']
plt.figure(figsize=(10, 6))
shap.dependence_plot(
top_feature, shap_values_class1, X_test,
show=False
)
plt.title(f'SHAP Dependence Plot: {top_feature}')
plt.tight_layout()
plt.show()
여러 특성 비교
# 032 상위 4개 특성의 dependence plot
top_4_features = shap_importance.head(4)['feature'].tolist()
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.ravel()
for i, feature in enumerate(top_4_features):
shap.dependence_plot(
feature, shap_values_class1, X_test,
ax=axes[i], show=False
)
axes[i].set_title(f'{feature}')
plt.tight_layout()
plt.show()
그룹별 SHAP 분석
# 032 예측이 맞은 경우 vs 틀린 경우
y_pred = automl.predict(X_test)
correct_mask = y_pred == y_test.values
incorrect_mask = ~correct_mask
print(f"정확한 예측: {correct_mask.sum()}개")
print(f"오류 예측: {incorrect_mask.sum()}개")
# 032 정확/오류 예측의 SHAP 비교
if incorrect_mask.sum() > 0:
correct_shap = np.abs(shap_values_class1[correct_mask]).mean(axis=0)
incorrect_shap = np.abs(shap_values_class1[incorrect_mask]).mean(axis=0)
comparison = pd.DataFrame({
'feature': X.columns,
'correct_mean_shap': correct_shap,
'incorrect_mean_shap': incorrect_shap,
'diff': incorrect_shap - correct_shap
}).sort_values('diff', ascending=False)
print("\n오류 예측에서 더 중요한 특성:")
print(comparison.head(5).to_string(index=False))
예측 설명 함수
def explain_prediction(model, explainer, X, sample_idx, feature_names):
"""개별 예측 설명"""
sample = X.iloc[[sample_idx]]
shap_vals = explainer.shap_values(sample)
# 이진 분류 처리
if isinstance(shap_vals, list):
shap_vals = shap_vals[1][0]
else:
shap_vals = shap_vals[0]
# 예측 정보
pred = model.predict(sample)[0]
prob = model.predict_proba(sample)[0]
# 기여도 정리
contributions = pd.DataFrame({
'feature': feature_names,
'value': sample.values[0],
'shap': shap_vals
}).sort_values('shap', key=abs, ascending=False)
print(f"=== 예측 설명 (샘플 #{sample_idx}) ===")
print(f"예측: {'양성' if pred == 1 else '음성'} (확률: {prob[1]:.4f})")
print(f"\n주요 기여 특성 (Top 5):")
for _, row in contributions.head(5).iterrows():
direction = "↑" if row['shap'] > 0 else "↓"
print(f" {row['feature']}: {row['value']:.4f} → {direction} {row['shap']:.4f}")
return contributions
# 032 사용 예
explain_prediction(automl.best_model, explainer, X_test, 0, X.columns.tolist())
SHAP과 특성 중요도 비교
# 032 트리 기반 중요도 vs SHAP 중요도
if hasattr(automl.best_model, 'feature_importances_'):
tree_importance = automl.best_model.feature_importances_
comparison = pd.DataFrame({
'feature': X.columns,
'tree_importance': tree_importance / tree_importance.sum(),
'shap_importance': mean_abs_shap / mean_abs_shap.sum()
})
# 순위 비교
comparison['tree_rank'] = comparison['tree_importance'].rank(ascending=False)
comparison['shap_rank'] = comparison['shap_importance'].rank(ascending=False)
comparison['rank_diff'] = abs(comparison['tree_rank'] - comparison['shap_rank'])
print("중요도 순위 비교 (차이가 큰 특성):")
print(comparison.sort_values('rank_diff', ascending=False).head(5).to_string(index=False))
# 산점도
plt.figure(figsize=(8, 8))
plt.scatter(comparison['tree_importance'], comparison['shap_importance'], alpha=0.6)
plt.xlabel('Tree Importance (normalized)')
plt.ylabel('SHAP Importance (normalized)')
plt.title('Tree vs SHAP Feature Importance')
plt.plot([0, 0.3], [0, 0.3], 'r--', label='y=x')
plt.legend()
plt.tight_layout()
plt.show()
정리
- SHAP은 게임 이론 기반의 모델 해석 방법입니다.
- 전역 해석으로 전체적인 특성 중요도를 파악합니다.
- 지역 해석으로 개별 예측의 이유를 설명합니다.
- Summary Plot은 특성별 영향의 분포를 보여줍니다.
- Waterfall Plot은 단일 예측의 구성을 보여줍니다.
- Dependence Plot은 특성 값과 SHAP의 관계를 보여줍니다.
- SHAP은 모델의 투명성과 신뢰성을 높입니다.
다음 글 예고
다음 글에서는 sklearn 파이프라인과 FLAML 연동에 대해 알아보겠습니다. 전처리 단계를 포함한 완전한 ML 파이프라인을 구축하는 방법을 다룹니다.
FLAML AutoML 마스터 시리즈 #032