Interpretabilidade - explicador tabular SHAP

Use o Kernel SHAP (SHapley Additive exPlanations) para explicar um modelo de classificação para dados tabulares. Kernel SHAP é um método independente de modelo que estima a contribuição de cada recurso para a previsão de um modelo. Você treina um modelo de regressão logística no conjunto de dados Adult Census Income e, em seguida, usa o transformador SynapseML TabularSHAP para calcular explicações em nível de atributo.

Pré-requisitos

  • Crie um novo notebook em seu espaço de trabalho e vincule-o a um lakehouse. Para obter mais informações, consulte Criar um bloco de anotações.

SynapseML, PySpark, pandas e plotly são pré-instalados em ambientes de notebook Fabric. Nenhuma instalação de pacote extra é necessária.

Importar pacotes e definir UDFs auxiliares

No bloco de anotações Fabric, cole o código a seguir em uma célula e execute-o. Esta etapa importa as bibliotecas necessárias e define duas UDFs (funções definidas pelo usuário) para extrair elementos de vetor posteriormente.

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

Verificar: execute o seguinte código em uma nova célula. Você deve ver a saída TabularSHAP imported successfully.

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

Carregar dados e treinar um modelo de classificação

Carregue o conjunto de dados de renda do Censo de Adultos no Armazenamento de Blobs do Azure, crie um índice para o rótulo de destino e treine um pipeline de regressão logística.

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)

Verifique: Execute a célula a seguir. Você deve ver a contagem de linhas dos dados de treinamento e a confirmação dos estágios do 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

Selecionar observações para explicar

Selecione aleatoriamente cinco observações dos dados de treinamento pontuados. Essas observações são as instâncias para as quais você gera explicações SHAP.

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

Verificar: confirme o tamanho da amostra.

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

Configurar e executar TabularSHAP

Crie um TabularSHAP explicador e aplique-o às observações selecionadas. Os principais parâmetros são:

Parâmetro Description
inputCols Colunas de atributos que o modelo usa para fazer previsões.
outputCol Nome da coluna que contém valores de saída SHAP.
numSamples Número de amostras de perturbação para estimativa do Kernel SHAP. Valores mais altos são mais precisos, mas mais lentos.
model O modelo de pipeline treinado que será explicado.
targetCol A coluna de saída do modelo a ser explicada. Neste exemplo, a coluna é probability.
targetClasses Índices de classe a serem explicados. [1] explica apenas a probabilidade da classe 1. Use [0, 1] para explicar ambas as classes.
backgroundData Um exemplo de dados de treinamento usados como a distribuição de referência para integrar recursos.
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

Essa etapa pode levar vários minutos, dependendo de numSamples e do tamanho do cluster. Com numSamples=5000 e cinco observações, espere de 3 a 10 minutos em um cluster spark Fabric padrão.

Verifique: Verifique se a coluna de saída SHAP existe.

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

Extrair valores SHAP

Extraia a probabilidade da classe 1 e os valores SHAP do DataFrame de resultados. Para cada observação, o vetor de valores SHAP começa com o valor base (saída média do conjunto de dados em segundo plano), seguido por um valor por recurso.

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)

Verificar: confirme a estrutura do DataFrame do 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")

Visualizar valores SHAP

Crie um gráfico de barras para cada observação que mostra como cada recurso contribui para a probabilidade prevista.

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

Verifique: confirme que o objeto de gráfico foi criado.

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

Interpretar os resultados

Cada subtrama representa uma observação. As barras mostram:

  • Base: a saída média do modelo no conjunto de dados em segundo plano (probabilidade de linha de base).
  • Valores SHAP positivos: recursos que empurram a previsão para a classe 1 (renda maior que 50 mil).
  • Valores SHAP negativos: recursos que empurram a previsão para a classe 0 (renda menor ou igual a 50 mil).

A soma do valor base e de todos os valores SHAP das características é igual à probabilidade prevista pelo modelo para essa observação.

Solução de problemas

Questão Cause Resolução
OutOfMemoryError durante TabularSHAP numSamples é muito grande para memória disponível. Reduza numSamples, por exemplo, para 1.000 ou aumente a memória do executor do Spark.
A transformação SHAP é lenta High numSamples com muitos recursos aumenta o tempo de processamento. Reduza numSamples para 1.000-2.000 para resultados exploratórios mais rápidos. Aumentar para análise final.
FileNotFoundException para parquet O acesso de rede a mmlspark.blob.core.windows.net está bloqueado. Verifique se o workspace Fabric tem acesso à Internet de saída. Como alternativa, carregue o conjunto de dados em seu lakehouse.
shapValues a coluna contém valores nulos Algumas observações poderão falhar se os valores do recurso estiverem fora da distribuição de treinamento. Verifique se há valores nulos ou inesperados nos recursos de entrada. Filtrar valores nulos dos resultados.
display() não exibe saída O código está em execução fora de um ambiente de bloco de anotações Fabric. Use shaps_local.head() ou print(shaps_local) em ambientes de Python padrão.

Limpeza

Se você carregou o conjunto de dados em um lakehouse para este tutorial, remova-o para liberar espaço de armazenamento:

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