通过随机森林的例子解释特征重要性
在许多(业务)案例中,同样重要的是不仅要有一个准确的机器学习模型,还要有一个可解释的机器学习模型。通常,除了想知道我们的机器学习模型的房价预测是什么之外,我们还想知道为什么它是这么高/低,以及哪些特征在确定预测时最重要。另一个例子是预测客户流失 - 拥有一个能够成功预测哪些客户容易流失的机器学习模型是非常好的,但确定哪些变量很重要可以帮助我们及早发现甚至改进产品/服务!
了解机器学习模型的特征重要性可以通过多种方式使您受益,例如:
- 通过更好地理解机器学习模型的逻辑,您不仅可以验证它是正确的,还可以通过仅关注重要变量来改进模型
- 您可以删除不那么重要的x变量,并且在更短的训练时间内具有类似或更好的性能
- 在某些商业案例中,为了解释性而牺牲一些准确性是有道理的。
这就是为什么在本文中,我想通过一个随机森林模型的例子来探索不同的方法来解释特征的重要性。它们中的大多数也适用于不同的模型,从线性回归开始,以XGBoost等黑盒结束。
需要注意的一点是,我们的模型越精确,我们就越能够信任特征重要性度量和其他解释。我假设我们构建的模型相当准确(因为每个数据科学家都会努力建立这样的模型),在本文中我将重点关注重要性度量。
数据
对于这个例子,我将使用波士顿房价数据集(所以回归问题)。但是本文中描述的方法与分类问题一样,唯一的区别是用于评估的度量。Python代码如下:
import pandas as pd from sklearn.datasets import load_boston boston = load_boston() y = boston.target X = pd.DataFrame(boston.data, columns = boston.feature_names) np.random.seed(seed = 42) X['random'] = np.random.random(size = len(X)) X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size = 0.8, random_state = 42)
下面我检查随机特征和目标变量之间的关系。可以看出,散点图上没有模式,相关性几乎为0。
sns.scatterplot(x = 'random', y = 'target', data = X.assign(target = y)).set_title('Random feature vs. target variable', fontsize = 16)
sns.heatmap(X.assign(target = y).corr().round(2), cmap = 'Blues', annot = True).set_title('Correlation matrix', fontsize = 16)
这里需要注意的一点是,为CHAS解释相关性没有多大意义,因为它是一个二元变量,应该使用不同的方法。
基准模型
我训练一个普通的随机森林模型来获得一个基准。我设置了一个random_state以确保结果的可比性。我也使用bootstrap并设置,oob_score = True以便稍后我可以使用out-of-bag error。
简单地说,随机森林中的每棵树都在不同的数据集上进行训练,从原始数据中进行替换取样。这导致每个训练集中大约有2/3的不同观测值。out-of-bag误差是根据所有观测值计算的,但是对于计算每一行的误差,模型只考虑在训练过程中没有看到这一行的树。这类似于在验证集上对模型进行评估。查看如下Python代码:
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor(n_estimators = 100, n_jobs = -1, oob_score = True, bootstrap = True, random_state = 42) rf.fit(X_train, y_train) print('R^2 Training Score: {:.2f} OOB Score: {:.2f} R^2 Validation Score: {:.2f}'.format(rf.score(X_train, y_train), rf.oob_score_, rf.score(X_valid, y_valid)))
R^2 Training Score: 0.93
OOB Score: 0.58
R^2 Validation Score: 0.76
模型中存在一些过度拟合,因为它在OOB样本上和验证集上的表现都差得多。但我们假设它已经足够好了,接下来是特征重要性(通过训练集的性能来衡量)。一些方法还可以用于验证/OOB集,以获得对不可见数据的进一步解释性。
特征重要性
默认Scikit-learn的特征重要性
让我们从决策树开始建立一些直觉。在决策树中,每个节点都是在一个特征中拆分值的条件,这样在拆分后,相类似的因变量值就会在同一个集合中结束。该条件基于杂质,分类问题为基尼杂质/信息增益(熵),回归树为其方差。因此,当训练一棵树时,我们可以计算出每个特征对减少加权杂质的贡献有多大。Scikit-Learn中的feature_importances_是基于这个逻辑的,但是在随机森林中,我们正在讨论的是对树上的杂质减少进行平均。
优点:
- 快速计算
- 易于检索
缺点:
- 有偏见的方法,因为它倾向于夸大连续特征或高基数分类变量的重要性
base_imp = imp_df(X_train.columns, rf.feature_importances_) var_imp_plot(base_imp, 'Default feature importance (scikit-learn)')
似乎最重要的三大特征是:
- RM:平均房间数量
- LSTAT:人口lower status的百分比
- DIS:到波士顿五个就业中心的加权距离
然而,令人惊讶的是,一列随机值竟然比以下内容更重要:
- INDUS:城镇非零售商业用地比例
- RAD:径向高速公路的可达性指数
- ZN:占地面积超过25,000平方尺的住宅用地比例。
- CHAS:Charles River虚拟变量(如果有河= 1;否则为0)
直观地说,这个特征对目标变量的重要性应该是零。让我们看看它是如何通过不同的方法进行评估的。
排列特征重要性
这种方法通过观察每个预测器的随机re-shuffling(从而保持变量的分布)如何影响模型性能来直接度量特征的重要性。
该方法可按以下步骤描述:
- 训练基线模型并通过传递验证集(或随机森林情况下设置的OOB)记录得分(准确度/R²/任何重要度量)。这也可以在训练集上完成,但代价是牺牲关于泛化的信息。
- 从所选数据集中的一个特征Re-shuffle值,再次将数据集传递给模型以获取预测并计算此已修改数据集的度量标准。特征重要性是基准分数与修改(置换)数据集之间的差异。
- 对数据集中的所有特征重复2.
优点:
- 适用于任何模型
- 合理有效
- 可靠的技术
- 无需在每次修改数据集时重新训练机器学习模型
缺点:
- 比默认的feature_importances在计算上更昂贵
- 排列重要性高估了相关预测因子的重要性
我将可视化Spearman的相关性。标准皮尔逊相关性的不同之处在于,它首先将变量转换为秩,然后在秩上运行皮尔逊相关性。
Spearman相关性:
- 是非参数的
- 不假设变量之间存在线性关系
- 它寻找单调的关系。
from rfpimp import plot_corr_heatmap viz = plot_corr_heatmap(X_train, figsize=(15,10)) viz.view()
rfpimp:关于这个库需要注意的一点是,我们必须我们必须以metric(model, X, y)的形式提供一个度量,这样我们就可以使用更高级的方法,比如使用随机森林的OOB评分。这个库已经包含了相应的函数(oob_regression_r2_score)。但是为了保持方法的一致性,我将计算训练集上的度量(失去了关于泛化的信息)。
from sklearn.metrics import r2_score from rfpimp import permutation_importances def r2(rf, X_train, y_train): return r2_score(y_train, rf.predict(X_train)) perm_imp_rfpimp = permutation_importances(rf, X_train, y_train, r2) perm_imp_rfpimp.reset_index(drop = False, inplace = True) var_imp_plot(perm_imp_rfpimp, 'Permutation feature importance (rfpimp)')
该图证实了我们上面所看到的,4个变量不如随机变量重要!令人惊讶的是......前4名保持不变。关于`rfpimp`的一个更好的特性是它包含处理共线特征问题的功能(这是显示Spearman相关矩阵背后的想法)。
eli5:rfpimp的基本方法和eli5的方法有一些不同。其中一些是:
- 有参数cv和refit连接到使用交叉验证。在这个例子中,我将它们设置为None,因为我不使用它,但在某些情况下它可能会派上用场。
- 有一个metric参数,在rfpimp中接受metric(model, X, y)形式的函数。如果未指定此参数,则该函数将使用score估计器的默认方法。
- n_iter - 随机shuffle迭代次数,最终得分是平均值
import eli5 from eli5.sklearn import PermutationImportance perm = PermutationImportance(rf, cv = None, refit = False, n_iter = 50).fit(X_train, y_train) perm_imp_eli5 = imp_df(X_train.columns, perm.feature_importances_) var_imp_plot(perm_imp_eli5, 'Permutation feature importance (eli5)')
结果与前面的结果非常相似,尽管这些结果来自于每列的多次reshuffles。eli5的另一个优点是,通过使用Scikit-learn的SelectFromModel或RFE,可以很容易地使用置换方法的结果进行特征选择。
删除列特征重要性
这种方法非常直观,因为我们通过将模型与所有特征进行比较来研究特征的重要性,而将模型与此特征进行比较以进行训练。
我为下面的方法创建了一个函数(基于rfpimp的实现),它显示了底层逻辑。
优点:
- 最准确的特征重要性
缺点:
- 由于为数据集的每个变体重新训练模型而导致潜在的高计算成本(在删除单个特征列之后)
from sklearn.base import clone def drop_col_feat_imp(model, X_train, y_train, random_state = 42): # clone the model to have the exact same specification as the one initially trained model_clone = clone(model) # set random_state for comparability model_clone.random_state = random_state # training and scoring the benchmark model model_clone.fit(X_train, y_train) benchmark_score = model_clone.score(X_train, y_train) # list for storing feature importances importances = [] # iterating over all columns and storing feature importance (difference between benchmark and new model) for col in X_train.columns: model_clone = clone(model) model_clone.random_state = random_state model_clone.fit(X_train.drop(col, axis = 1), y_train) drop_col_score = model_clone.score(X_train.drop(col, axis = 1), y_train) importances.append(benchmark_score - drop_col_score) importances_df = imp_df(X_train.columns, importances) return importances_df drop_imp = drop_col_feat_imp(rf, X_train, y_train) var_imp_plot(drop_imp, 'Drop Column feature importance')
这里很有趣。首先,在这种情况下的负面重要性意味着从模型中删除给定的特征实际上提高了性能。这在随机情况下是很好的,但奇怪的是,在删除DIS之后可以观察到最高的性能提升,DIS是之前方法中第三个最重要的变量。不幸的是,我没有一个很好的解释。
观察水平特征重要性
通过观察水平特征重要性,我指的是对解释给予模型的特定观察具有最大影响的那些。例如,在信用评分的情况下,我们可以说这些特征对确定客户的信用评分影响最大。
Treeinterpreter
treeinterpreter的主要思想是使用随机森林中的底层树来解释每个特性如何贡献最终值。我们可以观察到预测的值(定义为每个特征贡献的总和+基于整个训练集的初始节点给出的平均值)在决策树中的预测路径上(每次分割之后)是如何变化的,
以及导致分裂的信息(预测的变化也是如此)。
预测函数的公式(f(x))可以写为:
其中c_full是整个数据集(初始节点)的平均值,K是特征的总数。
这可能听起来很复杂,但请看一下库作者的一个例子:
由于随机森林的预测是树的平均值,因此平均预测的公式如下:
其中J是森林中树的数量
我首先确定具有最低和最高绝对预测误差的行,并尝试查看导致差异的原因。Python实现如下:
pred_diff = pd.DataFrame({'difference': abs(y_train - rf.predict(X_train))}) print('Index with smallest error:', pred_diff.sort_values('difference').head(1).index.values[0]) print('Index with largest error:', pred_diff.sort_values('difference', ascending = False).head(1).index.values[0])
Index with smallest error: 31
Index with largest error: 85
使用treeintrerpreter I获得3个对象:预测、偏差(数据集的平均值)和贡献。
for i in range(len(selected_rows)): print("Row", selected_rows[i]) print("Prediction:", prediction[i][0], 'Actual Value:', y_train[selected_rows[i]]) print("Bias (trainset mean)", bias[i]) print("Feature contributions:") for c, feature in sorted(zip(contributions[i], X_train.columns), key=lambda x: -abs(x[0])): print(feature, round(c, 2)) print("-"*20)
对于误差最小的观察,主要的影响因素是LSTAT和RM(在以前的例子中,这是最重要的变量)。在错误最高的情况下,最大的贡献来自DIS变量。
为了更深入地研究,我们可能还会对许多变量的联合贡献感兴趣(如这里的XOR所解释的)。
prediction1, bias1, contributions1 = ti.predict(rf, np.array([selected_df[0]]), joint_contribution=True) prediction2, bias2, contributions2 = ti.predict(rf, np.array([selected_df[1]]), joint_contribution=True) aggregated_contributions1 = utils.aggregated_contribution(contributions1) aggregated_contributions2 = utils.aggregated_contribution(contributions2) res = [] for k in set(aggregated_contributions1.keys()).union( set(aggregated_contributions2.keys())): res.append(([X_train.columns[index] for index in k] , aggregated_contributions1.get(k, 0) - aggregated_contributions2.get(k, 0))) for lst, v in (sorted(res, key=lambda x:-abs(x[1])))[:10]: print (lst, v)
['RM', 'DIS'] [-3.65430351]
['RM', 'DIS', 'LSTAT'] [-2.15540094]
['CRIM', 'LSTAT'] [-1.65588589]
['DIS', 'LSTAT'] [-1.64678593]
['NOX', 'LSTAT'] [-1.28406056]
['RM', 'TAX', 'LSTAT'] [-1.00245007]
['CRIM', 'RM', 'LSTAT'] [-0.62160057]
['TAX', 'LSTAT'] [-0.55742577]
['RM', 'LSTAT'] [-0.45002332]
['B', 'LSTAT'] [-0.3516441]
最佳和最差预测案例之间的差异主要来自房间(RM)特征的数量,以及到五个波士顿就业中心的加权距离(DIS)。
LIME
LIME (Local interpretation table Model-agnostic interpretation)是一种以可解释任何分类器/回归器的预测的技术。要做到这一点,可以通过使用可解释的模型(如带正则化的线性模型或决策树)局部逼近所选模型来获得解释。可解释模型是在原始观测(表格数据行)的小扰动(加噪声)上进行训练的,因此只能提供良好的局部逼近。
要注意的一些缺点:
- 只有线性模型用于近似局部行为
- 需要对数据执行以获得正确解释的扰动类型通常是特定于用例的
- 简单的(默认的)扰动通常是不够的。在理想的情况下,修改将由数据集中观察到的变化驱动
您可以在下面看到LIME解释的输出。
输出有3个部分:
- 预测值
- 特征重要性 - 在回归的情况下,它显示它是否对预测有负面或正面影响,按绝对影响下降排序。
- 这些特征的实际值用于解释行。
请注意,LIME已将解释中的特征离散化。这是因为在上面的构造函数中设置了`discretize_continuous = True`。离散化的原因在于它为连续特征提供了更直观的解释。
import lime import lime.lime_tabular explainer = lime.lime_tabular.LimeTabularExplainer(X_train.values, mode = 'regression', feature_names = X_train.columns, categorical_features = [3], categorical_names = ['CHAS'], discretize_continuous = True) np.random.seed(42) exp = explainer.explain_instance(X_train.values[31], rf.predict, num_features = 5) exp.show_in_notebook(show_all=False) #only the features used in the explanation are displayed exp = explainer.explain_instance(X_train.values[85], rf.predict, num_features = 5) exp.show_in_notebook(show_all=False)
LIME解释认为,对于这两种观测结果,最重要的特征是RM和LSTAT,这一点在之前的方法中也有体现。
结论
在本文中,我展示了一些从机器学习模型(不限于随机森林)中导出特征重要性的方法。我相信理解结果通常与获得良好结果一样重要,因此每个数据科学家都应尽力了解哪些变量对模型最重要,以及为什么。这不仅有助于更好地了解业务,还可以进一步改进机器学习模型。