机器学习之决策树(Decision Tree)
1 引言
决策树(Decision Tree)是一种非参数的有监督学习方法,它能够从一系列有特征和标签的数据中总结出决策规则,并用树状图的结构来呈现这些规则,以解决分类和回归问题。决策树中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果。
决策树算法包括ID3、C4.5以及C5.0等,这些算法容易理解,适用各种数据,在解决各种问题时都有良好表现,尤其是以树模型为核心的各种集成算法,在各个行业和领域都有广泛的应用。
我们用一个例子来了解决策树的工作原理以及它能解决什么问题。下面这个列表是一些动物的特征信息,左面第一列是动物的名字,第一行是特征的名称。决策树的本质是一种图结构,我们可以根据一些问题就可以对动物实现分类。如下表所示是一些已知物种以及特征,如果要实现一个根据物种特征将动物分为哺乳类和非哺乳类的决策树,可以将类别标号这一列作为标签,其他列作为决策树的训练数据,算法根据这些特征训练得到一棵决策树,然后我们就可以使用不存在列表表中的动物特征,利用决策树判断动物是否为哺乳类动物。
名字 |
体温 | 表皮覆盖 | 胎生 | 水生动物 | 飞行动物 | 有腿 | 冬眠 | 类标号 |
人类 | 恒温 | 毛发 | 是 | 否 | 否 | 是 | 否 | 哺乳类 |
鲑鱼 | 冷血 | 鳞片 | 否 | 是 | 否 | 否 | 否 | 鱼类 |
鲸 | 恒温 | 毛发 | 是 | 是 | 否 | 否 | 否 | 哺乳类 |
青蛙 | 冷血 | 无 | 否 | 半 | 否 | 是 | 是 | 两栖类 |
巨蜥 | 冷血 | 鳞片 | 否 | 否 | 否 | 是 | 否 | 爬行类 |
蝙蝠 | 恒温 | 毛发 | 是 | 否 | 是 | 是 | 是 | 哺乳类 |
鸽子 | 恒温 | 羽毛 | 是 | 否 | 是 | 是 | 否 | 鸟类 |
猫 | 恒温 | 毛发 | 是 | 否 | 否 | 是 | 否 | 哺乳类 |
豹纹鲨 | 冷血 | 鳞片 | 是 | 是 | 否 | 否 | 否 | 鱼类 |
海龟 | 冷血 | 鳞片 | 否 | 半 | 否 | 是 | 否 | 爬行类 |
企鹅 | 恒温 | 羽毛 | 否 | 半 | 否 | 是 | 否 | 鸟类 |
豪猪 | 恒温 | 刚毛 | 是 | 否 | 否 | 是 | 是 | 哺乳类 |
鮼 | 冷血 | 鳞片 | 否 | 是 | 否 | 否 | 否 | 鱼类 |
嵘螺 | 冷血 | 无 | 否 | 半 | 否 | 是 | 是 | 两栖类 |
下图就是一个简单的决策树,我们可以根据这各决策树对新物种进行预测,判断其是否为哺乳动物。当然这只是一个非常简单的决策树,我们可以根据大量的训练数据来补充完善、简化我们的决策树,以便我们的决策树能判断各种不同的新物种。
python的sklearn库中tree模块已经包含了我们平常使用到的决策树模型,可以直接调用,餐后通过调整合适的参数,获取分类结果比较理想的决策树。sklearn.tree模块包含以下五个类,接下来我们主要看一下分类树和回归树是如何使用的。
tree.DecisionTreeClassifier | 分类树 |
tree.DecisionTreeRegressor | 回归树 |
tree.export_graphviz | 将生成的决策树导出为DOT格式,画图专用 |
tree.ExtraTreeClassifier | 高随机版本的分类树 |
tree.ExtraTreeRegressor | 高随机版本的回归树 |
利用sklearn中的模型,决策树的构建流程以及核心代码如下:
from sklearn import tree #导入需要的模块 clf = tree.DecisionTreeClassifier() #实例化 clf = clf.fit(X_train,y_train) #用训练集数据训练模型 result = clf.score(X_test,y_test) #导入测试集,从接口中调用需要的信息
sklearn中我们最常用的两个模型是分类树和回归树,分类树适合于对事物进行分类,一般使用离散的数据;而回归树更适合预测连续、具体的数值。下面我们分别学习一下分类树和回归树原理和使用方法。
2 分类树—DecisionTreeClassifier
class sklearn.tree.DecisionTreeClassifier (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, min_impurity_split=None, class_weight=None, presort=False)
以上是sklearn库中分类树的构造函数,其中包含了很多参数,但是在我们实际应用中有些参数的使用频率并不是很高,我们只需要重点关注一下几个重要的参数。
2.1 重要参数
2.1.1 criterion
在解释criterion参数之前,我们需要先了解一下“不纯度”的概念。对于决策树来说,我们需要将样本转换为一棵树,首先要找到最佳结点和分枝方法,对分类树来说衡量这个“最佳“”的指标叫做“不纯度”,通常情况下,不纯度越低,决策树对训练集的拟合就越好。现在使用的决策树算法在分枝方法上的核心大多是围绕在对某个不纯度相关指标的最优化上。
不纯度是基于结点计算的,也就是说每一个结点都会有一个不纯度,并且子节点的不纯度一定是低于父节点的,也就是说,在一棵决策树上,叶子结点的不纯度一定是最低的。
解释完不纯度,我们看 criterion 这个参数时候就容易理解多了,criterion这个参数就是用来决定不纯度计算方法的。该参数有两种选项:“entropy”、“gini”。
criterion | 不纯度计算方法 |
"entropy" | 信息熵(Entropy) |
"gini" | 基尼系数(Gini Impurity) |
公式中 t 代表给定的结点,i 代表标签的任意分类,p(i | t)代表标签分类 i 在结点 t 上所占的比例。信息熵对不纯度更加敏感,对不纯度的惩罚最强,在实际应用中信息熵和基尼系数的效果基本相同,但是信息熵的计算所要花费的时间要比基尼系数更多,因为基尼系数的计算不涉及对数。在实际应用过程中可以调整参数,两种计算方式哪一种训练出的模型评分越高就使用哪一个。
2.1.2 random_state & splitter
random_state用来设置分枝中的随机模式的参数,默认None,在高维度时随机性会表现更明显,低维度的数据(比如鸢尾花数据集),随机性几乎不会显现。输入任意整数,会一直长出同一棵树,让模型稳定下来。
splitter也是用来控制决策树中的随机选项的,有两种输入值,输入”best",决策树在分枝时虽然随机,但是还是会优先选择更重要的特征进行分枝(重要性可以通过属性feature_importances_查看),输入“random",决策树在分枝时会更加随机,树会因为含有更多的不必要信息而更深更大,并因这些不必要信息而降低对训练集的拟合。这也是防止过拟合的一种方式。当你预测到你的模型会过拟合,用这两个参数来帮助你降低树建成之后过拟合的可能
2.1.3 剪枝参数
在不加限制的情况下,一棵决策树会生长到衡量不纯度的指标最优,或者没有更多的特征可用为止。这样的决策树往往会过拟合,这就是说,它会在训练集上表现很好,在测试集上却表现糟糕。我们收集的样本数据不可能和整体的状况完全一致,因此当一棵决策树对训练数据有了过于优秀的解释性,它找出的规则必然包含了训练样本中的噪声,并使它对未知数据的拟合程度不足。
为了让决策树有更好的泛化性,我们要对决策树进行剪枝。剪枝策略对决策树的影响巨大,正确的剪枝策略是优化决策树算法的核心。关于剪枝策略的参数包括:max_depth、min_samples、min_samples_split、max_features和min_impurity_decrease。
max_depth
max_depth作用在于限制树的最大深度,超过限定深度的树枝全部剪掉。这是用得最广泛的剪枝参数,在高维度低样本量时非常有效。决策树多生长一层,对样本量的需求会增加一倍,所以限制树深度能够有效地限制过拟合。在集成算法中也非常实用。实际使用时,建议从=3开始尝试,看看拟合的效果再决定是否增加设定深度。下面代码是一个max_depth对决策树模型评分影响的测试代码:
import graphviz from sklearn import tree from sklearn.datasets import load_wine from sklearn import model_selection import matplotlib.pyplot as plt wine = load_wine() x_train,x_test,y_train,y_test = model_selection.train_test_split(wine.data,wine.target,test_size=0.3) score = [] for i in range(10): # 实例化 clf = tree.DecisionTreeClassifier(criterion="entropy" ,random_state=30 ,splitter="random" ,max_depth=i+1 # ,min_samples_leaf=10 # ,min_samples_split=10 ) # 用训练集训练模型 clf = clf.fit(x_train,y_train) # 用测试数据对模型进行评估 once = clf.score(x_test,y_test) score.append(once) plt.plot(range(1,11),score,color="red",label="max_depth") plt.legend() plt.show()
min_samples & min_samples_split
min_samples_leaf限定,一个节点在分枝后的每个子节点都必须包含至少min_samples_leaf个训练样本,否则分枝就不会发生,或者,分枝会朝着满足每个子节点都包含min_samples_leaf个样本的方向去发生。一般搭配max_depth使用,在回归树中有神奇的效果,可以让模型变得更加平滑。这个参数的数量设置得太小会引起过拟合,设置得太大就会阻止模型学习数据。一般来说,建议从=5开始使用。如果叶节点中含有的样本量变化很大,建议输入浮点数作为样本量的百分比来使用。同时,这个参数可以保证每个叶子的最小尺寸,可以在回归问题中避免低方差,过拟合的叶子节点出现。对于类别不多的分类问题,=1通常就是最佳选择。
min_samples_split限定一个节点必须包含至少min_samples_split个训练样本,这个节点才允许被分支,否则分支就不会发生。
max_features & min_impurity_decrease
这两个参数一般和max_depth一起使用,max_features限制分枝时考虑的特征个数,超过限制个数的特征都会被舍弃。和max_depth异曲同工,max_features是用来限制高维度数据的过拟合的剪枝参数,但其方法比较暴力,是直接限制可以使用的特征数量而强行使决策树停下的参数,在不知道决策树中的各个特征的重要性的情况下,强行设定这个参数可能会导致模型学习不足。如果希望通过降维的方式防止过拟合,建议使用PCA,ICA或者特征选择模块中的降维算法。
min_impurity_decrease限制信息增益的大小,信息增益小于设定数值的分枝不会发生。
2.1.4 目标权重参数
能够调整样本标签平衡性的参数有两个:class_weight 和 min_weight_fraction_leaf。由于一些样本数据中某一类的数据本身就占比很大或者很小,例如真实网络流量中SQL注入攻击流量的占比是很小的,可能只占1%。如果用一个没有训练过的模型,将所有的流量判断为非SQL注入攻击流量,那该模型的准确率也是99%。所以我们需要使用class_weight参数对样本标签进行一定的均衡,给少量的标签更多的权重,让模型更偏向于少数类,向捕获少数类的方向建模。不设定该参数时,默认为None,数据集中所有标签的权重一样。
有了权重之后,样本量就不再是单纯地记录数目,而是受输入的权重影响了,因此这时候剪枝,就需要搭配min_weight_fraction_leaf这个基于权重的剪枝参数来使用。另请注意,基于权重的剪枝参数(例如min_weight_fraction_leaf)将比不知道样本权重的标准(比如min_samples_leaf)更少偏向主导类。如果样本是加权的,则使用基于权重的预修剪标准来更容易优化树结构,这确保叶节点至少包含样本权重的总和的一小部分。
2.2 重要属性和接口
模型训练之后我们可以调用查看模型的一些属性来了解模型的各种性质,其中比较重要的是feature_importances_,该属性包含了各个特征对模型的重要性。
除了一些通用的接口fit、score等,决策树常用的接口还有 apply 和 predict。apply的输入为测试集,返回值为每个测试样本所在的叶子结点的索引。predict输入为测试集,返回值为每个测试赝本的标签。需要注意的是当接口的输入是测试集或者训练集时,输入的特征矩阵必须至少是二维矩阵。
2.3 实例:泰坦尼克号幸存者的预测
泰坦尼克号的沉没是世界上最严重的海难事故之一,我们通过分类树模型来预测一下那些人可能成为幸存者。其中数据集来自著名数据分析竞赛平台kaggle(数据下载链接),train.csv为我们的训练数据集,test.csv为我们的测试数据集。
2.3.1 数据预处理
建立模型之前我们先对我们的数据进行一下预处理,下图是原始数据的相关信息,可以调用data.info()查看。我们的目标是建立一棵预测那些人可能存活的决策树,那么我们可以先剔除一些与该判断完全无关的特征,也就是剔除与存活完全无关的列。这里Cabin指的是小仓房间号,Name是乘客名字,Ticket是船票号,这三个信息并不能为我们预测乘客是否存活做出贡献,所以我们可以直接将其剔除。
某些列存在缺失值,比如年龄列,大部分列有 891 个数据,而Age列只有 714 个数据,所以可以进行填充,我们这里为了减小对原始数据的影响,空缺数据我们全部填充为年龄的均值。
由于我们机器学习模型处理的都是数值型数据,但是我们的训练数据集中包含一些对象数据,比如Sex、Embarked等,这里可能是字符串类型,所以我们需要将字符串类型转换为数值型,我们可以调用apply方法实现。代码如下:
# 将对象转换成数值,两种方法 labels = data["Embarked"].unique().tolist() data["Embarked"] = data["Embarked"].apply(lambda x:labels.index(x)) data.loc[:,"Sex"] = (data.loc[:,"Sex"] == "male").astype("int")
对存在缺失值和对象型数据处理过后我们就可以提取出标签和特征矩阵,我们预测的是乘客是否能存活,那么我们就将“Survived”列作为标签列,其他列作为特征。将数据的70%作为训练集,30%作为测试集。代码如下:
# 将标签和特征分离 x = data.iloc[:,data.columns != "Survived"] y = data.iloc[:,data.columns == "Survived"] # 划分训练集和测试集 xTrain,xTest,yTrain,yTest = train_test_split(x,y,test_size=0.3) for i in [xTrain,xTest,yTrain,yTest]: i = i.reset_index()
2.3.2 调整参数
数据处理之后我们就可以进行正常的实例化模型、训练数据,评估模型。为了评估比较好的模型,我们对参数需要进行不断调整。比如树的深度max_depth等等。对单一参数进行调整我们可以使用学习曲线的方法得到我们的最优参数,对于同时对多参数调整我们可以使用网格搜索方法进行调整。
学习曲线法的原理非常简单,通过调整某一参数,然后训练数据,评估模型,每一个参数对应于一个评估分数,这样我们可以的得到一条参数取值为横坐标,评估分数为纵坐标的曲线,从曲线中我们可以很直观的得到我们想要的最优参数。代码如下:
scoreTest = [] scoreTrain = [] for i in range(10): clf = DecisionTreeClassifier(random_state=0 ,max_depth=i+1 ,criterion="entropy" ) clf = clf.fit(xTrain,yTrain) onceTrain = clf.score(xTrain,yTrain) onceTest = cross_val_score(clf,x,y,cv=10).mean() scoreTest.append(onceTest) scoreTrain.append(onceTrain) print(max(scoreTest)) plt.figure() plt.plot(range(1,11),scoreTrain,color="red",label="train") plt.plot(range(1,11),scoreTest,color="blue",label="test") plt.xticks(range(1,11)) plt.legend() plt.show()
网格搜索是利用GridSearchCV类,首先构建一个参数字典,将我们要调整的多个参数放在一个字典中,通过GridSearchCV类的实例化对象,对模型进行训练,最后得出一个高分数的参数列表。通过best_params_查看最优参数列表,best_score_属性查看模型最高分。
网格搜索存在一个缺点就是不能动态的调整我们要控制的参数的个数,也就是说我们构造的parameters字典中存在的参数,在训练模型时候都需要用上,此时就会存在一个问题,某些参数使用默认值时候模型的分数比使用该参数时分数还要高,也就是说不能确定该参数是否需要调整。这就导致我们网格搜索得到的参数列表训练出来的模型分数并不一定会很高,跟对这一问题,还是需要靠我们的实际应用经验来得到我们想要的最优参数。
parameters = {"criterion":("gini","entropy") ,"splitter":("best","random") ,"max_depth":[*range(1,10)] ,"min_samples_leaf":[*range(1,50,5)] ,"min_impurity_decrease":[*np.linspace(0.0,0.5,50)] } clf = DecisionTreeClassifier(random_state=0) GS = GridSearchCV(clf,parameters,cv=10) GS = GS.fit(xTrain,yTrain) print(GS.best_params_) print(GS.best_score_)
3 回归树DecisionTreeRegressor
class sklearn.tree.DecisionTreeRegressor (criterion=’mse’, 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, min_impurity_split=None, presort=False)
3.1 重要参数
回归树中的参数大部分和分类树一样,比如max_depth、min_samples_split等,但是criterion参数和分类树的取值并不一样,它回归树衡量分支质量的指标,包含三种:"mse"、"friedman_mse" 和 "mae",三种参数它们代表三种计算方法。
- "mse"使用均方误差mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失
- “friedman_mse”使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差
- "mae"使用绝对平均误差MAE
3.2 属性和接口
属性和接口的话回归树和分类树基本一样,属性最重要的依然是feature_importances_,接口比较常用的是apply、fit、predict、score。
3.3 实例:一维回归图像绘制
问题:在二维平面上使用决策树来拟合一条正弦曲线,并添加一些噪声来观察回归树的表现。
1、创建数据
正弦曲线数据我们就用(x,y)表示,x轴坐标我们就创建一组随机分布在0~5上的数值。y坐标数据我们就取x轴坐标的sin值,再在y值上添加一部分噪声。我们使用Python的numpy来生成我们的数据。代码如下:
import numpy as np rng = np.random.RandomState(1) x_train = np.sort(5 * rng.rand(80, 1), axis=0) y = np.sin(x_train).ravel() y[::5] += 3 * (0.5 - rng.rand(16))
2、实例化训练模型
实例化两个回归树模型,用以我们调整参数,观察树的深度对训练模型的影响,一个模型的max_depth设置为2,另一个设置为5。代码如下:
from sklearn.tree import DecisionTreeRegressor regressor1 = DecisionTreeRegressor(max_depth=2) regressor2 = DecisionTreeRegressor(max_depth=5) regressor1.fit(x_train, y) regressor2.fit(x_train, y)
3、测试集导入模型,预测结果
x_test = np.arange(0.0,5.0,0.01)[:,np.newaxis] y_1 = regressor1.predict(x_test) y_2 = regressor2.predict(x_test) print(y_1) print(y_2)
4、绘制图像
将我们的原始训练数据以散点图形式展现出来,将深度为2和5的训练模型的预测结果以折线图的形式展现出来。代码如下:
plt.figure() plt.scatter(x_train, y, s = 20,edgecolors="black", c ="darkorange", label ="data") plt.plot(x_test,y_1,color="cornflowerblue",label="max_depth=2",linewidth=2) plt.plot(x_test,y_2,color="yellowgreen",label="max_depth=5",linewidth=2) plt.xlabel("data") plt.ylabel("target") plt.title("Decision Tree Regression") plt.legend() plt.show()
完整代码如下:
import numpy as np from sklearn.tree import DecisionTreeRegressor import matplotlib.pyplot as plt #创建包含噪声的sin函数散点数据 rng = np.random.RandomState(1) x_train = np.sort(5 * rng.rand(80, 1), axis=0) y = np.sin(x_train).ravel() y[::5] += 3 * (0.5 - rng.rand(16)) #实例化,并训练模型 regressor1 = DecisionTreeRegressor(max_depth=3) regressor2 = DecisionTreeRegressor(max_depth=5) regressor1.fit(x_train, y) regressor2.fit(x_train, y) x_test = np.arange(0.0,5.0,0.01)[:,np.newaxis] y_1 = regressor1.predict(x_test) y_2 = regressor2.predict(x_test) #print(y_1) #print(y_2) #画图 plt.figure() plt.scatter(x_train, y, s = 20,edgecolors="black", c ="darkorange", label ="data") plt.plot(x_test,y_1,color="cornflowerblue",label="max_depth=2",linewidth=2) plt.plot(x_test,y_2,color="yellowgreen",label="max_depth=5",linewidth=2) plt.xlabel("data") plt.ylabel("target") plt.title("Decision Tree Regression") plt.legend() plt.show()
结果如下如所示,蓝线代表的是决策树深度为2时,所预测的结果;绿线代表决策树深度为5时,所预测的结果。从途中我们可以看到深度为5时,决策树会出现训练数据的过拟合情况,也就是说模型在训练数据上表现的很好,但是在测试数据上表现得比较差。所以如果max_depth设置的太高,决策树就会学习的太过精细,从训练数据中的更多细节会被包含进去,其中很可能就包含一些噪声形成过拟合,这样很不利于我们将决策树用于测试数据中。对于其他参数我们也可以通过调整参数的不同取值来获得一个比较友好的参数取值,比如画学习曲线,我们就可以很直观的找到最佳参数取值点。
4 总结
4.1 决策树的优点
1. 易于理解和解释,因为树木可以画出来被看见
2. 需要很少的数据准备。其他很多算法通常都需要数据规范化,需要创建虚拟变量并删除空值等。但请注意,sklearn中的决策树模块不支持对缺失值的处理。
3. 使用树的成本(比如说,在预测数据的时候)是用于训练树的数据点的数量的对数,相比于其他算法,这是一个很低的成本。
4. 能够同时处理数字和分类数据,既可以做回归又可以做分类。其他技术通常专门用于分析仅具有一种变量类型的数据集。
5. 能够处理多输出问题,即含有多个标签的问题,注意与一个标签中含有多种标签分类的问题区别开
6. 是一个白盒模型,结果很容易能够被解释。如果在模型中可以观察到给定的情况,则可以通过布尔逻辑轻松解释条件。相反,在黑盒模型中(例如,在人工神经网络中),结果可能更难以解释。
7. 可以使用统计测试验证模型,这让我们可以考虑模型的可靠性。
4.2 决策树的缺点
1. 决策树学习者可能创建过于复杂的树,这些树不能很好地推广数据。这称为过度拟合。修剪,设置叶节点所需的最小样本数或设置树的最大深度等机制是避免此问题所必需的,而这些参数的整合和调整对初学者来说会比较晦涩
2. 决策树可能不稳定,数据中微小的变化可能导致生成完全不同的树,这个问题需要通过集成算法来解决。
3. 决策树的学习是基于贪婪算法,它靠优化局部最优(每个节点的最优)来试图达到整体的最优,但这种做法不能保证返回全局最优决策树。这个问题也可以由集成算法来解决,在随机森林中,特征和样本会在分枝过程中被随机采样。
4. 有些概念很难学习,因为决策树不容易表达它们,例如XOR,奇偶校验或多路复用器问题。
5. 如果标签中的某些类占主导地位,决策树学习者会创建偏向主导类的树。因此,建议在拟合决策树之前平衡数据集。