Ερμηνευτικότητα - Επεξήγηση SHAP σε μορφή πίνακα
Σε αυτό το παράδειγμα, χρησιμοποιούμε kernel SHAP για να εξηγήσουμε ένα μοντέλο ταξινόμησης σε μορφή πίνακα που δημιουργήθηκε από το σύνολο δεδομένων της απογραφής ενηλίκων.
Πρώτα εισάγουμε τα πακέτα και ορίζουμε ορισμένα UDF που χρειαζόμαστε αργότερα.
import pyspark
from synapse.ml.explainers import *
from pyspark.ml import Pipeline
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.feature import StringIndexer, OneHotEncoder, VectorAssembler
from pyspark.sql.types import *
from pyspark.sql.functions import *
import pandas as pd
from pyspark.sql import SparkSession
# Bootstrap Spark Session
spark = SparkSession.builder.getOrCreate()
from synapse.ml.core.platform import *
vec_access = udf(lambda v, i: float(v[i]), FloatType())
vec2array = udf(lambda vec: vec.toArray().tolist(), ArrayType(FloatType()))
Τώρα ας διαβάσουμε τα δεδομένα και να εκπαιδεύουμε ένα μοντέλο δυαδικής ταξινόμησης.
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 assigment: " + str(set(zip(labelIndexer.labels, [0, 1]))))
training = labelIndexer.transform(df).cache()
display(training)
categorical_features = [
"workclass",
"education",
"marital-status",
"occupation",
"relationship",
"race",
"sex",
"native-country",
]
categorical_features_idx = [col + "_idx" for col in categorical_features]
categorical_features_enc = [col + "_enc" for col 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)
Μετά την εκπαίδευση του μοντέλου, επιλέγουμε τυχαία ορισμένες παρατηρήσεις για να εξηγηθούν.
explain_instances = (
model.transform(training).orderBy(rand()).limit(5).repartition(200).cache()
)
display(explain_instances)
Δημιουργούμε μια επεξήγηση TabularSHAP, ορίζουμε τις στήλες εισόδου σε όλες τις δυνατότητες που λαμβάνει το μοντέλο, καθορίζετε το μοντέλο και τη στήλη εξόδου προορισμού που προσπαθούμε να εξηγήσουμε. Σε αυτή την περίπτωση, προσπαθούμε να εξηγήσουμε την έξοδο "πιθανότητας", η οποία είναι ένα διάνυσμα μήκους 2 και εξετάζουμε μόνο την πιθανότητα κλάσης 1. Καθορίστε το targetClasses στο [0, 1]
εάν θέλετε να εξηγήσετε την πιθανότητα κλάσης 0 και 1 ταυτόχρονα. Τέλος, κάνουμε δείγμα 100 γραμμών από τα δεδομένα εκπαίδευσης για δεδομένα παρασκηνίου, τα οποία χρησιμοποιούνται για την ενοποίηση δυνατοτήτων στο Kernel SHAP.
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)
Όταν έχουμε το πλαίσιο δεδομένων που προκύπτει, εξάγουμε την πιθανότητα κλάσης 1 της εξόδου μοντέλου, τις τιμές SHAP για την κλάση προορισμού, τις αρχικές δυνατότητες και την ετικέτα true. Στη συνέχεια, το μετατρέπουμε σε ένα πλαίσιο δεδομένων pandas για απεικόνιση. Για κάθε παρατήρηση, το πρώτο στοιχείο στο διάνυσμα τιμών 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)
shaps_local
Χρησιμοποιούμε υποπλόκο σχεδίασης για την απεικόνιση των τιμών SHAP.
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import pandas as pd
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()