0x01 剪枝
当训练数据量大、特征数量较多时构建的决策树可能很庞大,这样的决策树用来分类是否好?答案是否定的。
决策树是依据训练集进行构建的,为了尽可能正确地分类训练样本,结点划分过程将不断重复,有时会造成决策树分支过多。这就可能会把训练样本学的“太好”了,以至于把训练集自身的一些特点当作所有数据都具有的一般性质而导致过拟合。因此可主动去掉一些分支来降低过拟合风险。
决策树非常容易产生过拟合,实际所有非参数学习算法,都非常容易产生过拟合。
因此,对于决策树的构建还需要最后一步,即决策树的修剪。两个目的:降低复杂度,解决过拟合。
决策树的修剪,也就是剪枝操作,主要分为两种:
- 预剪枝(Pre-Pruning)
- 后剪枝(Post-Pruning)
接下来我们将详细地介绍这两种剪枝方法。
0x02 预剪枝
2.1 概念
预剪枝是指在决策树生成过程中,对每个节点在划分前先进行估计,若当前节点的划分不能带来决策树泛化性能的提升,则停止划分并将当前节点标记为叶节点。
那么所谓的“决策树泛化性能”如何来判定呢?这就可以使用性能评估中的留出法,即预留一部分数据用作“验证集”以进行性能评估。
如在下面的数据集中,将其划分成两部分:一部分作为训练集用来构建决策树,一部分作为验证集用来进行决策树的剪枝。具体划分如下。
2.2 具体实例
我们使用ID3算法,即使用信息增益进行决策树构建。得到的决策树如下图所示:
下面是手工计算的过程:
因为色泽和脐部的信息增益值最大,所以从这两个中随机挑选一个,这里选择脐部来对数据集进行划分,这会产生三个分支,如下图所示:
然而我们是否应该进行这次划分呢?
评判依据就是对划分前后的泛化性能进行估计:划分前后的泛华性能是否有提升,也就是如果划分后泛华性能有提升,则划分;否则,不划分。
下面来看看是否要用脐部进行划分,划分前:所有样本都在根节点,把该结点标记为叶结点,其类别标记为训练集中样本数量最多的类别,因此标记为好瓜,然后用验证集对其性能评估,可以看出样本{4,5,8}被正确分类,其他被错误分类,因此精度为43.9%。划分后:划分后的的决策树为:
则验证集在这颗决策树上的精度为:5/7 = 71.4% > 42.9%。泛化性能得到了提升,因此,用“脐部”进行划分。
接下来,决策树算法对结点 (2) 进行划分,再次使用信息增益挑选出值最大的那个特征,这里我就不算了,计算方法和上面类似,信息增益值最大的那个特征是“色泽”,则使用“色泽”划分后决策树为:
但到底该不该划分这个结点,还是要用验证集进行计算,可以看到划分后,精度为:4/7=0.571<0.714,因此,预剪枝策略将禁止划分结点 (2) 。对于结点 (3) 最优的属性为“根蒂”,划分后验证集精度仍为71.4%,因此这个划分不能提升验证集精度,所以预剪枝将禁止结点 (3) 划分。对于结点 (4) ,其所含训练样本已属于同一类,所以不再进行划分。
所以基于预剪枝策略生成的最终的决策树为:
对比未剪枝的决策树和经过预剪枝的决策树可以看出:预剪枝使得决策树的很多分支都没有“展开”,这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间开销。但是,另一方面,因为预剪枝是基于“贪心”的,所以,虽然当前划分不能提升泛化性能,但是基于该划分的后续划分却有可能导致性能提升,因此预剪枝决策树有可能带来欠拟合的风险。
2.3 伪代码
0x03 后剪枝
3.1 概念
后剪枝是先从训练集生成一颗完整的决策树,然后自底向上地对非叶节点进行考察,若将该节点对应的子树完全替换为叶节点能带来决策树繁花性的提升,则将该子树替换为叶节点。
3.2 具体实例
首先生成一棵完整决策树:
后剪枝算法首先考察上图中的结点 (6),若将以其为根节点的子树删除,即相当于把结点 (6) 替换为叶结点,替换后的叶结点包括编号为{7,15}的训练样本,因此把该叶结点标记为“好瓜”(因为这里正负样本数量相等,所以随便标记一个类别),因此此时的决策树在验证集上的精度为57.1%(为剪枝的决策树为42.9%),所以后剪枝策略决定剪枝,剪枝后的决策树如下图所示:
接着考察结点 5,同样的操作,把以其为根节点的子树替换为叶结点,替换后的叶结点包含编号为{6,7,15}的训练样本,根据“多数原则”把该叶结点标记为“好瓜”,测试的决策树精度认仍为57.1%,所以不进行剪枝。
考察结点 2 ,和上述操作一样,不多说了,叶结点包含编号为{1,2,3,14}的训练样本,标记为“好瓜”,此时决策树在验证集上的精度为71.4%,因此,后剪枝策略决定剪枝。剪枝后的决策树为:
接着考察结点 3 ,同样的操作,剪枝后的决策树在验证集上的精度为71.4%,没有提升,因此不剪枝;对于结点 1 ,剪枝后的决策树的精度为42.9%,精度下降,因此也不剪枝。
因此,基于后剪枝策略生成的最终的决策树如上图所示,其在验证集上的精度为71.4%。
3.3 伪代码
3.4 总结
对比预剪枝和后剪枝,能够发现,后剪枝决策树通常比预剪枝决策树保留了更多的分支,一般情形下,后剪枝决策树的欠拟合风险小,泛华性能往往也要优于预剪枝决策树。但后剪枝过程是在构建完全决策树之后进行的,并且要自底向上的对树中的所有非叶结点进行逐一考察,因此其训练时间开销要比未剪枝决策树和预剪枝决策树都大得多。
0x04 sklearn中的剪枝处理
4.1 展示
sklearn中现在能做的是预剪枝,就是设置Classifier或者Regression里的参数max_depth, min_samples_split, min_samples_leaf。
后剪枝的确是在sklearn中做不到的。
我们看一下具体的例子。首先构造数据:
代码语言:javascript复制import numpy as npimport matplotlib.pyplot as pltfrom sklearn import datasets
X,y = datasets.make_moons(noise=0.25,random_state=666)
plt.scatter(X[y==0,0],X[y==0,1])plt.scatter(X[y==1,0],X[y==1,1])plt.show()
然后加载描绘分类边界的函数:
代码语言:javascript复制def plot_decision_boundary(model, axis): # model是模型,axis是范围 x0, x1 = np.meshgrid( np.linspace(axis[0], axis[1],int((axis[1]-axis[0])*100)).reshape(-1,1), np.linspace(axis[2], axis[3],int((axis[3]-axis[2])*100)).reshape(-1,1), ) X_new = np.c_[x0.ravel(), x1.ravel()]
y_predict = model.predict(X_new) zz = y_predict.reshape(x0.shape)
from matplotlib.colors import ListedColormap custom_cmap = ListedColormap(['#EF9A9A','#FFF59D','#90CAF9']) plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)
下面开始进行决策树的构建和比较。首先创建决策树dt_clf1,在这里不限定决策树的最大深度,则决策树会一直向下划分,直到每一个节点的基尼系数为0为止。
代码语言:javascript复制from sklearn.tree import DecisionTreeClassifier
# 如果在构建时不传参数,则默认是使用基尼系数进行特征划分# 不限定max_depth,则决策树会一直向下划分,直到每一个节点的基尼系数为0为止dt_clf1 = DecisionTreeClassifier()dt_clf1.fit(X,y) plot_decision_boundary(dt_clf1, axis=[-1.5,2.5,-1.0,1.5])plt.scatter(X[y==0,0],X[y==0,1])plt.scatter(X[y==1,0],X[y==1,1])plt.show()
我们可以看到,决策边界的形状非常不规则,这就是典型的过拟合现象。在图中用红色箭头标出的部分,就是为了迁就现有的数据集样本,才会学习成这个样子的。
下面我们重新生成一个决策树dt_clf2,这里限制了决策树的深度为2,也就是划分到第二层就停止了。
代码语言:javascript复制dt_clf2 = DecisionTreeClassifier(max_depth=2)dt_clf2.fit(X,y)
plot_decision_boundary(dt_clf2, axis=[-1.5,2.5,-1.0,1.5])plt.scatter(X[y==0,0],X[y==0,1])plt.scatter(X[y==1,0],X[y==1,1])plt.show()
我们还可以设置最小样本划分,即对于一个节点来说,至少有多少个样本数据,才会对这个节点拆分下去。数值越高 越不容易过拟合,太高的话容易欠拟合。
代码语言:javascript复制dt_clf3 = DecisionTreeClassifier(min_samples_split=10)dt_clf3.fit(X,y)
plot_decision_boundary(dt_clf3, axis=[-1.5,2.5,-1.0,1.5])plt.scatter(X[y==0,0],X[y==0,1])plt.scatter(X[y==1,0],X[y==1,1])plt.show()
还可以设置最小样本叶节点,对于一个叶子节点来说,至少有几个样本。越少越容易过拟合。
代码语言:javascript复制dt_clf4 = DecisionTreeClassifier(min_samples_leaf=6)dt_clf4.fit(X,y)
plot_decision_boundary(dt_clf4, axis=[-1.5,2.5,-1.0,1.5])plt.scatter(X[y==0,0],X[y==0,1])plt.scatter(X[y==1,0],X[y==1,1])plt.show()
还可以设置最大叶子结点,即对于一个叶子节点来说,最多有几个叶子结点,叶子越多,树越复杂,越容易过拟合。
代码语言:javascript复制dt_clf5 = DecisionTreeClassifier(max_leaf_nodes=4)dt_clf5.fit(X,y)
plot_decision_boundary(dt_clf5, axis=[-1.5,2.5,-1.0,1.5])plt.scatter(X[y==0,0],X[y==0,1])plt.scatter(X[y==1,0],X[y==1,1])plt.show()
在实际使用这些参数时需要注意要避免欠拟合,其次这些参数之间可以互相组合,可以使用网格搜索的方式看哪些参数可以得到更好的结果。
4.2 总结
sklearn.tree:提供了决策树模型,用于解决分类和回归问题。
代码语言:javascript复制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)[source]
参数说明如下:
criterion
:特征选择标准,可选参数,默认是gini,可以设置为entropy。gini是基尼不纯度,是将来自集合的某种结果随机应用于某一数据项的预期误差率,是一种基于统计的思想。entropy是香农熵,也就是上篇文章讲过的内容,是一种基于信息论的思想。Sklearn把gini设为默认参数,应该也是做了相应的斟酌的,精度也许更高些?ID3算法使用的是entropy,CART算法使用的则是gini。splitter
:特征划分点选择标准,可选参数,默认是best,可以设置为random。每个结点的选择策略。best参数是根据算法选择最佳的切分特征,例如gini、entropy。random随机的在部分划分点中找局部最优的划分点。默认的”best”适合样本量不大的时候,而如果样本数据量非常大,此时决策树构建推荐”random”。max_features
:划分时考虑的最大特征数,可选参数,默认是None。寻找最佳切分时考虑的最大特征数(n_features为总共的特征数),有如下6种情况:- 如果max_features是整型的数,则考虑max_features个特征;
- 如果max_features是浮点型的数,则考虑int(max_features * n_features)个特征;
- 如果max_features设为auto,那么max_features = sqrt(n_features);
- 如果max_features设为sqrt,那么max_featrues = sqrt(n_features),跟auto一样;
- 如果max_features设为log2,那么max_features = log2(n_features);
- 如果max_features设为None,那么max_features = n_features,也就是所有特征都用。一般来说,如果样本特征数不多,比如小于50,我们用默认的”None”就可以了,如果特征数非常多,我们可以灵活使用刚才描述的其他取值来控制划分时考虑的最大特征数,以控制决策树的生成时间。
max_depth
:决策树最大深,可选参数,默认是None。这个参数是这是树的层数的。层数的概念就是,比如在贷款的例子中,决策树的层数是2层。如果这个参数设置为None,那么决策树在建立子树的时候不会限制子树的深度。一般来说,数据少或者特征少的时候可以不管这个值。或者如果设置了min_samples_slipt参数,那么直到少于min_smaples_split个样本为止。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。常用的可以取值10-100之间。min_samples_split
:内部节点再划分所需最小样本数,可选参数,默认是2。这个值限制了子树继续划分的条件。如果min_samples_split为整数,那么在切分内部结点的时候,min_samples_split作为最小的样本数,也就是说,如果样本已经少于min_samples_split个样本,则停止继续切分。如果min_samples_split为浮点数,那么min_samples_split就是一个百分比,ceil(min_samples_split * n_samples),数是向上取整的。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。min_weight_fraction_leaf
:叶子节点最小的样本权重和,可选参数,默认是0。这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。max_leaf_nodes
:最大叶子节点数,可选参数,默认是None。通过限制最大叶子节点数,可以防止过拟合。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制,具体的值可以通过交叉验证得到。class_weight
:类别权重,可选参数,默认是None,也可以字典、字典列表、balanced。指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别。类别的权重可以通过{class_label:weight}这样的格式给出,这里可以自己指定各个样本的权重,或者用balanced,如果使用balanced,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。当然,如果你的样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的None。random_state
:可选参数,默认是None。随机数种子。如果是证书,那么random_state会作为随机数生成器的随机数种子。随机数种子,如果没有设置随机数,随机出来的数与当前系统时间有关,每个时刻都是不同的。如果设置了随机数种子,那么相同随机数种子,不同时刻产生的随机数也是相同的。如果是RandomState instance,那么random_state是随机数生成器。如果为None,则随机数生成器使用np.random。min_impurity_split
:节点划分最小不纯度,可选参数,默认是1e-7。这是个阈值,这个值限制了决策树的增长,如果某节点的不纯度(基尼系数,信息增益,均方差,绝对差)小于这个阈值,则该节点不再生成子节点。即为叶子节点 。presort
:数据是否预排序,可选参数,默认为False,这个值是布尔值,默认是False不排序。一般来说,如果样本量少或者限制了一个深度很小的决策树,设置为true可以让划分点选择更加快,决策树建立的更加快。如果样本量太大的话,反而没有什么好处。问题是样本量少的时候,我速度本来就不慢。所以这个值一般懒得理它就可以了。
除了这些参数要注意以外,其他在调参时的注意点有:
- 当样本数量少但是样本特征非常多的时候,决策树很容易过拟合,一般来说,样本数比特征数多一些会比较容易建立健壮的模型如果样本数量少但是样本特征非常多,在拟合决策树模型前,推荐先做维度规约,比如主成分分析(PCA),特征选择(Losso)或者独立成分分析(ICA)。这样特征的维度会大大减小。再来拟合决策树模型效果会好。
- 推荐多用决策树的可视化,同时先限制决策树的深度,这样可以先观察下生成的决策树里数据的初步拟合情况,然后再决定是否要增加深度。
- 在训练模型时,注意观察样本的类别情况(主要指分类树),如果类别分布非常不均匀,就要考虑用class_weight来限制模型过于偏向样本多的类别。
- 决策树的数组使用的是numpy的float32类型,如果训练数据不是这样的格式,算法会先做copy再运行。
- 如果输入的样本矩阵是稀疏的,推荐在拟合前调用csc_matrix稀疏化,在预测前调用csr_matrix稀疏化。
sklearn.tree.DecisionTreeClassifier()提供了一些方法供我们使用,如下图所示: