很多文章介绍过决策树及其它的作用(如下链接)。
•https://medium.com/swlh/decision-tree-classification-de64fc4d5aac
•https://towardsdatascience.com/the-basics-decision-tree-classifiers-b0d20394eaeb
•https://medium.com/@borcandumitrumarius/decision-tree-classifiers-explained-e47a5b68477a
本文将重点介绍如何在iris数据集中使用python中的scikit-learn来实现决策树,以及在分析算法性能时有用的一些功能。
什么是分类器
分类器算法用于通过决策规则将输入数据映射到目标变量,并可用于预测和了解与特定类或目标相关联的特征。这是一个有监督的机器学习算法,因为我们已经有了最终的标签,只想知道他们如何被预测。我们利用决策树分类器,根据鸢尾花的花瓣长度、花瓣宽度、萼片长度和萼片宽度等特征来预测鸢尾花的类型。
什么是决策树
决策树,一种类似树的结构,其中内部节点表示属性,分支表示决策规则,叶节点表示结果。这是通过根据一个属性选择度量将数据分割成不同的分区,在本例中是Gini索引(尽管我们可以根据需要将其更改为信息熵增益)。这本质上意味着我们每个分裂的目的是减少基尼不纯度,根据错误的分类结果来衡量一个节点的不纯程度。
实现决策树
首先,我们希望将数据转换为正确的格式,以便创建决策树。这里,我们将使用sklearn数据集数据库中的iris数据集,它非常简单,可以展示如何实现决策树分类器。
scikit-learn提供的决策树分类器的好处是,目标变量可以是类别变量,也可以是数值变量。为了清晰起见,考虑到iris数据集,我更喜欢保留花的分类性质,因为以后解释起来更简单,尽管如果需要,标签可以稍后引入。因此,可以使用以下代码导入数据集:
import pandas as pdimport numpy as npfrom sklearn.datasets import load_iris# 加载数据data = load_iris()# 转换为数据帧df = pd.DataFrame(data.data, columns = data.feature_names)# 创建“Species”列df['Species'] = data.target# 将其替换为实际名称target = np.unique(data.target)target_names = np.unique(data.target_names)targets = dict(zip(target, target_names))df['Species'] = df['Species'].replace(targets)
接下来我们要提取训练和测试数据集。这样做的目的是确保模型没有针对所有可用数据进行训练,这样我们就可以测试它在看不见的数据上的表现。如果使用所有的数据作为训练数据,那么我们可能会过拟合模型,这意味着它可能在看不见的数据上表现不佳。
# 提取特征和目标变量x = df.drop(columns="Species")y = df["Species"]# 保存特征名称和目标变量feature_names = x.columnslabels = y.unique()# 拆分数据集from sklearn.model_selection import train_test_splitX_train, test_x, y_train, test_lab = train_test_split(x,y, test_size = 0.4, random_state = 42)
现在我们有了正确格式的数据,可以开始创建决策树,以便我们尝试预测不同花卉的分类。为此,首先要从sklearn包中导入DecisionTreeClassifier。
from sklearn.tree import DecisionTreeClassifier
接下来要做的是将此应用于训练数据。为此,将分类器赋值给clf并设置max_depth=3和random_state=42。这里,max_depth参数是树的最大深度,我们控制它以确保没有过拟合,并且我们可以很容易地跟踪最终结果是如何实现的。random_state参数确保可以在进一步的分析中复制结果。
然后我们将算法拟合:
clf = DecisionTreeClassifier(max_depth =3, random_state = 42)clf.fit(X_train, y_train)
我们希望能够了解算法的行为,使用决策树分类器的好处之一是输出直观易懂,并且易于可视化。
这可以通过两种方式实现:
1.作为树形图:
# 导入相关包from sklearn import treeimport matplotlib.pyplot as plt# 把这个数字画出来,设置一个黑色的背景plt.figure(figsize=(30,10), facecolor ='k')# 创建树图a = tree.plot_tree(clf, # 使用存储的特征名称 feature_names = feature_names, # 使用存储的类名 class_names = labels, rounded = True, filled = True, fontsize=14)# 展示plt.show()
2. 作为基于文本的图表
# 导入相关功能from sklearn.tree import export_text# 导出决策规则tree_rules = export_text(clf, feature_names = list(feature_names))# 输出结果print(tree_rules)# 输出:|--- PetalLengthCm <= 2.45| |--- class: Iris-setosa|--- PetalLengthCm > 2.45| |--- PetalWidthCm <= 1.75| | |--- PetalLengthCm <= 5.35| | | |--- class: Iris-versicolor| | |--- PetalLengthCm > 5.35| | | |--- class: Iris-virginica| |--- PetalWidthCm > 1.75| | |--- PetalLengthCm <= 4.85| | | |--- class: Iris-virginica| | |--- PetalLengthCm > 4.85| | | |--- class: Iris-virginica
这清楚地显示了算法的行为。在这里,第一个分类是根据花瓣的长度,小于2.45厘米被认为是毛鸢尾,更长的被归类为鸢尾花。然而,花瓣长度大于2.45的植物会进一步分类,再分类两次,最终得到更精确的分类结果。
当然,我们不只是对它在训练数据上的表现感兴趣,而是它在看不见的测试数据上的表现。这意味着我们必须使用它从测试集预测类,这是使用predict方法完成的。
test_pred_decision_tree = clf.predict(test_x)
我们感兴趣的是,在真正例(预测为真且实际为真)、假正例(预测为真但不实际为真)、假反例(预测为假但实际上为真)和真反例性(预测为假和实际为假)方面,这是如何执行的。
一种方法是在混淆矩阵中检查结果。利用混淆矩阵,我们能够通过在一个轴上显示预测值和在另一个轴上显示实际值来可视化预测和真实标签的匹配情况。这有助于确定我们在哪里可以得到误报或漏报,从而确定算法是如何执行的。
# 导入相关包from sklearn import metricsimport seaborn as snsimport matplotlib.pyplot as plt# 得到混淆矩阵confusion_matrix = metrics.confusion_matrix(test_lab, test_pred_decision_tree)# 把它变成一个数据帧matrix_df = pd.DataFrame(confusion_matrix)# 绘制结果图ax = plt.axes()sns.set(font_scale=1.3)plt.figure(figsize=(10,7))sns.heatmap(matrix_df, annot=True, fmt="g", ax=ax, cmap="magma")# 设置坐标轴标题ax.set_title('Confusion Matrix - Decision Tree')ax.set_xlabel("Predicted label", fontsize =15)ax.set_xticklabels(['']+labels)ax.set_ylabel("True Label", fontsize=15)ax.set_yticklabels(list(labels), rotation = 0)plt.show()
从这里可以看出,从看不见的数据中,只有一个值无法从Iris versicolor类中预测出来,这表明总体而言,该算法在预测未知数据方面做得很好。
要衡量性能,需要产生有几个指标。
准确度
准确度得分是真正例和真反例在指定标签总数中的比例,计算公式如下:
sum(混淆矩阵中的对角线)/sum(混淆矩阵中的所有值)
metrics.accuracy_score(test_lab, test_pred_decision_tree)# 输出: 0.9833333333333333
精确度
它能够体现,在某个类中,我们预测的值中有多少实际上在那个类中。从本质上讲这体现了我们在误报方面的表现。计算如下:
真正例(对角线上的数)/所有预测为正例(列的和)
# 得到精确度precision = metrics.precision_score(test_lab, test_pred_decision_tree, average=None)# 把它变成一个数据帧precision_results = pd.DataFrame(precision, index=labels)# 重命名结果列precision_results.rename(columns={0:'precision'}, inplace =True)precision_results# 输出: Precision Iris-setosa 1.00Iris-versicolor 0.95 Iris-Virginica 1.00
召回
这告诉我们每个类中有多少值被贴上了正确的标签,从而告诉我们相对于假反例它的表现如何。计算方法为:
真正例/ 正标签(行的和)
recall = metrics.recall_score(test_lab, test_pred_decision_tree, average =None)recall_results = pd.DataFrame(recall, index= labels)recall_results.rename(columns ={0:'Recall'}, inplace =True)recall_results# 输出: Recall Iris-setosa 1.00Iris-versicolor 1.00 Iris-Virginica 0.94
f1
这是准确度和召回量表的加权平均数,1是最好的,0是最差的。这使用了调和平均值,使值更接近较小的数字,并防止在一个参数高而另一个参数低的情况下高估模型的性能。计算如下:
2 (precision * recall)/(precision + recall)*
f1 = metrics.f1_score(test_lab, test_pred_decision_tree, average=None)f1_results = pd.DataFrame(f1, index=labels)f1_results.rename(columns={0:'f1'}, inplace=True)f1_results# 输出: f1 Iris-setosa 1.00Iris-versicolor 0.97 Iris-Virginica 0.97
当然,我们可以通过以下代码在单个输出中获得所有这些指标:
print(metrics.classification_report(test_lab, test_pred_decision_tree))# 输出: precision recall f1-score support Iris-setosa 1.00 1.00 1.00 23Iris-versicolor 0.95 1.00 0.97 19 Iris-virginica 1.00 0.94 0.97 18 accuracy 0.98 60 macro avg 0.98 0.98 0.98 60 weighted avg 0.98 0.98 0.98 60
其中对每个类的支持仅仅是测试标签中每个类的出现次数。
特征重要性
另一个有用的特性是计算每个特征在最终树输出中的重要性。这是基尼指数或熵指数(在我们的例子中,基尼指数)由于对给定特征的分割而减少的总量。这可以从以下代码获得:
# 提取重要性importance = pd.DataFrame({'feature': X_train.columns, 'importance' : np.round(clf.feature_importances_, 3)})importance.sort_values('importance', ascending=False, inplace = True)print(importance)# 输出: feature importance2 PetalLengthCm 0.5893 PetalWidthCm 0.4110 SepalLengthCm 0.0001 SepalWidthCm 0.000
这表明花瓣的长度是最重要的,因为第一次划分是基于此。但是,由于只运行了一个决策树,这并不意味着其他特征不重要,只是在这个决策树中不需要它们。对于一个真正的决策树必须运行多次(就像在随机林中一样)并聚合结果。然后可以将其与随机变量进行比较,或者根据特定的标准删除特征。
改进模型
我们可以尝试通过改变使用的特性来改进模型,但是我们也可以通过使用GridSearchCV来查看它如何响应超参数的变化。它通过对训练集的多个运行集执行算法,对模型执行交叉验证,并告诉我们模型如何响应。
为此,我们可以更改max_depth和min_samples_split参数,这些参数控制树的深度,以及分割内部节点所需的样本数。
from sklearn.model_selection import GridSearchCVtuned_parameters = [{'max_depth': [1,2,3,4,5], 'min_samples_split': [2,4,6,8,10]}]scores = ['recall']for score in scores: print() print(f"Tuning hyperparameters for {score}") print() clf = GridSearchCV( DecisionTreeClassifier(), tuned_parameters, scoring = f'{score}_macro' ) clf.fit(X_train, y_train) print("Best parameters set found on development set:") print() print(clf.best_params_) print() print("Grid scores on development set:") means = clf.cv_results_["mean_test_score"] stds = clf.cv_results_["std_test_score"] for mean, std, params in zip(means, stds, clf.cv_results_['params']): print(f"{mean:0.3f} (+/-{std*2:0.03f}) for {params}")# 输出:Tuning hyperparameters for recallBest parameters set found on development set:{'max_depth': 2, 'min_samples_split': 2}Grid scores on development set:nan (+/-nan) for {'max_depth': 2, 'min_samples_split': 1}0.916 (+/-0.194) for {'max_depth': 2, 'min_samples_split': 2}0.916 (+/-0.194) for {'max_depth': 2, 'min_samples_split': 3}0.887 (+/-0.177) for {'max_depth': 2, 'min_samples_split': 4}0.887 (+/-0.177) for {'max_depth': 2, 'min_samples_split': 5}nan (+/-nan) for {'max_depth': 3, 'min_samples_split': 1}0.916 (+/-0.183) for {'max_depth': 3, 'min_samples_split': 2}0.916 (+/-0.183) for {'max_depth': 3, 'min_samples_split': 3}0.906 (+/-0.179) for {'max_depth': 3, 'min_samples_split': 4}0.916 (+/-0.183) for {'max_depth': 3, 'min_samples_split': 5}nan (+/-nan) for {'max_depth': 4, 'min_samples_split': 1}0.916 (+/-0.194) for {'max_depth': 4, 'min_samples_split': 2}0.905 (+/-0.179) for {'max_depth': 4, 'min_samples_split': 3}0.905 (+/-0.179) for {'max_depth': 4, 'min_samples_split': 4}0.905 (+/-0.179) for {'max_depth': 4, 'min_samples_split': 5}
我们最好的超参数是max_depth=2和min_smaples_split=2。其他要更改的超参数可以在这里找到:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html。
以上,向你展示了如何在实践中实现一个简单的决策树!
使用决策树的优点是它们易于理解和解释,它们可以处理数值和分类数据,它们限制了不良预测因素的影响,我们可以提取它们的结构来可视化。
当然它也有缺点,如果一个类占主导地位,它们会创建有偏差的树,我们可能会得到过于复杂的树,导致过拟合,数据的微小变化可能会产生大相径庭的结果。然而它们在实践中非常有用,可以与其他分类算法一起使用,如k近邻或随机森林,以帮助决策和理解分类是如何产生的。