해석 가능성 - 테이블 형식 SHAP 설명 도구

커널 SHAP(SHapley 가산성 설명)을 사용하여 테이블 형식 분류 모델을 설명합니다. 커널 SHAP은 모델 예측에 대한 각 기능의 기여도를 예측하는 모델 중립적 메서드입니다. 성인 인구 조사 소득 데이터 세트에서 로지스틱 회귀 모델을 학습시킨 다음 SynapseML TabularSHAP 변환기를 사용하여 기능 수준 설명을 계산합니다.

사전 요구 사항

SynapseML, PySpark, pandas 및 plotly는 Fabric Notebook 환경에 미리 설치됩니다. 추가 패키지 설치가 필요하지 않습니다.

패키지 가져오기 및 도우미 UDF 정의

Fabric Notebook에서 다음 코드를 셀에 붙여넣고 실행합니다. 이 단계에서는 필요한 라이브러리를 가져오고 나중에 벡터 요소를 추출하기 위한 두 개의 UDF(사용자 정의 함수)를 정의합니다.

import pyspark
from synapse.ml.explainers import TabularSHAP
from pyspark.ml import Pipeline
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.feature import StringIndexer, OneHotEncoder, VectorAssembler
from pyspark.sql.types import FloatType, ArrayType
from pyspark.sql.functions import col, lit, rand, broadcast, udf
import pandas as pd

vec_access = udf(lambda v, i: float(v[i]), FloatType())
vec2array = udf(lambda vec: vec.toArray().tolist(), ArrayType(FloatType()))

확인: 새 셀에서 다음 코드를 실행합니다. 출력 TabularSHAP imported successfully이 표시됩니다.

print("TabularSHAP imported successfully")
print(f"PySpark version: {pyspark.__version__}")

데이터 로드 및 분류 모델 학습

Azure Blob Storage 성인 인구 조사 소득 데이터 세트를 로드하고, 대상 레이블을 인덱싱하고, 로지스틱 회귀 파이프라인을 학습시킵니다.

df = spark.read.parquet(
    "wasbs://publicwasb@mmlspark.blob.core.windows.net/AdultCensusIncome.parquet"
)

labelIndexer = StringIndexer(
    inputCol="income", outputCol="label", stringOrderType="alphabetAsc"
).fit(df)
print("Label index assignment: " + str(set(zip(labelIndexer.labels, [0, 1]))))

training = labelIndexer.transform(df).cache()

categorical_features = [
    "workclass",
    "education",
    "marital-status",
    "occupation",
    "relationship",
    "race",
    "sex",
    "native-country",
]
categorical_features_idx = [feat + "_idx" for feat in categorical_features]
categorical_features_enc = [feat + "_enc" for feat in categorical_features]
numeric_features = [
    "age",
    "education-num",
    "capital-gain",
    "capital-loss",
    "hours-per-week",
]

strIndexer = StringIndexer(
    inputCols=categorical_features, outputCols=categorical_features_idx
)
onehotEnc = OneHotEncoder(
    inputCols=categorical_features_idx, outputCols=categorical_features_enc
)
vectAssem = VectorAssembler(
    inputCols=categorical_features_enc + numeric_features, outputCol="features"
)
lr = LogisticRegression(featuresCol="features", labelCol="label", weightCol="fnlwgt")
pipeline = Pipeline(stages=[strIndexer, onehotEnc, vectAssem, lr])
model = pipeline.fit(training)

확인: 다음 셀을 실행합니다. 학습 데이터의 행 수와 파이프라인 단계 확인이 표시됩니다.

print(f"Training rows: {training.count()}")
print(f"Pipeline stages: {[type(s).__name__ for s in model.stages]}")
assert training.count() > 30000, "Dataset should contain over 30,000 rows"
print("Model trained successfully")

# Expected output:
#Training rows: 32561
#Pipeline stages: ['StringIndexerModel', 'OneHotEncoderModel', #'VectorAssembler', 'LogisticRegressionModel']
#Model trained successfully

설명할 관찰 선택

채점된 학습 데이터에서 5개의 관찰을 임의로 선택합니다. 이러한 관찰은 SHAP 설명을 생성하는 인스턴스입니다.

explain_instances = (
    model.transform(training).orderBy(rand()).limit(5).repartition(200).cache()
)
display(explain_instances)

확인: 샘플 크기를 확인합니다.

count = explain_instances.count()
print(f"Explain instances: {count}")
assert count == 5, f"Expected 5 rows, got {count}"
print("Sample selected successfully")

TabularSHAP 구성 및 실행

설명자를 TabularSHAP 만들고 선택한 관찰에 적용합니다. 주요 매개 변수는 다음과 같습니다.

매개 변수 Description
inputCols 모델이 예측에 사용하는 특성 열입니다.
outputCol SHAP 출력 값을 포함하는 열의 이름입니다.
numSamples 커널 SHAP 추정을 위한 섭동 샘플 수입니다. 값이 높을수록 정확하지만 속도가 느립니다.
model 설명할 학습된 파이프라인 모델입니다.
targetCol 설명할 모델 출력 열입니다. 이 예제에서 열은 probability입니다.
targetClasses 설명할 클래스 인덱스입니다. [1] 는 클래스 1 확률만 설명합니다. 두 클래스를 모두 설명하는 데 사용합니다 [0, 1] .
backgroundData 기능을 통합하기 위한 참조 배포로 사용되는 학습 데이터의 샘플입니다.
shap = TabularSHAP(
    inputCols=categorical_features + numeric_features,
    outputCol="shapValues",
    numSamples=5000,
    model=model,
    targetCol="probability",
    targetClasses=[1],
    backgroundData=broadcast(training.orderBy(rand()).limit(100).cache()),
)

