ml课程:决策树、随机森林、GBDT、XGBoost相关(含代码实现)

版权声明:===========版权所有,可随意转载,欢迎互相交流=========== https://blog.csdn.net/weixin_42446330/article/details/84171210

以下是我的学习笔记,以及总结,如有错误之处请不吝赐教。

基础概念:

熵Entropy:是衡量纯度的一个标准,表达式可以写为:

信息增益Information Gain:熵变化的一个量,表达式可以写为:

信息增益率Gain Ratio:信息增益的变化率,表达式可以写为:

基尼系数Gini Index:Gini(D)越小,数据集D的纯度越高,具体表达式如下:

实际上基尼指数、熵、分类误差率三者之间数学关系是统一的,将f(x)=-lnx在x=1处一阶泰勒展开,忽略高阶无穷小,可以得到f(x)\approx1-x:

总体流程:

决策树遵循‘分而治之(divide and conquer)’的思想:

  • 自根至叶的递归过程
  • 在每个中间节点寻找一个‘划分’属性

三种停止条件:

  • 当前节点包含的样本全属于同一类别,无需划分
  • 当前属性集为空,或是所有样本在所有属性上取值相同,无法划分
  • 当前节点包含的样本集合为空,不能划分

常见算法:

ID3算法:是根据信息增益来选择节点属性。

  1. 对当前样本集合,计算所有属性的信息增益;
  2. 选择信息增益最大的属性作为测试属性,把测试属性取值相同的样本划为同一子样本集;
  3. 若子样本集的类别属性只含有单个属性,则分支为子叶结点,判断其属性值并标上相应的符号,然后返回调用处;否则对子样本集递归调用本算法。

但是ID3算法有bug,就是容易将日期、排序等数目较多的属性有所偏好,那么就容易过拟合,如下图:

因此我们常常用剪枝Prune的方法来避免过拟合。

核心代码:


#先计算各类别出现频率(作为概率pi),然后调用信息熵公式计算 #H(D)-= (pi*logpi)
def calEntropy(dataset):
    n=len(dataset)
    labelCounts={}
    
    #对数据集各数据的类别计数
    for data in dataset:
        datalabel=data[-1] #取data最后一列,类别列
        if datalabel not in labelCounts.keys():
            labelCounts[datalabel]=0
        labelCounts[datalabel]+=1
    
    entropy=0.0
    
    #计算各类别出现频率(作为概率pi),调用信息熵公式计算 H(D)-=(pi*logpi)
    for key in labelCounts.keys():
        prob=float(labelCounts[key])/n
        entropy -= prob*log(prob,2)
    return entropy

C4.5算法:使用信息增益率选择节点属性。它是一种启发式的算法:先从划分属性中找出信息增益高于平均水平的,在从中选取信息增益率最高的。因此它可以既可以处理离散的属性,也可以处理连续值属性,具体过程和ID3相似。

CART算法:使用基尼系数选择节点属性,通过在候选属性集合中,选取那个使划分后基尼系数最小的属性。它是一种二叉树;也是一种十分有效的分参数分类和回归方法,当终节点是连续变量时,该树为回归树;当终节点是分类变量时,该树为分类树。

以下是三个算法的统计表格:

关于决策树的优劣比较:

回归树:基于cart算法我们推出了树回归算法。它的构建方法具体如下:

假设一个回归问题,预估结果y\epsilonR,特征向量为X=[x1,x2,x3...xp]\epsilonR,回归树的两个步骤是:

  1. 把整个特征空间X切分成J个没有重叠的区域R1,R2,R3...Rj
  2. 其中区域Rj中的每个样本我们都给一样的预测结果\tilde{y_{Rj}} = \frac{1}{n}\sum_{j\in Rj}y_{i},其中n是Rj中的总样本数。

因此我们就是为了找到如下RSS最小的划分方式R1,R2,R3...RJ:

但是这个过程计算量太大了,因此我们采用探索式递归二分类解决这个问题。

递归二分

  • 自顶向下的贪婪是递归方案:
  1. 自顶向下:从所有样本开始,不断从当前位置,把样本切分到2个分支里
  2. 贪婪:每一次的划分,只考虑当前最优,而不回过头考虑之前的划分
  • 选择切分的维度(特征)xj以及切分点s是的划分后的数RSS结果最小:

回归树剪枝:如果让回归树充分生长,会有过拟合的风险,因此我们添加正则化进行限制,考虑剪枝后得到的子树{T_{\alpha }},其中\alpha是正则化系数,当固定一个\alpha后,最佳的T_{\alpha }就是是的下列狮子子值最小的子树:

其中:|T|是回归树叶子节点个数,\alpha可以通过交叉验证选择。

核心代码:

#二分数据
def binSplitDataSet(dataset,feat,val):
    mat0=dataset[nonzero(dataset[:,feat]>val)[0],:] #数组过滤选择特征大于指定值的数据
    mat1=dataset[nonzero(dataset[:,feat]<=val)[0],:] #数组过滤选择特征小于指定值的数据
    return mat0,mat1

#定义回归树的叶子(该叶子上各样本标签的均值)
def regLeaf(dataset):
    return mean(dataset[:,-1])

#定义连续数据的混乱度(总方差,即连续数据的混乱度=(该组各数据-该组数据均值)**2,即方差*样本数))
def regErr(dataset):
    return var(dataset[:-1])*shape(dataset)[0]

#最佳特征以及最佳特征值选择函数
def chooseBestSplit(dataset,leafType=regLeaf,errType=regErr,ops=(1,4)):
    tolS=ops[0];tolN=ops[1];m,n=shape(dataset)
    S=errType(dataset);bestS=inf;beatIndex=0;bestVal=0
    if len(set(dataset[:,-1].T.tolist()[0]))==1: #若只有一个类别
        return None,leafType(dataset)
    for featIndex in range(n-1):
        for splitVal in set(dataset[:,featIndex].T.tolist()[0]):
            mat0,mat1=binSplitDataSet(dataset,featIndex,splitVal)
            #若切分后两块数据的最少样本数少于设定值,不切分
            if (shape(mat0)[0]<tolN) or (shape(mat1)[0]<tolN): 
                continue
            newS=errType(mat0)+errType(mat1)
            if newS<bestS:
                bestIndex=featIndex;bestVal=splitVal;bestS=newS
    #若以最佳特征及特征值切分后的数据混乱度与原数据混乱度差值小于阈值,不切分
    if (S-bestS)<tolS:
        return None,leafType(dataset)
    mat0,mat1=binSplitDataSet(dataset,bestIndex,bestVal)
    #若以最佳特征及特征值切分后两块数据的最少样本数少于设定值,不切分
    if (shape(mat0)[0]<tolN) or (shape(mat1)[0]<tolN):
        return None,leafType(dataset)
    return bestIndex,bestVal

"""构建回归树"""
def createTree(dataset,leafType=regLeaf,errType=regErr,ops=(1,4)):
    feat,val=chooseBestSplit(dataset,leafType,errType,ops)
    if feat==None:
        return val
    regTree={}
    regTree['spFeat']=feat
    regTree['spVal']=val
    lSet,rSet=binSplitDataSet(dataset,feat,val)
    regTree['left']=createTree(lSet,leafType,errType,ops)
    regTree['right']=createTree(rSet,leafType,errType,ops)
    return regTree

决策树的集成Ensemble:

首先要说下Booststraping思想:这个名字来自成语‘pull up by your own booststraps’,意思是靠你自己的资源,简称自助法,他是一种又放回的抽样方法,是非参数统计中一种重要的估计统计量方差而进行区间估计量的统计方法。该方法在小样本时效果很好,通过方差的估计可以构造置信区间。核心步骤如下:

  1. 采用重复抽样技术从原始样本中抽取一定数量(自己给定)的样本,此过程允许重复抽样。
  2. 根据抽出的样本计算给定的统计量T.
  3. 重复上述N次(一般大于1000),得到N个统计量T。
  4. 计算上述N个统计量T的样本方差,得到统计量的方差。

Bagging算法:是boostrap aggregating的缩写,使用了上述的bootstraping思想,可以降低过拟合的风险,提高泛化能力,具体流程如下:

  1. 输入样本集D={(x,y1),(x2,y2),...(xm,ym)}
  2. 对于t = 1,2,... ,T:

        a)对训练集进行第t次随机采样,共采集m次,得到包含m个样本的采样集Dm

        b)用采样集Dm训练第m个基学习器Gm(x)

    3.分类场景,则T个学习器投出最多票数的类别为最终类别。回归场景,T个学习器得到的回归出结果进行算数平均得到的值为最终的模型输出。 

 随机森林RandomForest算法:与Bagging基本类似,使用了CART决策树作为基学习器,不同点在于bagging是随机抽取样本,而随机森林是对样本、属性都进行随机抽取。

  1. 从原始训练集中,应用boostrap方法有放回的随机抽取k个新的自助样本集,并由此构建k棵分类回归树,每次未被抽到的样本组成了k个袋外数据(out-of-bag)
  2. 设有n个特征,则在每一个树的每个节点处随机抽取m个特征,通过计算每个特征蕴含的信息量,特征中选择一个最具有分类能力的特征进行节点分裂。
  3. 每棵树最大限度的生长,不做任何剪裁。
  4. 将生成的最多棵树组成随机森林,用随机森林对新的数据进行分类,分类结果按分类器投票多少而定。回归场景,T个基模型(回归树)得到的回归结果进行算术平均得到的值为最终的模型输出。

