简介
作为机器学习中的一大类模型,树模型一直以来都颇受学界和业界的重视。目前无论是各大比赛各种大杀器的XGBoost、lightgbm、还是像随机森林、AdaBoost等典型集成学习模型,都是以决策树模型为基础的。今天由我来向大家介绍决策树的相关算法以及如何构造和实现决策树。
一、决策树概述
1.1决策树的定义
-
决策树(Decision Tree)是从一组无次序、无规则,但有类别标号的样本集中推导出的、树形表示的分类规则。
-
一般的,一棵决策树包含一个根结点、若干个内部结点(中间结点)和若干个叶子结点。
-
树的叶子结点表示类别标号,即分类属性的取值,也可以说是决策结果;树的内部结点为条件属性,每个结点包含的样本集合根据属性测试结果被划分到子结点中;根结点包含样本全集。从树根到叶子结点的一条路径称为一条决策规则,它可以对未知数据进行分类或预测。每条有向边都用其出点的属性值标记。
-
通常,一个属性有多少种取值,就从该结点引出多少条有向边,每一条边代表属性的一种取值。
-
树深度是树根到树叶的最大层数,通常作为决策树模型复杂度的一种度量。
1.2决策树的优缺点
决策树的优点:
- 决策树算法中学习简单的决策规则建立决策树模型的过程非常容易理解;
- 决策树模型可以可视化,非常直观;
- 应用范围广,可用于分类和回归,而且非常容易做多类别的分类;
- 能够处理数值型和连续的样本特征。
决策树的缺点:
- 很容易在训练数据中生成复杂的树结构,造成过拟合(overfitting)。剪枝可以缓解过拟合的负作用,常用方法是限制树的高度、叶子节点中的最少样本数量。
1.3决策树实例
采用老师上课时介绍的例子: 现在有人给你介绍对象,你打听到对方的特点:白不白,富不富,美不美,然后决定去不去相亲。根据以往经验,我们给出所有可能性:
白 | 富 | 美 | 行动 |
是 | 是 | 是 | 去 |
是 | 是 | 否 | 去 |
是 | 否 | 是 | 犹豫 |
是 | 否 | 否 | 犹豫 |
否 | 是 | 是 | 去 |
否 | 是 | 否 | 去 |
否 | 否 | 是 | 犹豫 |
否 | 否 | 否 | 不去 |
由上述表格,我么可以画出一棵树:
这就是决策树,每一层我们提出一个问题,根据问题的回答来走向不同的子树,最终到达叶子节点时,做出决策(去还是不去)。
以上描述首先考虑白不白,如果我们先考虑富不富,再考虑白不白,得到的树也会发生变化:
由以上 描述我们可以知道决策树其实是就是根据已知的经验来构建一棵树。可以认为是根据数据的某个维度进行切分,不断重复这个过程。当然,如果切分的顺序不同,会得到不同的树。
如果我们仔细观察,可以发现决策树中的又一些叶子节点是可以合并的,合并之后,到达某个节点时就不需要进行额外的决策,例如切分“白,富,美”得到的决策树合并后如下:
而“富,白,美”的决策树合并后如下:
可以看到上面这棵树只有4个叶子节点,少于“白,富,美”的5个节点。这就是决策树间的最大区别,不同决策树合并后得到的叶子节点的个数是不同的。由于叶子节点越少,往往决策树的泛化能力越高,因此我们可以认为训练决策树的一个目标是减少决策树的叶子结点。
二、决策树的构造
使用决策树做预测的每一步骤都很重要,数据收集不到位,将会导致没有足够的特征让我们构建错误率低的决策树。数据特征充足,但是不知道用哪些特征好,将会导致无法构建出分类效果好的决策树模型。从算法方面看,决策树的构建是我们的核心内容。通常,这一过程可以概括为3个步骤:特征选择、决策树的生成和决策树的修剪。
2.1特征选择
2.1.1熵
熵(entropy)表示随机变量不确定性的度量,也就是熵越大,变量的不确定性就越大。设是一个有限值的离散随机变量,其概率分布为:
假设变量X的随机取值为,每一种取值的概率分别是
,则变量X 的熵为:
实例分析:
- 如果有4个球,1个颜色。
则该颜色的球所占比例为1,可计算该集合的信息熵=−1∗log2(1)=0 - 如果有4个球,4个不同颜色。
则每种颜色的球所占比例为1/4,可计算该集合的信息熵=-4*1/4*log2(1/4)=2 - 如果有8个球,8个不同颜色。
则每种颜色的球所占比例为1/8,可计算该集合的信息熵=-8*1/8*log2(1/8)=3
由上述分析可知,系统的熵最小为0,最大可以无穷。熵越小,说明集合的纯度越高。
2.1.2条件熵
条件熵H(Y|X)表示在已知随机变量X条件下随机变量Y的不确定性。随机变量X给定的条件下随机变量Y的条件熵为:
2.1.3信息增益
信息增益指的就是划分可以带来纯度的提高,信息熵的下降。它的计算公式,是信息增益 = 划分前熵 - 划分后熵
。在计算的过程中,我们会计算条件下每个分支的归一化信息熵,即用每个分支在该属性中出现的概率,来乘以该分支的信息熵。所以信息增益的公式可以表示为:
Gain(D,a)
中 D
是当前结点包含的所有结果(来自父节点某一分支), a
是为划分这些结果所选的属性,Di
则是属性a下不同的分支所包含的结果 。
2.1.4信息增益率
以信息增益作为划分训练数据集的特征,存在偏向于选择取值较多的特征的问题。使用信息增益率可以对这个问题进行校正,这是特征选择的另一个标准。
定义
特征A对训练数据集D的信息增益率GainRatio(D,A)定义为其信息增益Gain(D,A),Gain(D,A)与训练数据集D关于特征A的值的经验熵 IV(A)之比。
其中,Gain(D,A)的计算公式见式,Gain(D,A) = E (D) − E (D,A) ,IV(A)如下:
称为属性A的“固有值”,属性A的取值数目越多,即v越大,则IV(A)的值通常会越大。
2.1.5基尼指数
定义
基尼指数(Gini不纯度)表示在样本集合中一个随机选中的样本被分错的概率。
注意:Gini指数越小表示集合中被选中的样本被参错的概率越小,也就是说集合的纯度越高,反之,集合越不纯。当集合中所有样本为一个类时,基尼指数为0。
2.2决策树的生成(决策树算法)
构建决策树的算法有很多,比如C4.5、ID3和CART。
- ID3:1979年由澳大利亚的计算机科学家罗斯·昆兰所发表。其他科学家也根据ID3算法相继提出了ID4和ID5等算法。
- C4.5:考虑到ID4等名已被占用,昆兰只好将1993年更新的ID3算法命名为C4.5算法。
- CART:Classification and Regression Tree,可解决分类和回归问题。
2.2.1ID3算法
ID3算法的核心是在决策树各个结点上应用信息增益准则选择特征,递归地构建决策树。
ID3算法的决策树生成实现过程如下:
1)从根结点(root node)开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征。
2)由该特征的不同取值建立子节点,再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止。
3)最后得到一个决策树。
决策树ID3算法的局限性
ID3算法虽然提出了新思路,但是还是有很多值得改进的地方。
1) ID3没有考虑连续特征,比如长度,密度都是连续值,无法在ID3运用。这大大限制了ID3的用途。
2) ID3采用信息增益大的特征优先建立决策树的节点。很快就被人发现,在相同条件下,取值比较多的特征比取值少的特征信息增益大。比如一个变量有2个值,各为1/2,另一个变量为3个值,各为1/3,其实他们都是完全不确定的变量,但是取3个值的比取2个值的信息增益大。
3) ID3算法对于缺失值的情况没有做考虑
4) 没有考虑过拟合的问题
ID3 算法的作者昆兰基于上述不足,对ID3算法做了改进,这就是C4.5算法,下面我们就来聊下C4.5算法。
2.2.2C4.5算法
上一节我们讲到ID3算法有四个主要的不足,一是不能处理连续特征,第二个就是用信息增益作为标准容易偏向于取值较多的特征,最后两个是缺失值处理的问和过拟合问题。昆兰在C4.5算法中改进了上述4个问题:
- 对于第一个问题,不能处理连续特征, C4.5的思路是将连续的特征离散化。比如m个样本的连续特征A有m个,从小到大排列为
,则C4.5取相邻两样本值的平均数,一共取得m-1个划分点,其中第i个划分点Ti表示为:
对于这m-1个点,分别计算以该点作为二元分类点时的信息增益。选择信息增益最大的点作为该连续特征的二元离散分类点。比如取到的增益最大的点为
,则小于
的值为类别1,大于
的值为类别2,这样我们就做到了连续特征的离散化。要注意的是,与离散属性不同的是,如果当前节点为连续属性,则该属性后面还可以参与子节点的产生选择过程。
- 对于第二个问题,信息增益作为标准容易偏向于取值较多的特征的问题。我们引入一个信息增益比,用信息增益率选择特征。
- 对于第三个缺失值处理的问题,主要需要解决的是两个问题,一是在样本某些特征缺失的情况下选择划分的属性,二是选定了划分属性,对于在该属性上缺失特征的样本的处理。
- 对于第四个问题,C4.5算法引入了正则化系数进行初步的剪枝。
C4.5算法的决策树生成实现过程如下:
1)选择当前节点的最优划分属性,即使得信息增益率最大的属性,如果不存在则该节点为叶子节点。
2)对选择的最优属性的每个取值,分别构建一个子节点,并将样本点分配到这些子节点中。
3)对每个子节点,递归地执行步骤1-2,直至满足终止条件,即到达叶子节点或无法继续划分。
4)构建好决策树,用它进行测试数据的分类预测。
C4.5算法在构建决策树时,采用了自上而下递归的方法,每次选择最优划分属性进行划分,并在该属性的每个取值上递归地划分数据集。这样,就能得到一个覆盖了训练集所有样本的决策树模型,并可用来对新的测试样本进行分类预测。
C4.5算法的局限性
C4.5虽然改进或者改善了ID3算法的几个主要的问题,仍然有优化的空间。
1)由于决策树算法非常容易过拟合,因此对于生成的决策树必须要进行剪枝。剪枝的算法有非常多,C4.5的剪枝方法有优化的空间。思路主要是两种,一种是预剪枝,即在生成决策树的时候就决定是否剪枝。另一个是后剪枝,即先生成决策树,再通过交叉验证来剪枝。
2)C4.5生成的是多叉树,即一个父节点可以有多个节点。很多时候,在计算机中二叉树模型会比多叉树运算效率高。如果采用二叉树,可以提高效率。
3)C4.5只能用于分类,如果能将决策树用于回归的话可以扩大它的使用范围。
4)C4.5由于使用了熵模型,里面有大量的耗时的对数运算,如果是连续值还有大量的排序运算。如果能够加以模型简化可以减少运算强度但又不牺牲太多准确性的话,那就更好了。
这4个问题在CART算法里面部分加以了改进。
2.2.3CART算法
CART算法的决策树生成实现过程如下:
-
使用CART算法选择特征
- 对回归树用平方误差最小化准测,进行特征选择;
- 对分类树用基尼指数(GINI)最小化准则,进行特征选择,
-
根据特征切分数据集合
-
构建树
2.2.4三种算法之间的区别
- ID3(Iterative Dichotomiser 3)算法是基于信息增益的决策树算法,用于处理离散的输入变量,并且在生成树的过程中不考虑数据的概率分布。
- C4.5 算法是 ID3 的改进版,它能够处理离散和连续的输入变量,并且在生成树的过程中考虑数据的概率分布。
- CART (Classification and Regression Trees)算法是基于基尼系数的决策树算法,可以用于二分类和多分类的问题,也可以用于回归问题。
2.3决策树的修剪(剪枝)
2.3.1什么是决策树的剪枝?
对比日常生活中,环卫工人在大街上给生长茂密的树进行枝叶的修剪。在机器学习的决策树算法中,有对应的剪枝算法。将比较复杂的决策树,化简为较为简单的版本,并且不损失算法的性能。
2.3.2为什么要剪枝?
决策树的剪枝是为了简化决策树模型,避免过拟合。
- 同样层数的决策树,叶结点的个数越多就越复杂;同样的叶结点个数的决策树,层数越多越复杂。
- 剪枝前相比于剪枝后,叶结点个数和层数只能更多或者其中一特征一样多,剪枝前必然更复杂。
- 层数越多,叶结点越多,分的越细致,对训练数据分的也越深,越容易过拟合,导致对测试数据预测时反而效果差,泛化能力差。
2.3.3剪枝的分类
剪枝算法可以分为:预剪枝和后剪枝。
预剪枝
预剪枝就是在决策树生成过程中,在每次划分时,考虑是否能够带来决策树性能的提升。如果可以提升决策树的性能则会进行划分,如果不能则会停止生长。
一般的方法有如下几种:
- 当树的深度达到一定的规模,则停止生长。
- 达到当前节点的样本数量小于某个阈值的时候。
- 计算每次分裂对测试集的准确性提升,当小于某个阈值,或不再提升甚至有所下降时,停止生长。
- 当信息增益,增益率和基尼指数增益小于某个阈值的时候不在生长。
预剪枝的优缺点
优点:思想简单,算法高效,采用了贪心的思想,适合大规模问题。
缺点:提前停止生长,有可能存在欠拟合的风险。
后剪枝
后剪枝是先从训练集生成一颗完整的决策树,然后自底向上的对决策树进行剪枝。与预剪枝最大的不同就是决策树是否生长完整。决策树的生成是学习局部的模型,后剪枝则是学习整体的模型。
一般的方法有错误率降低剪枝(REP)等。
后剪枝的优缺点
优点:可以最大限度的保留树的各个节点,避免了欠拟合的风险。
缺点:相较于预剪枝的时间开销巨大。
三、决策树的实现
介绍了决策树的相关定义及如何构造决策树后,接下来就是如何用代码进行实现了。
泰坦尼克号沉没是历史上最臭名昭着的沉船之一。1912年4月15日,在她的处女航中,泰坦尼克号在与冰山相撞后沉没,在2224名乘客和机组人员中造成1502人死亡。这场耸人听闻的悲剧震惊了国际社会,并为船舶制定了更好的安全规定。 造成海难失事的原因之一是乘客和机组人员没有足够的救生艇。尽管幸存下沉有一些运气因素,但有些人比其他人更容易生存,例如妇女,儿童和上流社会。 在这个案例中,我们对哪些人可能存活分析。运用机器学习工具来预测哪些乘客幸免于悲剧。
3.13.1以信息增益划分属性/ID3算法
数据加载
#数据加载
def loadData( ):
data = pd.read_csv("titanic.csv")
data["Age"] = data["Age"].fillna(data["Age"].mean()) #针对Age字段,采用均值进行填充
data = data.dropna()
data.isna().sum()
dataset = data.values.tolist()
#四个属性
labels=['Pclass','Age','Sex','Survived']
return dataset,labels
计算给定数据的香农熵
#计算给定数据的香农熵
def calShannonEnt(dataset):
numEntries = len(dataset) #获得数据集函数
labelCounts={} #用于保存每个标签出现的次数
for data in dataset:
#提取标签信息
classlabel = data[-1]
if(classlabel not in labelCounts.keys()): #如果标签未放入统计次数的字典,则添加进去
labelCounts[classlabel]=0
labelCounts[classlabel]+=1 #标签计数
shannonEnt=0.0 #熵初始化
for key in labelCounts:
p = float(labelCounts[key])/numEntries #选择该标签的概率
shannonEnt-= p*np.log2(p)
return shannonEnt #返回经验熵
根据某一特征划分数据集
#根据某一特征划分数据集
def splitDataset(dataset,axis,value):
# dataset 待划分的数据集 axis 划分数据集的特征 value 返回数据属性值为value
retDataSet = [] #创建新的list对象
for featVec in dataset: #遍历元素
if featVec[axis]==value: #符合条件的抽取出来
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
统计出现次数最多的元素(类标签)
# 统计出现次数最多的元素(类标签)
def majorityCnt(classList):
classCount={} #统计classList中每个类标签出现的次数
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] #返回出现次数最多的类标签
创建并递归遍历该树
def createTree(dataset,labels):#数据集和标签列表
classList =[example[-1] for example in dataset]#数据所属类得值
if classList.count(classList[0])==len(classList):#条件1:classList只剩下一种值
return classList[0]
if len(dataset[0])==1:#条件2:数据dataset中属性已使用完毕,但没有分配完毕
return majorityCnt(classList)#取数量多的作为分类
bestFeat = chooseBestFeatureToSplit(dataset)#选择最好的分类点,即香农熵值最小的
labels2 = labels.copy()#复制一分labels值,防止原数据被修改。
bestFeatLabel = labels2[bestFeat]
myTree = {bestFeatLabel:{}}#选取获取的最好的属性作为
del(labels2[bestFeat])
featValues = [example[bestFeat] for example in dataset]#获取该属性下的几类值
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels2[:]#剩余属性列表
myTree[bestFeatLabel][value] = createTree(splitDataset(dataset,bestFeat,value),subLabels)
return myTree
添加主函数
if __name__ == '__main__':
dataSet, labels = loadData( )
print("数据集信息熵:"+str(calShannonEnt(dataSet)))
mytree = createTree(dataSet,labels)
print(mytree)
运行结果
3.2以信息增益率划分属性/C4.5算法
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量
baseEntropy = calcShannonEnt(dataSet) #计数数据集的香农熵
bestInfoGain = 0.0 #信息增益
bestFeature = -1 #最优特征的索引值
for i in range(numFeatures): #遍历数据集的所有特征
featList = [example[i] for example in dataSet] #获取dataSet的第i个所有特征
uniqueVals = set(featList) #创建set集合{},元素不可重复
newEntropy = 0.0
splitInfo = 0.0 #信息熵
for value in uniqueVals: #循环特征的值
subDataSet = splitDataSet(dataSet, i, value) #subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
newEntropy += prob * calcShannonEnt((subDataSet)) #计算划分过数据集的信息熵
splitInfo -= prob * np.log2(prob)
infoGain = (baseEntropy - newEntropy)/splitInfo #求出第i列属性的信息增益率
#print("第%d个特征的信息增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain): #计算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #记录信息增益最大的特征的索引值
return bestFeature #返回信息增益最大特征的索引值
运行结果
3.3 决策树的绘制
import numpy as np
import pandas as pd
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.feature_extraction import DictVectorizer
from sklearn.tree import export_graphviz
import matplotlib.pyplot as plt
# 加载数据
data = pd.read_csv('titanic.csv')
# print(data.head())
# print(data.shape) # (891, 12)
# 获取数据
x = data[['Pclass','Age','Sex']]
y = data['Survived']
# print(x.head())
# print(y.head())
# 缺失值处理
x['Age'].fillna(x['Age'].mean(),inplace=True)
# 特征处理
x['Sex'] = np.array([0 if i == 'male' else 1 for i in x['Sex']]).T
# 打印查看是否替换成功
# print(x.head())
# 划分数据集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2,random_state=22)
# 创建模型
model = DecisionTreeClassifier(criterion='entropy',max_depth=3)
# 3 准确率: 0.7653631284916201
# 4 准确率: 0.7653631284916201
# 5 准确率: 0.7430167597765364
# 6 准确率: 0.7486033519553073
# 8 准确率: 0.7541899441340782
# 12 准确率: 0.770949720670391
model.fit(x_train,y_train)
# 评估
score = model.score(x_test,y_test)
pred = model.predict(x_test)
print('准确率:',score)
print('预测值:',pred)
# 可视化方法一:
# export_graphviz(model, out_file="tree.dot", feature_names=['pclass','age','sex'])
# 更简单的方法:
plt.figure(figsize=(20,20))
feature_name = ['pclass','age','sex']
class_name = ['survived','death']
tree.plot_tree(model,feature_names=feature_name,class_names=class_name)
plt.savefig('tree.png')
运行结果
四、问题及总结
问题
ValueError: Length of feature_names, 6 does not match number of features, 4
feature_names这一参数的作用是:使决策树图中的各个小块可以显示其对应的特征名
这一参数要求参数值必须与被分析的数据集中的特征名(也叫属性名)对应,数量一致且顺序一致。我打印了feature_names ,发现feature_names 的个数一共有4个。系统要求的feature的个数为4个,但我却传进去了6个参数。
解决方法
将feature——names中的特征数由6改为4
总结
本次实验我们学习了决策树,并学习了它的算法,剪枝及代码实现。决策树非常易于使用,完整的决策树结构会很漂亮。从某种程度上来说,它们能以图形化方式很好地剖析数据,能让我们根据需求相对准确的对数据进行分类。但是它们也存在一些不足:决策树存在的一个缺点就是容易过拟合。尤其对于具有包含大量特征的数据时,复杂的决策树可能会过拟合数据。所以需要谨慎对待决策树的参数,仔细调整参数以避免过拟合。对于节点上只有单个数据点的决策树,这几乎肯定是发生了过拟合。所以,测量决策树的准确率是非常重要的,需要在适当的时候停止决策树的生长。总的来说是一次收获满满的实验。