决策树实战

文章目录


一、入门基础案例

py 复制代码
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
from math import log
import operator
import pickle
from audioop import reverse
from _functools import reduce

# 创建数据集
def createDataSet():
    # 每个样本包含四个特征(年龄、是否有工作、是否有房、是否贷款),一个标签
    dataSet = [[0, 0, 0, 0, 'no'],
               [0, 0, 0, 1, 'no'],
               [0, 1, 0, 1, 'yes'],
               [0, 1, 1, 0, 'yes'],
               [0, 0, 0, 0, 'no'],
               [1, 0, 0, 0, 'no'],
               [1, 0, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [1, 0, 1, 2, 'yes'],
               [2, 0, 1, 2, 'yes'],
               [2, 1, 0, 1, 'yes'],
               [2, 0, 0, 0, 'no']
    ]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况']
    return dataSet, labels

# 创建决策树
def createTree(dataSet, labels,featLabels): # 定义featLabels存储已使用的特征
    classList = [example[-1] for example in dataSet] # 获取标签列表
    if classList.count(classList[0]) == len(classList):
        return classList[0] # 当前节点只有一个类别,直接返回
    if len(dataSet[0]) == 1: # 当前节点没有特征,只含有标签,返回出现次数最多的类别
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet) # 选择最优特征的下标
    bestFeatLabel = labels[bestFeat] # 通过下标获取最优特征名
    featLabels.append(bestFeatLabel) # 将最优特征名添加到列表中
    myTree = {bestFeatLabel:{}} # 创建嵌套字典,键为特征名,值为字典
    del(labels[bestFeat]) # 删除已经使用的特征
    featValues = [example[bestFeat] for example in dataSet] # 获取最优特征的所有属性值(两种属性值时分二叉,三种属性值时分三叉)
    uniqueVals = set(featValues)
    for value in uniqueVals: # 遍历最优特征所有属性值,依次进行分支
        subLabels = labels[:] # 复制列表,避免对原列表修改
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels,featLabels) # 递归调用,为特征的每个属性值创建子树
    return myTree
# 获取出现次数最多的类别
def majorityCnt(classList):
    classCount = {} # 定义字典,键为类别,值为出现次数
    for vote in classList:
        if vote not in classCount.keys(): classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) # 按出现次数降序排序
    return sortedClassCount[0][0] # 返回出现次数最多的类别('yes'或'no')
# 计算信息熵
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1 # 获取特征数量
    baseEntropy = calcShannonEnt(dataSet) # 计算数据集未划分时的系统熵值
    bestInfoGain = 0 # 记录最大的信息增益
    bestFeature = -1 # 记录最优特征的下标
    for i in range(numFeatures): # 遍历每一个未使用的特征
        featList = [example[i] for example in dataSet] # 取数据集中的第i列数据(即对应特征值)
        uniqueVals = set(featList) # 获取特征值集合
        newEntropy = 0
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet) / float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)
        infoGain = baseEntropy - newEntropy # 计算信息增益
        if (infoGain > bestInfoGain): # 如果当前信息增益比之前最大的大,则更新信息增益与最优特征的下标
            bestInfoGain = infoGain
            bestFeature = i
        return bestFeature
# 计算信息熵
def calcShannonEnt(dataSet):
    numEntries = len(dataSet) # 获取数据集长度
    labelCounts = {} # 定义字典,记录每个类别出现的次数
    for featVec in dataSet: # 遍历每一个样本数据
        currentLabel = featVec[-1] # 获取样本的标签('yes'或'no')
        if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1 # 记录标签出现的次数
    shannon = 0
    for key in labelCounts:
        prob = float(labelCounts[key]) / numEntries
        shannon -= prob * log(prob, 2)
    return shannon # 返回数据集熵值
# 使用特征的每一个值来划分数据集
def splitDataSet(dataSet, axis, value): # axis为特征下标,value为特征的某一特征值
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:]) # 对于特征labels[axis]且特征值为value的列,删除该列并加入数据子集
            retDataSet.append(reducedFeatVec)
    return retDataSet

if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    print(myTree)

二、基于sklearn的决策树模型

2.1sklearn中的决策树实现

sklearn库中决策树的类都在sklearn.tree模块下,该模块共包含五个类:

类名 功能
tree.DecisionTreeClassifier 使用决策树解决分类问题。
tree.DecisionTreeRegressor 使用决策树解决回归问题。
tree.export_graphviz 将构建的决策树可视化。
tree.ExtraTreeClassifier 高随机版本的分类决策树。
tree.ExtraTreeRegressor 高随机版本的回归决策树。

使用sklearn训练模型并预测的基本流程如下:

  • 1.实例化模型对象,并传入相关参数。
  • 2.通过模型对象提供的接口传入数据集训练模型。
  • 3.从模型对象获取有用信息,如导入测试集,获取模型性能信息。

由此可得出使用决策树模型时的基本代码框架:

py 复制代码
from sklearn import tree                                #导入需要的模块
 
clf = tree.DecisionTreeClassifier()                     #实例化分类决策树模型对象
clf = clf.fit(X_train,y_train)                          #用训练集数据训练模型 
result = clf.score(X_test,y_test)                       #导入测试集,从接口中调用需要的信息 

2.2分类型决策树:DecisionTreeClassifier

sklearn.tree.DecisionTreeClassifier类主要用于解决分类问题,类的初始化函数定义如下:

py 复制代码
def __init__(
    self,
    *,
    criterion="gini",
    splitter="best",
    max_depth=None,
    min_samples_split=2,
    min_samples_leaf=1,
    min_weight_fraction_leaf=0.0,
    max_features=None,
    random_state=None,
    max_leaf_nodes=None,
    min_impurity_decrease=0.0,
    class_weight=None,
    ccp_alpha=0.0,
)

2.2.1重要参数

  • criterion=gini:选择基尼系数(gini)或信息熵作为选择特征的依据。
    • criterion=gini:选择基尼系数(Gini Impurity)。
    • criterion=Entropy:选择信息熵(Entropy)。

在实际使用过程中,信息熵和基尼系数的效果基本相同,但由于基尼系数不涉及对数运算,使得程序运行速度更快,故默认情况下选择基尼系数作为评价指标。另外,由于信息熵对不纯度更加敏 感,所以信息熵作为指标时,决策树的生长会更加"精细",因此对于高维数据或者噪音很多的数据,信息熵很容易过拟合,基尼系数在这种情况下效果往往比较好。当模型拟合程度不足的时候,即当模型在训练集和测试集上都表现不太好的时候,可以使用信息熵。

  • random_state=None:设置分支中的随机模式参数,默认为None。这是因为决策树模型会优先选择使整个系统的基尼系数下降最大(或信息增益最大)的划分方式来进行节点划分,但是有可能根据不同的划分方式获得的基尼系数下降(或信息增益)是一样的,如果不设置random_state参数,就会导致程序每次运行时会获得不同的决策树。下图所示为不设置random_state参数时多次运行后获得的两棵不同的决策树。例如:

左树采用下标为1的特征进行划分,右树采用下表为0的特征进行划分,但二者产生的基尼系数下降值是一样的,均为 0.48 − ( 0.6 ∗ 0.444 + 0.4 ∗ 0 ) = 0.2136 0.48-(0.6*0.444+0.4*0)=0.2136 0.48−(0.6∗0.444+0.4∗0)=0.2136,因此出现两种决策树模型。但此时输入数据,如[7,7]时,左树预测为类别0,右数预测为类别1。为保证程序每次运行都能采用相同的节点划分方式,以获得相同的决策树,就需要设置random_state参数。该参数其实是一个随机数生成器的种子,可以设置为0、1、123等任意数字。

  • splitter=best
    • best:优先选择更重要的特征进行分枝(重要性由熵或基尼系数判断,可以通过属性feature_importances_查看)
    • random:随机选择特征进行分支,树会因为含有更多的不必要信息而更深更大,并因这些不必要信息而降低对训练集的拟合,从而降低过拟合。

