背景
假设正面临一个真实业务场景:某电信公司希望预测客户是否可能流失,为了实现这个目标,可以使用客户特征信息(如服务类型、消费金额等)来预测客户是否会流失,这个问题可以被建模成一个二分类问题,即客户是否会流失 (Churn),用 0 或 1 表示
在本文中,将使用深度学习模型来解决这个问题,并重点讨论如何通过 SHAP 来解释模型预测的结果
代码实现
数据读取
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Times New Roman'
plt.rcParams['axes.unicode_minus'] = False
df = pd.read_csv("WA_Fn-UseC_-Telco-Customer-Churn.csv")
df = df.drop(["customerID"], axis=1)
df.head()
使用 Kaggle 上的 Telco Customer Churn 数据集,数据集包含了丰富的客户特征及流失信息,在数据分析的起步阶段,需要对数据进行清洗和转换,以确保数据适用于后续的建模和分析
数据预处理
数据基本信息输出
df.info()
数据集包含 7043 条记录和 20 列特征,其中 tenure、MonthlyCharges 和 TotalCharges 是数值类型,其余的特征多为分类类型(object),目标标签为 Churn,用来预测客户是否流失
数据类型转换
# 处理 TotalCharges 列的空字符串或非数值数据
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
# 将数据类型转换为 float64
df['TotalCharges'] = df['TotalCharges'].astype('float64')
df['TotalCharges'].dtype
可以发现 TotalCharges 列在dataframe中展示为数值但是实际为object数据类型存在数据类型混乱,将 TotalCharges 列中的非数值数据和空字符串转换为缺失值(NaN),并将其数据类型转换为 float64 以便后续数值处理
缺失值检验
df.isnull().sum()
这里可以发 现 TotalCharges 列存在11个缺失值数量由于占比较小,简化工作直接删除存在缺失的样本行即可
df.dropna(subset=['TotalCharges'], inplace=True)
数据编码
from sklearn.preprocessing import LabelEncoder
# 自动选择数据类型为 'object' 的列
columns_to_encode = df.select_dtypes(include=['object']).columns
# 初始化字典来存储每列的编码信息
label_mappings = {}
# 对需要编码的列进行标签编码
for column in columns_to_encode:
le = LabelEncoder()
df[column] = le.fit_transform(df[column])
# 将编码的类别及其对应的值保存到字典中
label_mappings[column] = dict(zip(le.classes_, le.transform(le.classes_)))
# 输出每个特征列的编码信息
for column, mapping in label_mappings.items():
print(f"Feature: {column}")
for category, code in mapping.items():
print(f" {category}: {code}")
print("\n")
由于原始数据存在大量类别数据,使用 LabelEncoder 对数据中的类别特征进行编码,将这些字符型特征转换为机器学习模型可以理解的数值型数据
样本采样
from imblearn.over_sampling import SMOTE
# 将特征 (X) 和标签 (y) 分开
X = df.drop(columns=['Churn']) # 特征数据,去掉 'Churn' 列
y = df['Churn'] # 目标标签,即 'Churn'
# 初始化 SMOTE
smote = SMOTE(random_state=42)
# 对数据进行过采样
X_res, y_res = smote.fit_resample(X, y)
# 输出过采样后的类别分布
print("原始数据类别分布:\n", y.value_counts())
print("过采样后的类别分布:\n", y_res.value_counts())
在数据预处理的最后一步,使用SMOTE方法来平衡类别分布,防止模型因为类别不平衡而产生偏差
构建深度学习模型
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
X = X_res
y = y_res
from sklearn.model_selection import train_test_split
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y_res)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.125, random_state=42, stratify=y_temp)
# 输入形状为 (samples, features)
input_shape = (X_train.shape[1],)
# 构建全连接神经网络模型
model = Sequential()
# 添加全连接层
model.add(Dense(units=64, input_shape=input_shape, activation='relu'))
model.add(Dropout(0.2)) # 添加 Dropout 防止过拟合
# 添加第二个全连接层
model.add(Dense(units=32, activation='relu'))
model.add(Dropout(0.2))
# 添加输出层,使用sigmoid作为激活函数处理二分类
model.add(Dense(units=1, activation='sigmoid'))
# 编译模型
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
# 训练模型
history = model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32)
# 绘制训练和验证的损失曲线
plt.plot(history.history['loss'], label='train loss')
plt.plot(history.history['val_loss'], label='val loss')
plt.title('Loss over epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
# 打印模型摘要
model.summary()
选择一个简单的全连接神经网络架构来处理这个分类任务。模型包含两个隐藏层,每层分别有 64 和 32 个神经元,并使用 ReLU 激活函数。为了防止过拟合,在每一层后加入了 Dropout 层,选择 binary_crossentropy 作为损失函数,并使用 Adam 优化器。模型的评价指标为准确率(accuracy),模型训练使用了 100 个 Epoch,并在训练集中使用了 80% 的数据进行训练,剩余的 20% 作为验证集
模型性能评估
分类报告
# 使用模型在测试集上进行预测
y_pred_prob = model.predict(X_test)
# 将概率转换为二分类标签 (如果概率 >= 0.5,预测为 1,否则为 0)
y_pred = (y_pred_prob >= 0.5).astype(int)
from sklearn.metrics import classification_report
# 输出模型报告, 查看评价指标
print(classification_report(y_test, y_pred))
混淆矩阵
from sklearn.metrics import confusion_matrix
# 计算混淆矩阵
confusion_matrix = confusion_matrix(y_test, y_pred)
# 绘制混淆矩阵
fig, ax = plt.subplots(figsize=(10, 7),dpi=1200)
cax = ax.matshow(confusion_matrix, cmap='Blues')
fig.colorbar(cax)
# 设置英文标签
ax.set_xlabel('Predicted')
ax.set_ylabel('Actual')
ax.set_xticks(np.arange(2))
ax.set_yticks(np.arange(2))
ax.set_xticklabels(['Class 0', 'Class 1'])
ax.set_yticklabels(['Class 0', 'Class 1'])
for (i, j), val in np.ndenumerate(confusion_matrix):
ax.text(j, i, f'{val}', ha='center', va='center', color='black')
plt.title('Confusion Matrix Heatmap')
plt.savefig('Confusion Matrix Heatmap.pdf', format='pdf', bbox_inches='tight')
plt.show()
ROC曲线
from sklearn.metrics import roc_curve, auc
# 预测概率
y_score = model.predict(X_test).ravel() # 确保将输出展平为1D
# 计算ROC曲线
fpr, tpr, _ = roc_curve(y_test, y_score)
roc_auc = auc(fpr, tpr)
# 绘制ROC曲线
plt.figure(dpi=1200)
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc="lower right")
plt.savefig('Receiver Operating Characteristic.pdf', format='pdf', bbox_inches='tight')
plt.show()
训练完成后,模型在测试集上的性能评估使用了混淆矩阵、ROC 曲线以及分类报告,通过这些指标,能够了解模型的整体性能表现。但由于深度学习模型本质上是黑盒模型,仅通过这些指标很难解释模型的决策过程。这就引出了下一步:如何使用 SHAP 来解释模型预测
SHAP:解释深度学习模型
什么是SHAP?
SHAP是一种基于博弈论的解释方法,可以为每个特征分配一个重要性分数,解释模型的预测结果。SHAP 的核心思想是通过对每个特征的贡献进行分解,计算特征对模型输出的边际贡献值,使用 SHAP,我们能够可视化模型对每个样本的预测依据,让模型更加透明
SHAP值计算
import shap
# 1. 创建 SHAP 解释器
# 使用训练集的一部分作为背景数据
background = X_train.sample(n=100, random_state=42) # 根据数据量调整样本数量
# 将背景数据和解释数据转换为 NumPy 数组
background_np = background.to_numpy()
X_explain_np = X_test[:100].to_numpy() # 选择要解释的样本
首先,从训练集中抽取了 100 个样本作为背景数据(背景数据用于模型的解释计算,它代表了模型在训练时见过的数据范围),然后将背景数据和要解释的测试数据转换为 NumPy 数组,供 SHAP 后续计算使用,背景数据是解释模型全局行为的关键,解释器会以背景数据为基础来计算每个样本中的特征对预测结果的贡献
# 使用 shap.Explainer 自动选择合适的解释器
explainer = shap.Explainer(model, background_np)
接着,通过 shap.Explainer 创建了一个 SHAP 解释器,shap.Explainer 会根据输入的模型和背景数据,自动选择合适的 SHAP 算法(比如针对深度学习模型,通常会选择基于深度模型的 SHAP 解释方法),model 是之前训练好的深度学习模型,background_np 是背景数据
# 2. 计算 SHAP 值
# 计算shap值为numpy.array数组
shap_values_numpy = explainer.shap_values(X_explain_np)
在这一步,使用 SHAP 解释器来计算测试集前 100 个样本的 SHAP 值,SHAP 值本质上是每个特征对预测结果的边际贡献值,shap_values_numpy 是一个 NumPy 数组,包含了每个样本每个特征的 SHAP 值,这些值表示该特征如何影响模型的预测结果
# 计算shap值为Explanation格式
shap_values_raw = explainer(X_explain_np)
这一步通过 explainer() 方法生成 shap_values_raw,它是一个包含更多信息的 SHAP 解释结果对象,称为 shap.Explanation,与 NumPy 数组不同,shap.Explanation 可以直接用于绘图和可视化,它包含了 SHAP 值、基准值(base value,模型的平均预测值),以及样本的特征数据
feature_names = X_test.columns
# 手动创建一个 shap.Explanation 对象,并传递特征名
shap_values_Explanation = shap.Explanation(values=shap_values_raw.values,
base_values=shap_values_raw.base_values,
data=X_explain_np, # 样本数据
feature_names=feature_names) # 特征名称
在这一部分,手动创建一个 shap.Explanation 对象,该对象将 SHAP 值、基准值(base_values),输入数据(data),以及每个特征的名称(feature_names)整合在一起,这个对象将会被用来绘制 SHAP 图,解释每个特征对模型预测的贡献情况
SHAP 摘要图
plt.figure(figsize=(10, 5), dpi=1200)
# 使用 shap_values_Explanation 绘制摘要图,显示每个特征的影响力
shap.summary_plot(shap_values_Explanation, X_test[:100], feature_names=feature_names, plot_type="dot", show=False)
plt.savefig("SHAP_Summary_Plot.pdf", format='pdf', bbox_inches='tight')
plt.show()
- 特征重要性排序: 从图中可以看出,MonthlyCharges、Contract、tenure、OnlineSecurity 等特征对模型预测具有较大的影响, 它们位于图的顶部,表明这些特征在预测客户流失时起到了关键作用
- 特征值对预测结果的影响: 以 MonthlyCharges 为例,红色的点主要分布在正 SHAP 值区域,说明较高的月度费用(红色)倾向于增加客户流失的可能性, 相反,较低的月度费用(蓝色)更可能减少客户流失
- 对比高低特征值的影响: 在特征 Contract 中,可以看到蓝色(低值)和红色(高值)分别对正负 SHAP 值产生不同的影响, 意味着合约类型也显著影响客户流失行为
SHAP特征重要性柱状图
# 绘制SHAP值总结图(Summary Plot)
plt.figure(figsize=(10, 5), dpi=1200)
shap.summary_plot(shap_values_numpy, X_test[:100], plot_type="bar", show=False)
plt.title('SHAP_numpy Sorted Feature Importance')
plt.savefig("SHAP_numpy Sorted Feature Importance.pdf", format='pdf',bbox_inches='tight')
plt.tight_layout()
plt.show()
- MonthlyCharges(月度费用) 对模型的预测影响最大,表示月度费用对客户是否会流失(Churn)有较大的决定性作用
- tenure(客户服务时长) 也是影响模型的一个重要因素,表明用户在公司的时间长短对于预测客户是否流失很关键
- Contract(合同类型) 和 TotalCharges(总费用) 也对预测结果有较大的影响
- 像 PaperlessBilling 和 MultipleLines 等特征 对模型预测的影响则较小
总结来说,这个柱状图说明了模型认为哪些特征对预测客户流失(Churn)最重要,并且量化了每个特征的重要性
SHAP瀑布图
plt.figure(figsize=(10, 5), dpi=1200)
# 绘制第1个样本的 SHAP 瀑布图,并设置 show=False 以避免直接显示
shap.plots.waterfall(shap_values_Explanation[1], show=False, max_display=10)
# 保存图像为 PDF 文件
plt.savefig("SHAP_Waterfall_Plot_Sample_1.pdf", format='pdf', bbox_inches='tight')
plt.tight_layout()
plt.show()
模型输出的初始值:
- 图中最上方标注的 f(x) = 0.212 是模型对这个样本的预测值,表示该样本的预测结果为 0.212,也就是对于类别0(no)的预测概率, 这个值是基于每个特征对模型的贡献累计得出的
- 右下角的 E[f(X)] = 0.597 是模型的 基线值,表示在不考虑任何特征影响时,模型的平均预测值(全局平均值)
每个特征的影响:
- tenure(服务时长) 对预测的贡献最大,SHAP 值为 -0.14,表示它将预测结果降低了 0.14
- OnlineSecurity(在线安全服务) 也有较大的负向影响,SHAP 值为 -0.1
- 其他特征如 MonthlyCharges(月度费用)、OnlineBackup(在线备份) 等对预测结果也有不同程度的负向贡献
- 而一些特征如 Contract(合同类型) 和 Partner(配偶) 则对预测有正向的推动作用(红色),使得最终的预测值有所提高
通过这个 SHAP 瀑布图,能够清晰地看到哪些特征对于预测结果的重要性,以及它们具体对模型输出是增加还是减少,这个可视化有助于解释黑箱模型的决策逻辑,特别是在理解每个样本单独预测时,各个特征的贡献
SHAP力图
# 绘制单个样本的SHAP解释(Force Plot)
sample_index = 1 # 选择一个样本索引进行解释
expected_value = 0.504
shap.force_plot(expected_value, shap_values_numpy[sample_index], X_test[:100].iloc[sample_index], matplotlib=True,show=False)
plt.savefig("Shap Force.pdf", format='pdf',bbox_inches='tight')
力图解释原理和瀑布图类似都是针对单样本解释,详情参考文章——从基础到进阶:优化SHAP力图,让样本解读更直观,更多shap模型解释绘图参考文章——SHAP进阶解析:机器学习、深度学习模型解释保姆级教程、SHAP全解析:机器学习、深度学习模型解释保姆级教程
往期推荐
从零开始:手把手教你部署顶刊机器学习在线预测APP并解读模型结果
微信号|deep_ML
欢迎添加作者微信进入Python、ChatGPT群
进群请备注Python或AI进入相关群
无需科学上网、同步官网所有功能、使用无限制
如果你对类似于这样的文章感兴趣。
欢迎关注、点赞、转发~
个人观点,仅供参考