콘텐츠로 이동
Data Prep
상세

Explainable AI (XAI)

개요

항목 내용
분류 Machine Learning / Interpretability / Trustworthy AI
핵심 개념 모델의 예측 근거를 인간이 이해할 수 있도록 설명하는 기법
동기 블랙박스 모델의 신뢰성, 디버깅, 규제 준수
관련 분야 Fairness, Accountability, Transparency (FAT), Mechanistic Interpretability
주요 라이브러리 shap, lime, captum, alibi, InterpretML

XAI 분류 체계

분류 축

XAI 분류
============================================================

[범위]               [대상]              [시점]
Global vs Local      Model-agnostic      Post-hoc
                     vs                  vs
전체 모델 행동       Model-specific      Intrinsic (ante-hoc)

[출력 형태]
Feature Attribution | Rule-based | Example-based | Concept-based

분류 기준 비교

기준 범주 설명 예시
범위 Global 모델 전체 행동 패턴 PDP, ALE, Global SHAP
Local 개별 예측 설명 LIME, Local SHAP, Grad-CAM
대상 Model-agnostic 어떤 모델에든 적용 SHAP, LIME, PDP
Model-specific 특정 모델 구조 활용 TreeSHAP, Grad-CAM, Attention
시점 Post-hoc 학습 완료 후 사후 설명 SHAP, LIME
Intrinsic 모델 자체가 해석 가능 선형 회귀, 결정 트리, GAM

Model-Agnostic 방법

1. SHAP (SHapley Additive exPlanations)

항목 내용
논문 Lundberg & Lee, "A Unified Approach to Interpreting Model Predictions" (NeurIPS 2017)
이론 기반 Shapley value (협조 게임 이론, 1953)
핵심 성질 유일하게 효율성, 대칭성, 더미, 가법성을 모두 만족하는 기여도 할당

Shapley Value 정의:

\[\phi_j = \sum_{S \subseteq N \setminus \{j\}} \frac{|S|!(|N|-|S|-1)!}{|N|!} \left[ f(S \cup \{j\}) - f(S) \right]\]
  • \(N\): 전체 특성 집합
  • \(S\): \(j\)를 제외한 특성의 부분집합
  • \(f(S)\): 특성 집합 \(S\)만 사용할 때의 모델 출력 기대값

SHAP의 가법 모델:

\[g(z') = \phi_0 + \sum_{j=1}^{M} \phi_j z_j'\]
  • \(z' \in \{0, 1\}^M\): 특성 포함/제외 이진 벡터
  • \(\phi_0 = E[f(X)]\): 기저값 (데이터 전체 평균 예측)
  • 효율성: \(\sum_{j=0}^{M} \phi_j = f(x)\)

SHAP 변형 비교

변형 적용 모델 시간 복잡도 정확도 특징
KernelSHAP 모든 모델 O(2^M) -> 샘플링 근사 근사 LIME + Shapley 결합, 모델 호출 횟수 많음
TreeSHAP 트리 앙상블 O(TLD^2) 정확 트리 구조 활용 다항시간 정확 계산
DeepSHAP 딥러닝 O(forward pass) 근사 DeepLIFT + Shapley, 역전파 기반
LinearSHAP 선형 모델 O(M) 정확 계수에서 직접 계산
PermutationSHAP 모든 모델 O(M * n_samples) 근사 순열 기반 Shapley 근사

TreeSHAP의 핵심: - 일반 Shapley: O(2^M) 부분집합 열거 필요 - TreeSHAP: 트리의 경로 구조를 활용하여 O(TLD^2)로 단축 - T: 트리 수, L: 리프 수, D: 트리 깊이 - 조건부 기대값(Interventional vs Observational) 방식 선택 가능

2. LIME (Local Interpretable Model-agnostic Explanations)

항목 내용
논문 Ribeiro et al., "Why Should I Trust You?" (KDD 2016)
핵심 아이디어 예측 주변에서 해석 가능한 대리 모델(surrogate)을 학습

수학적 정의:

\[\xi(x) = \arg\min_{g \in G} \ \mathcal{L}(f, g, \pi_x) + \Omega(g)\]
  • \(f\): 원본 블랙박스 모델
  • \(g\): 해석 가능한 대리 모델 (선형 모델, 결정 트리 등)
  • \(\pi_x\): \(x\) 주변의 근접도 커널 (가까울수록 높은 가중치)
  • \(\Omega(g)\): 복잡도 페널티

알고리즘:

LIME 알고리즘
==============================================
입력: 블랙박스 모델 f, 설명할 인스턴스 x
출력: 특성 기여도

1. x 주변에서 교란 샘플 생성
   z_i ~ 특성별 독립 분포 (연속: 가우시안, 범주: 빈도)

2. 각 교란 샘플에 대해 f(z_i) 계산

3. 거리 기반 가중치 계산
   w_i = exp(-D(x, z_i)^2 / sigma^2)

4. 가중 선형 회귀 학습
   g = argmin sum(w_i * (f(z_i) - g(z_i))^2)

5. g의 계수 -> 특성 기여도
==============================================

LIME vs SHAP 비교:

항목 LIME SHAP
이론 기반 지역 대리 모델 Shapley value (게임 이론)
일관성 X (커널/샘플링에 의존) O (공리적 보장)
속도 빠름 (샘플 수 제어) 느림 (KernelSHAP) / 빠름 (TreeSHAP)
안정성 낮음 (실행마다 결과 변동) 높음 (수렴 보장)
Global 설명 제한적 (SP-LIME) 자연스러운 확장

3. Partial Dependence Plot (PDP)

\[\hat{f}_S(x_S) = \frac{1}{n} \sum_{i=1}^{n} f(x_S, x_{C}^{(i)})\]
  • \(x_S\): 관심 특성, \(x_C\): 나머지 특성
  • 관심 특성 값을 고정하고 나머지를 데이터에서 주변화(marginalize)
  • Global 방법: 모델의 평균적 반응을 시각화

한계: - 특성 간 상관관계 무시 (비현실적 조합 생성) - 이질적 효과 평균화 (상호작용 숨김)

4. ALE (Accumulated Local Effects)

\[\hat{f}_{j,\text{ALE}}(x_j) = \int_{z_{0,j}}^{x_j} E\left[\frac{\partial f}{\partial x_j} \bigg| X_j = z_j\right] dz_j - c\]
  • PDP의 상관관계 문제를 해결
  • 조건부 기대값을 사용하여 현실적인 데이터 분포 반영
  • 국소 효과를 누적하여 전체 효과 파악

PDP vs ALE:

항목 PDP ALE
상관 특성 편향됨 비편향
계산 방식 주변화 (marginalization) 조건부 미분 누적
해석 평균 예측값 평균 대비 효과
속도 느림 (n번 예측) 빠름 (구간별 평균)

5. Permutation Importance

\[\text{PI}_j = s - \frac{1}{K} \sum_{k=1}^{K} s_{j,k}^{\text{perm}}\]
  • \(s\): 원래 성능 점수
  • \(s_{j,k}^{\text{perm}}\): \(j\)번째 특성을 순열한 후의 성능
  • 특성 \(j\)가 중요하면 순열 후 성능이 크게 하락

주의사항: - 상관된 특성: 순열이 비현실적 조합 생성 -> 과소평가 가능 - 대안: Conditional Permutation Importance

Model-Specific 방법

1. Grad-CAM (Gradient-weighted Class Activation Mapping)

항목 내용
논문 Selvaraju et al. (ICCV 2017)
적용 CNN (이미지 분류)
출력 클래스별 히트맵

수식:

가중치: $\(\alpha_k^c = \frac{1}{Z} \sum_i \sum_j \frac{\partial y^c}{\partial A_{ij}^k}\)$

히트맵: $\(L_{\text{Grad-CAM}}^c = \text{ReLU}\left(\sum_k \alpha_k^c A^k\right)\)$

  • \(A^k\): 마지막 합성곱 층의 \(k\)번째 특성맵
  • \(y^c\): 클래스 \(c\)에 대한 로짓
  • ReLU: 양의 영향만 시각화

2. Integrated Gradients

항목 내용
논문 Sundararajan et al. (ICML 2017)
적용 미분 가능한 모든 모델
특징 완전성(Completeness) 공리 만족

수식:

\[\text{IG}_j(x) = (x_j - x_j') \int_0^1 \frac{\partial f(x' + \alpha(x - x'))}{\partial x_j} d\alpha\]
  • \(x'\): 기준선 (baseline, 보통 영벡터)
  • 기준선에서 입력까지의 경로를 따라 그래디언트를 적분
  • 완전성: \(\sum_j \text{IG}_j(x) = f(x) - f(x')\)

3. Attention Visualization

Transformer의 Attention 가중치를 설명으로 사용:

\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V\]
장점 한계
직관적 시각화 Attention != Explanation (Jain & Wallace, 2019)
추가 계산 불필요 다층 Attention의 해석 어려움
토큰 간 관계 파악 그래디언트 기반 방법과 불일치 사례

Attention Rollout: - 다층 Attention 가중치를 곱하여 입력까지의 정보 흐름 추적 - 잔차 연결(skip connection) 고려

Global vs Local Explanation

Global Explanation                    Local Explanation
(모델 전체 행동)                       (개별 예측 설명)

+---------------------------+          +---------------------------+
| "이 모델은 전반적으로      |          | "이 고객의 이탈 확률이    |
|  소득과 연령이 가장        |          |  높은 이유는 최근 3개월   |
|  중요한 특성이다"          |          |  사용량이 70% 감소했기    |
|                           |          |  때문이다"                |
| 방법: PDP, ALE, Global    |          |                           |
|   SHAP, PI                |          | 방법: LIME, Local SHAP,  |
+---------------------------+          |   Grad-CAM, IG           |
                                       +---------------------------+
항목 Global Local
목적 모델 동작 원리 파악 개별 의사결정 근거 제시
대상 사용자 데이터 사이언티스트, 감사자 최종 사용자, 규제 기관
계산 비용 높음 (전체 데이터) 낮음 (단일 인스턴스)
활용 모델 디버깅, 편향 감지 대출 거절 사유, 진단 근거

Counterfactual Explanations

정의

"최소한의 변경으로 예측 결과를 바꾸려면 어떤 특성을 어떻게 바꿔야 하는가?"

\[\mathbf{x}^* = \arg\min_{\mathbf{x}'} \ d(\mathbf{x}, \mathbf{x}') \quad \text{s.t.} \quad f(\mathbf{x}') = y'\]
  • \(y'\): 원하는 목표 클래스
  • \(d\): 거리 함수 (L1, L2, Gower 등)

예시

현재 상태:            대출 거절 (f(x) = 0)
  소득: 35,000
  부채: 15,000
  신용등급: 620
  재직기간: 2년

반사실:               대출 승인 (f(x') = 1)
  소득: 35,000        (변경 없음)
  부채: 10,000        (5,000 감소)
  신용등급: 620       (변경 없음)
  재직기간: 2년       (변경 없음)

=> "부채를 5,000만 줄이면 대출이 승인됩니다"

주요 방법

방법 특징
Wachter et al. (2017) 최초의 반사실 설명 프레임워크
DiCE (Mothilal et al., 2020) 다양한 반사실 집합 생성
FACE (Poyiadzi et al., 2020) 데이터 매니폴드 위의 실현 가능한 반사실
GrowingSpheres 점진적 반경 확장으로 탐색

좋은 반사실의 조건

  1. 유효성: 목표 클래스로 전환
  2. 근접성: 원본과 최소한의 차이
  3. 희소성: 변경 특성 수 최소화
  4. 실현가능성: 물리적/논리적으로 가능한 변경
  5. 인과적 타당성: 인과 구조를 위반하지 않는 변경

Concept-based Explanations

TCAV (Testing with Concept Activation Vectors)

항목 내용
논문 Kim et al. (ICML 2018)
핵심 인간이 이해하는 고수준 개념으로 설명
예시 "이 이미지가 얼룩말로 분류된 것은 '줄무늬' 개념 때문"

알고리즘:

TCAV 알고리즘
==============================================
1. 개념 데이터 수집
   - 양성 예시: "줄무늬" 패턴이 있는 이미지
   - 음성 예시: 무작위 이미지

2. CAV (Concept Activation Vector) 학습
   - 활성화 공간에서 선형 분류기 학습
   - 분류 초평면의 법선 벡터 = CAV

3. TCAV 점수 계산
   TCAV_k^c = |{x: S_c(f_l(x), y_k) > 0}| / |X_k|

   S_c = grad(h_k(f_l(x))) . v_c
   (개념 방향으로의 기울기 성분)

4. 통계 검정
   - 무작위 개념 대비 유의성 검정
==============================================
  • 장점: 특성이 아닌 개념 수준의 설명
  • 한계: 개념 데이터 수동 수집 필요, 선형 분리 가능 가정

LLM 해석과 Mechanistic Interpretability

Mechanistic Interpretability 개요

LLM 내부 메커니즘을 역공학하여 "어떻게" 작동하는지 이해:

접근법 설명 대표 연구
Probing 내부 표현에 분류기를 학습하여 정보 존재 확인 Belinkov (2022)
Circuit Analysis 특정 행동을 담당하는 최소 하위 네트워크 식별 Olsson et al. (2022)
Sparse Autoencoders 중첩된(superposed) 특성을 분리 Cunningham et al. (2023)
Logit Lens 중간 레이어의 출력을 어휘 공간으로 투영 nostalgebraist (2020)
Activation Patching 활성화를 교체하여 인과적 역할 검증 Meng et al. (2022)
Representation Engineering 표현 수준에서 모델 행동 제어 Zou et al. (2023)

Superposition과 Sparse Autoencoders

Superposition 문제:
===================================
뉴런 하나에 여러 특성이 중첩

뉴런 1: [특성A 0.7] + [특성B 0.3]
뉴런 2: [특성A 0.2] + [특성C 0.8]
뉴런 3: [특성B 0.5] + [특성C 0.5]

    |
    v  Sparse Autoencoder로 분리

SAE 특성 1: [특성A] (순수)
SAE 특성 2: [특성B] (순수)
SAE 특성 3: [특성C] (순수)
===================================
  • SAE는 모델의 은닉 표현을 더 큰 차원의 희소 공간으로 사영
  • Anthropic (2024): Claude에서 수백만 개의 해석 가능한 특성 발견
  • 안전성 연구의 핵심 도구

전통 XAI vs Mechanistic Interpretability

항목 전통 XAI Mechanistic Interpretability
질문 "왜 이런 예측?" "어떤 메커니즘으로 작동?"
수준 입력-출력 관계 내부 계산 과정
대상 범용 ML 모델 주로 신경망/LLM
출력 특성 기여도, 규칙 회로, 특성, 알고리즘
목적 신뢰/규제 준수 안전성/정렬(alignment)

규제 요구사항

EU AI Act (2024)

위험 등급 설명 XAI 요구사항
Unacceptable 사회 점수 시스템 등 사용 금지
High-risk 채용, 신용, 의료, 사법 설명 가능성 필수, 인간 감독
Limited 챗봇, 감정 인식 AI 사용 고지 의무
Minimal 스팸 필터 등 의무 없음

High-risk AI에 대한 구체적 요구: - 투명성: 사용자가 출력을 해석할 수 있어야 함 - 인간 감독: 자동화된 의사결정에 대한 인간 개입 보장 - 기술 문서: AI 시스템의 작동 방식 문서화

금융 규제

규제 관할 핵심 요구
SR 11-7 (Fed) 미국 모델 리스크 관리, 모델 검증 필수
ECOA / Reg B 미국 신용 거절 시 구체적 사유 제공 의무
GDPR Art. 22 EU 자동화된 의사결정에 대한 설명 권리
EBA Guidelines EU ML 모델의 해석 가능성 요구

금융에서 XAI 실무: - 신용 평가: 거절 사유 상위 4개 특성 제공 (SHAP 활용) - 이상 거래 탐지: 경고 발생 근거 설명 - 보험 심사: 보험료 산정 근거의 투명성

방법별 비교표

방법 범위 모델 의존성 충실도 안정성 속도 이론적 보장
SHAP (Kernel) Local/Global Agnostic 높음 높음 느림 Shapley 공리
TreeSHAP Local/Global Tree only 정확 높음 빠름 Shapley 공리
DeepSHAP Local/Global DL only 근사 중간 빠름 근사적
LIME Local Agnostic 중간 낮음 빠름 없음
PDP Global Agnostic 중간 높음 중간 없음
ALE Global Agnostic 높음 높음 빠름 없음
Grad-CAM Local CNN only 중간 높음 빠름 없음
Integrated Gradients Local 미분가능 높음 높음 중간 완전성 공리
Permutation Imp. Global Agnostic 중간 중간 느림 없음
Counterfactual Local Agnostic 높음 낮음 중간 없음
TCAV Global DL 높음 중간 중간 통계 검정

실무 코드 예시

SHAP을 이용한 종합 분석

import shap
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import fetch_openml

# -------------------------------------------------------
# 데이터 준비 (Adult Income)
# -------------------------------------------------------
data = fetch_openml("adult", version=2, as_frame=True)
df = data.frame.dropna()

# 수치형 특성만 사용 (간략화)
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
target = "class"  # <=50K, >50K
X = df[numeric_cols].drop(columns=[target], errors="ignore")
y = (df[target] == ">50K").astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# -------------------------------------------------------
# 모델 학습
# -------------------------------------------------------
model = GradientBoostingClassifier(
    n_estimators=200, max_depth=4, learning_rate=0.1, random_state=42
)
model.fit(X_train, y_train)

# -------------------------------------------------------
# TreeSHAP
# -------------------------------------------------------
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

# 개별 예측 설명 (Local)
idx = 0
print(f"Prediction: {model.predict_proba(X_test.iloc[[idx]])[0]}")
print(f"Base value: {explainer.expected_value:.4f}")
print(f"SHAP values for instance {idx}:")
for col, sv in zip(X_test.columns, shap_values[idx]):
    if abs(sv) > 0.01:
        print(f"  {col}: {sv:+.4f} (value={X_test.iloc[idx][col]:.1f})")

# Global 특성 중요도 (평균 절대 SHAP)
global_importance = pd.DataFrame({
    "feature": X_test.columns,
    "mean_abs_shap": np.abs(shap_values).mean(axis=0),
}).sort_values("mean_abs_shap", ascending=False)

print("\nGlobal Feature Importance:")
print(global_importance.to_string(index=False))

# -------------------------------------------------------
# SHAP 상호작용 (Interaction Values)
# -------------------------------------------------------
shap_interaction = explainer.shap_interaction_values(X_test[:100])
# shape: (n_samples, n_features, n_features)
print(f"\nInteraction shape: {np.array(shap_interaction).shape}")

LIME을 이용한 텍스트 분류 설명

import lime
import lime.lime_text
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

# -------------------------------------------------------
# 텍스트 분류기 학습
# -------------------------------------------------------
texts = [
    "This movie was great and I loved every minute",
    "Terrible film, waste of time and money",
    "Brilliant acting and wonderful storyline",
    "Boring plot, bad acting, do not recommend",
    "An absolute masterpiece of cinema",
    "The worst movie I have ever watched",
] * 50  # 반복으로 충분한 데이터 확보

labels = [1, 0, 1, 0, 1, 0] * 50

pipe = Pipeline([
    ("tfidf", TfidfVectorizer(max_features=1000)),
    ("clf", LogisticRegression(max_iter=200)),
])
pipe.fit(texts, labels)

# -------------------------------------------------------
# LIME 설명
# -------------------------------------------------------
explainer = lime.lime_text.LimeTextExplainer(class_names=["negative", "positive"])

text_to_explain = "The movie had great acting but a terrible plot"
exp = explainer.explain_instance(
    text_to_explain,
    pipe.predict_proba,
    num_features=6,
    num_samples=500,
)

print(f"Text: {text_to_explain}")
print(f"Prediction: {pipe.predict_proba([text_to_explain])[0]}")
print("\nLIME Explanation:")
for feature, weight in exp.as_list():
    direction = "positive" if weight > 0 else "negative"
    print(f"  '{feature}': {weight:+.4f} ({direction})")

Captum을 이용한 Integrated Gradients (PyTorch)

import torch
import torch.nn as nn
from captum.attr import (
    IntegratedGradients,
    LayerGradCam,
    visualization,
)
import numpy as np

# -------------------------------------------------------
# 간단한 분류 모델
# -------------------------------------------------------
class SimpleNet(nn.Module):
    def __init__(self, input_dim: int, hidden_dim: int = 64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 2),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.net(x)


# 모델 준비
input_dim = 10
model = SimpleNet(input_dim)
model.eval()

# 더미 데이터
x = torch.randn(1, input_dim, requires_grad=True)
baseline = torch.zeros(1, input_dim)

# -------------------------------------------------------
# Integrated Gradients
# -------------------------------------------------------
ig = IntegratedGradients(model)
attributions, delta = ig.attribute(
    x,
    baselines=baseline,
    target=1,             # 클래스 1에 대한 기여도
    n_steps=200,          # 적분 스텝 수
    return_convergence_delta=True,
)

print("Input:", x.detach().numpy().round(3))
print("Attributions:", attributions.detach().numpy().round(4))
print(f"Convergence delta: {delta.item():.6f}")  # 0에 가까워야 함

# 완전성 검증
pred_diff = model(x)[0, 1].item() - model(baseline)[0, 1].item()
attr_sum = attributions.sum().item()
print(f"f(x) - f(baseline) = {pred_diff:.4f}")
print(f"Sum of attributions = {attr_sum:.4f}")
print(f"Difference (should be ~0): {abs(pred_diff - attr_sum):.6f}")

# -------------------------------------------------------
# Grad-CAM (CNN 예시)
# -------------------------------------------------------
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.relu2 = nn.ReLU()
        self.pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(32, 10)

    def forward(self, x):
        x = self.relu1(self.conv1(x))
        x = self.relu2(self.conv2(x))
        x = self.pool(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)


cnn = SimpleCNN()
cnn.eval()

img = torch.randn(1, 3, 32, 32)

# Grad-CAM on conv2 layer
grad_cam = LayerGradCam(cnn, cnn.conv2)
cam_attr = grad_cam.attribute(img, target=5)

print(f"\nGrad-CAM attribution shape: {cam_attr.shape}")
print(f"Grad-CAM min: {cam_attr.min():.4f}, max: {cam_attr.max():.4f}")

SHAP + XGBoost 실무 파이프라인

import shap
import xgboost as xgb
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

# -------------------------------------------------------
# 고객 이탈 예측 예시 (합성 데이터)
# -------------------------------------------------------
np.random.seed(42)
n = 5000
df = pd.DataFrame({
    "tenure_months": np.random.randint(1, 72, n),
    "monthly_charges": np.random.uniform(20, 100, n),
    "total_charges": np.random.uniform(100, 5000, n),
    "num_complaints": np.random.poisson(1.5, n),
    "contract_type": np.random.choice([0, 1, 2], n),  # month/year/2year
    "usage_decline_pct": np.random.uniform(-20, 50, n),
})

# 이탈 확률 생성 (실제 관계 내장)
logit = (
    -2
    - 0.05 * df["tenure_months"]
    + 0.02 * df["monthly_charges"]
    + 0.5 * df["num_complaints"]
    + 0.03 * df["usage_decline_pct"]
    - 0.8 * (df["contract_type"] == 2).astype(float)
)
df["churn"] = (np.random.uniform(0, 1, n) < 1 / (1 + np.exp(-logit))).astype(int)

features = [c for c in df.columns if c != "churn"]
X_train, X_test, y_train, y_test = train_test_split(
    df[features], df["churn"], test_size=0.2, random_state=42
)

# -------------------------------------------------------
# XGBoost 학습
# -------------------------------------------------------
model = xgb.XGBClassifier(
    n_estimators=200, max_depth=4, learning_rate=0.1,
    use_label_encoder=False, eval_metric="logloss",
    random_state=42,
)
model.fit(X_train, y_train)
auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])
print(f"AUC: {auc:.4f}")

# -------------------------------------------------------
# TreeSHAP 분석
# -------------------------------------------------------
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

# 1) Global 중요도
print("\n=== Global Feature Importance (mean |SHAP|) ===")
importance = pd.DataFrame({
    "feature": features,
    "importance": np.abs(shap_values).mean(axis=0),
}).sort_values("importance", ascending=False)
print(importance.to_string(index=False))

# 2) 개별 고객 설명
customer_idx = 10
customer = X_test.iloc[customer_idx]
pred_prob = model.predict_proba(X_test.iloc[[customer_idx]])[0, 1]

print(f"\n=== Customer {customer_idx} ===")
print(f"Churn probability: {pred_prob:.3f}")
print(f"Base rate: {explainer.expected_value:.3f}")
print("Feature contributions:")

contributions = sorted(
    zip(features, shap_values[customer_idx], customer[features]),
    key=lambda x: abs(x[1]),
    reverse=True,
)
for feat, sv, val in contributions:
    if abs(sv) > 0.01:
        direction = "increases" if sv > 0 else "decreases"
        print(f"  {feat}={val:.1f}: {sv:+.3f} ({direction} churn risk)")

# 3) 규제 대응: 상위 4개 거절 사유 자동 생성
def generate_decline_reasons(shap_vals, feature_names, top_k=4):
    """신용 거절 등 의사결정 사유를 SHAP 기반으로 생성"""
    sorted_idx = np.argsort(-np.abs(shap_vals))
    reasons = []
    for i in sorted_idx[:top_k]:
        feat = feature_names[i]
        sv = shap_vals[i]
        reasons.append({
            "feature": feat,
            "shap_value": round(sv, 4),
            "direction": "negative" if sv > 0 else "positive",
        })
    return reasons

reasons = generate_decline_reasons(shap_values[customer_idx], features)
print(f"\nTop decline reasons:")
for i, r in enumerate(reasons, 1):
    print(f"  {i}. {r['feature']} (impact: {r['shap_value']:+.4f}, {r['direction']})")

한계와 발전 방향

현재 한계

  1. 설명의 충실도: Post-hoc 설명이 모델의 실제 메커니즘을 정확히 반영하지 않을 수 있음
  2. 안정성: LIME 등 교란 기반 방법은 실행마다 결과 변동
  3. 상관 특성: 다중공선성이 있을 때 기여도 할당이 임의적
  4. 스케일: 대규모 모델(LLM)에 대한 XAI 적용은 계산적으로 비실용적
  5. 사용자 이해: 기술적 설명이 비전문가에게는 여전히 불투명

발전 방향

  1. 인과적 설명 (Causal XAI): Shapley value를 인과 그래프 기반으로 확장
  2. 대화형 설명: 사용자가 질문하면 맞춤 설명 제공 (LLM + XAI)
  3. 자연어 설명: SHAP 값을 자연어로 자동 변환
  4. 인증된 설명: 설명의 충실도를 수학적으로 보장
  5. Mechanistic Interpretability 확장: LLM 내부 회로 분석을 통한 근본적 이해
  6. 다중 이해관계자 설명: 전문가/비전문가/규제기관 각각에 맞는 설명 수준