【机器学习BDT】python代码实现(上)

BDT(Bootstrap Decision Tree)简介

问题

在高能物理中,BDT用于对事例的分类,将信号和本底分开。

对于简单例子,比如有一个信号,和一种本底,目前把BDT想象成一个函数,它的输入是事例的一些特征(例如: 质心能量s)[(N,)形状变量],输出是一个实数[(1,)形状变量],对于给定输入,输出是确定的。

其实这个问题就转化成

构建一个映射Y:X->R,使得预测结果最优

神经网络也是相同的问题,BDT只是不同的范式,但是它的模型适合于分类问题。

解决方法

BDT(Bootstrap Decision Tree)是一种集成学习方法,它通过构建多个决策树,并通过投票机制来决定最终的预测结果。

BDT的基本思想是通过构建多个决策树,并通过投票机制来决定最终的预测结果。具体来说,BDT的过程如下:

  1. 首先,对训练数据集进行采样,得到多个子集。
  2. 然后,对每个子集,构建一棵决策树。
  3. 最后,对每个子树的预测结果进行投票,得出最终的预测结果。

BDT的优点是可以有效地克服单一决策树的偏差,并通过投票机制来集成多个决策树的预测结果,从而提高预测精度。

简单来说,最终模型对应的函数 F ( x ) = ∑ i = 1 n α i φ i ( x ) F(x)=\sum_{i=1}^n\alpha_i\varphi_i(x) F(x)=i=1nαiφi(x),其中 φ i ( x ) \varphi_i(x) φi(x)是第 i i i个决策树的预测函数, α i \alpha_i αi是第 i i i个决策树的权重。

算法

https://root.cern/manual/tmva/
https://blog.csdn.net/weixin_45666566/article/details/107942444
https://www.cnblogs.com/LeftNotEasy/archive/2011/03/07/random-forest-and-gbdt.html

总结:

  1. BDT的基学习器用CART决策树,CART是一种二叉树,给定Loss寻求分类参数最优,训练问题为最优化问题.
  2. 将多个基学习器和到一起的方法,决定使用:(回归)平方损失函数下的拟合残差,(分类)更新数据权重.

参考

https://www.cnblogs.com/kukri/p/8458236.html
https://blog.csdn.net/zhaodedong/article/details/103590540
https://www.cnblogs.com/keye/p/10564914.html
https://blog.csdn.net/m2xgo/article/details/109457705

BDT
https://blog.csdn.net/weixin_45666566/article/details/107942444

GBDT
https://www.cnblogs.com/LeftNotEasy/archive/2011/03/07/random-forest-and-gbdt.html

CART(Classification and Regression Tree)python实现

代码仅供参考

判别标准

Gini系数:

G i n i ( D ) = 1 − s u m ( p k ∗ ∗ 2 ) Gini(D)=1-sum(pk**2) Gini(D)=1sum(pk2)

    def Gini(self,标签,权重,select:numpy.ndarray=None) -> float:
        '''
        计算Gini系数
        Gini(D)=1-sum(pk**2)
        Gini(D,A)=D1/D*Gini(D1)+D2/D*Gini(D2)
        #仅适用与离散标签!!!

        输入
        标签,D:数据
        select:划分bool
        '''
        CN=numpy.sum(权重)
        if type(select)==type(None):
            G=1.0
            for i,label in enumerate(set(标签)):
                Ci=numpy.sum(权重[标签==label])
                G-=(float(Ci)/CN)**2
        else:
            CI=numpy.sum(权重[select])
            G=self.Gini(标签[select],权重[select])*CI/CN\
            +self.Gini(标签[~select],权重[~select])*(CN-CI)/CN
        return G

方差:

V a r ( D ) = 1 N − 1 ∑ i = 1 N ( y i − y ˉ ) 2 Var(D)=\frac{1}{N-1}\sum_{i=1}^N(y_i-\bar{y})^2 Var(D)=N11i=1N(yiyˉ)2

    def 平方误差(self,标签,权重,select:numpy.ndarray=None) -> float:
        '''
        计算平方误差
        平方误差(D)=sum (y-y.mean())**2
        平方误差(D,A)=平方误差(D1)+平方误差(D2)
        #可以支持多维输出[[y1,y2],evtoutarr2,...]
        这部分加权做法:以如果0.1,那么平方差贡献为0.1,

        输入
        标签,D:数据
        A:划分bool
        '''
        CN=numpy.sum(权重)
        if type(select)==type(None):
            mean标签=标签*权重/sum(权重)
            G=sum((标签-mean标签)**2*权重)
        else:
            G=self.平方误差(标签[select],权重[select])+self.平方误差(标签[~select],权重[~select])
        return G

