"""
def xai_box(method, status, era, items, takeaway):
"""세대별 XAI 구현 방식을 표시하는 박스 (새로 추가).
method: XAI 기법 명칭 (예: 'TreeSHAP', 'GNNExplainer')
status: 'native' (내재) | 'post-hoc' (사후) | 'none' (불필요) | 'generative' (생성형)
era: 해당 기법이 표준화된 시기
items: [(항목, 설명)] 리스트
takeaway: 강의 포인트 한 줄
"""
status_map = {
'none': ('XAI 불필요', '#EAF3DE', '#3B6D11'),
'native': ('내재적 설명력', '#E6F1FB', '#0C447C'),
'post-hoc': ('사후 설명 기법', '#FAEEDA', '#854F0B'),
'generative': ('생성형 설명', '#FAECE7', '#993C1D'),
}
status_label, badge_bg, badge_fg = status_map.get(status, status_map['post-hoc'])
rows = ""
for name, desc in items:
rows += (
f"
"
f"
{name}
"
f"
{desc}
"
f"
"
)
return f"""
🔍 XAI 구현 방식
{status_label}기법: {method}· {era}
{rows}
💡 {takeaway}
"""
CARD_STYLE = ("background:#fff; border:0.5px solid rgba(0,0,0,0.15); "
"border-radius:12px; padding:16px 20px; margin-bottom:14px;")
# ============================================================
# 3. 세대별 HTML 생성 함수
# ============================================================
def render_gen1(amount, hour, new_payee_bin, ratio):
triggered, score = evaluate_gen1(amount, hour, new_payee_bin, ratio)
dec, bg, fg = decide(score, is_score=True)
rows = ""
for rule, t in zip(GEN1_RULES, triggered):
applied = rule["weight"] if t else 0
row_bg = "#FAECE7" if t else "#ffffff"
td_color = "#4A1B0C" if t else "#444"
sub_color = "#712B13" if t else "#666"
mark = "✓" if t else "—"
rows += f"""
{rule['name']}
{rule['condition']}
+{rule['weight']}
{mark}
+{applied}
"""
gen1_setup = feature_setup_box(
actor_label="100% 사람 결정",
actor_color='human',
items=[
("입력 Feature 4개", "사람", "도메인 전문가가 '금액·시간·신규수취인·금액비율'을 사기 판단 기준으로 선정"),
("룰 조건 (임계값)", "사람", "≥500만원, ≤6시 또는 ≥22시, =1, ≥5배 — 모두 사람이 직접 결정"),
("룰별 가중치", "사람", "40 / 30 / 20 / 10점 — 도메인 경험에 따라 사람이 부여"),
("판정 임계값", "사람", "70점 이상 차단, 40점 이상 추가인증 — 운영팀이 비즈니스 판단으로 결정"),
],
explanation="모든 결정이 사람의 도메인 지식에 의존. 학습 데이터는 사용하지 않음. 새 사기 패턴 등장 시 사람이 룰을 추가해야 함."
)
# ─── XAI 구현 박스 (1세대) ─────────────────────────────
gen1_xai = xai_box(
method="해당 없음 (Self-Explanatory)",
status='none',
era="~ 2000년대 중반",
items=[
("설명 생성 방식", "룰북 자체가 설명서. 발동된 룰의 이름·조건·가중치를 그대로 노출하면 사람이 즉시 이해 가능"),
("개별 거래 설명", "위 표가 바로 설명. '왜 차단됐냐 → R1(고액)+R3(신규수취인)+R4(급증) = 70점'으로 즉시 답변"),
("감독당국 대응", "룰 매트릭스 그대로 제출. 별도 XAI 알고리즘 필요 없음"),
("한계", "룰이 많아지면(수백 개) 사람도 따라가기 힘들어짐 → 룰 간 상호작용·우선순위가 새로운 블랙박스가 됨"),
],
takeaway="1세대에는 '설명력'이라는 개념이 따로 존재하지 않았음. 판단 로직 = 설명 로직이기 때문. XAI라는 단어가 등장한 것은 모델이 비선형으로 진화한 후의 일."
)
return f"""
"""
contrib_str = " + ".join([f"({c:+.4f})" for c in contributions])
calc_html = (
f"z = {contrib_str} + ({GEN2_INTERCEPT:+.4f}) "
f"z = {logit:+.4f} "
f"P = 1 / (1 + e{-logit:+.4f}) = "
f"{prob:.4f} → {prob*100:.2f}%"
)
gen2_setup = feature_setup_box(
actor_label="피처는 사람, 가중치는 모델",
actor_color='mixed',
items=[
("입력 Feature 4개", "사람", "1세대와 동일한 4개 컬럼을 사람이 선정 (피처 엔지니어링)"),
("학습 데이터", "사람", "250건의 거래에 사기/정상 라벨을 사람이 부여"),
("가중치 w₁~w₄", "모델", "fit() 호출 시 L-BFGS 알고리즘이 자동 학습"),
("절편 b", "모델", "데이터의 사기 비율(50/250=20%)에 맞춰 자동 조정"),
("판정 임계값", "사람", "0.5(추가인증) / 0.7(차단) — 운영팀이 결정"),
],
explanation="피처는 여전히 사람이 정의. 모델이 학습하는 건 '4개 피처에 어떤 가중치를 곱해야 사기를 잘 맞추는가'뿐."
)
# ─── XAI 구현 박스 (2세대) ─────────────────────────────
# 최대 기여 피처 찾기
top_idx = int(np.argmax(np.abs(contributions)))
top_feat = FEATURES[top_idx]
top_contrib = contributions[top_idx]
gen2_xai = xai_box(
method="Coefficient-based Attribution (계수 분해)",
status='native',
era="통계 모델 시대부터 자연 발생",
items=[
("설명 생성 방식", "수식 z = Σ(wᵢ·xᵢ) + b 가 그대로 설명. 별도 XAI 알고리즘 없이 가중치만 보면 됨"),
("개별 거래 설명", f"본 거래에서 가장 큰 기여 = {top_feat} ({top_contrib:+.4f}). 표의 마지막 컬럼이 곧 SHAP 값의 정확한 선형 버전"),
("전역 설명 (global)", "가중치 부호와 크기가 곧 변수 중요도. 양수 = 사기 방향, 음수 = 정상 방향"),
("한계", "선형 가정이라 비선형 패턴 학습 불가. '금액 + 시간 조합' 같은 상호작용을 못 잡음 → 정확도 부족이 3세대 등장의 배경"),
],
takeaway="로지스틱 회귀의 'wᵢ·xᵢ' 분해는 사실상 정확한 SHAP 값과 동치(선형 모델 한정). XAI라는 개념이 별도로 필요 없는 마지막 세대."
)
return f"""
"""
final_calc = formula_box(
f"P(사기) = sigmoid({raw_score:+.4f}) "
f"= 1 / (1 + e{-raw_score:+.4f}) "
f"= {sigmoid_result:.4f} ({sigmoid_result*100:.2f}%)"
)
# ─── TreeSHAP 계산 및 시각화 ──────────────────────────
shap_values, shap_base = compute_shap_values_gen3(amount, hour, new_payee_bin, ratio)
# SHAP 검증: base + Σ(shap) ≈ raw_score (logit space)
shap_sum = shap_base + sum(shap_values)
# SHAP waterfall: 절댓값 큰 순으로 정렬
shap_pairs = sorted(zip(FEATURES, shap_values), key=lambda x: -abs(x[1]))
max_abs_shap = max(abs(v) for v in shap_values) if any(shap_values) else 1.0
shap_rows = ""
for feat, sv in shap_pairs:
direction = "사기 ↑" if sv > 0 else "정상 ↓"
color = "#A32D2D" if sv > 0 else "#3B6D11"
bar_bg = "#FAECE7" if sv > 0 else "#E1F5EE"
bar_w = (abs(sv) / max_abs_shap) * 100 if max_abs_shap > 0 else 0
bar_align = "flex-start" if sv > 0 else "flex-end"
# 좌우로 분리된 막대 (양수는 오른쪽, 음수는 왼쪽)
if sv > 0:
bar_html = f"""
"""
else:
bar_html = f"""
"""
shap_rows += f"""
{feat}
{sv:+.4f}
{bar_html}
{direction}
"""
shap_table = f"""
피처
SHAP 값
← 정상 방향 | 사기 방향 →
기여
{shap_rows}
base value (평균 거래)
{shap_base:+.4f}
학습 데이터 전체의 평균 logit
합계 = base + Σ(SHAP)
{shap_sum:+.4f}
≈ raw score {raw_score:+.4f} (가산성 보장)
"""
gen3_setup = feature_setup_box(
actor_label="피처는 사람, 트리 구조는 모델",
actor_color='mixed',
items=[
("입력 Feature 4개", "사람", "1·2세대와 완전히 동일한 4개 컬럼"),
("학습 데이터", "사람", "1·2세대와 동일한 250건"),
("트리 분기 임계값", "모델", "Gain 최대화로 자동 결정 (예: 시간 < 8.69, 금액비율 < 1.77)"),
("각 leaf 값", "모델", "각 leaf에 도달한 샘플들의 잔차로 자동 계산"),
("트리 개수 / 깊이", "사람", "n_estimators=10, max_depth=3 (하이퍼파라미터)"),
("판정 임계값", "사람", "0.5(추가인증) / 0.7(차단)"),
],
explanation="2세대보다 학습되는 부분이 훨씬 많아짐. 분기 임계값과 leaf 값 모두 데이터에서 자동 발견."
)
# ─── XAI 구현 박스 (3세대) ─────────────────────────────
top_shap_feat, top_shap_val = shap_pairs[0]
gen3_xai = xai_box(
method="TreeSHAP (Lundberg 2017)",
status='post-hoc',
era="2017년 발표 → 2018-2020년 금융권 표준화",
items=[
("설명 생성 방식", "모델 학습 후 별도 알고리즘으로 SHAP 값 계산. XGBoost는 TreeSHAP이라는 트리 구조 특화 알고리즘이 내장되어 있어 실시간 추론에도 사용 가능 (개별 거래당 ~1ms)"),
("개별 거래 설명", f"본 거래의 SHAP 분해: 가장 큰 기여는 {top_shap_feat} ({top_shap_val:+.4f}). 부호가 양수면 사기 방향, 음수면 정상 방향으로 끌어당김"),
("수학적 보장", f"가산성(Additive): base({shap_base:+.4f}) + Σ(SHAP) = {shap_sum:+.4f} ≈ raw score {raw_score:+.4f}. 즉 '평균 거래 대비 이 거래가 왜 더 의심스러운가'를 정확히 분해"),
("금융권 활용", "감독당국 보고서(설명가능성 자료), 고객 거절 사유 통보(Adverse Action Notice), 모델 디버깅·검증"),
("한계", "어디까지나 '근사된 기여도'. 모델이 실제로 그렇게 사고했다는 보장은 아님 → 4세대 GNN에서는 더 큰 한계가 됨"),
],
takeaway="3세대부터 'XAI'가 본격적으로 별도 모듈로 등장. 모델 = 판단기, SHAP = 설명기로 역할이 분리됨. 이게 현재 금융권 FDS의 표준 아키텍처."
)
return f"""
{card_header("GEN 3 · XGBOOST (TREE ENSEMBLE)", "XGBoost (트리 10개 앙상블)", dec, bg, fg, f"사기 확률 {prob*100:.2f}%")}
{gen3_setup}
{gen3_xai}
{formula_box("F(x) = Σk=1..K fk(x), fk ∈ 트리 공간 P(사기) = sigmoid(F(x)) [K=10, max_depth=3, lr=0.1]")}
피처 중요도 (Gain 기반, 전역 설명)
{imp_bars}
⚠️ 위 중요도는 모델 전체 평균이라 개별 거래 설명에는 부적합 → 그래서 SHAP이 필요
🎯 TreeSHAP — 본 거래에 대한 개별 기여도 분해 (Local Explanation)
'이 거래가 평균보다 왜 더 의심스러운가'를 피처별로 정량 분해. 합산하면 모델의 raw score와 일치 (가산성 보장).
{shap_table}
🌳 학습된 트리 10개의 leaf 값 누적 (부스팅 본질)
각 트리가 이전 트리의 잔차를 보정하며 leaf 값을 더해감 → 누적된 raw score를 sigmoid로 변환
{tree_table}
🧮 최종 확률 계산
{final_calc}
강점: 비선형 패턴·피처 상호작용 자동 학습 + SHAP으로 개별 설명 확보. 한계: 학습 데이터 분포 밖의 케이스(예: 전세 잔금)는 여전히 못 잡음
"""
def render_gen4(amount, hour, new_payee_bin, ratio, prob3):
"""4세대 GNN - 진짜 학습된 mini GNN의 forward pass 결과 + GNNExplainer 스타일 XAI"""
node_features = build_graph_features(amount, hour, new_payee_bin, ratio)
prob, intermediates = gnn_model.forward(node_features, return_intermediates=True)
prob = float(prob)
dec, bg, fg = decide(prob)
h1 = intermediates['h1']
h2 = intermediates['h2']
trans_emb = intermediates['trans_embedding']
logit = intermediates['logit']
# 그래프 시각화 색상
edge2_color = "#D85A30" if new_payee_bin == 1 else "#888780"
edge2_dash = 'stroke-dasharray="" ' if new_payee_bin == 1 else 'stroke-dasharray="3,3" '
risk_fill = "#F7C1C1" if new_payee_bin == 1 else "#D3D1C7"
risk_stroke = "#A32D2D" if new_payee_bin == 1 else "#5F5E5A"
risk_text = "사기계좌" if new_payee_bin == 1 else "일반"
risk_color = "#501313" if new_payee_bin == 1 else "#444441"
payee_type = "신규" if new_payee_bin == 1 else "기존"
svg = f"""
"""
# 16차원 임베딩을 4×4 그리드로 시각화 + 색상 강조
h1_trans = h1[0]
h2_trans = h2[0]
def render_node_grid(values, prefix, base_x, base_y):
cells = ""
max_v = max(abs(values.max()), abs(values.min())) if len(values) > 0 else 1.0
for i, v in enumerate(values):
r, c = i // 4, i % 4
x = base_x + c * 16
y = base_y + r * 16
intensity = abs(v) / max_v if max_v > 0 else 0
if v > 0:
fill = f"rgb({int(240 - 100*intensity)}, {int(160 - 70*intensity)}, {int(150 - 70*intensity)})"
stroke = "#993C1D"
text_color = "#4A1B0C" if intensity > 0.4 else "#5F4308"
else:
fill = f"rgb({int(220 - 60*intensity)}, {int(220 - 30*intensity)}, {int(210 - 50*intensity)})"
stroke = "#888"
text_color = "#555"
cells += (
f''
f'{v:.1f}'
)
return cells
grid_h1 = render_node_grid(h1_trans, "h", 195, 55)
grid_h2 = render_node_grid(h2_trans, "h'", 395, 55)
feature_expansion_svg = f"""
"""
# Layer별 활성도
layer1_active = (h1_trans > 0).sum()
layer2_active = (h2_trans > 0).sum()
layer1_mean = float(h1_trans[h1_trans > 0].mean()) if layer1_active > 0 else 0.0
layer2_mean = float(h2_trans[h2_trans > 0].mean()) if layer2_active > 0 else 0.0
# ─── GNNExplainer 스타일 XAI: 엣지 마스킹 (logit space) ─
# NOTE: delta는 logit 차이. sigmoid가 saturate(prob≈0 또는 1)되어도
# logit space에서는 변화가 그대로 보존되므로 시각화에 적합.
_, edge_contribs = compute_gnn_edge_attribution(amount, hour, new_payee_bin, ratio)
max_edge_delta = max(abs(d) for _, _, d in edge_contribs) if edge_contribs else 1.0
edge_rows = ""
for label, masked_p, delta in edge_contribs:
# logit space 기준 임계값 (0.1 이상이면 유의미)
if abs(delta) < 0.1:
interp = "거의 영향 없음"
color, row_bg = "#888", "#ffffff"
elif delta > 0:
interp = "이 엣지를 끄면 위험도 ↓ → 이 엣지가 위험을 만드는 핵심"
color, row_bg = "#A32D2D", "#FAECE7"
else:
interp = "이 엣지를 끄면 위험도 ↑ → 이 엣지가 안전 신호였음"
color, row_bg = "#3B6D11", "#E1F5EE"
bar_w = (abs(delta) / max_edge_delta) * 100 if max_edge_delta > 0 else 0
edge_rows += f"""
{label}
{masked_p:.4f}
{delta:+.4f}
{interp}
"""
edge_table = f"""
제거 대상 엣지
제거 후 P
Δ logit
크기
해석
{edge_rows}
기준선 (전체 그래프)
{prob:.4f}
엣지를 모두 살린 원본 예측. ※ P는 sigmoid 후 값이라 saturate 가능 → 기여도는 logit space에서 측정
"""
# ─── GNNExplainer 스타일 XAI: 노드 피처 마스킹 ─────────
_, node_feat_contribs = compute_gnn_node_feature_attribution(amount, hour, new_payee_bin, ratio)
max_feat_delta = max(abs(d) for _, d in node_feat_contribs) if node_feat_contribs else 1.0
feat_rows = ""
for feat, delta in sorted(node_feat_contribs, key=lambda x: -abs(x[1])):
color = "#A32D2D" if delta > 0 else ("#3B6D11" if delta < 0 else "#888")
row_bg = "#FAECE7" if delta > 0 else ("#E1F5EE" if delta < 0 else "#ffffff")
bar_w = (abs(delta) / max_feat_delta) * 100 if max_feat_delta > 0 else 0
feat_rows += f"""
{feat}
{delta:+.4f}
"""
feat_table = f"""
피처 (baseline으로 대체 시)
Δ logit
크기
{feat_rows}
"""
# 가장 강하게 활성화된 차원 top 3
top3_indices = np.argsort(h2_trans)[-3:][::-1]
top3_rows = ""
for i, idx in enumerate(top3_indices):
val = h2_trans[idx]
if val < 0.01:
interpretation = "(거의 활성화 안 됨)"
color = "#888"
bg_row = "#ffffff"
else:
interpretation_pool = [
"수취인 위험도 신호 추정 (모델만 아는 추상 패턴)",
"송금인 평소 행동 이탈도 추정",
"거래 시간대 + 금액 조합 신호",
"사람이 해석 불가 (모델 내부 표현)",
"그래프 2-hop 위험 클러스터 신호 추정",
]
interpretation = interpretation_pool[i % len(interpretation_pool)]
color = "#4A1B0C" if val > 0.5 else "#5F4308"
bg_row = "#FAECE7" if val > 0.5 else "#FAEEDA"
top3_rows += (
f"
"
f"
h'{idx} (은닉층 2)
"
f"
{val:+.3f}
"
f"
{interpretation}
"
f"
"
)
interpret_table = f"""
차원
활성값
사람의 추정 (모델은 알려주지 않음)
{top3_rows}
나머지 13개 차원: 대부분 해석 불가
"""
gen4_setup = feature_setup_box(
actor_label="구조는 사람, 임베딩은 모델 생성",
actor_color='model',
items=[
("그래프 구조 정의", "사람", "노드 종류(거래·송금인·수취인·단말기)와 엣지 관계는 사람이 설계"),
("학습 데이터", "사람", "1-3세대와 동일한 250건 거래에 라벨 부여"),
("노드 초기 임베딩", "사람", "각 노드의 4차원 초기값은 사람이 인코딩 규칙 작성"),
("은닉층 16차원 Feature", "모델", "사람이 정의 안 함. 모델이 학습으로 16개 익명 차원을 자동 생성"),
("가중치 W₁, W₂, W_mlp", "모델", "337개 파라미터를 backpropagation으로 자동 학습"),
("판정 임계값", "사람", "0.5(추가인증) / 0.7(차단)"),
],
explanation="결정적 차이: 1-3세대는 사람이 정한 4개 피처만 봤지만, 4세대는 모델이 16+16=32개의 새 익명 피처를 스스로 만들어냄."
)
# ─── XAI 구현 박스 (4세대) ─────────────────────────────
# 가장 영향력 큰 엣지·피처 찾기
top_edge_label, _, top_edge_delta = max(edge_contribs, key=lambda x: abs(x[2]))
top_feat_name, top_feat_delta = max(node_feat_contribs, key=lambda x: abs(x[1]))
gen4_xai = xai_box(
method="GNNExplainer (Ying 2019) — 엣지·노드 마스킹",
status='post-hoc',
era="2019년 발표 → 2021-2023년 그래프 FDS 도입",
items=[
("왜 SHAP만으론 부족한가", "GNN은 입력이 '거래 1건의 피처'가 아니라 '거래를 둘러싼 그래프(노드+엣지) 전체'. SHAP은 피처 기여도만 분해할 뿐, '어느 이웃과의 관계가 위험을 만들었는가'는 답하지 못함"),
("엣지 마스킹 (Edge Attribution)", f"각 엣지를 차례로 끄고 예측 변화 측정 (logit space). 본 거래에서 가장 큰 영향 엣지 = {top_edge_label} (Δ logit = {top_edge_delta:+.4f}). 이게 그래프 구조 설명의 핵심"),
("노드 피처 마스킹", f"각 입력 피처를 평균값으로 대체하고 예측 변화 측정. 본 거래에서 가장 큰 영향 피처 = {top_feat_name} (Δ logit = {top_feat_delta:+.4f})"),
("logit space 측정 이유", "사기 케이스에서 sigmoid가 saturate(P≈1.0)되면 확률 차이가 0에 수렴해 시각화 불가. 그래서 sigmoid 직전의 logit에서 차이를 측정 (실무 GNNExplainer 구현도 동일)"),
("실무 운영 가치", "예: '이 거래가 차단된 이유: 수취인이 사기계좌와 2-hop 거리에 있기 때문' 같은 그래프 기반 설명이 가능 → 콜센터·심사팀이 고객 응대에 활용"),
("한계", "여전히 16차원 은닉 임베딩 자체의 의미는 해석 불가. 위 '활성화 상위 3차원'에 적힌 해석은 모두 사람의 사후 추측일 뿐"),
],
takeaway="4세대 XAI의 핵심은 '피처 기여도 → 그래프 구조 기여도'로 설명 단위가 확장된 것. 보이스피싱 클러스터 탐지 같은 작업에서 결정적으로 유용함."
)
return prob, f"""
"
f"{judgment_text}"
)
# ─── Counterfactual 시각화 ─────────────────────────────
cf_rows = ""
for item in cf_results:
# 새 시그니처: (label, cf_prob, prob_drop, flipped, logit_drop)
if len(item) == 5:
label, cf_prob, prob_drop, flipped, logit_drop = item
else: # 이전 시그니처 호환
label, cf_prob, prob_drop, flipped = item
logit_drop = 0.0
flip_badge = ("판정 뒤집힘 ✓"
if flipped else "")
# logit drop이 의미있는 신호 (saturate되어도 살아남음)
logit_color = "#3B6D11" if logit_drop > 0.5 else "#888"
prob_color = "#3B6D11" if prob_drop > 0.05 else "#888"
cf_rows += (
f"
"
f"
{label}{flip_badge}
"
f"
{cf_prob:.4f}
"
f"
−{prob_drop:.4f}
"
f"
−{logit_drop:.4f}
"
f"
"
)
cf_table = f"""
반사실 가정
변경 후 P
ΔP
Δ logit
{cf_rows}
※ saturate 영역(P≈1.0)에서는 ΔP가 작아도 Δ logit이 크면 실제로는 강하게 정상 쪽으로 끌어당기는 변경임
"""
cf_natural = ""
if counterfactual_text:
cf_natural = (
f"
"
f"💬 LLM이 생성한 반사실 설명: {counterfactual_text}
"
)
gen5_setup = feature_setup_box(
actor_label="프롬프트만 사람, 추론은 전적으로 모델",
actor_color='model',
items=[
("시스템 프롬프트", "사람", "'당신은 FDS 분석가입니다' 등의 역할 부여만 사람이 작성"),
("학습 데이터", "모델", "Anthropic이 인터넷 규모 데이터로 사전 학습 (수조 토큰)"),
("도메인 지식", "모델", "보이스피싱 패턴, 부동산 거래 정형 등을 사전 학습으로 보유"),
("추론 단계 (CoT)", "모델", "각 단계에서 무엇에 주목할지 모델이 스스로 결정"),
("최종 판단 문장", "모델", "자연어로 자동 생성"),
("판정 임계값", "사람", "0.5(추가인증) / 0.7(차단)"),
],
explanation="모델이 사전 학습된 도메인 지식으로 '왜 사기인지/아닌지'를 자연어로 추론. 4세대까지의 250건 학습과는 차원이 다른 규모의 사전 학습이 깔려 있음."
)
# ─── XAI 구현 박스 (5세대) — 핵심 변화 ──────────────────
gen5_xai = xai_box(
method="CoT + Tool Use + Grounded Reasoning + Counterfactual",
status='generative',
era="2023년 ChatGPT 이후 ~ 현재",
items=[
("패러다임 전환", "3·4세대: 모델이 판단 후 → 별도 알고리즘이 설명 추출 (Post-hoc). 5세대: 추론 과정 자체가 자연어로 생성되어 그게 곧 설명 (Generative)"),
("Chain-of-Thought (CoT)", "각 추론 단계를 모델이 직접 자연어로 출력. 위 '추론 체인' 표가 그것. 어느 신호에 얼마나 주목했는지(attention)까지 함께 산출"),
("Tool Use로 구조 강제", "submit_fds_judgment 도구의 JSON 스키마로 출력 형식을 강제 → 자유 텍스트 파싱 오류 원천 차단, DB 적재·감사 추적 가능"),
("Grounded Reasoning", "3세대 SHAP과 4세대 GNNExplainer 결과를 프롬프트에 함께 주입 → LLM이 단순 자체 추론이 아니라 하위 모델의 설명까지 참조. evidence_source 필드로 출처 추적"),
("Counterfactual Explanation", "'이 거래가 통과되려면 무엇이 달라져야 했나'를 자연어로 생성 + GNN으로 빠르게 검증 (아래 표). 고객 응대·이의 제기 처리에 직접 활용 가능"),
("Faithfulness 한계", "LLM이 출력한 reasoning이 실제 내부 계산을 정확히 반영한다는 보장은 학계 미해결 문제 → 그래서 1-4세대 점수와 교차 검증하는 하이브리드가 권장됨"),
],
takeaway="5세대 XAI의 본질: '설명이 사후 추출되는 무엇'에서 '판단의 산출물 그 자체'로 변화. SHAP·GNNExplainer는 사라지지 않고 LLM의 입력 컨텍스트로 흡수되어 함께 작동."
)
return f"""
{card_header("GEN 5 · FOUNDATION MODEL (LLM)", f"초거대 추론 모델 ({source_label})", dec, bg, fg, f"의심도 {prob*100:.0f}%")}
{gen5_setup}
{gen5_xai}
강점: 1-4세대 XAI 결과를 컨텍스트로 받아 자연어 추론 + 권고 조치 + 반사실 설명까지 한 번에 생성
"""
def render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
gen3_shap, gen4_edges, gen4_feats, cf_results):
"""API 호출 실패/미설정 시 fallback용 시뮬레이션"""
is_high_risk = (new_payee_bin == 1 and (hour <= 6 or hour >= 22) and amount >= 500)
prob = 0.95 if is_high_risk else min(prior_avg + 0.02, 0.98)
payee_str = "신규 수취인" if new_payee_bin == 1 else "기존 수취인"
time_str = "새벽" if (hour <= 6 or hour >= 22) else "일반"
# SHAP/GNN XAI 결과에서 가장 큰 신호 추출하여 reasoning에 반영
top_shap = max(gen3_shap, key=lambda x: abs(x[1])) if gen3_shap else ("금액", 0)
top_edge = max(gen4_edges, key=lambda x: abs(x[2])) if gen4_edges else ("이웃", 0, 0)
reasoning_steps = [
{
"step": f"4세대 GNNExplainer: '{top_edge[0]}' 엣지 기여도 {top_edge[2]:+.3f} — 그래프 구조상 핵심 신호",
"attention": 0.32,
"evidence_source": "4세대 GNNExplainer"
},
{
"step": f"3세대 SHAP: '{top_shap[0]}' 기여도 {top_shap[1]:+.3f} — 평균 거래 대비 위험 방향",
"attention": 0.28,
"evidence_source": "3세대 SHAP"
},
{
"step": f"시간 {int(hour):02d}시 + {payee_str} → 보이스피싱 정형 패턴 매칭",
"attention": 0.24,
"evidence_source": "도메인 지식"
},
{
"step": f"평소 {ratio:.1f}배 금액 → 송금인 평소 행동 이탈도 측정",
"attention": 0.16,
"evidence_source": "거래 컨텍스트"
},
]
if is_high_risk:
decision_text = "차단"
judgment_text = (
f"{int(hour):02d}시 {time_str} 시간대에 평소보다 {ratio:.1f}배 급증한 {amount:.0f}만원이 "
f"{payee_str}에게 이체되는 것은 전형적인 보이스피싱 패턴입니다. "
f"3세대 SHAP에서도 '{top_shap[0]}'이 가장 강한 위험 신호로 나타났고, "
f"4세대 GNNExplainer 결과 '{top_edge[0]}'이 그래프 구조 차원에서 핵심 기여를 했습니다.
"
f"권고 조치: ① 즉시 거래 보류, ② 등록된 전화번호로 본인 직접 확인, ③ 확인 전 자금 동결 24시간 유지"
)
counterfactual_text = "수취인이 송금인의 기존 거래 이력이 있는 계좌였거나, 거래 시간이 주간(9-18시)이었다면 위험도가 큰 폭으로 감소했을 것입니다."
elif prob >= 0.5:
decision_text = "추가 인증"
judgment_text = (
f"{int(hour):02d}시 거래에서 일부 이상 신호({ratio:.1f}배 금액, {payee_str})가 감지되었으나 "
f"결정적 위험 패턴은 아닙니다. 차단보다는 추가 인증으로 본인 의사를 확인하는 것이 적절합니다.
"
f"권고 조치: ① ARS 또는 OTP 추가 인증, ② 송금 의도 재확인 메시지 발송"
)
counterfactual_text = "신규 수취인 플래그가 없었거나 금액이 평소 수준이었다면 통과 가능했을 것입니다."
else:
decision_text = "통과"
judgment_text = (
f"{int(hour):02d}시 거래의 패턴이 송금인의 평소 행동 범위 내에 있으며, "
f"1-4세대 모델 모두 위험 신호를 강하게 보내지 않았습니다. 정상 거래로 판단됩니다.
"
f"권고 조치: 별도 조치 없이 거래 진행"
)
counterfactual_text = "(통과 거래이므로 반사실 분석은 적용 대상 아님)"
meta_html = (
"mode: SIMULATION (API 미연결) "
"이 결과는 if-else 하드코딩으로 생성된 시뮬레이션입니다. "
"단, reasoning_steps에는 실제 3세대 SHAP과 4세대 GNNExplainer 결과가 반영됨. "
"실제 Claude 호출을 활성화하려면 ANTHROPIC_API_KEY 환경변수를 설정하세요."
)
return render_gen5_card(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
prob, decision_text, reasoning_steps, judgment_text,
counterfactual_text, cf_results,
meta_html, source_label="시뮬레이션 모드"
)
def render_gen5(amount, hour, new_payee_bin, ratio, prior_avg, use_claude_api=True):
"""5세대 진입점.
1-4세대 XAI 결과(SHAP, GNNExplainer)를 모두 수집하여 LLM에 함께 전달.
"""
prior_dec, _, _ = decide(prior_avg)
# 3·4세대 XAI 결과 수집 (Grounded Reasoning용)
gen3_shap_vals, _ = compute_shap_values_gen3(amount, hour, new_payee_bin, ratio)
gen3_shap = list(zip(FEATURES, gen3_shap_vals))
_, gen4_edges = compute_gnn_edge_attribution(amount, hour, new_payee_bin, ratio)
_, gen4_feats = compute_gnn_node_feature_attribution(amount, hour, new_payee_bin, ratio)
# Counterfactual 후보 평가 (GNN으로 빠르게)
cf_results = build_counterfactual_gen5(amount, hour, new_payee_bin, ratio)
if not use_claude_api or not CLAUDE_AVAILABLE:
return render_gen5_simulation(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
gen3_shap, gen4_edges, gen4_feats, cf_results
)
parsed, meta_or_err = call_claude_api(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
gen3_shap=gen3_shap, gen4_edges=gen4_edges, gen4_feats=gen4_feats
)
if parsed is None:
fallback_html = render_gen5_simulation(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
gen3_shap, gen4_edges, gen4_feats, cf_results
)
warning = (
f'
'
f'⚠️ Claude API 호출 실패. 시뮬레이션 모드로 대체합니다. '
f'사유: {meta_or_err}'
f'
'
f'⚠️ Claude 응답 파싱 실패. 시뮬레이션 모드로 대체합니다. '
f''
f'{type(e).__name__}: {str(e)[:150]}
'
)
return warning + fallback_html
# ============================================================
# 4. 메인 분석 함수
# ============================================================
def render_xai_evolution_summary():
"""모든 카드 위에 표시되는 XAI 진화 요약 박스 (강의용)"""
return """
🔍 세대별 XAI 구현 방식 — 한눈에 보기
세대
XAI 기법
설명 단위
한 줄 핵심
1세대
불필요 (Self-Explanatory)
발동된 룰
룰북 자체가 설명. 별도 알고리즘 필요 없음
2세대
계수 분해 (Coefficient)
피처별 wᵢ·xᵢ
선형 모델 = 정확한 SHAP. 내재적 설명력 유지
3세대 ⚡
TreeSHAP (2017~)
피처 기여도
XAI가 본격 등장한 세대. 모델·설명기 분리. 현재 금융권 표준
4세대
GNNExplainer (2019~)
엣지 + 노드 피처
설명 단위가 '그래프 구조'로 확장. 관계망 기반 설명
5세대 🆕
CoT + Tool Use + Counterfactual
자연어 추론 체인
패러다임 전환: 설명이 사후 추출 → 판단의 산출물 그 자체. 3·4세대 XAI를 컨텍스트로 흡수
💡 강의 포인트: XAI가 '문제'가 된 것은 3세대(XGBoost)부터. 1-2세대는 내재적 설명력이, 5세대는 생성형 설명이 있어 별도 알고리즘이 덜 중요. 3-4세대에서 SHAP/GNNExplainer가 핵심.