关于决策树、随机森林、GBDT、XGBoost、LightGBM的那些事

决策树

在我们日常生活中,往往是需要根据各种因素来决定我们最后的选择。比如要去打篮球,就要看今天是什么天气、温度、湿度等因素。如下图所示,就是一棵典型的决策树。当一个问题:我们要不要打球产生在我们脑海中时,就会有某些因素,类似温度,湿度,最后来让我们决定结果是去打球还是不去打球。
在这里插入图片描述
一棵决策树是由两个部分构成的:构造和剪枝。

构造

所谓的构造就是怎么生成一棵决策树。在这个过程中,会存在三种节点。
1.根节点,位于树的顶部,最开始的那个节点。
2.内部节点,也就是我们上面说的特征:温度、湿度等等。
3.叶节点,顾名思义树的叶子,也就是我们最后的结果。

可是,就这样不断构造树,树就不停地分裂生长,那么什么时候会停止生长呢?

当前结点包含的样本全属于同一类别,无需划分;例如:样本当中都是决定去相亲的,属于同一类别,就是不管特征如何改变都不会影响结果,这种就不需要划分了。
当前属性集为空,或是所有样本在所有属性上取值相同,无法划分;例如:所有的样本特征都是一样的,就造成无法划分了,训练集太单一。
当前结点包含的样本集合为空,不能划分。

剪枝

当特征过多的时候就会出现过拟合,我们不希望这么多的特征来判断,所以就用剪枝来给决策树瘦身。造成过拟合的原因之一在于训练集的样本太少,而选择的属性太多,构造出来了一个过于完美地决策树,于是就不允许存在训练样本中不存在的数据,也就相当于误差。
那么在剪枝中又分为预剪枝和后剪枝。

预剪枝是在决策树构造时就进行剪枝。方法是在构造的过程中对节点进行评估,如果对某个节点进行划分,在验证集中不能带来准确性的提升,那么对这个节点进行划分就没有意义,这时就会把当前节点作为叶节点,不对其进行划分。

后剪枝就是在生成决策树之后再进行剪枝,通常会从决策树的叶节点开始,逐层向上对每个节点进行评估。如果剪掉这个节点子树,与保留该节点子树在分类准确性上差别不大,或者剪掉该节点子树,能在验证集中带来准确性的提升,那么就可以把该节点子树进行剪枝。方法是:用这个节点子树的叶子节点来替代该节点,类标记为这个节点子树中最频繁的那个类。

纯度和信息熵

首先我们介绍一下纯度。你可以把决策树的构造过程看做是寻找纯净化的过程,在数学上来说,就是将目标变量的分歧最小化,这么说很抽象。那让我们来看一下下面这个例子:
在这里插入图片描述

讲完纯度,然后就是讲信息熵的定义。我们知道在热力学中,熵表示该体系中混乱程度的度量,那么信息熵就是信息的不确定程度。一般来说,信息熵越大,纯度越低。
在这里插入图片描述

构造决策树的算法

1.ID3
ID3是通过信息增益来计算的,可以提高纯度而降低信息熵。计算思想就是:通过父亲节点的信息熵减去子节点的信息熵,类似梯度的概念。 ID3 有一个缺陷就是,有些属性可能对分类任务没有太大作用,但是他们仍然可能会被选为最优属性。这种缺陷不是每次都会发生,只是存在一定的概率。在大部分情况下,ID3 都能生成不错的决策树分类。

2.C4.5
C4.5是改良后的ID3,可以解决对噪声敏感的问题。那么是在哪些方面进行改进了呢?
a.在C4.5中避免倾向较多选择的属性,在属性有许多值得时候,相当于被划分了许多份,虽然信息增益变大了,但是对于C4.5来说,属性熵也会变大,所以从整体上来看,信息增益率并没有增大。
b.采用悲观剪枝,悲观剪枝是剪枝技术中的其中一种,通过递归来估算每个内部节点的分类错误率,再比较剪枝前后的错误率大小,再决定是否需要进行剪枝。
c.C4.5可以处理连续分布的情况,对连续分布的属性进行离散化的处理。比如湿度不是按照高中低分布,而是按照百分比分布,在C4.5中就可以选择具有最高信息增益的划分所对应的阈值

