🤖AI/ML

SHAP

SHapley Additive exPlanations

게임 이론 기반 모델 해석 기법. 각 피처가 예측에 기여한 정도를 정량화. XAI(설명 가능한 AI)의 핵심 도구.

📖 상세 설명

SHAP(SHapley Additive exPlanations)는 게임 이론의 Shapley Value를 머신러닝에 적용한 모델 해석 기법입니다. 각 피처가 개별 예측에 얼마나 기여했는지 수치로 정량화하여 "왜 이 예측이 나왔는지"를 설명합니다. 예를 들어 대출 거절 시 "소득이 -0.3, 신용점수가 -0.25 기여했다"처럼 정량적 근거를 제시할 수 있습니다.

2017년 워싱턴 대학의 Scott Lundberg가 NIPS 논문에서 발표했고, 현재 XAI(Explainable AI) 분야에서 가장 널리 사용되는 기법입니다. LIME(Local Interpretable Model-agnostic Explanations)과 달리 이론적 기반이 탄탄하고, 일관성(Consistency), 지역 정확성(Local Accuracy), 효율성(Efficiency), 대칭성(Symmetry) 등 Shapley Value의 공리적 성질을 상속받습니다.

핵심 아이디어는 각 피처를 "플레이어"로, 예측 결과를 "보상"으로 보고, 각 플레이어가 팀(모델)에 기여한 정도를 게임 이론적으로 공정하게 배분하는 것입니다. 수학적으로 SHAP 값은 모든 가능한 피처 부분집합(2^n개)에 대한 marginal contribution의 가중 평균입니다. 계산 비용이 지수적이지만, TreeSHAP(트리 기반 모델, O(TL^2)), DeepSHAP(딥러닝), KernelSHAP(모델 불가지론) 등 효율적인 근사 알고리즘이 있습니다.

금융, 의료, 법률, 보험 등 규제 산업에서 모델 설명 의무가 있을 때 필수로 사용됩니다. EU GDPR의 "설명 받을 권리"(Right to Explanation), 미국 FCRA/ECOA의 대출 거절 사유 고지 의무, 금융감독원의 AI 모델 검증 가이드라인 등에 대응할 수 있습니다. 또한 모델 디버깅, 피처 엔지니어링 검증, 데이터 드리프트 탐지에도 활용됩니다. 최근에는 LLM의 토큰 수준 기여도 분석에도 SHAP이 확장 적용되고 있습니다.

💻 코드 예제

import shap
import xgboost as xgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

# 데이터 로드 및 분할
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, random_state=42
)

# XGBoost 모델 학습
model = xgb.XGBClassifier(n_estimators=100, max_depth=4)
model.fit(X_train, y_train)

# SHAP Explainer 생성 (TreeSHAP 사용)
explainer = shap.TreeExplainer(model)

# 테스트 데이터에 대한 SHAP 값 계산
shap_values = explainer.shap_values(X_test)

# 전체 피처 중요도 시각화 (Summary Plot)
shap.summary_plot(shap_values, X_test, feature_names=data.feature_names)

# 개별 예측 설명 (Force Plot)
shap.force_plot(
    explainer.expected_value,
    shap_values[0],
    X_test[0],
    feature_names=data.feature_names
)

# Waterfall Plot (단일 샘플 상세 분석)
shap.waterfall_plot(
    shap.Explanation(
        values=shap_values[0],
        base_values=explainer.expected_value,
        data=X_test[0],
        feature_names=data.feature_names
    )
)

# SHAP 값 해석:
# - 양수: 해당 피처가 예측을 높이는 방향으로 기여
# - 음수: 해당 피처가 예측을 낮추는 방향으로 기여
# - 절대값: 기여의 크기

# === DeepSHAP: 딥러닝 모델 설명 ===
import tensorflow as tf

