决策树原理
首先我们有一个用于训练的群组,前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)