使决策树产生更好结果的一步

提升决策树效果的关键一步

背景、实现和模型改进

在树林中(作者提供的照片)

决策树(DT)经常被人们过早地抛弃。

经常是这样的情况:

DT被训练了。自然的过拟合现象出现了。超参数被调整(并不令人满意)。最后,树被随机森林取代。

虽然这可能在性能上是一种快速获胜的办法,但是这种替换优先考虑了一个“黑箱”算法。这并不理想。只有DT能够产生直观的结果,为业务领导提供比较权衡的能力,并且在流程改进中给他们起到关键的作用。

你不能理解或者解释的东西不适合投入生产。特别是在即使小的失败也会带来极大风险的行业,如医疗保健领域。

(旁注:人们经常问“随机森林产生特征的重要性,那不就解释了哪些特征是重要的吗?”其实不然。特征的重要性几乎立即被解释为因果驱动因素(例如,与目标存在依赖性的特征),但它们只是模型驱动因素而已。尽管在这方面对技术人员有所帮助,但特征的重要性在弱模型中通常毫无用处,并且在具有高基数的特征中膨胀,并且对相关特征产生偏见。这是另一条完全不同的探究途径,但基本上就是这个意思。)

决策树的决策原理

坚持使用DT将保持您良好传达结果的能力,但是如何使它们性能更好呢?仅仅进行超参数调整是不够的。无论如何,都应该进行深思熟虑的特征工程。

事实证明,特征数据的特定结构可能使其更好地适应底层的DT算法,从而使DT产生更好的决策。

在底层,DT通过在您提供的所有数据之间创建正交决策边界(垂直分割)来分离类别。它以一种贪婪的算法方式完成这一过程-先选择最佳分割的特征,然后再尝试其他特征的次优分割。

我们可以通过视觉检查特征来寻找正交决策边界。让我们在下面查看公开可用的乳腺癌数据集中的特征。在下面的顶部图中,绘制“Worst Perimeter”和“Mean Perimeter”产生了一个良好的正交决策边界,可以很好地区分恶性和良性类别。因此,这些特征非常适合DT模型的包含。

作者提供的图片

上面的底部图展示了“Mean Area”和“Mean Perimeter”,决策树通过它们建立了正交决策边界(这是它的固有特性),但是这些边界是不必要地复杂的。也许,在这种情况下,对角线分离会更好,但这不是DT分类器的分割方式。此外,DT对训练数据的微小变化非常敏感,例如异常值会产生完全不同的树。

为了适应DT的独特机制,并最终改善性能和泛化能力,可以应用主成分分析(PCA)。

PCA以两种重要方式改进了DT的性能:

(1)将关键特征排列在一起(解释了最大方差的特征)

(2)减少特征空间

事实上,PCA + DT的过程自然地展示了您在上面顶部图中看到的“Worst Perimeter”和“Mean Perimeter”特征。这些是两个最具预测性的变量,不足为奇地具有出色的正交决策边界。

实施过程

请记住,PCA适用于连续数据。乳腺癌数据集完全由连续变量组成。(另一个旁注:我看到有人在分类变量上使用PCA-不建议这样做。名义水平没有隐含的距离,有序水平并不总是等距的,并且在离散特征上强制表示距离通常会使变量变得毫无意义。这是另一个时间的旁段)。

让我们从下载所需的包开始,将我们的乳腺癌数据集转化为特征X和目标变量y

import numpy as npfrom sklearn.tree import DecisionTreeClassifierfrom sklearn.decomposition import PCAfrom sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitfrom sklearn.metrics import accuracy_score, confusion_matrix, precision_score, recall_scoreimport matplotlib.pyplot as pltimport seaborn as snsn# 加载乳腺癌数据集data = load_breast_cancer()X = data.datay = data.target

为了检查,可以调用该数据集的数据框头部。

cancer = load_breast_cancer()df = pd.DataFrame(np.c_[cancer['data'], cancer['target']],                  columns= np.append(cancer['feature_names'], ['target']))df.head()
Picture by author

首先,不使用PCA训练DecisionTreeClassifier,并收集这些预测值(original_predictions)。

# 将数据分割为训练集和测试集X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)# 在非PCA变换的数据集上拟合决策树分类器original_tree = DecisionTreeClassifier(random_state=42)original_tree.fit(X_train, y_train)# 对原始数据集进行预测original_predictions = original_tree.predict(X_test)

现在,应用PCA选择能够解释训练集中大部分方差的最小维度数。不要任意选择这个维度数,可以使用“肘方法”来确定能解释99%的方差的维度数(如下代码所示)。

# 使用肘方法找到PCA组件的最佳数量pca = PCA()pca.fit(X_train)explained_variance = pca.explained_variance_ratio_cumulative_explained_variance = np.cumsum(explained_variance)# 绘制解释的方差plt.plot(range(1, len(cumulative_explained_variance) + 1), cumulative_explained_variance, marker='o')plt.xlabel('组件个数')plt.ylabel('累积解释方差')plt.title('PCA解释方差')plt.grid()plt.show()# 确定最佳的组件数量(肘点)optimal_num_components = np.where(cumulative_explained_variance >= 0.99999)[0][0] + 1print(f"最佳PCA组件数量:{optimal_num_components}")