3.Cart
ID3 和 C4.5 算法可以生成二叉树或多叉树,而 CART 只支持二叉树。同时 CART 决策树比较特殊,既可以作分类树,又可以作回归树。
决策树的核心思想就是寻找纯度的划分,与上面介绍的两种算法不同,CART采用的选择指标是基尼系数。而ID3是通过信息增益来判断,C4.5是通过信息增益率来计算。
基尼系数反应的是样本的不确定度。当基尼系数越小的时候,样本的差异越小,不确定程度越低,反之。在决策树分类的过程,就是一个不确定不断降低的过程,也就是提高纯度的过程。所以 CART 算法在构造分类树的时候,会选择基尼系数最小的属性作为属性的划分。

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
# 准备数据集
iris=load_iris()
# 获取特征集和分类标识
features = iris.data
labels = iris.target
# 随机抽取33%的数据作为测试集,其余为训练集
train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.33, random_state=0)
# 创建CART分类树
clf = DecisionTreeClassifier(criterion='gini')
# 拟合构造CART分类树
clf = clf.fit(train_features, train_labels)
# 用CART分类树做预测
test_predict = clf.predict(test_features)
# 预测结果与测试集结果作比对
score = accuracy_score(test_labels, test_predict)
print("CART分类树准确率 %.4lf" % score)

一个问题:

决策树的剪枝在 sklearn 中是如何实现的?
实际上决策树分类器,以及决策树回归器(对应 DecisionTreeRegressor 类)都没有集成剪枝步骤。一般对决策树进行缩减,常用的方法是在构造 DecisionTreeClassifier 类时,对参数进行设置,比如 max_depth 表示树的最大深度,max_leaf_nodes 表示最大的叶子节点数。通过调整这两个参数,就能对决策树进行剪枝。当然也可以自己编写剪枝程序完成剪枝。

GBDT

GBDT,也叫梯度提升决策树,英文叫Gradient Boosting Decision Tree,可以用于分类也可以用于回归,使用的是CART回归树,并且采用Boosting思想。简单地说,Boosting的基本思想是将串行在一起的基分类器层层叠加,使每个分类器之间都有依赖。在训练的时候,会对前一层分类器分类错误的样本赋予较高的权重。在GBDT中,每一个分类器都会去拟合预测值与实际结果的残差,也就是误差,然后一层层地作用下来,最后根据每个分类器的结果,经过加权后得到最终预测结果。

举一个非常简单的例子,比如我今年30岁了,但计算机或者模型GBDT并不知道我今年多少岁,那GBDT咋办呢?

它会在第一个弱分类器(或第一棵树中)随便用一个年龄比如20岁来拟合,然后发现误差有10岁;
接下来在第二棵树中,用6岁去拟合剩下的损失,发现差距还有4岁; 接着在第三棵树中用3岁拟合剩下的差距,发现差距只有1岁了;
最后在第四课树中用1岁拟合剩下的残差,完美。
最终,四棵树的结论加起来,就是真实年龄30岁(实际工程中,gbdt是计算负梯度,用负梯度近似残差)。

GBDT中使用的损失函数是均方差损失函数,每一次拟合后的结果为(y-predict),也就是残差。所以,GBDT就可以用负梯度近似残差。
在这里插入图片描述

梯度上升和梯度下降的使用场景

我们经常会听到梯度下降,但是梯度上反而很少听闻。其实,在每一轮迭代中,都会利用损失函数对模型的正梯度或者负梯度方向进行更新,只不过在梯度下降中,模型是以参数化的形式表示,从而模型的更新等价于参数的更新。而在梯度上升中,模型不需要进行参数化表示,而是直接定义在函数空间中,从而大大扩展了使用模型的种类。

优缺点

