R机器学习:基于树的分类算法的原理及实现
基于决策数的分类方法是一种非常直观的,非常好解释的,初中生都可以看得懂的分类算法,所以今天就给大家写写这个简单实用的分类算法。
决策树的基本流程就是通过一系列只能回答是否的问题将数据进行分类,这种方法还可以很直观地出图,非常适合初学者。

递归分类算法

先看一个概念,递归,今天要写的决策树就是递归分类算法(recursive partitioning algorithm)的一种,所谓递归就是重复,把一个大问题化成可以重复的小问题从而达到问题的解决,决策树就是把一个大问题化成很多个回答是否的小问题,重复重复就把大问题解决了。
其实自己是很不愿意在文章中整过多的专业名词的,但是如果你想深入了解某个领域,想进一步提高姿势水平,这些专业名词肯定得学,所以大家见谅,我尽量会写的非常浅显易懂。
想象一下,人们的通勤工具的分类情形,我们会通过这个工具有几个轮子,有没有发动机,多重等信息来判断工具的类型,知道了这些特征信息,你自己就会在脑海里形成一系列是否问题,达到对交通工具分类的目的,比如你的脑海里形成了像下图一样的流程:

先回答这个工具有没有发动机,如果有,再回答多重,如果小于3吨,就是小汽车,大于3吨就是坦克;如果没有发动机,再回答有多少轮子,小于2的话就是独轮车,大于2的话再回答有没有踏板,有则是自行车,无则是小摩托,就是这么一个流程。这就是我们判断交通工具时我们都会用的一个流程,也就是一个决策树,也是你自己的思维的一个过程。
在上面的流程中,问题叫做节点node,最终的分类结局叫做叶leave,那么整个决策树画出来就像一个倒立的树一样,所以叫做决策树,就是这么简单。
对于我们来说,决策树的整个流程中,先问哪个问题至关重要。
你想想,先通过交通工具的重量分类和先通过交通工具的轮子分类肯定是不一样的嘛。
那么机器学习中,递归分类算法是如何选择这个顺序的呢?
At each stage of the tree-building process, the rpart algorithm considers all of the predictor variables and selects the predictor that does the best job of discriminating the classes.
一个总的原则就是优先用可以将数据划分为最纯的类的问题作为节点
这个纯度可以用两个指标来衡量,一个是entropy,另一个是Gini index,本文重点介绍Gini index。

用Gini index对树进行划分

所谓最纯就是说通过某个问题划分出的两类,也就是树分叉过后形成的两类,这两类内部的一致性越大越好。Gini index就是纯度的一个指标,在形成树的过程中,我们希望每个节点的Gini index都是最大的。
Gini index的计算公式如下:

其中p(A) p(B)是两类在划分过后的数据中的占比。从这儿式子大家也可以悟出来Gini index越大就说明纯度越大。
看下面的例子,比如我们父节点有20个个案,2个类别AB,那么我们先进行随意划分形成左右两类,比如我们得到:左边14个其中11个A3个B,右边6个其中1个A5个B,这个时候我们的Gini index算法就如下图:

上面算出来每个节点的Gini index,我们还需要算出划分后的Gini index,算法如下:

那么上面其实是数据通过了决策树的一个节点,通过前后我们的数据纯度怎么变的呢?
在前面已经写了,每一次划分我们期望的结果都是越纯越好,所以我们是希望我们的子节点的Gini index越小越好,因为我们可以用不同的问题来划分我们的数据,所以整个过程就等价于我们希望划分之前和划分之后的Gini index的差(这个叫做Gini gain)越大越好。
所以就是哪个问题(也就是变量)带来的Gini gain越大,我们就越先使用哪个问题来划分数据。
在我们上面这个例子中我们父节点的Gini index是0.48,划分后是0.32,我们的Gini gain就是0.16。就是说如果你能找到别的变量,用别的变量形成的问题来划分得到的Gini gain大于0.16的话我们就会先用那个别的变量来划分。

连续变量和多分类变量怎么办?

之前写的都是用是否回答的问题进行树的划分,那么遇到连续或者多分类的特征,决策树还能不能行呢?
可以的
看下图,下图中的数据有3类,有两个连续的特征,variable1和variable2,我们在这两个连续特征形成的平面上可以画出两条线,这两条线形成的问题就可以作为节点,比如我们先看variable1,如果一个个案variable1大于等于20我们化成一个分支,小于20化成另一个分支,再用variable2,如果一个个案在variable1大于等于20的情况下,variable2小于10000则为一个分支,否则为另一个分支,这样,我们就得到了下图右边的决策树:

但是进一步想想,有些同学就要问了,这个20和10000又是如何确定的啊
依然是通过GIni gain确定的.
算法会首先将个案按照连续特征进行排序,然后先以相邻两个个案中点进行划分,并计算Gini gain,如果恰好这个点就是最好的点,那么就选它,如果不是,那就这样挨着挨着试直到找到最好的哪一个:

同样的,对于多分类特征也是如此,我们先按照分类变量的水平的Gini index进行排序,然后在每个相邻水平进行划分,找最大的gini gain的点作为我们最佳的划分点:


决策树算法的超参

想象一下,如果我们的数据特征非常多,然后每个变量一个节点,那么最终形成的树肯定非常“茂盛”,太茂盛也不是好事,不仅耗费算力,还容易过拟合,所以我们得认为控制这个树的茂盛程度,方法有2:
先生成全树,然后修剪
设定标准,不让树长那么茂盛
具体地,第一种方法只用设置你想剪掉的枝叶就行,对于第二种方法我们可以设定的标准就多了,比如你可以设定每个节点的最小个案数量,设定树的深度,设定最小拟合优度增加值等等。
看下图:左上就是设定不同个案数量的情况,右上是设定树的深度,左下是设定拟合优度,右下是设定最小个案:

这儿给大家讲讲这个cp指标,这个指标的英文全称是complexity parameter,是用来评价树的优度的,这个值计算方法如下:

由上面的式子可以看出,cp值越大表面该节点相对于上节点表现越差。
上面介绍的4个超参都是我们在训练决策树模型时需要调试的,接着我们看实例

决策树实例操练

现在有一个关于动物特征的数据集,我希望训练一个决策树模型,通过这些特征将我们的个案划分为不同的类型的动物,数据集大概长这样:

我们的响应变量是type,其余的变量全是预测变量,模型的训练依然是3步,第一定义任务,第二定义学习器,第三训练,前两步的代码如下:
zooTask <- makeClassifTask(data = zooTib, target = "type")
tree <- makeLearner("classif.rpart")
接下来就是超参调试,前面也给大家写了,我们主要需要调试的超参为minsplit, minbucket, cp, 和maxdepth这4个超参数,我们定义超参空间,搜索方法和验证方法的代码如下:
treeParamSpace <- makeParamSet(
makeIntegerParam("minsplit", lower = 5, upper = 20),
makeIntegerParam("minbucket", lower = 3, upper = 10),
makeNumericParam("cp", lower = 0.01, upper = 0.1),
makeIntegerParam("maxdepth", lower = 3, upper = 10))
randSearch <- makeTuneControlRandom(maxit = 200)
cvForTuning <- makeResampleDesc("CV", iters = 5)
定义好相应的参数之后,接下来我们就可以来进行调试了:
tunedTreePars <- tuneParams(tree, task = zooTask,
resampling = cvForTuning,
par.set = treeParamSpace,
control = randSearch)
parallelStop()
运行上面的代码我们得到的最优超参组合如下图所示:

然后我们要做的就是用最优超参重新训练我们的模型并进行模型的交叉验证,用最优超参训练模型的代码如下:
tunedTree <- setHyperPars(tree, par.vals = tunedTreePars$x)
tunedTreeModel <- train(tunedTree, zooTask)
运行上面的代码我们就训练好了我们决策树模型tunedTreeModel,我们还可以通过以下代码将决策树可视化出来:
treeModelData <- getLearnerModel(tunedTreeModel)
rpart.plot(treeModelData, roundint = FALSE,
box.palette = "BuBn",
type = 5)
printcp(treeModelData, digits = 3)
树的样子如下:

可以看到决策树这个算法的可解释性是真的好,对于我们的例子,我们首先用milk变量形成一个问题,再用feathers变量形成另外一个问题等等,然后根据问题的答案一直这么顺下去就达到了我们给数据分类的目的。
不止上面的信息,代码还会给我们输出每一次划分的cp值,如下图:


模型的交叉验证

交叉验证的部分依然从两个方面理解,首先是外部,就是我们如何随机抽不同的数据进行预测,另一面就是内部验证,就是我在训练模型的时候如何调试超参,从而使得模型对训练集拟合的更好,内外部代码如下:
outer <- makeResampleDesc("CV", iters = 5)
treeWrapper <- makeTuneWrapper("classif.rpart", resampling = cvForTuning,
par.set = treeParamSpace,
control = randSearch)
parallelStartSocket(cpus = detectCores())
cvWithTuning <- resample(treeWrapper, zooTask, resampling = outer)
parallelStop()
cvWithTuning
运行上面代码然后看验证结果:

可以看到,模型的mean misclassification error (MMCE)挺高的,甚至比我们调试超参的时候得到的0.078还大,这个其实是可以理解的,我们调试超参只是为了让模型更好拟合我们的训练集,通过交叉验证虽然拟合降低了,但是外推性更好了,可以很好地避免模型的过拟合问题。
好了,今天的文章就到这儿。最近微信粉丝增加的有点猛哈,很多同学私信发来了感谢和鼓励,真的谢谢大家啦,嘿嘿。