理论上决策树可一直划分,直到将子数据集完全划分为只有单一数据或没有更多特征可用为止,此时决策树出现过拟合,即,在训练集上表现很好,在测试集上却表现糟糕。为使得决策树有更好的泛化性,需要对决策树进行剪枝,以下是常用的剪枝参数。

  • max_depth:预剪枝参数。限制树的最大深度,超过设定深度的树枝全部剪掉,可采用控制变量法找出该参数的最佳值。
  • min_samples_split:预剪枝参数。限制分支节点含有的最少样本数目,当节点未含有min_samples_leaf个样本时,就不会发生分支。
  • min_samples_leaf:预剪枝参数。一个节点分支后,每个子节点含有的最少样本个数,若某子节点所含样本个数小于min_samples_leaf,就不会发生分支。此参数一般搭配max_depth使用,设置太小时容易过拟合,太大时会组织模型学习数据。
  • max_features:预剪枝参数。限制分支时考虑的特征个数,超过限制个数的特征都会被舍弃。max_features是用来限制高维度数据的过拟合的剪枝参数,其可强行使决策树停止构建。
  • min_impurity_decrease:限制信息增益的大小,当信息增益小于min_impurity_decrease时不会发生分支。
  • class_weight:目标权重参数,用于实现样本标签的平衡。样本不平衡是指在一组数据集中,标签的一类天生占有很大的比例。比如说,在银行要判断"一个办了信用卡的人是否会违约",就是,是vs否(1%:99%)的比例。这种分类状况下,即便模型什么也不做,全把结果预测成"否",正确率也能有99%。因此我们要使用class_weight参数对样本标签进行一定的均衡,给少量的标签更多的权重,让模型更偏向少数类,向捕获少数类的方向建模。该参数默认None,此模式表示自动给与数据集中的所有标签相同的权重。
  • min_weight_fraction_leaf:设置权重后,样本会受到输入权重的影响,此时的剪枝操作需配合min_weight_fraction_leaf参数进行。且基于权重的剪枝参数(例如min_weight_ fraction_leaf)将比不知道样本权重的标准(比如min_samples_leaf)更少偏向主导类。如果样本是加权的,则使 用基于权重的预修剪标准来更容易优化树结构,这确保叶节点至少包含样本权重的总和的一小部分。

2.2.2重要属性与接口

四个重要接口:

  • fit()/score():基本接口,用于训练模型、获取模型精度。
  • apply():输入测试集,返回每个测试样本所在叶子节点的索引。
  • predict():输入测试集,返回每个测试样本的标签。

注意,所有接口中要求输入X_train和X_test的部分,输入的特征矩阵必须至少是一个二维矩阵。 sklearn不接受任何一维矩阵作为特征矩阵被输入。如果你的数据的确只有一个特征,那必须用reshape(-1,1)来给 矩阵增维;如果你的数据只有一个特征和一个样本,使用reshape(1,-1)来给你的数据增维。

一个重要属性:

  • feature_importances_:查看各个特征对模型的重要性。

2.2.3基本案例:wine葡萄酒数据集

  • 1.获取wine葡萄酒数据集
py 复制代码
from sklearn import tree 
from sklearn.datasets import load_wine 
from sklearn.model_selection import train_test_split
# 1.获取wine葡萄酒数据集
wine = load_wine()
print('wine数据集特征名:',wine.feature_names)
print('wine数据集标签名:',wine.target_names)
print('wine数据集样本尺寸:',wine.data.shape) # 含178个样本,13个特征
print('wine数据集标签尺寸:',wine.target.shape) # 含178个标签
  • 2.按7:3划分训练集与测试集
py 复制代码
# 2.按7:3划分训练集与测试集
Xtrain, Xtest, Ytrain, Ytest = train_test_split(wine.data,wine.target,test_size=0.3)
print('训练集样本尺寸:',Xtrain.shape)
print('测试集样本尺寸',Xtest.shape)
  • 3.训练模型
py 复制代码
# 3.训练模型
decisionTree=tree.DecisionTreeClassifier(criterion="entropy",random_state=2)
decisionTree=decisionTree.fit(Xtrain,Ytrain)
score=decisionTree.score(Xtest,Ytest)
print(score)
  • 4.可视化决策树
py 复制代码
# 4.可视化决策树
feature_name = ['酒精','苹果酸','灰','灰的碱性','镁','总酚','类黄酮','非黄烷类酚类','花青素','颜 色强度','色调','od280/od315稀释葡萄酒','脯氨酸']
import graphviz 
dot_data = tree.export_graphviz(decisionTree
                                ,out_file = None                                
                                ,feature_names= feature_name                                
                                ,class_names=["琴酒","雪莉","贝尔摩德"]                                
                                ,filled=True                                
                                ,rounded=True                               
                                ) 