# 딥러닝 모델 생성
deep_model = tf.keras.Sequential([
    tf.keras.layers.Dense(64, activation='relu', input_shape=(30,)),
    tf.keras.layers.Dense(32, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
deep_model.compile(optimizer='adam', loss='binary_crossentropy')
deep_model.fit(X_train, y_train, epochs=10, verbose=0)

# DeepExplainer 사용
deep_explainer = shap.DeepExplainer(deep_model, X_train[:100])
deep_shap_values = deep_explainer.shap_values(X_test[:10])

# === KernelSHAP: 모델 불가지론 (어떤 모델이든 적용) ===
def model_predict(X):
    """블랙박스 모델의 예측 함수"""
    return model.predict_proba(X)[:, 1]

kernel_explainer = shap.KernelExplainer(model_predict, shap.sample(X_train, 100))
kernel_shap_values = kernel_explainer.shap_values(X_test[:5], nsamples=500)

# === 전역 피처 중요도 (Global Feature Importance) ===
# 모든 샘플에 대한 SHAP 값의 절대값 평균
global_importance = np.abs(shap_values).mean(axis=0)
feature_ranking = sorted(
    zip(data.feature_names, global_importance),
    key=lambda x: x[1], reverse=True
)
print("전역 피처 중요도 Top 5:")
for name, importance in feature_ranking[:5]:
    print(f"  {name}: {importance:.4f}")

# === Dependence Plot: 피처 간 상호작용 분석 ===
shap.dependence_plot(
    "mean radius",
    shap_values, X_test,
    feature_names=data.feature_names,
    interaction_index="mean texture"  # 상호작용 피처
)

# === Force Plot: 개별 예측 설명 ===
# 단일 샘플의 SHAP 값을 막대 그래프로 표시
shap.force_plot(
    explainer.expected_value,
    shap_values[0],
    X_test[0],
    feature_names=data.feature_names,
    matplotlib=True
)

# === Waterfall Plot: 기준값에서 예측까지 ===
# 각 피처가 예측을 어떻게 밀어올리고 내리는지 시각화
shap.waterfall_plot(
    shap.Explanation(
        values=shap_values[0],
        base_values=explainer.expected_value,
        data=X_test[0],
        feature_names=data.feature_names
    )
)

# === Batch 처리 및 저장 ===
# 대규모 데이터셋에서 효율적으로 SHAP 계산
def compute_shap_batch(model, X, batch_size=100):
    """배치 단위로 SHAP 값 계산"""
    all_shap_values = []
    explainer = shap.TreeExplainer(model)

    for i in range(0, len(X), batch_size):
        batch = X[i:i+batch_size]
        shap_values = explainer.shap_values(batch)
        all_shap_values.append(shap_values)

    return np.concatenate(all_shap_values, axis=0)

# SHAP 값 저장 및 로드
import pickle
with open('shap_values.pkl', 'wb') as f:
    pickle.dump(shap_values, f)

# === LLM/Transformer 모델 SHAP ===
# Transformer 기반 모델에도 SHAP 적용 가능
# PartitionExplainer: 토큰 수준 기여도 분석

import shap

def explain_text_classification(text, model, tokenizer):
    """텍스트 분류 모델의 SHAP 분석"""

    def predict_fn(texts):
        inputs = tokenizer(texts, return_tensors='pt', padding=True)
        outputs = model(**inputs)
        return outputs.logits.softmax(dim=-1).detach().numpy()

    explainer = shap.Explainer(predict_fn, tokenizer)
    shap_values = explainer([text])
    return shap_values

# SHAP은 모델 디버깅과 규제 대응의 필수 도구입니다

🗣️ 실무에서 이렇게 말하세요

💬 모델 검증 회의에서
"금융감독원 검사 대응하려면 SHAP 리포트 필수예요. 대출 거절 사유를 '신용점수가 낮아서'가 아니라 'SHAP 분석 결과 신용점수가 -0.35, 연체 이력이 -0.28 기여'처럼 정량적으로 제시해야 합니다."
💬 면접에서
"SHAP은 게임 이론의 Shapley Value를 ML에 적용한 기법입니다. 각 피처의 기여도를 모든 가능한 피처 조합에 대해 평균하므로 이론적으로 공정합니다. TreeSHAP을 쓰면 트리 기반 모델에서 O(TL^2)로 효율적인 계산이 가능해요."
💬 디버깅 상황에서
"모델이 이상한 예측 내는 케이스 있으면 SHAP waterfall 플롯 보세요. 어떤 피처가 예측을 얼마나 밀어올렸는지 바로 보여요. 데이터 이상치인지, 피처 엔지니어링 문제인지 바로 구분됩니다."

⚠️ 흔한 실수 & 주의사항

계산 비용 무시

Exact SHAP은 피처 수에 지수적으로 증가합니다. 100개 피처면 계산 불가능. TreeSHAP, KernelSHAP 등 근사 알고리즘을 사용하세요.

상관 피처 해석 오류

높은 상관관계를 가진 피처들은 SHAP 값이 분산됩니다. 피처 A와 B가 0.95 상관이면 기여도가 둘 사이에 나뉘어 개별 해석이 어렵습니다.

올바른 방법

background 데이터를 대표성 있게 샘플링하고, 피처 상관관계를 먼저 분석하세요. 규제 대응 시 SHAP 값과 함께 피처 정의, 데이터 범위도 문서화합니다.

🔗 관련 용어

📚 더 배우기