从图形上观察,可以发现6个PCA组件能够解释99%的训练集方差。

Picture by author

现在,在训练集上应用PCA来捕捉6个主成分。可以使用奇异值分解(SVD)来完成这一过程,它是一种标准的矩阵分解技术(超出本文范围)。与之前一样,在PCA变换的训练集上训练DecisionTreeClassifier并收集这些预测值(pca_predictions)。

# 使用最佳组件数量应用PCApca = PCA(n_components=optimal_num_components, svd_solver="full")X_train_pca = pca.fit_transform(X_train)X_test_pca = pca.transform(X_test)# 在PCA变换后的数据集上拟合决策树分类器pca_tree = DecisionTreeClassifier(random_state=42)pca_tree.fit(X_train_pca, y_train)# 对PCA变换后的数据集进行预测pca_predictions = pca_tree.predict(X_test_pca)

# 混淆矩阵pca_cm = confusion_matrix(y_test, pca_predictions)# 原始数据集的精确度和召回率original_precision = precision_score(y_test, original_predictions, average=’weighted’)original_recall = recall_score(y_test, original_predictions, average='weighted')original_accuracy = accuracy_score(y_test, original_predictions)# 精确度和召回率pca_precision = precision_score(y_test, pca_predictions)pca_recall = recall_score(y_test, pca_predictions)pca_accuracy = accuracy_score(y_test, pca_predictions)# 输出精确度和召回率print(f"原始数据集 - 精确度:{original_precision:.4f},召回率:{original_recall:.4f},准确率:{original_accuracy:.4f}")print(f"PCA变换后的数据集 - 精确度:{pca_precision:.4f},召回率:{pca_recall:.4f},准确率:{pca_accuracy:.4f}")

现在,我们可以将我们的原始预测(非PCA转换)与PCA预测(PCA转换)进行比较,观察准确度、精确度和召回率等评估指标是否有相对的改善。

与原始的DT训练数据相比,当我们首先对数据集进行PCA转换,然后进行DT训练时,我们在各方面都得到了改善:

我们可以绘制混淆矩阵,显示两个DT之间恶性和良性肿瘤分类的相对改善。

# 绘制混淆矩阵plt.figure(figsize=(12, 5))plt.subplot(1, 2, 1)sns.heatmap(original_cm, annot=True, fmt="d", cmap="Blues", xticklabels=data.target_names, yticklabels=data.target_names)plt.title("原始决策树混淆矩阵\n精确度: {:.2f}, 召回率: {:.2f}".format(original_precision, original_recall))plt.xlabel("预测")plt.ylabel("实际")plt.subplot(1, 2, 2)sns.heatmap(pca_cm, annot=True, fmt="d", cmap="Blues", xticklabels=data.target_names, yticklabels=data.target_names)plt.title("带有PCA的决策树混淆矩阵\n精确度: {:.2f}, 召回率: {:.2f}".format(pca_precision, pca_recall))plt.xlabel("预测")plt.ylabel("实际")plt.tight_layout()plt.show()
作者提供的图片

最后,确定用于生成6个主成分的原始特征是很有价值的。从技术上讲,PCA生成的新特征是原始特征的线性组合。这些新特征彼此正交,并按他们解释的方差排序。然而,通过调用components_attribute可以确定在创建这些分量时使用的特征。

# 获取每个主成分的解释方差比explained_variance_ratio = pca.explained_variance_ratio_# 获取PCA组件pca_components = pca.components_# 创建一个DataFrame来显示每个主成分对原始特征的贡献df_components = pd.DataFrame(data=pca_components, columns=data.feature_names)# 打印对每个主成分贡献最大的特征for i in range(optimal_num_components):    print(f"PC{i+1}的前几个特征:")    sorted_features = df_components.iloc[i].abs().sort_values(ascending=False)    print(sorted_features.head())    print("\n解释方差比:", explained_variance_ratio[i])    print("=" * 50)

因此,对于我们选择的6个主成分,模型使用了以下5个特征进行创建:

作者提供的图片

结论

决策树往往在更高性能的算法面前丢弃得太早。尽管性能最重要,但并不一定是最好的选择——这个决策最终取决于您的利益相关者的需求以及解释为什么模型建议特定结果(请参阅“可解释的AI”)。

与其追求最先进的技术算法,不如通过思考周到的特征工程和主成分分析来为决策树提供揭示直观决策能力的最佳机会。

感谢阅读。欢迎与我在 LinkedIn 上互动!如果您有任何有趣的数据科学挑战,欢迎留言或私信,我将乐于尝试并探索/撰写相关内容。

我最近的文章:

调试逻辑回归错误的最佳实践 – 它们的含义和如何做

使用贝叶斯网络预测医院辅助服务量

为什么平衡班级被过度炒作