graph = graphviz.Source(dot_data) 
graph
  • 5.查看各种特征的重要程度
py 复制代码
# 5.查看各特征的重要程度
[*zip(feature_name,decisionTree.feature_importances_)] 
  • 6.确认最优的剪枝参数

超参数曲线是一条以超参数的取值为横坐标,模型的度量指标为纵坐标的曲线。可通过超参数学习曲线来确定超参数的值,它是衡量不同超参数取值下模型性能表现的线。本例中以模型的score值作为衡量模型的标准:

py 复制代码
# 6.确认最优的剪枝参数
import matplotlib.pyplot as plt
test = [] 
for i in range(10):
    clf = tree.DecisionTreeClassifier(max_depth=i+1
                                          ,criterion="entropy"                                      
                                          ,random_state=2                                      
                                          ,splitter="random" # 使用random后,决策树分支会更加随机,树会因为含有更多的不必要信息而更深更大,并因这些不必要信息而降低对训练集的拟合,从而避免过拟合。                                   
                                          )
    clf = clf.fit(Xtrain, Ytrain)
    score = clf.score(Xtest, Ytest)    
    test.append(score) 

plt.plot(range(1,11),test,color="red",label="max_depth") 
plt.legend() 
plt.show()

模型精度在最大深度为6时收敛,可认为该超参数最优值为6.

  • 7.查看重要属性与接口
py 复制代码
#7.查看重要属性与接口
clf = tree.DecisionTreeClassifier(max_depth=6
                                          ,criterion="entropy"                                      
                                          ,random_state=2                                      
                                          ,splitter="random" # 使用random后,决策树分支会更加随机,树会因为含有更多的不必要信息而更深更大,并因这些不必要信息而降低对训练集的拟合,从而避免过拟合。                                   
                                          )
clf = clf.fit(Xtrain, Ytrain)
##apply返回每个测试样本所在叶子节点的索引
print(clf.apply(Xtest))
##predict返回每个测试样本的分类/回归结果
print(clf.predict(Xtest))

2.3回归型决策树:DecisionTreeRegressor

当数据集的因变量为连续性数值时,分类型决策树就可演变为回归型决策树,使用叶节点的均值作为预测值。sklearn.tree.DecisionTreeRegressor类主要用于解决回归问题,类的初始化函数定义如下:

py 复制代码
def __init__(
	self,
	*,
	criterion="squared_error",
	splitter="best",
	max_depth=None,
	min_samples_split=2,
	min_samples_leaf=1,
	min_weight_fraction_leaf=0.0,
    max_features=None,
    random_state=None,
    max_leaf_nodes=None,
    min_impurity_decrease=0.0,
    ccp_alpha=0.0,
)

回归决策树模型初始化函数几乎所有参数都与分类决策树相同,但在回归树中没有标签是否均衡的问题,因此没有class_weight这样的参数。

2.3.1重要参数、属性及接口

回归型决策树与分类型决策树的参数基本相同,但其选择特征时的依据不一样:

  • criterion="squared_error":回归树选择特征进行分支时的依据。
    • squared_error:使用均方误差函数(mean squared error)。此时父节点和子节点之间的均方误差的差额将被用来作为特征选择的标准。
    • friedman_mse:使用费尔德曼均方误差,对潜在的分支问题进行了改进。
    • mae:使用绝对平均误差MAE(mean absolute error),这种指标使用叶节点的中值作为特征选择的标准。

以均方误差为例,分析回归型决策树模型:

【均方误差函数】

  • y ( i ) y^{(i)} y(i):样本对应的真实值。
  • y ^ ( i ) \hat{y}^{(i)} y^(i):模型对样本的预测值。
    【回归型决策树】
  • X[0]:该数据样本的第1个特征。
  • mse:该节点的均方误差。
  • samples:该节点的样本数。
  • value:该节点的拟合值。