优点:
1.在预测的时候,树与树之间可以并行运算。
2.在分布稠密的数据集中,泛化能力表现好。
3.当采用决策树作为弱分类器使得GBDT模型具有较好的解释性和鲁棒性,能够自动发现特征间的高阶关系,并且也不需要对数据进行特殊的预处理如归一化等。
缺点:
1.在高维稀疏的数据集中,模型能力不佳,表现不入SVM。
2.当训练过程需要串行的时候,只能在决策树中采用局部的并行手段提高速度。

import numpy as np
from sklearn.ensemble import GradientBoostingClassifier

'''
调参:
loss:损失函数。有deviance和exponential两种。deviance是采用对数似然,exponential是指数损失,后者相当于AdaBoost。
n_estimators:最大弱学习器个数,默认是100,调参时要注意过拟合或欠拟合,一般和learning_rate一起考虑。
learning_rate:步长,即每个弱学习器的权重缩减系数,默认为0.1,取值范围0-1,当取值为1时,相当于权重不缩减。较小的learning_rate相当于更多的迭代次数。
subsample:子采样,默认为1,取值范围(0,1],当取值为1时,相当于没有采样。小于1时,即进行采样,按比例采样得到的样本去构建弱学习器。这样做可以防止过拟合,但是值不能太低,会造成高方差。
init:初始化弱学习器。不使用的话就是第一轮迭代构建的弱学习器.如果没有先验的话就可以不用管
由于GBDT使用CART回归决策树。以下参数用于调优弱学习器,主要都是为了防止过拟合
max_feature:树分裂时考虑的最大特征数,默认为None,也就是考虑所有特征。可以取值有:log2,auto,sqrt
max_depth:CART最大深度,默认为None
min_sample_split:划分节点时需要保留的样本数。当某节点的样本数小于某个值时,就当做叶子节点,不允许再分裂。默认是2
min_sample_leaf:叶子节点最少样本数。如果某个叶子节点数量少于某个值,会同它的兄弟节点一起被剪枝。默认是1
min_weight_fraction_leaf:叶子节点最小的样本权重和。如果小于某个值,会同它的兄弟节点一起被剪枝。一般用于权重变化的样本。默认是0
min_leaf_nodes:最大叶子节点数
'''

gbdt = GradientBoostingClassifier(loss='deviance', learning_rate=0.1, n_estimators=5, subsample=1
                                  , min_samples_split=2, min_samples_leaf=1, max_depth=3
                                  , init=None, random_state=None, max_features=None
                                  , verbose=0, max_leaf_nodes=None, warm_start=False
                                  )

train_feat = np.array([[1, 5, 20],
                       [2, 7, 30],
                       [3, 21, 70],
                       [4, 30, 60],
                       ])
train_label = np.array([[0], [0], [1], [1]]).ravel()

test_feat = np.array([[5, 25, 65]])
test_label = np.array([[1]])
print(train_feat.shape, train_label.shape, test_feat.shape, test_label.shape)

gbdt.fit(train_feat, train_label)
pred = gbdt.predict(test_feat)

total_err = 0
for i in range(pred.shape[0]):
    print(pred[i], test_label[i])
    err = (pred[i] - test_label[i]) / test_label[i]
    total_err += err * err
print(total_err / pred.shape[0])

XGBoost又是什么东西?

XGBoost实质上还是一个GBDT,两者都是使用boosting方法,只不是XGBoost速度和效率更优秀。
XGBoost的核心思想就是不断地添加树,然后不断地进行特征分裂来生成下一棵树。其实每添加一棵树,都相当于去学习一个新的函数来拟合上次预测的残差。当我们训练完之后,会得到k棵树。根据这个样本的不同特征共同作用来进行预测。那么,每棵树的叶子结点都会得到一个相对应的分数,当累加完这些分数后,就是预测结果。
不过,XGBoost中添加了正则化这一项。
XGBoost对树的复杂度主要包含两个部分:
一个是树的叶子结点个数,
另一个则是叶子节点的权重,并且对该权重进行L2正则化,来避免过拟合。
所以XGBoost的损失函数就为误差+正则化

