Python决策树模型的基本原理和参数调优【附代码】
下图所示为一个典型的决策树模型:员工离职预测模型的简单演示。该决策树首先判断员工满意度是否小于5,答案为“是”则认为该员工会离职,答案为“否”则接着判断其收入是否小于10,000元,答案为“是”则认为该员工会离职,答案为“否”则认为该员工不会离职。这里。
目录
1、决策树模型的基本原理
(1)决策树模型简介
下图所示为一个典型的决策树模型:员工离职预测模型的简单演示。该决策树首先判断员工满意度是否小于5,答案为“是”则认为该员工会离职,答案为“否”则接着判断其收入是否小于10,000元,答案为“是”则认为该员工会离职,答案为“否”则认为该员工不会离职。
这里解释几个决策树模型的重要关键词:根节点、父节点、子节点和叶子节点
(2)决策树模型的建树依据
决策树模型的建树依据主要用到一个基尼系数(gini)的概念。基尼系数用于计算一个系统中的失序现象,也即系统的混乱程度。基尼系数越高,系统混乱程度越高,建立决策树模型的目的就是通过合适的分类来降低系统的混乱程度,其计算公式如下:
其中pi为类别i在样本T中出现的频率,即类别为i的样本占总样本个数的比率。为∑为求和公式,即把所有的pi^2进行求和。
举例来说,对于一个全部都是违约客户的样本来说,里面只有一个类别:违约客户,其出现的频率是100%,所以该系统基尼系数为1−12=0 1-1^2=0 ,表示该系统没有混乱,或者说该系统的“纯度”很高。如果样本里一半是违约客户,另一半是非违约客户,那么类别个数为2,每个类别出现的频率都为50%,所以其基尼系数为1−(0.52+0.52)=0.5 1-([0.5]^2+[0.5]^2)=0.5 ,也即其混乱程度很高。
当引入某个用于进行分类的变量(比如“曾经违约”),则分割后的基尼系数公式为:
其中S1、S2为划分成两类的样本量,gini(T1)和gini(T2)为划分后的两类各自的基尼系数。
可以看到未划分时的基尼系数为0.48,以“满意度<5”为初始节点进行划分后的基尼系数为0.3,而以“收入<10,000”为初始节点进行划分后的基尼系数为0.45。基尼系数越低表示系统的混乱程度越低,区分度越高,能够比较好地作为一个分类预测模型,因此这里选择“满意度<5”作为初始节点。这里演示了如何选择初始节点,初始节点下面的节点也是用类似的方法来进行选择。
同理,对于“收入”这一变量来说,是选择“收入<10,000”还是选择“收入<100,000”进行划分,也是通过计算在这两种情况下划分后的基尼系数来进行判断。若还有其他的变量,如“工龄”、“月工时”等,也是通过类似的手段计算划分后的系统的基尼系数,来看如何进行节点的划分,从而搭建一个较为完善的决策树模型。采用基尼系数进行运算的决策树也称之为CART决策树。
(3)决策树模型的代码实现
决策树模型既可以做分类分析(即预测分类变量值),也可以做回归分析(即预测连续变量值),分别对应的模型为分类决策树模型(DecisionTreeClassifier)及回归决策树模型(DecisionTreeRegressor)。
分类决策树模型(DecisionTreeClassifier)代码演示如下所示(运行结果为0):
from sklearn.tree import DecisionTreeClassifier
X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
y = [1, 0, 0, 1, 1]
model = DecisionTreeClassifier(random_state=0)
model.fit(X, y)
print(model.predict([[5, 5]]))
如果要同时预测多个数据,则可以写成如下形式:
print(model.predict([[5, 5], [7, 7], [9, 9]]))
为大家理解,利用决策树可视化技巧将决策树可视化,如下图所示:
在引入决策树模型的时候,我们设置了random_state随机状态参数,设置这个参数的原因是,决策树模型会优先选择使整个系统基尼系数下降最大的划分方式来进行节点划分,但是有可能(尤其当数据量较少的时候),根据不同的划分方式获得的基尼系数下降是一样的。下图所示为不设置random_state参数时多次运行后获得的不同的决策树:
为什么模型训练后会产生两颗不同的树呢,哪棵树是正确的呢?其实两颗树都是正确的,出现这种情况的原因,是因为根据“X[1]<=7”或者“X[0]<=6”进行节点划分时产生的基尼系数下降是一样的(都是0.48 - (0.6*0.444 + 0.4*0) = 0.2136),所以无论以哪种形式进行节点划分都是合理的。产生这一现象的原因大程度是因为数据量较少所以容易产生不同划分方式产生的基尼系数下降是一样的情况,当数据量较大时出现该现象的几率则较小。
回归决策树模型(DecisionTreeRegressor)除了进行分类分析外,决策树还可以进行回归分析,即预测连续变量,此时决策树便被称之为回归决策树,回归决策树模型简单代码如下所示:
from sklearn.tree import DecisionTreeRegressor
X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
y = [1, 2, 3, 4, 5]
model = DecisionTreeRegressor(max_depth=2, random_state=0)
model.fit(X, y)
print(model.predict([[9, 9]]))
回归决策树模型的概念和分类决策树基本一致,最大的不同就是其切分标准不再是信息熵或是基尼系数,而是均方误差MSE,均方误差MSE的计算公式如下所示:
利用决策树可视化技巧将决策树可视化,如下图所示:
对于根节点,它里面一共有5个数据,这里是将节点中所有数据的均值作为该节点的拟合值,因此对于该节点来说,其拟合值为(1+2+3+4+5)/5=3,因此其均方误差MSE如下所示为数字2,和程序获得的结果是一致的。
这里因为设置了树的最大深度参数max_depth为2,所以决策树在根节点往下共有两层,如果不设置这一参数,那么右下角的节点还将继续分裂,直至所有节点的均分误差值都为0为止。这里设置最大深度参数max_depth的原因,一是为了方便演示拟合的效果(拟合结果是4.5而不是一个整数,显得是回归结果,而不是分类结果),二是为了防止模型出现过拟合的现象。在实战中我们也通常会设置最大深度参数max_depth主要防止模型出现过拟合的现象
2、案例:员工离职预测模型搭建
1、模型搭建
(1) 数据读取与预处理
员工离职预测模型的目的是通过已有的员工信息和离职表现来搭建合适的模型,从而预测之后的员工是否会离职。
import pandas as pd
df = pd.read_excel('员工离职预测模型.xlsx')
df.head()
运行结果如下表所示,其中共有15000组历史数据,其中前3571个为离职员工数据,后11429个为非离职员工数据,其中“离职”列中,数字1代表离职,数字0代表未离职。我们的目的就是根据这些历史数据搭建决策树模型来预测之后的员工离职可能性。
因为Python数学建模中无法识别文本内容,而在原始数据中,“工资”被分成了三个等级“高”、“中”、“低”。所以“工资”列中的内容需要进行数值处理,这里通过pandas库中的中replace()函数进行处理。
df = df.replace({'工资': {'低': 0, '中': 1, '高': 2}})
df.head()
(2) 提取特征变量和目标变量
首先将特征变量和目标变量单独提取出来,代码如下:
X = df.drop(columns='离职')
y = df['离职']
数据表中“离职”作为目标变量,剩下的字段作为特征变量,通过一个员工的特征来判断他是否会离职。为了方便演示,这里只选取了6个特征变量,在商业实战中用到的特征变量会比案例中的多得多。
(3) 划分训练集和测试集
提取完特征变量后,我们需要将原来的15000个数据拆分为训练集及测试集。顾名思义,训练集拿来做训练,而测试集拿来检验模型训练的结果。划分训练集和测试集的代码如下:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=123)
(4) 模型训练及搭建
划分为训练集和测试集之后,就可以从Scikit-Learn库中引入决策树模型进行模型训练了,代码如下:
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=3, random_state=123)
model.fit(X_train, y_train)
2、模型预测及评估
(1)直接预测是否离职
搭建模型的目的是为了利用它来预测数据,这里把测试集中的数据导入到模型中来进行预测,代码如下,其中model就是上一节搭建的决策树模型。
y_pred = model.predict(X_test)
print(y_pred[0:100])
利用创建DataFrame知识点,将预测的y_pred和测试集实际的y_test汇总到一起,代码如下:
a = pd.DataFrame()
a['预测值'] = list(y_pred)
a['实际值'] = list(y_test)
a.head()
可以看到测试数据集中前五组数据的预测准确度为100%,如果要查看整体的预测准确度,可以采用如下代码:
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
print(score)
通过将score打印输出:0.9573,即3000个测试集数据中,有2872人预测和实际结果相符。
(2)预测不离职&离职概率
其实分类决策树模型本质预测的并不是准确的0或1的分类,而是预测其属于某一分类的概率, y_pred_proba是一个二维数组,其左侧一列数为分类为0的概率,右侧一列数为分类为1的概率,可以通过如下代码查看预测属于各个分类的概率:
y_pred_proba = model.predict_proba(X_test)
print(y_pred_proba[0:5])
b = pd.DataFrame(y_pred_proba, columns=['不离职概率', '离职概率'])
b.head()
print(y_pred_proba[:,1])
(3)模型预测效果评估
对于分类模型而言,我们不仅关心其预测的准确度,更关心下面两个指标:命中率 (TPR)(所有实际离职的员工中被预测为离职的比率)和假警报率 (FPR)(所有实际不离职的员工中被预测为离职的概率)阈值(thres)也即通过两者绘制的ROC曲线来评判模型。
from sklearn.metrics import roc_curve
fpr, tpr, thres = roc_curve(y_test, y_pred_proba[:,1])
通过相关代码可以查看不同阈值下的假警报率和命中率,代码如下:
a = pd.DataFrame()
a['阈值'] = list(thres)
a['假警报率'] = list(fpr)
a['命中率'] = list(tpr)
a.head()
通过如下代码则可以快速求出模型的AUC值:
from sklearn.metrics import roc_auc_score
score = roc_auc_score(y_test, y_pred_proba[:,1])
print(score)
已知了不同阈值下的假警报率和命中率,可通过matplotlib库可绘制ROC曲线,代码如下:
import matplotlib.pyplot as plt
plt.plot(fpr, tpr)
plt.show()
(4) 特征重要性评估
模型搭建完成后,有时我们希望能够知道各个特征变量的重要程度,即哪些特征变量在模型中起的作用更大,而在决策树模型中,通过如下一行代码即可查看特征重要性:
print(model.feature_importances_)
对于特征变量不多的模型,我们通过上面一行代码即可查看各个特征变量的重要性,但是如果特征变量多了之后,可以使用如下的代码将特征重要性和变量名称一一对应:
features = X.columns
importances = model.feature_importances_
importances_df = pd.DataFrame()
importances_df['特征名称'] = features
importances_df['特征重要性'] = importances
importances_df.sort_values('特征重要性', ascending=False)
特征名称 | 特征重要性 | |
1 | 满意度 | 0.598109 |
5 | 工龄 | 0.150866 |
2 | 考核得分 | 0.140074 |
3 | 工程数量 | 0.106387 |
4 | 月工时 | 0.004565 |
0 | 工资 | 0.000000 |
3、决策树模型可视化呈现
from sklearn.tree import export_graphviz
import graphviz
import os
os.environ['PATH'] = os.pathsep + r'C:\Program Files (x86)\Graphviz2.38\bin'
dot_data = export_graphviz(model, out_file=None, class_names=['0', '1'])
graph = graphviz.Source(dot_data)
graph.render("result")
print('可视化文件result.pdf已经保存在代码所在文件夹!')
print(graph)
dot_data = export_graphviz(model, out_file=None, feature_names=['income', 'satisfication', 'score', 'project_num', 'hours', 'year'], class_names=['0', '1'], filled=True)
graph = graphviz.Source(dot_data)
print(graph)
下图所示便是通过graphviz生成的可视化决策树模型:
知识点1:节点各元素含义
以根节点为例:
知识点2:节点划分与其依据验证
根节点分裂完后产生两个子节点,其中左边的子节点中大部分为离职员工,而右边的节点中大部分为不离职员工,这也的确符合现实中如果满意度较低则出现离职可能性较大的经验。
计算经过根节点分裂后的系统的基尼系数为:
这个也是机器通过不停的训练和计算获得的最优解,如果通过别的方式进行根节点分裂后的系统基尼系数一定会比这个大,基尼系数的下降值一定比这个小。
知识点3:特征重要性与整棵树的关系
可以看到这里重要性最高的便是满意度。同样这里可以更好地解释下为什么工资这一特征变量的特征重要性为0,这个是因为该因素在该模型中没有发挥作用,可从可视化的图形中看出,每一个分叉的节点都没有依据“工资”这个特征变量进行分裂,所以说这个特征变量并没有发挥作用。
此外,我们之前提到过,在决策树模型中,特征重要性的大小在于该变量对于整体的基尼系数下降的贡献度大小,这里可以利用这个可视化后的决策树,通过演示第二个特征变量“满意度”的特征重要性为什么为0.598来验证下该观点。
首先需要计算整体的基尼系数下降,如下图所示,根据不同叶子节点的样本数进行权重求和,新系统的基尼系数为0.0814,整体基尼系数下降为0.3650−0.0814=0.28360.3650-0.0814=0.2836
以“满意度”为例,在上面的决策树中它共在根节点和下部中间的节点发挥了作用,对系统产生的基尼系数下降分别为0.105和0.0646:
两者之和为0.1696,这个就是“满意度”这一特征变量在整个模型中发挥的作用:
将其除以整体的基尼系数下降值便是它的特征重要性,这个与代码的获得特征重要性一致:
知识点4:叶子停止分裂的依据
叶子停止分裂的依据主要有两个:已经分裂结束无法再分裂,或者达到限定的分裂条件。比如右下角的叶子节点的基尼系数都为0了,也就是说这个叶子节点的纯度已经最高了(即里面所有的元素都是同一类别),已经不需要也无法再分裂了。而有些叶子节点的基尼系数还没有到达0的,因为限制了树的最大深度为3,所以也不会继续向下分裂了。
知识点5:不离职&离职概率与叶子节点的关系
提到的不离职&离职概率的计算就是基于叶子节点,如果分到了左下角第三个叶子节点,那么其不离职概率为0,离职概率为100%,所以判断其为离职;如果分到最左边叶子节点,这个节点里总共有1295个数据,不离职的有70人,离职的有1225人,那么如果一个新的员工被分到该叶子节点,那么判定该员工离职概率为1225/1295=0.946,离职概率则为70/1295=0.054,因为离职概率大于不离职概率,所以判定其离职,其余则依次类推。
知识点6:ROC曲线与叶子节点的关系
将其余叶子节点所反映出来的离职概率计算一下(从左边叶子节点至右边的叶子节点,各节点离职概率分别为94.6%、5.94%、100%、7.72%、1.47%、100%、4.58%、71.4%),再观察上一小节绘制ROC曲线时用到的阈值,会发现上一小节绘制ROC曲线时使用的阈值并不是随意选取的,而就是这些不同叶子节点反映出来的离职概率。ROC曲线的绘制就是以这些离职概率的值作为阈值来看不同阈值下的命中率(TPR)和假警报率(FPR)。此外,因为这里很多叶子节点的离职概率是100%,这也解释了为什么之前5.2.2小节取100%作为阈值,仍然有24.7%命中率的原因。
ROC曲线的绘制就是以这些离职概率的值作为阈值来看不同阈值下的命中率(TPR)和假警报率(FPR)。此外,因为这里很多叶子节点的离职概率是100%,这也解释了为什么之前5.2.2小节取100%作为阈值,仍然有24.7%命中率的原因。通过这个图便能够较好的理解决策树的运行逻辑,当来了一个新的数据的时候,就会从最上面的根节点开始进行判断,如果满足满意度<=4.65,则划分到左边的节点进行之后一系列的判断,如果不满足则分到右边的节点进行之后一系列的判断,最终这个新的数据会被划分到其中的一个叶子节点中去,从而完成对数据的预测。
3、参数调优-K折交叉验证&GridSearch网络搜索
(1)K折交叉验证
交叉验证有三种方法,分为简单交叉验证、K折交叉验证和留一交叉验证。其中K折交叉验证应用较为广泛,K折交叉验证是指将数据集随机等分为K份,每次选取K-1份为训练集训练模型,然后用剩下的1份作为测试集,得到K个模型后将这K个模型的平均测试效果作为最终的模型效果。
举例来说,下图所示便是3折交叉验证,即将数据随机等分为3份,然后每次随机选取2份数据作为训练集,剩下的1份作为测试集,反复3次。
通常来说,如果训练数据集相对较小,则增大k值,这样在每次迭代过程中将会有更多的数据用于模型训练,同时算法时间延长;如果训练集相对较大,则减小k值,这样降低模型在不同的数据块上进行重复拟合的性能评估的计算成本,在平均性能的基础上获得模型的准确评估。
通过如下代码可以实现K折交叉验证,并获得每次验证的得分情况: (cv=k)
from sklearn.model_selection import cross_val_score
acc = cross_val_score(model, X, y, cv=5)
acc
打印acc可以看到5次交叉验证得到的打分如下所示:
array([0.96666667, 0.96066667, 0.959 , 0.96233333, 0.91366667])
上面交叉验证默认以准确度作为评价标准,如果想以ROC曲线的AUC值作为评分标准,则可以设置scoring参数为'roc_auc',代码如下:
from sklearn.model_selection import cross_val_score
acc = cross_val_score(model, X, y, scoring='roc_auc', cv=5)
acc
(2)GridSearch网格搜索
网格搜索是一种穷举搜索的调参手段:遍历所有的候选参数,循环建立模型并对模型的有效性和准确性进行评估,选取表现最好的参数作为最终结果。以决策树模型最大深度max_depth为例,我们可以在[1, 3, 5, 7, 9]这些不同的值中遍历,以准确度或者ROC曲线的AUC值作为评判标准来搜索最合适的max_depth值。如果要同时调节多个模型参数,例如模型有2个参数,第一个参数有4种可能,第二个参数有5种可能,所有的可能性列出来可以表示成4*5的网格,遍历的过程像是在网格(Grid)里搜索(Search),因此该方法也称之为GridSearch网格搜索。
1、单参数的参数调优
这里先以1个参数(max_depth)为例进行网格搜索演示,来快速了解机器学习如何进行参数调优。使用Scikit-Learn库中的GridSearchCV()方法对上方的决策树模型进行参数调优。
from sklearn.model_selection import GridSearchCV
parameters = {'max_depth': [3, 5, 7, 9, 11]}
model = DecisionTreeClassifier()
grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)
下面我们将数据传入网格搜索模型并输出参数最优值:
grid_search.fit(X_train, y_train)
grid_search.best_params_
因为max_depth参数我们设置了5个候选项,又设置了5折交叉验证(cv=5),这样对于每个候选项模型都会运行5遍(因此模型共运行5*5=25遍),每个候选项都通过5折交叉验证获得一个平均分,根据平均分进行排序,参数最优值如下:
{'max_depth': 7}
此外,如果不想一个数字一个数字的敲参数值,使用np.arange()函数,例如通过如下代码,便可以构造1到9,间隔为2的数据集(np.arange(1, 10, 2))。这与单独写出1,3,5,7,9的效果相同:
import numpy as np
parameters = {'max_depth': np.arange(1, 10, 2)}
2、参数调优的效果检验
下面我们根据新的参数建模,并通过查看新模型的预测准确度以及ROC曲线的AUC值来验证参数调整后是否提高了模型的有效性。首先重新搭建决策树模型,并将训练集数据传入其中:
model = DecisionTreeClassifier(max_depth=7)
model.fit(X_train, y_train)
把测试集中的数据导入模型进行预测并通过Scikit-Learn库中的accuracy_score查看整体预测准确度:
y_pred = model.predict(X_test)
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
print(score)
通过将score打印输出,发现整个模型在测试集上的预测准确度为0.982,即3000个测试集数据中,有2946人预测结果和实际结果相符。原模型在测试集上的预测准确度为0.957。
查看完预测准确度后,我们来查看ROC曲线的AUC值,首先通过如下代码查看预测属于各个分类的概率:
y_pred_proba = model.predict_proba(X_test)
y_pred_proba[:,1]
将获得的AUC值打印出来为:0.987,原来获得的AUC值为0.973,相比之下可以证明调参后模型的有效性的确有所提高。
调参之后决策树模型的深度从3增加到7,树的子节点和叶子节点都有所增加,特征的重要性也可能发生变化。使用如下代码查看各个特征的重要性:
model.feature_importances_
3、多参数调优
除了可以进行单参数调优外,GridSearch网格搜索还可以进行多参数同时调优,下面我们选择DecisionTreeClassifier()模型三个超参数:
使用GridSearchCV()方法进行多参数调优,各参数含义可参考本节补充知识点:决策树模型的超参数。
from sklearn.model_selection import GridSearchCV
parameters = {'max_depth': [5, 7, 9, 11, 13], 'criterion':['gini', 'entropy'], 'min_samples_split':[5, 7, 9, 11, 13, 15]}
model = DecisionTreeClassifier()
grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)
grid_search.fit(X_train, y_train)
grid_search.best_params_
多参数调优和单参数分别调优是有区别的,比如为了省事,对上面的3个参数进行3次单独的单参数调优,然后将结果汇总,这样的做法其实是不严谨的。以上面的代码示例来说,使用多参数调优时,它是5*2*6=60种组合可能,而如果是进行3次单参数调优,则只是5+2+6=13种组合可能。因此,如果只需要调节一个参数,那么可以使用单参数调优,如果需要调节多个参数,则推荐使用多参数调优。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)