回归型决策树模型中,节点的拟合值是节点中所有数据样本的均值,而叶子节点的拟合值就是最终回归模型的预测值。例如,根节点包含5个数据样本,其值分别为1、2、3、4、5,则该节点的拟合值为 y ^ ( i ) = ( 1 + 2 + 3 + 4 + 5 ) / 5 = 3 \hat{y}^{(i)}=(1+2+3+4+5)/5=3 y^(i)=(1+2+3+4+5)/5=3,则系统的均方误差MSE计算为:

由于回归型决策树模型需要使得最终系统的均方误差最小,其节点划分时也应当遵循此原则。比如上图中根节点根据第二个特征值以5为分界点划分,划分后系统的均方误差为:
M S E = 0.4 × 0.25 + 0.6 × 0.667 = 0.5 MSE=0.4×0.25+0.6×0.667=0.5 MSE=0.4×0.25+0.6×0.667=0.5

此时系统均方误差下降了1.5。

回归型决策树的其他参数、重要属性及接口(fit()、score()、predict()、apply()、feature_importances_)均与分类型决策树相同。

2.3.2基本案例:员工离职预测模型

数据下载地址

  • 1.读取数据集
py 复制代码
# 1.读取数据集
import pandas as pd
df = pd.read_csv('HR_comma_sep.csv')
df.head()
  • 2.数据预处理
py 复制代码
# 2.数据预处理
## 将工资等级、职位数值化处理
df=df.replace({'salary':{'low':0,'medium':1,'high':2}})
df=df.replace({'sales':{'support':0,'technical':1,'marketing':2,'RandD':3,'accounting':4,'sales':5,'hr':6,'IT':7,'management':8,'product_mng':9}})
## 提取标签数据
x=df.drop(columns='left')
y=df['left']
x.head()
  • 3.训练回归决策树模型
py 复制代码
# 3.训练回归决策树模型
## 划分数据集
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)
## 训练模型
from sklearn.tree import DecisionTreeRegressor
model = DecisionTreeRegressor(max_depth=3, random_state=123) 
model.fit(X_train, y_train)
  • 4.对比预测数据与真实数据
py 复制代码
# 4.对比预测数据与真实数据
label_pred=[]
for i in y_pred:
    if i>0.5:
        label_pred.append(1)
    else:
        label_pred.append(0)
a=pd.DataFrame()
a['预测值']=list(label_pred)
a['真实值']=list(y_test)
a.head(1000)
count=0
label_pred=list(label_pred)
label_true=list(y_test)
## 计算模型精度
for i in range(len(y_test)):
    if label_pred[i]==label_true[i]:
        count=count+1
print('模型精度:',count/len(label_true))

注意,回归决策树预测结果是标签的概率值,故需先转化为对应的概率再比较预测标签与真实标签的值,也因此不能使用score()函数作为模型精度。

  • 5.可视化决策树
py 复制代码
# 5.可视化决策树
import graphviz 
from sklearn import tree
feature_name = ['satisfaction_level','last_evaluation','number_project','average_montly_hours','time_spend_company','Work_accident','promotion_last_5years','sales','salary']
dot_data = tree.export_graphviz(model
                                ,out_file = None                                
                                ,feature_names= feature_name                                
                                ,class_names=["不离职","离职"]                                
                                ,filled=True                                
                                ,rounded=True                               
                                ) 
graph = graphviz.Source(dot_data) 
graph

同样的,也可使用分类型决策树解决此问题。

py 复制代码
# 划分数据集
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)
# 训练模型
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=3, random_state=123) 
model.fit(X_train, y_train)
# 查看整体模型精度
model.score(X_test,y_test)
py 复制代码
# 可视化决策树
import graphviz 
from sklearn import tree
feature_name = ['satisfaction_level','last_evaluation','number_project','average_montly_hours','time_spend_company','Work_accident','promotion_last_5years','sales','salary']
dot_data = tree.export_graphviz(model
                                ,out_file = None                                
                                ,feature_names= feature_name                                
                                ,class_names=["不离职","离职"]                                
                                ,filled=True                                
                                ,rounded=True                               
                                ) 
graph = graphviz.Source(dot_data) 
graph

此图的具体分析可见:点击跳转

py 复制代码
# 查看各特征变量的重要程度
print(model.feature_importances_)
相关推荐
xiaoshiguang33 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇3 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
yuanbenshidiaos5 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习5 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo5 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc5 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法