用泰勒展开的XGBoost

为什么XGBoost要用泰勒公式展开呢?又有什么优势,这里就进行揭晓:
XGBoost使用了一阶偏导和二阶偏导,而二阶偏导对梯度下降更加准,也更加快。使用泰勒展开式后可以取得函数做自变量的二阶导数形式。那么就可以在不定义损失函数的情况下,对叶子进行分裂优化计算。也就是说可以将算法的优化和参数的选择分开,增强了XGBoost的适用性。

XGBoost与GBDT有什么不同

1.GBDT是机器学习算法,而XGBoost是这个算法的工程实现
2.在使用CARt作基分类器的时候,XGBoost添加了正则项来防止过拟合,从而提高模型的泛化
3.GBDT只使用了损失函数的一阶导信息,而XGBoost对损失函数进行泰勒公式的二阶展开,所以可以同时使用一阶和二阶。
4.GBDT采用了CART作基分类器,而XGBoost则是支持多种基分类器,如线性分类器
5.GBDT没有对缺失值做处理,而XGBoost会自动处理缺失值
6.GBDT在每一次迭代都会使用全部的数据,而XGBoost是对数据进行采样

停止树的分裂

为了防止过拟合,我们就要设置这种迭代的循环结束的条件。一般来说,设置树的最大深度;当样本权重小于设定的阈值时停止分裂;当分裂后的增益小于所设定的阈值时,都可以作为停止分裂的条件。

随机森林

现在来介绍随机森林,在随机森林中,是使用一种基于Bagging的算法。和上面提到的Boosting不同,Bagging的思想是从总体样本中随机抽取一部分的样本进行训练,然后经过N次这样的训练,最后将结果取平均值之后作为输出。这样,通过随机使用不同的样本进行训练,就可以避免一些不好的样本数据作为噪声对模型造成影响,从而提高准确度,也提高了泛化能力。

注意点

1.但是如果随机森林中两颗树之间的相关性很大,那么错误率就越大;
2.森林中每棵树的分类能力:每棵树的分类能力越强,整个森林的错误率越低;
3.最后值得确定的是确定随机森林的参数,这个参数就是特征选择的数目。减少特征选择个数M,树与树之间的相关性以及分类能力就会降低;而增加M,相关性与分类能力就会增强。

随机森林的优缺点

优点:
1.对分布不平衡的数据可以减小误差;
2.在训练的过程中,可以检测到特征之间的影响,并且在训练完之后可以得知哪些特征更为重要。
3.训练速度快,因为树与树之间是相互独立的,所以容易做成并行的方法。
4.它能够处理很高维度(feature很多)的数据,并且不用做特征选择(因为特征子集是随机选择的)。

缺点:
.1.当取值划分过多的时候,会对随机森林产生更大影响。

随机森林中对缺失值的处理

对于随机森林中的缺失值,首先会计算缺失特征与其他特征的相似度,然后再通过权重的数值得到最终的估计。

OOB

上面我们提到,构建随机森林的关键问题就是如何选择最优的m,要解决这个问题主要依据计算袋外错误率oob error(out-of-bag error)。

bagging方法中Bootstrap每次约有1/3的样本不会出现在Bootstrap所采集的样本集合中,当然也就没有参加决策树的建立,把这1/3的数据称为袋外数据oob(out
of bag),它可以用于取代测试集误差估计方法。

袋外数据(oob)误差的计算方法如下:

对于已经生成的随机森林,用袋外数据测试其性能,假设袋外数据总数为O,用这O个袋外数据作为输入,带进之前已经生成的随机森林分类器,分类器会给出O个数据相应的分类
因为这O条数据的类型是已知的,则用正确的分类与随机森林分类器的结果进行比较,统计随机森林分类器分类错误的数目,设为X,则袋外数据误差大小=X/O
优缺点:

这已经经过证明是无偏估计的,所以在随机森林算法中不需要再进行交叉验证或者单独的测试集来获取测试集误差的无偏估计。