Boosting算法:与之前两种算法不同,具体流程如下:

  1. 先在原数据集长出一个树
  2. 把先前一个树没能完美分类的数据重新weight
  3. 用新的re-weighted 树在训练出一个树
  4. 最终的分类结果由加权投票决定

公式:

Adaboost算法:这个算法是boosting算法的实现之一,也是比较常用的算法,具体流程如下:

  1. 初始化训练数据的权值分布,每一个训练样本最开始时都被赋予相同的权值:1/n

  2. 进行多轮迭代,用m=1,2,...,m表示迭代的第多少轮:

        a)使用具有权值分布Dm的训练集学习,得到基本分类器(选取让误差率最低的阈值来设计基本分类器):

        b)计算Gm在训练数据集上的分类误差率(Gm在训练数据集上的误差率em就是被Gm误分类样本的权值之和),这里I表示0或者1取值,\omega _{mi}表示错误的次数:

        c)计算Gm的系数,\alpha _{m}表示Gm在最终分类器中的重要程度(得到基本分类器在最终分类器中所占的权重),e_{m}<=\frac{1}{2}时,\alpha _{m}>=0,且\alpha _{m}随着e_{m}的减小而增大,意味着分类误差率越小的分类器在最终分类器中的作用越大:

        d)更新训练数据集的权值分布(为了得到样本的新的权值分布),用于下一轮迭代。

使得被基本分类器Gm误分类样本的权值增大,而被正确分类样本的权值减小。通过这样的方式,Adaboost能重点关注于那些较难分的样本上。

其中:Zm表示规范化因子,使得D_{m+1}成为一个概率分布。

    3.组合各个弱分类器:

    得到最终分类器,如下:

核心代码:

#label为样本标签,shape=(1,m)
def adaBoostTrainDS(dataArr,classLabels,numIt=40):
    weakClassArr=[];m=shape(dataArr)[0]
    D=mat(ones((m,1))/m) #初始化样本权重
    aggClassEst=mat(zeros((m,1)))
    for i in range(numIt):
        bestStump,error,classEst=buildStump(dataArr,classLabels,D)
        alpha = float(0.5*log((1.0-error)/max(error,1e-16))) #max(error,1e-16)防止0误差的计算溢出
        bestStump["alpha"]=alpha
        weakClassArr.append(bestStump)
        #print ("D:",D.T,"\n","predClass:",classEst.T)
        
        #为下一次迭代更新D(很关键,矩阵运算容易写错!)
        expon=multiply(-1*alpha*mat(classLabels).T,classEst) #shape(5,1)*shape(5,1)对应元素相乘,得到各样本的-alpha*yi*G(xi)
        D=multiply(D,exp(expon)) #shape(5,1)*shape(5,1)对应元素相乘,
        D=D/D.sum() #得到各样本更新后的wi
        
        #计算汇集的分类结果aggClassEst(即,将每次最佳决策树分类结果bestClass*alpha相加)
        aggClassEst+=alpha*classEst
        #print("aggClassEst:",aggClassEst.T)
        aggErrors=multiply(sign(aggClassEst)!=mat(classLabels).T,ones((m,1)))
        errorRate=aggErrors.sum()/m
        #print("total error:",errorRate)
        if errorRate==0.0:
            break
    return weakClassArr,errorRate,aggClassEst

GBDT算法(Gradient Boosted Decison Tree):是Adaboost的回归版本,是将原来adaboost中0或者1的误差率的计算,变成err这个计算(err代表多种误差计算,例如前面说过的平方误差,log误差等等),具体公式如下:

计算得到的残差,作为下一轮迭代的学习目标,最终的结果有加权和值得到,不在是简单的多数投票:

XGBoost算法(Extreme GBoosted):从名字可以看出,该算法是GBDT的高级版,它是将GBDT的速度和效率做到了极致。主要变化如下:

  1. 使用L1和L2正则化防止过拟合:

  2. 对代价函数进行一阶和二阶求导,更快的收敛;
  3. 对其进行从下之上的剪枝,防止算法贪婪。

后续我们会继续学习如何对XGboost及GBDT等集成算法进行参数调节,To be continue......

猜你喜欢

转载自blog.csdn.net/weixin_42446330/article/details/84171210
今日推荐