决策树CART--原理与实现

决策树原理

首先我们有一个用于训练的群组,前n列是属性,最后一列是标签;我们对决策树的划分是基于群组的混乱程度来划分的,也即是每次寻找一个属性,对该属性分为两组,使得他们的群组混乱程度降低。本文通过以下几个方面来讲解:

  • 定义树节点
  • 划分群组
  • 混乱程度的表示方式
  • 构造树
  • 决策树的显示
  • 对测试样本分类
  • 剪枝
  • 处理缺失数据

1 定义树节点

我们来构造树的表达形式,新建一个类,代表数的每一个节点:

class decisionnode:
    def __init__(self,col=-1,value=None,results=None,tb=None,fb=None):
        self.col = col#待检验的判断条件对应的列索引
        self.value=value#使得结果为True,当前列所匹配的值
        self.results = results#除叶子节点外,其他都为None;叶子节点处代表了分类情况
        self.tb = tb#当前节点左右子树的节点
        self.fb =fb
    

2 划分群组

我们需要通过表中某一列的数据将列表拆分成两个数据集,划分群组,使得群组的混乱程度降低

def divideset(self,rows,col,value):
        #先定义一个划分的函数,根据value值的类型来定义成不同的形式
        split = None
        if isinstance(value,int) or isinstance(value,float):
            split = lambda x:x[col]>=value
        else:
            split = lambda x:x[col]==value
        
        set1 = [row for row in rows if split(row)]
        set2 = [row for row in rows if not split(row)]
        return (set1,set2)

3 混乱程度的表达方式

上面说要通过表中某一列的数据来划分,这里我们的目的是找到最合适的一列,使得生成的两个数据集合在混乱程度上能够尽可能小。这里介绍混乱程度的表示。

我们先通过一个函数来把群组的每一项结果进行计数

 def uniquecount(self,rows):
        results = {}
        for row in rows:
            result = row[len(row)-1]
            results.setdefault(result,0)
            results[result] += 1
        return results
    

下面通过三种方法来表示混乱程度,其中:基尼不纯度和熵用来表示分类数据,方差更适合来表示数值数据

3.1 基尼不纯度

是指集合中的某种结果随机应用到某一个数据项的预期误差率。sum = ΣiΣj(pi * pj)(i≠j),值越高,拆分越不理想,值为0,最为理想。

def geiniimpurity(self,rows):
        n = len(rows)
        count = self.uniquecount(rows)
        sum = 0
        for ci in count:
            pi = float(count[ci])/n
            for cj in count:
                if ci == cj : continue
                pj = float(count[cj])/n
                sum += pi*pj
        return sum

3.2 熵

遍历所有可能结果之后得到的 Σ-p(x)*logp(x)

 def entropy(self,rows):
        from math import log
        log2 = lambda x:log(x)/log(2)
        count = self.uniquecount(rows)
        sum = 0
        for c in count :
            p = float(count[c])/len(rows)
            sum -= p * log2(p)
        return sum

3.3 方差

对我们面对以数字为输出结果的数据集时,我们可以使用方差来作为评价函数,它计算一个数据集的统计方差,偏低的方差表示数字彼此接近,偏高的方差表示数字分散,

def variance(self,rows):
        import math
        results = [row[len(row)-1] for row in rows]
        mean = float(sum(results))/len(rows)
        variance = float(sum([math.pow(result-mean,2) for result in results]))/len(rows)
        return variance

4 构造树

  • 为了弄明白一个属性的好坏程度,我们首先求出整个群组的熵;
  • 然后尝试利用每个属性的各种可能取值去进行划分群组,并求出两个群组的熵;
  • 为了确定哪个属性最合适,我们计算相应的信息增益--当前熵与加权平均拆分的两个群组的熵的差值,选出信息增益最大的那个属性。
 def buildtree(self,rows):
        current_entropy = self.entropy(rows)#当前群组的熵
        best_gain = 0
        best_criteria = None
        best_sets = None
        
        column = len(rows[0])-1
        for col in range(column):#遍历属性值,寻找最佳属性来划分群组
            values = {}#col列可能的值
            for row in rows:
                values[row[col]] = 1
            
            for value in values:#寻找该属性值下最佳的一个值
                (set1,set2)= self.divideset(rows,col,value)
                p = float(len(set1))/len(rows)
                gain = current_entropy - p*self.entropy(set1)-(1-p)*self.entropy(set2)#信息增益
                if gain>best_gain and len(set1)>0 and len(set2)>0:
                    best_gain = gain
                    best_criteria = (col,value)
                    best_sets = (set1,set2)
        if best_gain>0:
            trueBranch = self.bulidtree(best_sets[0])#递归,得到左右子树
            falseBranch = self.buildtree(best_sets[1])
            return decisionnode(col=best_criteria[0],value=best_criteria[1],tb=trueBranch,fb=falseBranch)#返回当前节点
        else:
            return decisionnode(results=self.uniquecount(rows))#信息增益小于等于零,返回节点,results!=None,表示当前分支结果。
        

 5 决策树的显示