主体构建Tree过程

采用递归的方式构建树,每个Tree被分为左子,右子.
值的注意的是求最优分别参数过程为暴力搜索,输入已经被画为一个个小格子(setA)(不能比这个区域更小了,否则很有可能过拟合)

    def 分类回归树(self,setA,标签,D,权重,树的最大深度=5,最小样本数=10,最小不纯度=0.1,损失函数='平方',输出='平均'):
        '''
        回归树(连续变量)
        这个是确定了给定Loss下分类模型,确定了输入空间分开的M个区间,
          如果输入落入第m个区域,那么输出是其实应该是输出的相应y值的平均
                                        还可以是输出y值的众数(分类问题)
        #此函数对Loss最小方式为暴力搜索,后续可能添加剃度下降版本
        输入
        setA:划分的步长,按分bin来
            (binN,xmin,xmax) (区间划分点数N-1)
            [(50,0,1,0.01),(2,-0.1,1.1),...]
        最小样本数:划分阶段实现
        最小不纯度:###还在施工###
        损失函数:str|function,'平方':平方差,'Gini',Gini函数
                其他可定义,格式为 float=fun(标签,权重,select:numpy.ndarray=None)
                当有select表示划分后Loss
        输出:str|function,'平均':标签平均,'众数':最大标签数(离散输出)
            其他可定义,格式为 number=fun(标签,权重)

        输出
        Tree: ([左子,右子],besti,bestj,xj,output)
            output:[[-1,sumevent],[标签i,counti],...]
        '''
        assert len(setA)==self.维数
        CN=len(标签)
        if 损失函数=='平方':
            损失函数=self.平方误差
        elif 损失函数=='Gini':
            损失函数=self.Gini
        else:
            print('使用用户函数!')
        if 输出=='平均':
            输出值=sum(标签*权重)/sum(权重)
        elif 输出=='众数':
            最大计数=0.0
            for i,label in enumerate(set(标签)):
                CI=sum(权重[标签==label])
                if CI>最大计数:
                    输出值=label
                    最大计数=CI
        else:
            输出值=输出(标签,权重)
        print()
        theGini=损失函数(标签,权重)
        Ginimin=numpy.inf
        besti=-1;bestj=(-1,None)
        for i,fAi in enumerate(setA):#最优划分变量
            for j,xj in enumerate(numpy.linspace(fAi[1],fAi[2],fAi[0]+1)[1:-1]):#最优划分区间
                fun=lambda x:x[:,i]<xj
                select=fun(D)
                CI=numpy.sum(select);
                if CI< 最小样本数 or CN-CI < 最小样本数:continue
                newGini=损失函数(标签,权重,select)
                if newGini<Ginimin:
                    print('newmin',newGini)
                    Ginimin=newGini
                    besti=i
                    bestj=(j,xj)
                    bestselect=select.copy()
        if 树的最大深度>1 and besti>=0:
            setA左=deepcopy(setA)
            setA左[besti][0]=bestj[0]+1
            setA左[besti][1]=setA[besti][1]
            setA左[besti][2]=bestj[1]
            setA右=deepcopy(setA)
            setA右[besti][0]=setA[besti][0]-bestj[0]-1
            setA右[besti][1]=bestj[1]
            setA右[besti][2]=setA[besti][2]
            左子=self.分类回归树(setA左,标签[bestselect],D[bestselect],权重[bestselect],树的最大深度=树的最大深度-1,最小样本数=最小样本数,最小不纯度=最小不纯度,损失函数=损失函数,输出=输出)
            右子=self.分类回归树(setA右,标签[~bestselect],D[~bestselect],权重[~bestselect],树的最大深度=树的最大深度-1,最小样本数=最小样本数,最小不纯度=最小不纯度,损失函数=损失函数,输出=输出)
            return [[左子,右子],theGini,Ginimin,besti,bestj[0],bestj[1],输出值]
        else:
            return [[],theGini,Ginimin,besti,bestj[0],bestj[1],输出值]

