Interpretabilità - Spiegatore SHAP Tabellare

Usare Kernel SHAP (SHapley Additive exPlanations) per spiegare un modello di classificazione tabulare. Kernel SHAP è un metodo indipendente dal modello che stima il contributo di ogni funzionalità alla stima di un modello. Si addestra un modello di regressione logistica sul set di dati Adult Census Income e quindi si usa il trasformatore SynapseML TabularSHAP per calcolare spiegazioni per singola caratteristica.

Prerequisiti

  • Abbonati a Microsoft Fabric. Oppure, registrati per una versione di prova gratuita di Microsoft Fabric.

  • Accedi a Microsoft Fabric.

  • Passare a Fabric usando il selettore di esperienza nell'angolo in basso a sinistra della home page.

    Screenshot che mostra la selezione di

  • Crea un nuovo notebook nell'area di lavoro e associalo a una lakehouse. Per altre informazioni, vedere Creare un notebook.

SynapseML, PySpark, pandas e plotly sono preinstallati in ambienti notebook Fabric. Non è necessaria alcuna installazione aggiuntiva del pacchetto.

Importare i pacchetti e definire UDF di supporto

Nel notebook di Fabric incollare il codice seguente in una cella ed eseguirlo. Questo passaggio importa le librerie necessarie e definisce due funzioni definite dall'utente (UDF) per estrarre gli elementi vettoriali in un secondo momento.

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()))

Verificare: eseguire il codice seguente in una nuova cella. Verrà visualizzato l'output TabularSHAP imported successfully.

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

Caricare i dati ed eseguire il training di un modello di classificazione

Carica il dataset Adult Census Income da Archiviazione BLOB di Azure, indicizza l'etichetta di destinazione e addestra una pipeline di regressione logistica.

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)

Verificare: eseguire la cella seguente. Dovresti vedere il numero di righe dei dati di addestramento e la conferma delle fasi della pipeline.

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

Selezionare le osservazioni da spiegare

Selezionare casualmente cinque osservazioni dai dati di addestramento con punteggio. Queste osservazioni sono le istanze per cui si generano spiegazioni SHAP.

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

Verificare: confermare le dimensioni del campione.

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

Configurare ed eseguire TabularSHAP

Crea una TabularSHAP spiegazione e applicala alle osservazioni selezionate. I parametri chiave sono:

Parametro Descrizione
inputCols Colonne delle caratteristiche utilizzate dal modello per la previsione.
outputCol Nome della colonna contenente i valori di output SHAP.
numSamples Numero di campioni di perturbazione per la stima SHAP del kernel. I valori più elevati sono più accurati ma più lenti.
model Il modello di pipeline addestrato da spiegare.
targetCol Colonna di output del modello da spiegare. In questo esempio la colonna è probability.
targetClasses Indici di classe da spiegare. [1] spiega solo la probabilità della classe 1. Usare [0, 1] per spiegare entrambe le classi.
backgroundData Esempio di dati di training usati come distribuzione di riferimento per l'integrazione delle funzionalità.
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)

Note

Questo passaggio può richiedere alcuni minuti a seconda di numSamples e delle dimensioni del cluster. Con numSamples=5000 e cinque osservazioni, prevedere 3-10 minuti in un cluster Spark Fabric predefinito.

Verificare che la colonna di output SHAP esista.

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

Estrarre valori SHAP

Estrarre i valori di probabilità e SHAP della classe 1 dal dataframe del risultato. Per ogni osservazione, il vettore di valori SHAP inizia con il valore di base (output medio del set di dati in background), seguito da un valore per funzionalità.

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)

Verifica: confermare la struttura del dataframe pandas.

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")

Visualizzare i valori SHAP

Creare un grafico a barre per ogni osservazione che mostra come ogni caratteristica contribuisce alla probabilità stimata.

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()

Verifica: verificare che l'oggetto tracciato sia stato creato.

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")

Interpretare i risultati

Ogni sottolot rappresenta un'osservazione. Le barre mostrano:

  • Base: output medio del modello nel set di dati in background (probabilità di base).
  • Valori SHAP positivi: funzionalità che spingono la stima verso la classe 1 (reddito maggiore di 50.000).
  • Valori SHAP negativi: caratteristiche che spingono la previsione verso la classe 0 (reddito inferiore o pari a 50K).

La somma del valore di base e di tutti i valori SHAP della funzionalità è uguale alla probabilità stimata del modello per tale osservazione.

Risoluzione dei problemi

Problema Motivo Risoluzione
OutOfMemoryError durante TabularSHAP numSamples è troppo grande per la memoria disponibile. Ridurre numSamples, ad esempio a 1.000, oppure aumentare la memoria dell'executor di Spark.
La trasformazione SHAP è lenta Una numSamples elevata con numerose funzionalità aumenta il tempo di calcolo. Ridurre numSamples a 1.000-2.000 per risultati esplorativi più veloci. Aumento per l'analisi finale.
FileNotFoundException per parquet L'accesso alla rete a mmlspark.blob.core.windows.net è bloccato. Verificare che l'area di lavoro Fabric abbia accesso a Internet in uscita. In alternativa, carica il set di dati nel tuo lakehouse.
shapValues la colonna contiene valori Null Alcune osservazioni potrebbero dare esito negativo se i valori delle variabili escono dalla distribuzione di addestramento. Verificare la presenza di valori Null o imprevisti nelle funzionalità di input. Filtrare i valori Null dai risultati.
display() non mostra alcun output Il codice viene eseguito all'esterno di un ambiente notebook Fabric. Usare shaps_local.head() o print(shaps_local) in ambienti di Python standard.

Pulizia

Se il set di dati è stato caricato in una lakehouse per questa esercitazione, rimuoverlo per liberare spazio di archiviazione:

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