树已经构造完成之后,需要能够将这棵树显示出来,方法有两种:1种是直接print 显示。

 def printtree(self,tree,indent=''):
        if tree.results!=None:
            return tree.results
        else:
            print str(tree.col)+':'+str(tree.value)+'?'
            print indent+'T-->'
            self.printtree(tree.tb,indent+'  ')
            print indent+'F-->'
            self.printtree(tree.fb,indent+'  ')
            

 

6 对测试样本分类

现在我们构造了一棵树,我们可以通过这棵树对观测数据进行分类。

 def classify(self,tree,observation):
        if tree.results!= None:
            return tree.results
        else:
            v = observation[tree.col]
            branch = None
            if isinstance(v,int) or isinstance(v,float):
                if v>= tree.value: branch = tree.tb
                else: branch = tree.fb
            else:
                if v==tree.value : branch = tree.tb
                else: branch = tree.fb
            return self.classify(branch,observation)

 

7 剪枝

利用上述方法来训练得到的决策树,往往存在一个问题:决策树可能会出现过度拟合。也就是说,它可能过于针对训练数据,专门针对训练集所创造出的分支,其熵值比真实情况

可能会有所降低。

我们采取得策略是先构造好一棵树,然后尝试着去消除多余的节点,这个过程即是剪枝。

具体过程是对具有相同父节点的一组节点进行检查,判断如果将其合并,熵的增加量是否会小于某一个指定的阈值。

 def prune(self,tree,mingain):
        if tree.tb.results!= None:
            self.prune(tree.tb,mingain)
        if tree.fb.results!= None:
            self.prune(tree.fb,mingain)
        
        if tree.tb.results==None and tree.fb.results ==None:
            tb,fb =[],[]
            for v,c in tree.tb.results.items():
                tb += [[v]]*c
            for v,c in tree.fb.results.items():
                fb += [[v]]*c
                
            delta = self.entropy(tb+fb)-(self.entropy(tb)+self.entropy(fb)/2)
            if delta < mingain:
                tree.tb,tree.fb = None,None
                tree.results = self.uniquecount(tb+fb)
            
        

 

8 处理缺失数据

决策树还有一个优点,就是处理缺失数据的能力。如果我们缺失了某些数据,而这些数据是确定分支走向所必需的,那么实际上我们选择两个方向都走。

但,我们不是平均的统计各分支对应的结果值,而是对其进行加权统计。

def mdclassify(self,tree,observation):
        if tree.results!= None:
            return tree.results
        else:
            v = observation[tree.col]
            branch = None
            if v == None:
                tb,fb = self.mdclassify(tree.tb,observation),self.mdclassify(tree.fb,observation)
                tcount = sum(tb.values())
                fcount = sum(fb.values())
                tw = float(tcount)/(tcount+fcount)
                fw = float(fcount)/(tcount+fcount)
                results = {}
                for result,value in tb.items():
                    results[result] = value * tw
                for result ,value in fb.items():
                    results.setdefault(result,0)
                    results[result] += value* fw
                return results
            else:
                if isinstance(v,int) or isinstance(v,float):
                    if v>= tree.value: branch = tree.tb
                    else: branch = tree.fb
                else:
                    if v == tree.value: branch = tree.tb
                    else : branch = tree.fb
                return mdclassify(branch,observation)
        

 

posted @ 2015-09-20 22:02  走那条小路  阅读(677)  评论(0编辑  收藏  举报