shap_df = shap.transform(explain_instances)

메모

이 단계는 클러스터 크기에 따라 numSamples 몇 분 정도 걸릴 수 있습니다. numSamples=5000 및 5개의 관찰을 통해 기본 Fabric Spark 클러스터에서 3-10분을 예상합니다.

확인: SHAP 출력 열이 있는지 확인합니다.

assert "shapValues" in shap_df.columns, "shapValues column missing"
print(f"SHAP output columns: {shap_df.columns}")
print("TabularSHAP transform completed")

SHAP 값 추출

결과 DataFrame에서 클래스 1 확률 및 SHAP 값을 추출합니다. 각 관찰에 대해 SHAP 값 벡터는 기본 값(백그라운드 데이터 세트의 평균 출력)으로 시작하고 기능당 하나의 값으로 시작합니다.

shaps = (
    shap_df.withColumn("probability", vec_access(col("probability"), lit(1)))
    .withColumn("shapValues", vec2array(col("shapValues").getItem(0)))
    .select(
        ["shapValues", "probability", "label"] + categorical_features + numeric_features
    )
)

shaps_local = shaps.toPandas()
shaps_local.sort_values("probability", ascending=False, inplace=True, ignore_index=True)
pd.set_option("display.max_colwidth", None)
display(shaps_local)

확인: pandas DataFrame 구조를 확인합니다.

expected_cols = len(categorical_features) + len(numeric_features) + 3
print(f"DataFrame shape: {shaps_local.shape}")
print(f"Expected columns: {expected_cols}, Actual: {shaps_local.shape[1]}")
assert shaps_local.shape == (5, expected_cols), f"Unexpected shape: {shaps_local.shape}"
print("SHAP values extracted successfully")

SHAP 값 시각화

각 기능이 예측 확률에 어떻게 기여하는지를 보여 주는 각 관찰에 대한 가로 막대형 차트를 만듭니다.

from plotly.subplots import make_subplots
import plotly.graph_objects as go

features = categorical_features + numeric_features
features_with_base = ["Base"] + features

rows = shaps_local.shape[0]

fig = make_subplots(
    rows=rows,
    cols=1,
    subplot_titles="Probability: "
    + shaps_local["probability"].apply("{:.2%}".format)
    + "; Label: "
    + shaps_local["label"].astype(str),
)

for index, row in shaps_local.iterrows():
    feature_values = [0] + [row[feature] for feature in features]
    shap_values = row["shapValues"]
    list_of_tuples = list(zip(features_with_base, feature_values, shap_values))
    shap_pdf = pd.DataFrame(list_of_tuples, columns=["name", "value", "shap"])
    fig.add_trace(
        go.Bar(
            x=shap_pdf["name"],
            y=shap_pdf["shap"],
            hovertext="value: " + shap_pdf["value"].astype(str),
        ),
        row=index + 1,
        col=1,
    )

fig.update_yaxes(range=[-1, 1], fixedrange=True, zerolinecolor="black")
fig.update_xaxes(type="category", tickangle=45, fixedrange=True)
fig.update_layout(height=400 * rows, title_text="SHAP explanations")
fig.show()

확인: 플롯 개체가 만들어졌는지 확인합니다.

print(f"Figure traces: {len(fig.data)}")
print(f"Figure height: {fig.layout.height}px")
assert len(fig.data) == 5, f"Expected 5 traces, got {len(fig.data)}"
print("Visualization created successfully")

결과 해석

각 서브플롯은 하나의 관찰을 나타냅니다. 막대는 다음과 같이 표시됩니다.

  • 기준: 백그라운드 데이터 세트 전체의 평균 모델 출력(기준 확률)입니다.
  • 양수 SHAP 값: 클래스 1(50K보다 큰 소득)으로 예측을 푸시하는 기능입니다.
  • 음수 SHAP 값: 클래스 0(소득이 50K 미만 또는 같음)으로 예측을 푸시하는 기능입니다.

기본 값과 모든 기능 SHAP 값의 합계는 해당 관찰에 대한 모델의 예측 확률과 같습니다.

Troubleshooting

Issue 원인 해결 방법
OutOfMemoryError TabularSHAP 중 numSamples 가 너무 커서 사용 가능한 메모리가 부족합니다. numSamples를 예를 들어 1,000으로 줄이거나 Spark 실행기 메모리를 늘리세요.
SHAP 변환 속도가 느림 numSamples 기능이 많을 경우 컴퓨팅 시간이 늘어나게 됩니다. 더 빠른 탐색 결과를 위해 numSamples를 1,000-2,000으로 줄이세요. 최종 분석을 위해 늘입니다.
FileNotFoundException parquet용 네트워크 액세스 mmlspark.blob.core.windows.net 가 차단됩니다. Fabric 작업 영역에 아웃바운드 인터넷 액세스 권한이 있는지 확인합니다. 또는 데이터 세트를 레이크하우스에 업로드하세요.
shapValues 열에 null이 포함됩니다. 기능 값이 학습 배포 외부에 있는 경우 일부 관찰이 실패할 수 있습니다. 입력 기능에서 null 또는 예기치 않은 값을 확인합니다. 결과에서 null을 필터링합니다.
display() 출력을 표시하지 않음 코드가 Fabric Notebook 환경 외부에서 실행되고 있습니다. 표준 Python 환경에서 shaps_local.head() 또는 print(shaps_local) 사용합니다.

정리 작업

이 자습서를 위해 레이크하우스에 데이터 세트를 업로드한 경우 데이터 세트를 제거하여 저장 공간을 확보합니다.

# Remove cached DataFrames from memory
training.unpersist()
explain_instances.unpersist()
print("Cached DataFrames released")