剪枝

    def 剪枝(self,T0):
        '''
        给Tree剪枝

        输出:
        cut:
        '''
        galist=self.获得树剪枝阈值序列(T0,'')
        位置映射={}
        ga=numpy.zeros(len(self.Tree节点分析信息ga))
        for i,x in enumerate(self.Tree节点分析信息ga):
            ga[i]=self.Tree节点分析信息ga[x]
            位置映射[i]=x
        sortindex=numpy.argsort(ga)
        cut=[]
        gainv=[]
        for i in range(len(ga)):
            x=位置映射[sortindex[i]]
            if not self.位置匹配(cut,x) and len(x)>=self.树的最小深度:
                gainv.append(ga[sortindex[i]])
                cut.append(x)

完整训练函数

    def 训练(self,setA,标签,D,权重,验标签,验D,验权重,树的最大深度=5,最小样本数=10,最小不纯度=0.1,损失函数='平方',输出='平均'):
        '''
        完整流程
        '''
        Tree=self.分类回归树(setA,标签,D,权重,
                        树的最大深度=树的最大深度,最小样本数=最小样本数,最小不纯度=最小不纯度,损失函数=损失函数,输出=输出)
        cut,gainv=self.剪枝(Tree)
        #cut=[]##################################3剪枝策略有问题
        #问题1剪枝条件中err判断方法
        Treelist=[Tree]
        a0,output0=self.计算准确率(Treelist[-1],验标签,验D,权重=验权重)
        outputall=[output0]
        alist=[a0]
        for i,dcut in enumerate(cut):
            Treelist.append(self.copytree(Treelist[-1],cut=dcut))
            ai,output=self.计算准确率(Treelist[-1],验标签,验D,权重=验权重)#此处用验证集
            alist.append(ai)
            outputall.append(output)
            if alist[-1]>alist[-2]:break
        bestindex=numpy.argmin(alist)
        self.Tree=Treelist[bestindex]
        return Treelist[bestindex],alist[bestindex],outputall[bestindex]

输出函数

    def 叶子输出_批量(self,T,D):
        '''
        输出分类树的输出

        '''
        output=numpy.zeros(len(D))
        for i in range(len(D)):
            output[i]=self.叶子输出(T,D[i])
        return output

完整代码

'''
CART树的一些算法
定义数据结构
标签:[0,0,0,1,...]:int(分类)    [[1,2,...],outarr,...]float(回归)
D:array([EVt1,evt2,...])
    Evi:[变量1,变量2,...]
'''
import numpy

def deepcopy(A:list):
    '''
    深度复制
    '''
    A0=[]
    for x in A:
        if type(x)==type([]):
            A0.append(deepcopy(x))
        else:
            A0.append(x)
    return A0