import urllib.request
import re
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split,cross_val_score
from sklearn.metrics import classification_report
import pandas as pd
import numpy as np

url="https://archive.ics.uci.edu/ml/machine-learning-databases/undocumented/connectionist-bench/sonar/sonar.all-data"

str=urllib.request.urlopen(url).read()
print(str)
str=str.decode('utf-8')
data=re.split("\n",str)
print(data)
dataset=[]
for x in data:
    dataset.append(re.split(",",x))
    
dataset=dataset[0:len(dataset)-1]

L=len(dataset[0])
train_set=[]
lable_set=[]
for y in dataset:
    train_set.append(y[0:L-1])
    lable_set.append(y[L-1])
    
for k in range(len(dataset)):
    train_set[k]=list(map(float,train_set[k]))


lable_num=[]
for h in lable_set:
    if h=="R":
        lable_num.append(1)
    else:
        lable_num.append(0)
        
##使用随机森林算法
x=train_set
y=lable_num
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.3)
dataset = RandomForestClassifier(n_estimators=100)
dataset.fit(x_train,y_train)
answer = dataset.predict(x)
answer_array = np.array([y,answer])
answer_mat = np.matrix(answer_array).T
result=pd.DataFrame(answer_mat)
result.columns=["y","answer"]

print("随机森林测试集判别:")
print(classification_report(y_test,dataset.predict(x_test)))
print("-*60")
print("随机森林全部数据判别:")
print(classification_report(y,dataset.predict(x)))

scores=cross_val_score(dataset,x,y,cv=6)
cross=pd.DataFrame(scores)
cross.columns = ["5折交叉:"]
print(cross.T)
print(sum(scores)/5)

其他:

LightGBM的背景介绍

LightGBM是一个实现GBDT算法的框架,不仅清晰易懂,速度快,而且支持分布式。
LightGBM在训练速度上,比XGBoost快将近10倍。那么这个LightGBM相对于XGBoost做了哪一些的优化呢?

  • 基于Histogram的决策树算法
  • 带深度限制的Leaf-wise的叶子生长策略
  • 直方图做差加速直接
  • 支持类别特征(Categorical Feature)
  • Cache命中率优化 (更低的内存消耗)
  • 基于直方图的稀疏特征优化多线程优化
Histogram算法

这里提到了一个Histogram算法。
这个算法中文名叫直方图算法,它的思想在于先把连续的浮点特征值离散化成n个整数,构造出一个宽度为n的直方图。
在遍历数据的时候,根据这些离散化后的值作为索引,然后在直方图中累积统计量。当累积完所有的统计量后,根据直方图中的离散值,遍历寻找最优的分割点。
一个叶子的直方图可以由它的父亲节点的直方图和它兄弟的直方图做差得到。而通常构造直方图,需要遍历叶子上的所有数据,但直方图做差后仅仅需要遍历直方图k次。

在XGBoost中,树是按层生长的,称为Level-wise tree growth,同一层的所有节点都做分裂,最后剪枝。

在这里插入图片描述
Level-wise过一次数据可以同时分裂同一层的叶子,容易进行多线程优化,也好控制模型复杂度,不容易过拟合。但实际上Level-wise是一种低效的算法,因为它不加区分的对待同一层的叶子,带来了很多没必要的开销,因为实际上很多叶子的分裂增益较低,没必要进行搜索和分裂。

在Histogram算法之上,LightGBM进行进一步的优化。首先它抛弃了大多数GBDT工具使用的按层生长 (level-wise) 的决策树生长策略,而使用了带有深度限制的按叶子生长 (leaf-wise)算法。
在这里插入图片描述
Leaf-wise则是一种更为高效的策略,每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。因此同Level-wise相比,在分裂次数相同的情况下,Leaf-wise可以降低更多的误差,得到更好的精度。Leaf-wise的缺点是可能会长出比较深的决策树,产生过拟合。因此LightGBM在Leaf-wise之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值