我的机器学习之旅(七):决策树(下)
ID3、C4.5生成决策树的算法,当训练数据量很大的时候,建立的决策树模型往往非常复杂,树的深度很大。此时虽然对训练数据拟合得很好,但是其泛化能力即预测新数据的能力并不一定很好,也就是出现了过拟合现象。这个时候我们就需要对决策树进行剪枝处理以简化模型。另外,CART算法也可用于建立回归树。
CART算法
CART,即分类与回归树(classification and regression tree),也是一种应用很广泛的决策树学习方法。但是CART算法比较强大,既可用作分类树,也可以用作回归树。作为分类树时,其本质与ID3、C4.5并有多大区别,只是选择特征的依据不同而已。当CART用作回归树时,以最小平方误差作为划分样本的依据。
1.分类树
(1)基尼指数
1、是一种不等性度量;
2、通常用来度量收入不平衡,可以用来度量任何不均匀分布;
3、是介于0~1之间的数,0-完全相等,1-完全不相等;
4、总体内包含的类别越杂乱,GINI指数就越大(跟熵的概念很相似)
分类树采用基尼指数选择最优特征。假设有K个类,样本点属于第k类的概率为pk,则概率分布的基尼指数定义为:
\(Gini(p)=\sum_{k=1}^K p_k(1-p_k)=1-\sum_{k=1}^K p_k^2\)
对于给定的样本集合D,其基尼指数为:
\(Gini(D)=1-\sum_{k=1}^K\left(\frac{|C_k|}{|D|}\right)^2\)
这里,Ck是D中属于第k类的样本子集,K是类的个数。
基尼指数Gini(D)表示集合D的不确定性,基尼指数Gini(D,A)表示经过A=a分割后集合D的不确定性。基尼指数越大,样本的不确定性也就越大。
def calcGini(dataSet): ''' 计算基尼指数 :param dataSet:数据集 :return: 计算结果 ''' numEntries = len(dataSet) labelCounts = {} for featVec in dataSet: # 遍历每个实例,统计标签的频数 currentLabel = featVec[-1] if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 Gini = 1.0 for key in labelCounts: prob = float(labelCounts[key]) / numEntries Gini -= prob * prob # 以2为底的对数 return Gini
那么在给定特征A的条件下,集合D的基尼指数定义为:
\(Gini(D,A)=\frac{|D_1|}{D}Gini(D_1)+\frac{|D_2|}{|D|}Gini(D_2)\)
基尼指数Gini(D)表示集合D的不确定性,基尼指数Gini(D,A)表示经A=a分割后集合D的不确定性。基尼指数值越大,样本集合的不确定性也就越大,这一点与熵相似。
def calcGiniWithFeat(dataSet, feature, value): ''' 计算给定特征下的基尼指数 :param dataSet:数据集 :param feature:特征维度 :param value:该特征变量所取的值 :return: 计算结果 ''' numEntries=len(dataSet) featList = [example[feature] for example in dataSet] # 第i维特征列表 uniqueVals = set(featList) # 转换成集合 Gini_feat=0 for value in uniqueVals: subDataSet = splitDataSet(dataSet, feature, value) prob = len(subDataSet ) / numEntries Gini_feat+= prob * calcGini(subDataSet ) # 以2为底的对数 return Gini_feat def chooseBestSplit(dataSet): numFeatures = len(dataSet[0])-1 bestGini = float('inf'); bestFeat = 0; bestValue = 0; newGini = 0 for i in range(numFeatures): featList = [example[i] for example in dataSet] uniqueVals = set(featList) for splitVal in uniqueVals: newGini = calcGiniWithFeat(dataSet, i, splitVal) if newGini < bestGini: bestFeat = i bestGini = newGini return bestFeat def createTree_CART(dataSet,labels): ''' 创建决策树 :param: dataSet:训练数据集 :return: labels:所有的类标签 ''' classList = [example[-1] for example in dataSet] if classList.count(classList[0]) == len(classList): return classList[0] # 第一个递归结束条件:所有的类标签完全相同 if len(dataSet[0]) == 1: return majorityCnt(classList) # 第二个递归结束条件:用完了所有特征 bestFeat = chooseBestSplit(dataSet) # 最优划分特征 bestFeatLabel = labels[bestFeat] myTree = {bestFeatLabel:{}} # 使用字典类型储存树的信息 label_tmp=labels[:bestFeat]+labels[bestFeat+1:] featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: subLabels = label_tmp[:] # 复制所有类标签,保证每次递归调用时不改变原始列表的内容 myTree[bestFeatLabel][value] = createTree_CART(splitDataSet(dataSet, bestFeat, value),subLabels) return myTree
剪枝
在决策树学习中将已生成的树进行简化的过程称为剪枝。决策树的剪枝往往通过极小化决策树的损失函数或代价函数来实现。实际上剪枝的过程就是一个动态规划的过程:从叶结点开始,自底向上地对内部结点计算预测误差以及剪枝后的预测误差,如果两者的预测误差是相等或者剪枝后预测误差更小,当然是剪掉的好。但是如果剪枝后的预测误差更大,那就不要剪了。剪枝后,原内部结点会变成新的叶结点,其决策类别由多数表决法决定。不断重复这个过程往上剪枝,直到预测误差最小为止。
import operator def isTree(obj): return (type(obj).__name__=='dict') def testMajor(major,testData): errorCount = 0.0 for i in range(len(testData)): if major != testData[i][-1]: errorCount += 1 return float(errorCount) def calcTestErr(myTree,testData,labels): errorCount = 0.0 for i in range(len(testData)): if classify(myTree,labels,testData[i]) != testData[i][-1]: errorCount += 1 return float(errorCount) def pruningTree(inputTree,dataSet,testData,labels): import copy firstStr = list(inputTree.keys())[0] secondDict = inputTree[firstStr] # 获取子树 classList = [example[-1] for example in dataSet] featKey = copy.deepcopy(firstStr) labelIndex = labels.index(featKey) subLabels = copy.deepcopy(labels) label_tmp=labels[:labelIndex]+labels[labelIndex+1:] for key in list(secondDict.keys()): if isTree(secondDict[key]): # 深度优先搜索,递归剪枝 subDataSet = splitDataSet(dataSet,labelIndex,key) subTestSet = splitDataSet(testData,labelIndex,key) if len(subDataSet) > 0 and len(subTestSet) > 0: inputTree[firstStr][key] = pruningTree(secondDict[key],subDataSet,subTestSet,copy.deepcopy(label_tmp)) if calcTestErr(inputTree,testData,subLabels) < testMajor(majorityCnt(classList),testData): # 剪枝后的误差反而变大,不作处理,直接返回 return inputTree else: # 剪枝,原父结点变成子结点,其类别由多数表决法决定 return majorityCnt(classList)
回归树
回归树的生成实际上也是贪心算法。与分类树不同的是回归树处理的数据连续分布的。
算法步骤:
from numpy import * def regLeaf(dataSet): #建立叶节点函数,value为所有y的均值 return mean(dataSet[:,-1]) def regErr(dataset): return var(dataset[:, -1]) * shape(dataset)[0]#y的方差×y的数量=平方误差 def binSplitDataset(dataSet, feature, value):#以这一列的每个值为界限,大于它和小于它的值,返回的是以这个特征值为界限分割的数据集 mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0], :]#返回索引,切割数据集 mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :] return mat0, mat1 def chooseBestsplit(dataset, leafType=regLeaf, errtype = regErr,ops=(1, 4)):#找到最好的分割叶子节点 tolS = ops[0]##允许的误差下降值 tolN = ops[1] #切分的最小样本数 #判断是否可以分开二叉树 # print(len(set(dataset[:, -1].T.tolist()[0])))#不是一下子分开,然后就是先分割整个数据集,然后分割左边,然后右边 if len(set(dataset[:, -1].T.tolist())) == 1: # #如果剩余特征值的数量等于1,不需要再切分直接返回,(退出条件1) return None, leafType(dataset) m, n = shape(dataset)#行列数 S = errtype(dataset)#计算平方差 bestS = inf bestIndex = 0 bestValue = 0 for featIndex in range(n - 1):#特征索引 for splitVal in set((dataset[:, featIndex].T.tolist())): #每一列的每个值 mat0, mat1 = binSplitDataset(dataset, featIndex, splitVal)#整个数据集,第几列,那一列的每个值 if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): continue#样本数最小限制 # print(errtype(mat0)) newS = errtype(mat0) + errtype(mat1)#计算平方误差 if newS < bestS: bestIndex = featIndex bestValue = splitVal bestS = newS if (S - bestS) < tolS:#如果切分后误差效果下降不大,则取消切分,直接创建叶结点 return None, leafType(dataset) mat0, mat1 = binSplitDataset(dataset, bestIndex, bestValue) # 按照保存的最佳分割来划分集合 # #判断切分后子集大小,小于最小允许样本数停止切分3 if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): return None, leafType(dataset) # 返回最佳二元切割的bestIndex和bestValue return bestIndex, bestValue#返回特征编号和用于切分的特征值 def createTree_reg(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):#assume dataSet is NumPy Mat so we can array filtering feat, val = chooseBestsplit(dataSet, leafType, errType, ops) #采用最佳分割,将数据集分成两个部分 if feat == None: return val #递归结束条件 retTree = {} #建立返回的字典 retTree['spInd'] = feat retTree['spVal'] = val lSet, rSet = binSplitDataset(dataSet, feat, val) #得到左子树集合和右子树集合 retTree['left'] = createTree_reg(lSet, leafType, errType, ops) #递归左子树 retTree['right'] = createTree_reg(rSet, leafType, errType, ops) #递归右子树 return retTree
后剪枝
def getMean(tree): if isTree(tree['right']): tree['right'] = getMean(tree['right']) if isTree(tree['left']): tree['left'] = getMean(tree['left']) return (tree['left']+tree['right'])/2.0 #树的后剪枝, def prune(tree, testData):#待剪枝的树和剪枝所需的测试数据 if shape(testData)[0] == 0:# 确认数据集非空 return getMean(tree) #假设发生过拟合,采用测试数据对树进行剪枝 if (isTree(tree['right']) or isTree(tree['left'])): #左右子树非空 lSet, rSet = binSplitDataset(testData, tree['spInd'], tree['spVal'])#按照索引,和值分割数据集 if isTree(tree['left']): tree['left'] = prune(tree['left'], lSet) if isTree(tree['right']): tree['right'] = prune(tree['right'], rSet) #剪枝后判断是否还是有子树 if not isTree(tree['left']) and not isTree(tree['right']):#只要有一个空 lSet, rSet = binSplitDataset(testData, tree['spInd'], tree['spVal']) #判断是否融合 errorNoMerge = sum(power(lSet[:, -1] - tree['left'], 2)) + sum(power(rSet[:, -1] - tree['right'], 2))#未熔合的方差 treeMean = (tree['left'] + tree['right']) / 2.0#平均值 errorMerge = sum(power(testData[:, -1] - treeMean, 2))#查看融合后的方差 #如果合并后误差变小,融合,将两个叶子的均值作为节点 if errorMerge < errorNoMerge: print("merging") return treeMean else: return tree else: return tree