class CART模型:
    '''
    CART分类树
    '''
    def __init__(self,标签,Data,树的最小深度=5) -> None:
        assert len(标签)==len(Data),'标签应该和数据个数一致!'
        self.数据个数=len(标签)
        self.维数=len(Data[0])
        self.标签=numpy.array(标签)
        self.类数=len(set(标签))
        assert self.类数<=9,'目前位置编码方式不支持这个'
        self.Data=numpy.array(Data)
        self.Tree节点分析信息={}#节点位置[]表示root,[0]表示root/左子
        self.Tree节点分析信息ga={}#节点位置[]表示root,[0]表示root/左子
        self.树的最小深度=树的最小深度

    #一些损失函数
    def Gini(self,标签,权重,select:numpy.ndarray=None) -> float:
        '''
        计算Gini系数
        Gini(D)=1-sum(pk**2)
        Gini(D,A)=D1/D*Gini(D1)+D2/D*Gini(D2)
        #仅适用与离散标签!!!

        输入
        标签,D:数据
        select:划分bool
        '''
        CN=numpy.sum(权重)
        if type(select)==type(None):
            G=1.0
            for i,label in enumerate(set(标签)):
                Ci=numpy.sum(权重[标签==label])
                G-=(float(Ci)/CN)**2
        else:
            CI=numpy.sum(权重[select])
            G=self.Gini(标签[select],权重[select])*CI/CN\
            +self.Gini(标签[~select],权重[~select])*(CN-CI)/CN
        return G
    
    def 平方误差(self,标签,权重,select:numpy.ndarray=None) -> float:
        '''
        计算平方误差
        平方误差(D)=sum (y-y.mean())**2
        平方误差(D,A)=平方误差(D1)+平方误差(D2)
        #可以支持多维输出[[y1,y2],evtoutarr2,...]
        这部分加权做法:以如果0.1,那么平方差贡献为0.1,

        输入
        标签,D:数据
        A:划分bool
        '''
        CN=numpy.sum(权重)
        if type(select)==type(None):
            mean标签=标签*权重/sum(权重)
            G=sum((标签-mean标签)**2*权重)
        else:
            G=self.平方误差(标签[select],权重[select])+self.平方误差(标签[~select],权重[~select])
        return G
    
    def 分类回归树(self,setA,标签,D,权重,树的最大深度=5,最小样本数=10,最小不纯度=0.1,损失函数='平方',输出='平均'):
        '''
        回归树(连续变量)
        这个是确定了给定Loss下分类模型,确定了输入空间分开的M个区间,
          如果输入落入第m个区域,那么输出是其实应该是输出的相应y值的平均
                                        还可以是输出y值的众数(分类问题)
        #此函数对Loss最小方式为暴力搜索,后续可能添加剃度下降版本
        输入
        setA:划分的步长,按分bin来
            (binN,xmin,xmax) (区间划分点数N-1)
            [(50,0,1,0.01),(2,-0.1,1.1),...]
        最小样本数:划分阶段实现
        最小不纯度:###还在施工###
        损失函数:str|function,'平方':平方差,'Gini',Gini函数
                其他可定义,格式为 float=fun(标签,权重,select:numpy.ndarray=None)
                当有select表示划分后Loss
        输出:str|function,'平均':标签平均,'众数':最大标签数(离散输出)
            其他可定义,格式为 number=fun(标签,权重)

        输出
        Tree: ([左子,右子],besti,bestj,xj,output)
            output:[[-1,sumevent],[标签i,counti],...]
        '''
        assert len(setA)==self.维数
        CN=len(标签)
        if 损失函数=='平方':
            损失函数=self.平方误差
        elif 损失函数=='Gini':
            损失函数=self.Gini
        else:
            print('使用用户函数!')
        if 输出=='平均':
            输出值=sum(标签*权重)/sum(权重)
        elif 输出=='众数':
            最大计数=0.0
            for i,label in enumerate(set(标签)):
                CI=sum(权重[标签==label])
                if CI>最大计数:
                    输出值=label
                    最大计数=CI
        else:
            输出值=输出(标签,权重)
        print()
        theGini=损失函数(标签,权重)
        Ginimin=numpy.inf
        besti=-1;bestj=(-1,None)
        for i,fAi in enumerate(setA):#最优划分变量
            for j,xj in enumerate(numpy.linspace(fAi[1],fAi[2],fAi[0]+1)[1:-1]):#最优划分区间
                fun=lambda x:x[:,i]<xj
                select=fun(D)
                CI=numpy.sum(select);
                if CI< 最小样本数 or CN-CI < 最小样本数:continue
                newGini=损失函数(标签,权重,select)
                if newGini<Ginimin:
                    print('newmin',newGini)
                    Ginimin=newGini
                    besti=i
                    bestj=(j,xj)
                    bestselect=select.copy()
        if 树的最大深度>1 and besti>=0:
            setA左=deepcopy(setA)
            setA左[besti][0]=bestj[0]+1
            setA左[besti][1]=setA[besti][1]
            setA左[besti][2]=bestj[1]
            setA右=deepcopy(setA)
            setA右[besti][0]=setA[besti][0]-bestj[0]-1
            setA右[besti][1]=bestj[1]
            setA右[besti][2]=setA[besti][2]
            左子=self.分类回归树(setA左,标签[bestselect],D[bestselect],权重[bestselect],树的最大深度=树的最大深度-1,最小样本数=最小样本数,最小不纯度=最小不纯度,损失函数=损失函数,输出=输出)
            右子=self.分类回归树(setA右,标签[~bestselect],D[~bestselect],权重[~bestselect],树的最大深度=树的最大深度-1,最小样本数=最小样本数,最小不纯度=最小不纯度,损失函数=损失函数,输出=输出)
            return [[左子,右子],theGini,Ginimin,besti,bestj[0],bestj[1],输出值]
        else:
            return [[],theGini,Ginimin,besti,bestj[0],bestj[1],输出值]

    def 获取叶节点个数(self,T,节点位置:str):
        '''
        节点位置:'0101'#表示从根开始移动到这里
        #当子数大于10会出问题!#################################
        '''
        if 节点位置 in self.Tree节点分析信息:return self.Tree节点分析信息[节点位置]
        if len(T[0])==0:
            self.Tree节点分析信息[节点位置]=1
            return 1
        k=0
        for i in range(len(T[0])):
            know=self.获取叶节点个数(T[0][i],节点位置+str(i))
            k+=know
        self.Tree节点分析信息[节点位置]=k
        return k

    def 获得树剪枝阈值序列(self,T,节点位置:str):
        if len(T[0])==0:return []
        Ginimin=T[2]
        theGini=T[1]
        N=self.获取叶节点个数(T,节点位置)
        if N<=1:print('what?')
        ga=(theGini-Ginimin)/(N-1)
        k=[]
        for i in range(len(T[0])):
            know=self.获得树剪枝阈值序列(T[0][i],节点位置+str(i))
            k+=know
        k.append(ga)
        self.Tree节点分析信息ga[节点位置]=ga
        return k

    def 位置匹配(self,xlist,x0):
        '''
        判断xlist中有没有比x0高阶的存在
        '''
        for xl in xlist:
            if len(xl)>=len(x0):continue
            k=True
            for j in range(len(xl)):
                if xl[j]!=x0[j]:
                    k=False
                    break
            if k:return k
        return False

    def 剪枝(self,T0):
        '''
        给Tree剪枝

        输出:
        cut:
        '''
        galist=self.获得树剪枝阈值序列(T0,'')
        位置映射={}
        ga=numpy.zeros(len(self.Tree节点分析信息ga))
        for i,x in enumerate(self.Tree节点分析信息ga):
            ga[i]=self.Tree节点分析信息ga[x]
            位置映射[i]=x
        sortindex=numpy.argsort(ga)
        cut=[]
        gainv=[]
        for i in range(len(ga)):
            x=位置映射[sortindex[i]]
            if not self.位置匹配(cut,x) and len(x)>=self.树的最小深度:
                gainv.append(ga[sortindex[i]])
                cut.append(x)

        return cut,gainv
    
    def 叶子输出(self,T,x):
        if len(T[0])==0:
            return T[6]#输出值
        else:
            if x[T[3]]<T[5]:#x[besti]<xj
                return self.叶子输出(T[0][0],x)
            else:
                return self.叶子输出(T[0][1],x)

    def 叶子输出_批量(self,T,D):
        '''
        输出分类树的输出

        '''
        output=numpy.zeros(len(D))
        for i in range(len(D)):
            output[i]=self.叶子输出(T,D[i])
        return output

    def 计算准确率(self,T,标签,D,权重:numpy.ndarray|float=1):
        '''
        '''
        output=self.叶子输出_批量(T,D)
        print('警告,计算准确率只用int!')
        set标签=numpy.array(list(set(标签)))
        outputlabel=output.copy().astype(标签[0])
        for i,oi in enumerate(output):
            oilabelindex=numpy.argmin(abs(oi-set标签))
            outputlabel[i]=set标签[oilabelindex]
        正确率=float(sum((outputlabel==标签)*权重))/sum(权重)
        return 正确率,output

    def copytree(self,T,cut=None):
        if len(T[0])==0:
            new=[[],]
            for j in range(len(T)):
                if j!=0:new.append(T[j])
            return new
        else:
            new=[[],]
            if type(cut)==type(None):
                for i in range(len(T[0])):
                    new[0].append(self.copytree(T[0][i]))
            elif len(cut)!=0:
                for i in range(len(T[0])):
                    if str(i)==cut[0]:
                        new[0].append(self.copytree(T[0][i],cut=cut[1:]))
                    else:
                        new[0].append(self.copytree(T[0][i]))
            for j in range(len(T)):
                if j!=0:new.append(T[j])
            return new
            
    def 训练(self,setA,标签,D,权重,验标签,验D,验权重,树的最大深度=5,最小样本数=10,最小不纯度=0.1,损失函数='平方',输出='平均'):
        '''
        完整流程
        '''
        Tree=self.分类回归树(setA,标签,D,权重,
                        树的最大深度=树的最大深度,最小样本数=最小样本数,最小不纯度=最小不纯度,损失函数=损失函数,输出=输出)
        cut,gainv=self.剪枝(Tree)
        Treelist=[Tree]
        a0,output0=self.计算准确率(Treelist[-1],验标签,验D,权重=验权重)
        outputall=[output0]
        alist=[a0]
        for i,dcut in enumerate(cut):
            Treelist.append(self.copytree(Treelist[-1],cut=dcut))
            ai,output=self.计算准确率(Treelist[-1],验标签,验D,权重=验权重)#此处用验证集
            alist.append(ai)
            outputall.append(output)
            if alist[-1]>alist[-2]:break
        bestindex=numpy.argmin(alist)
        self.Tree=Treelist[bestindex]
        return Treelist[bestindex],alist[bestindex],outputall[bestindex]
if __name__=='__main__':
    pass
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值