集美大学三好学生评选表
— | 是否挂科 | 获得奖学金次数 | 综测评价 | 体质健康是否达标 | 宿舍检评 | 是否符合条件 |
---|---|---|---|---|---|---|
1 | no | 4 | excellect | yes | excellent | yes |
2 | no | 1 | good | no | excellent | no |
3 | no | 0 | excellect | yes | excellent | yes |
4 | no | 1 | excellect | no | excellent | no |
5 | no | 2 | good | yes | excellent | yes |
6 | no | 1 | excellect | yes | excellent | no |
7 | no | 1 | excellect | yes | excellent | yes |
8 | yes | 0 | good | yes | excellent | no |
9 | no | 2 | good | yes | good | no |
10 | no | 2 | excellect | yes | excellent | yes |
11 | yes | 2 | excellect | yes | excellent | no |
12 | yes | 0 | good | yes | good | no |
13 | yes | 0 | excellect | yes | pass | no |
14 | no | 4 | excellect | yes | excellent | yes |
15 | no | 2 | excellect | yes | excellent | yes |
def createDataSet1(): # 创造示例数据
dataSet = [['no', '4','excllent', 'yes','excllent','yes'],
['no', '1', 'good','no','excllent','no'],
['no', '0', 'excllent','yes','excllent','yes'],
['no', '1', 'excllent','no','excllent','no'],
['no', '2', 'good','yes','excllent','yes'],
['no', '1', 'excllent','yes','excllent','no'],
['no', '1', 'excllent','yes','excllent','yes'],
['yes', '0', 'good','yes','excllent','no'],
['no', '2', 'good','yes','good','no'],
['no', '2', 'excllent','yes','excllent','yes'],
['yes', '2', 'excllent','yes','excllent','no'],
['yes', '0', 'good','yes','good','no'],
['yes', '0', 'excllent','yes','pass','no'],
['no', '4', 'excllent','yes','excllent','yes'],
['no', '2', 'excllent','yes','excllent','yes']]
labels = ['Failclass','Scholarship-num','Grade-ranking','Physically-fit','Hostel-assessment']
#是否挂科,奖学金次数,综测分评价,体质健康是否符合标准,宿舍检评
return dataSet,labels
def createTree(dataSet,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=chooseBestFeatureToSplit(dataSet)#遍历数据集,选择最优的特征去进行分割
bestFeatLabel=labels[bestFeat]#找到对应的标签
myTree={bestFeatLabel:{}} #字典嵌套字典,对应根节点下面的节点,第一次是根节点,之后嵌套节点
del(labels[bestFeat])#嵌套一个节点后要删掉,列名
featValues=[example[bestFeat] for example in dataSet]#统计里面有多少个相同的属性,就是分多少个树杈
uniqueVals=set(featValues)
for value in uniqueVals:
subLabels=labels[:]#去掉一列的标签
myTree[bestFeatLabel][value]=createTree(splitDataSet\
(dataSet,bestFeat,value),subLabels)#在当前最好节点下继续做,递归创建添加最好的节点下面,splitDataSet切分后的数据集
return myTree
def majorityCnt(classList): #返回最多类别
classCount={}
for vote in classList:
if vote not in classCount.keys():#如果说这个vote不在那赋值为0
classCount[vote]=0
classCount[vote]+=1#在就加等于一
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)#排序后的
return sortedClassCount[0][0]
def chooseBestFeatureToSplit(dataSet): #选择最优的特征
numFeatures = len(dataSet[0])-1#当前特征数量,要减去labels
baseEntropy = calcShannonEnt(dataSet) #基础的熵值,啥都没做的时候的熵值
bestInfoGain = 0#最好的信息增益
bestFeature = -1#最好的特征
for i in range(numFeatures):#遍历特征列
featList = [example[i] for example in dataSet]#得到当前列的特征
uniqueVals = set(featList)#得到唯一的个数
newEntropy = 0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob =len(subDataSet)/float(len(dataSet))#看下去掉列后占总体的比值,后面要用到剩下的占总体的概率值
newEntropy +=prob*calcShannonEnt(subDataSet) #计算累加后面新的熵值,对每一个特征进行操作
print("信息熵:%f" %newEntropy)
infoGain = baseEntropy - newEntropy #信息增益
print("信息增益:%f" %infoGain)
if (infoGain>bestInfoGain):
bestInfoGain=infoGain
bestFeature = i#选最好的特征
return bestFeature
def calcShannonEnt(dataSet): # 计算数据的熵(entropy)
numEntries=len(dataSet) # 所有的样本个数
labelCounts={}#看labels里面的类出现的次数
for featVec in dataSet:
currentLabel=featVec[-1] # 每行样本的最后一个labels
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel]=0#因为第一次都没在这个里面
labelCounts[currentLabel]+=1 #在的话就统计,分别有多少个
shannonEnt=0
for key in labelCounts:
prob=float(labelCounts[key])/numEntries # 计算labels中的一个类的熵值
shannonEnt-=prob*log(prob,2) # 累加每个类的熵值,按照那个信息熵的公式
return shannonEnt
def splitDataSet(dataSet,axis,value): #切分数据集,
retDataSet=[]
for featVec in dataSet:#遍历样本
if featVec[axis]==value:#要从传进来的value列找到labels里面是同一个值,同个类
reducedFeatVec =featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])#直接把featVec[axis]这一列就给去掉了
retDataSet.append(reducedFeatVec)#要删除正在遍历的这一列
return retDataSet
对上述模型进行输出:
if __name__ == '__main__':
dataSet, labels=createDataSet1() # 创造示列数据
myTree = createTree(dataSet, labels)
print(myTree) # 输出决策树模型结果
#获取叶节点的数目
def getNumLeafs(myTree):
# 定义叶子结点数目
numLeaf=0
# 获得myTree的第一个键值,即第一个特征,分割的标签
firstStr=list(myTree.keys())[0]
# 根据键值得到对应的值,即根据第一个特征分类的结果
secondDict=myTree[firstStr]
# 遍历得到的secondDict
for key in secondDict.keys():
# 如果secondDict[key]为一个字典,即决策树结点,type()可以判断子节点是否为字典类型
if type(secondDict[key]).__name__=='dict':
# 则递归的计算secondDict中的叶子结点数,并加到numLeafs上
numLeaf+=getNumLeafs(secondDict[key])
# 如果secondDict[key]为叶子结点
else:
# 则将叶子结点数加1
numLeaf+=1
# 返回求的叶子结点数目
return numLeaf
#获取树的层数
def getTreeDepth(myTree):
# 定义树的深度
maxDepth=0
# 获得myTree的第一个键值,即第一个特征,分割的标签
firstStr=list(myTree.keys())[0]
# 根据键值得到对应的值,即根据第一个特征分类的结果
secondDict=myTree[firstStr]
for key in secondDict.keys():
# 如果secondDict[key]为一个字典
if type(secondDict[key]).__name__=='dict':
# 则当前树的深度等于1加上secondDict的深度,只有当前点为决策树点深度才会加1
thisDepth=1+getTreeDepth(secondDict[key])
# 如果secondDict[key]为叶子结点
else:
# 则将当前树的深度设为1
thisDepth=1
# 比较当前树的深度与最大数的深度
if thisDepth>maxDepth:
maxDepth=thisDepth
# 返回树的深度
return maxDepth
#预先存储树的信息
#def retrieveTree(i):
listOfTree=[{'no surfacing':{0:'no',1:{'flippers':{0:'no',1:'yes'}}}},
{'no surfacing':{0:'no',1:{'flippers':{0:{'head':{0:'no',1:'yes'}},1:'no'}}}}]
return listOfTree[i]
# 绘制中间文本
def plotMidText(cntrPt,parentPt,txtString):
# 求中间点的横坐标
xMid=(parentPt[0]-cntrPt[0])/2.0+cntrPt[0]
# 求中间点的纵坐标
yMid=(parentPt[1]-cntrPt[1])/2.0+cntrPt[1]
# 绘制树结点
createPlot.ax1.text(xMid,yMid,txtString)
# 绘制决策树
def plotTree(myTree,parentPt,nodeTxt):
# 定义并获得决策树的叶子结点数
numLeafs=getNumLeafs(myTree)
depth=getTreeDepth(myTree)
# 得到第一个特征
firstStr=list(myTree.keys())[0]
# 计算坐标,x坐标为当前树的叶子结点数目除以整个树的叶子结点数再除以2,y为起点
cntrPt=(plotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,plotTree.yOff)
# 绘制中间结点,即决策树结点,也是当前树的根结点
plotMidText(cntrPt,parentPt,nodeTxt)
# 绘制决策树结点
plotNode(firstStr,cntrPt,parentPt,decisionNode)
# 根据firstStr找到对应的值
secondDict=myTree[firstStr]
# 因为进入了下一层,所以y的坐标要变 ,图像坐标是从左上角为原点
plotTree.yOff=plotTree.yOff-1.0/plotTree.totalD
# 遍历secondDict
for key in secondDict.keys():
# 如果secondDict[key]为一棵子决策树,即字典
if type(secondDict[key]).__name__=='dict':
# 递归的绘制决策树
plotTree(secondDict[key],cntrPt,str(key))
# 若secondDict[key]为叶子结点
else:
# 计算叶子结点的横坐标
plotTree.xOff=plotTree.xOff+1.0/plotTree.totalW
# 绘制叶子结点
plotNode(secondDict[key],(plotTree.xOff,plotTree.yOff),cntrPt,leafNode)
#特征值
plotMidText((plotTree.xOff,plotTree.yOff),cntrPt,str(key))
# 计算纵坐标
plotTree.yOff=plotTree.yOff+1.0/plotTree.totalD
#主函数 绘图
def createPlot(inTree):
# 定义一块画布
fig=plt.figure(1,facecolor='white')
# 清空画布
fig.clf()
# 定义横纵坐标轴,无内容
axprops=dict(xticks=[],yticks=[])
# 绘制图像,无边框,无坐标轴
createPlot.ax1=plt.subplot(111,frameon=False,**axprops)
# plotTree.totalW保存的是树的宽
plotTree.totalW=float(getNumLeafs(inTree))
# plotTree.totalD保存的是树的高
plotTree.totalD=float(getTreeDepth(inTree))
# 决策树起始横坐标
plotTree.xOff=-0.5/plotTree.totalW
# 决策树的起始纵坐标
plotTree.yOff=1.0
# 绘制决策树
plotTree(inTree,(0.5,1.0),'')
# 显示图像
plt.show()
通过将连续型转换成离散型,再按照上述代码进行。就是以这个集美大学三好学生评选表为例,先从小到大排序,再从第一个开始在第1,2个样本的获奖率切一刀,然后左边只有一个0.089,右边14个,这是计算获奖率小于0.089的Gain(D,获奖率<0.089),这是第一个信息增益,再在第2,3个样本来一刀,左边两个右边13个,计算Gain(D,获奖率<0.101),以此类推,求初最后Gain(D,获奖率<651),然后再选出最大的信息增益,从而选出最合适的分割点。具体代码修改如下:
def createTree(dataSet,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=chooseBestFeatureToSplit(dataSet)#遍历数据集,选择最优的特征去进行分割
bestFeatLabel=''#得到最好特征的名称
# 记录此刻是连续值还是离散值,1连续,2离散
flagSeries = 0
# 如果是连续值,记录连续值的划分点
midSeries = 0.0
# 如果是元组的话,说明此时是连续值
if isinstance(bestFeat, tuple):
# 重新修改分叉点信息
bestFeatLabel = str(labels[bestFeat[0]]) + '小于' + str(bestFeat[1]) + '?'
# 得到当前的划分点
midSeries = bestFeat[1]
# 得到下标值
bestFeat = bestFeat[0]
# 连续值标志
flagSeries = 1
else:
# 得到分叉点信息
bestFeatLabel = labels[bestFeat]
# 离散值标志
flagSeries = 0
myTree={bestFeatLabel:{}} #字典嵌套字典,对应根节点下面的节点,第一次是根节点,之后嵌套节点
featValues=[example[bestFeat] for example in dataSet]#统计里面有多少个相同的属性,就是分多少个树杈
# 连续值处理
if flagSeries:
# 将连续值划分为不大于当前划分点和大于当前划分点两部分
eltDataSet, gtDataSet = splitDataSetForSeries(dataSet, bestFeat, midSeries)
# 得到剩下的特征标签
subLabels = labels[:]
# 递归处理小于划分点的子树
subTree = createTree(eltDataSet, subLabels)
myTree[bestFeatLabel]['小于'] = subTree
# 递归处理大于当前划分点的子树
subTree = createTree(gtDataSet, subLabels)
myTree[bestFeatLabel]['大于'] = subTree
return myTree
# 离散值处理
else:
del(labels[bestFeat])#嵌套一个节点后要删掉,列名
uniqueVals=set(featValues)
for value in uniqueVals:
subLabels=labels[:]#去掉一列的标签
myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLabels)#在当前最好节点下继续做,递归创建添加最好的节点下面,splitDataSet切分后的数据集
return myTree
最优解特征就是上述连续型数据处理的过程,分成两个小于分割点的和大于分割点的之后在递归按照离散的处理就可以了。
def chooseBestFeatureToSplit(dataSet): #选择最优的特征
numFeatures = len(dataSet[0])-1#当前特征数量,要减去labels
baseEntropy = calcShannonEnt(dataSet) #基础的熵值,啥都没做的时候的熵值
flagSeries = 0# 标记当前最好的特征值是不是连续值
bestSeriesMid = 0.0# 如果是连续值的话,用来记录连续值的划分点
bestInfoGain = 0#最好的数据列
bestFeature = -1#最好的特征
for i in range(numFeatures):#遍历特征列
featList = [example[i] for example in dataSet]#得到当前列的特征
if isinstance(featList[0], str):
uniqueVals = set(featList)#得到唯一的个数
newEntropy = 0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob =len(subDataSet)/float(len(dataSet))#看下去掉列后占总体的比值,后面要用到剩下的占总体的概率值
newEntropy +=prob*calcShannonEnt(subDataSet) #计算累加后面新的熵值,对每一个特征进行操作
#print("信息熵:%f" %newEntropy)
infoGain = baseEntropy - newEntropy #信息增益
#print("信息增益:%f" %infoGain)
else:
maxInfoGain = 0.0 # 记录最大的信息增益
bestMid = -1# 最好的划分点
featList = [example[i] for example in dataSet]# 得到数据集中所有的当前特征值列表
classList = [example[-1] for example in dataSet]# 得到分类列表
dictList = dict(zip(featList, classList))
sortedFeatList = sorted(dictList.items(), key=operator.itemgetter(0)) # 将其从小到大排序,按照连续值的大小排列
numberForFeatList = len(sortedFeatList)# 计算连续值有多少个
midFeatList = [round((sortedFeatList[i][0] + sortedFeatList[i+1][0])/2.0, 3)for i in range(numberForFeatList - 1)]# 计算划分点,保留三位小数
# 计算出各个划分点信息增益
for mid in midFeatList:
eltDataSet, gtDataSet = splitDataSetForSeries(dataSet, i, mid)# 将连续值划分为不大于当前划分点和大于当前划分点两部分
newEntropy = len(eltDataSet)/len(sortedFeatList)*calcShannonEnt(eltDataSet) + len(gtDataSet)/len(sortedFeatList)*calcShannonEnt(gtDataSet)# 计算两部分的特征值熵和权重的乘积之和
infoGain = baseEntropy - newEntropy # 计算出信息增益
if infoGain > maxInfoGain:
bestMid = mid
maxInfoGain = infoGain
print('当前特征值为:' + labels[i] + ',对应的信息增益值为:' + str(infoGain))
if infoGain > bestInfoGain:
# 最好的信息增益
bestInfoGain = infoGain
# 新的最好的用来划分的特征值
bestFeature = i
flagSeries = 0
if not isinstance(dataSet[0][bestFeature], str):
flagSeries = 1
bestSeriesMid = bestMid
print('信息增益最大的特征为:' + labels[bestFeature])
if flagSeries:
return bestFeature, bestSeriesMid
else:
return bestFeature
也新添加了切分连续数据集的方法
def splitDataSetForSeries(dataSet, axis, value):
"""
按照给定的数值,将数据集分为不大于和大于两部分
:param dataSet: 要划分的数据集
:param i: 特征值所在的下标
:param value: 划分值
:return:
"""
eltDataSet = []# 用来保存不大于划分值的集合
gtDataSet = []# 用来保存大于划分值的集合
# 进行划分,保留该特征值
for feat in dataSet:
if feat[axis] <= value:
eltDataSet.append(feat)
else:
gtDataSet.append(feat)
return eltDataSet, gtDataSet
最后运行得到如下图:
在选择最有特征方法适当print,使结果更易看出。
列表索引必须是整数或切片,而不是元组,而下方又是以是否元组为判断连续值的依据,所以直接改最好特征标签,同时也不影响离散型操作。
预剪枝 , 即在生成决策树的过程中提前停止树的增长。核心思想是在树中结点进行扩展之前,先计算当前的划分是否能带来模型泛化能力的提升,如果不能,则不再继续生长子树。此时可能存在不同类别的样本同时存于结点中,按照多数投票的原则判断该结点所属类别。预剪枝对于何时停止决策树的生长有以下几种方法:
( 1 )当树到达一定深度的时候,停止树的生长。
( 2 )当到达当前结点的样本数量小于某个阈值的时候,停止树的生长。
( 3 )计算每次分裂对测试集的准确度提升,当小于某个阈值的时候 ,不再继续扩展。
集美大学三好学生评选表
— | 是否挂科 | 获得奖学金次数 | 综测评价 | 体质健康是否达标 | 宿舍检评 | 是否符合条件 |
---|---|---|---|---|---|---|
1 | no | 4 | excellect | yes | excellent | yes |
3 | no | 0 | excellect | yes | excellent | yes |
7 | no | 1 | excellect | yes | excellent | yes |
5 | no | 2 | good | yes | excellent | yes |
4 | no | 1 | excellect | no | excellent | no |
2 | no | 1 | good | no | excellent | no |
6 | no | 1 | excellect | yes | excellent | no |
8 | yes | 0 | good | yes | excellent | no |
9 | no | 2 | good | yes | good | no |
– | – | – | – | – | – | |
10 | no | 2 | excellect | yes | excellent | yes |
14 | no | 4 | excellect | yes | excellent | yes |
15 | no | 2 | excellect | yes | excellent | yes |
11 | yes | 2 | excellect | yes | excellent | no |
12 | yes | 0 | good | yes | good | no |
13 | yes | 0 | excellect | yes | pass | no |
还是以集美大学三好学生评选表为例:
将1-9作为训练集,10-15作为验证集
未剪枝之前的图是这样:
划分后(验证过程:首先划分前对验证集可以看出3yes,3no所以验证集精度为3 / 6 = 50%,对其进行划分得到no,yes两者,在训练集里yes对应的标签都为no,no对应的标签4yes,4no,所以就以yes为标签,在转移到验证集里no对应的标签都是yes,所以{10,14,15}还是三个,3 / 6 = 50%,结果还是50%没得到提升,那就不划分了):
最后得到的结果是:
然后继续对这个Scholarship-num划分:得到0对应标签是yes,1对应标签是no,2对应标签是yes,4对应标签是yes,所以到验证里,0,2,4对应标签都是yes,{10,14,15,11,12,13}六个全是,6 / 6 = 100%,精度提高了所以,那不是应该要继续分吗,但是之前又不划分,这不就矛盾了吗?
显然,根据上图可知预剪枝使得很多分支没有展开,虽然这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间。但是,有些分支虽当前不能提升泛化性。甚至可能导致泛化性暂时降低,但在其基础上进行后续划分却有可能导致显著提高,因此预剪枝的这种贪心本质,给决策树带来了欠拟合的风险。所以,接下来带来后剪枝方法
后剪枝,是在已经生成的过拟合决策树上进行剪枝,得到简化版的剪枝决策树。核心思想是让算法生成一棵完全生长的决策树,然后从最底层向上计算是否剪枝。剪枝过程将子树删除,用一个叶子结点替代,该结点的类别同样按照多数投票的原则进行判断。 同样地 ,后剪枝也可以通过在测试集上的准确率进行判断,如果剪枝过后准确率有所提升,则进行剪枝。 相比于预剪枝,后剪枝方法通常可以得到泛化能力更强的决策树,但时间开销会更大。
还是以上面的集美大学三好学生评选表为例:
未剪枝结果如下:
未剪枝前还是之前算的50%,自底向上,对Hostel-assessment进行剪枝excellent对应的标签是yes,good对应的标签是no,转到验证集{10,14,15,11}是yes,那么4 / 6 = 66.7%是划分后验证集精度,那么看Hostel-assessment在Physically-fit是yes的范围里看yes多还是no多,所以可代替为yes,那么则变为
继续对Physically-fit划分,此时验证集精度为如图:
Physically-fit在训练集里yes对应的标签为4yes,3no,则为yes,no对应的标签全为no,则为no,所以{10,14,15,11,12,13}全为yes,则6 / 6 = 100%验证集精度提升,那么就将此节点代替为no(在Scholarship-num=1下no标签大于1)所以变成如下图:
然后Hostel-assessment划分后的验证集精度是66.7%,也是没有提升,跟全部树刚开始的精度一样没提升所以,最后剪枝后的树如下图:
显然,从上图可知后剪枝决策树通常比预剪枝决策树保留了更多的分支。一般情况下,后剪枝决策树的欠拟合风险很小,泛化性能往往由于预剪枝决策树,但是后剪枝过程是在生成完全决策树后进行的,并且要自下往上地对树中的非叶子节点逐一进行考察计算,因此训练时间的开销比为剪枝和预剪枝决策树都要大得多。
# 创建预剪枝决策树
def createTreePrePruning(dataTrain, labelTrain, dataTest, labelTest, names):
trainData = np.asarray(dataTrain)
labelTrain = np.asarray(labelTrain)
testData = np.asarray(dataTest)
labelTest = np.asarray(labelTest)
names = np.asarray(names)
# 如果结果为单一结果
if len(set(labelTrain)) == 1:
return labelTrain[0]
# 如果没有待分类特征
elif trainData.size == 0:
return majorityCnt(labelTrain)
# 其他情况则选取特征
bestFeat, bestEnt = chooseBestFeatureToSplit(dataTrain, labelTrain)
# 取特征名称
bestFeatName = names[bestFeat]
# 从特征名称列表删除已取得特征名称
names = np.delete(names, [bestFeat])
# 根据最优特征进行分割
dataTrainSet, labelTrainSet = splitFeatureData(dataTrain, labelTrain, bestFeat)
# 预剪枝评估
# 划分前的分类标签
labelTrainLabelPre = majorityCnt(labelTrain)
labelTrainRatioPre = equalNums(labelTrain, labelTrainLabelPre) / labelTrain.size
# 划分后的精度计算
if dataTest is not None:
dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, bestFeat)
# 划分前的测试标签正确比例
labelTestRatioPre = equalNums(labelTest, labelTrainLabelPre) / labelTest.size
# 划分后 每个特征值的分类标签正确的数量
labelTrainEqNumPost = 0
for val in labelTrainSet.keys():
labelTrainEqNumPost += equalNums(labelTestSet.get(val), majorityCnt(labelTrainSet.get(val))) + 0.0
# 划分后 正确的比例
labelTestRatioPost = labelTrainEqNumPost / labelTest.size
# 如果没有评估数据 但划分前的精度等于最小值0.5 则继续划分
if dataTest is None and labelTrainRatioPre == 0.5:
decisionTree = {bestFeatName: {}}
for featValue in dataTrainSet.keys():
decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue), labelTrainSet.get(featValue)
, None, None, names)
elif dataTest is None:
return labelTrainLabelPre
# 如果划分后的精度相比划分前的精度下降, 则直接作为叶子节点返回
elif labelTestRatioPost < labelTestRatioPre:
return labelTrainLabelPre
else :
# 根据选取的特征名称创建树节点
decisionTree = {bestFeatName: {}}
# 对最优特征的每个特征值所分的数据子集进行计算
for featValue in dataTrainSet.keys():
decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue), labelTrainSet.get(featValue)
, dataTestSet.get(featValue), labelTestSet.get(featValue)
, names)
return decisionTree
xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest = splitXgData20(xgData, xgLabel)
# 生成不剪枝的树
xgTreeTrain = createTree(xgDataTrain, xgLabelTrain, xgName)
# 生成预剪枝的树
xgTreePrePruning = createTreePrePruning(xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest, xgName)
# 画剪枝前的树
print("剪枝前的树")
createPlot(xgTreeTrain)
# 画剪枝后的树
print("剪枝后的树")
createPlot(xgTreePrePruning)
对数据集进行分类前9个为训练集,后6个为验证集
def splitXgData20(xgData, xgLabel):
xgDataTrain = xgData[[0, 1, 2, 3, 4, 5, 6, 7, 8],:]
xgDataTest = xgData[[9, 10, 11, 12, 13, 14],:]
xgLabelTrain = xgLabel[[0, 1, 2, 3, 4, 5, 6, 7, 8]]
xgLabelTest = xgLabel[[9, 10, 11, 12, 13, 14]]
return xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest
# 后剪枝 训练完成后决策节点进行替换评估 这里可以直接对xgTreeTrain进行操作
def treePostPruning(labeledTree, dataTest, labelTest, names):
newTree = labeledTree.copy()
dataTest = np.asarray(dataTest)
labelTest = np.asarray(labelTest)
names = np.asarray(names)
# 取决策节点的名称 即特征的名称
featName = list(labeledTree.keys())[0]
print("\n当前节点:" + featName)
# 取特征的列
featCol = np.argwhere(names==featName)[0][0]
names = np.delete(names, [featCol])
print("当前节点划分的数据维度:" + str(names))
print("当前节点划分的数据:" )
print(dataTest)
print(labelTest)
# 该特征下所有值的字典
newTree[featName] = labeledTree[featName].copy()
featValueDict = newTree[featName]
featPreLabel = featValueDict.pop("_vpdl")
print("当前节点预划分标签:" + featPreLabel)
# 是否为子树的标记
subTreeFlag = 0
# 分割测试数据 如果有数据 则进行测试或递归调用 np的array我不知道怎么判断是否None, 用is None是错的
dataFlag = 1 if sum(dataTest.shape) > 0 else 0
if dataFlag == 1:
print("当前节点有划分数据!")
dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, featCol)
for featValue in featValueDict.keys():
print("当前节点属性 {0} 的子节点:{1}".format(featValue ,str(featValueDict[featValue])))
if dataFlag == 1 and type(featValueDict[featValue]) == dict:
subTreeFlag = 1
# 如果是子树则递归
newTree[featName][featValue] = treePostPruning(featValueDict[featValue], dataTestSet.get(featValue), labelTestSet.get(featValue), names)
# 如果递归后为叶子 则后续进行评估
if type(featValueDict[featValue]) != dict:
subTreeFlag = 0
# 如果没有数据 则转换子树
if dataFlag == 0 and type(featValueDict[featValue]) == dict:
subTreeFlag = 1
print("当前节点无划分数据!直接转换树:"+str(featValueDict[featValue]))
newTree[featName][featValue] = convertTree(featValueDict[featValue])
print("转换结果:" + str(convertTree(featValueDict[featValue])))
# 如果全为叶子节点, 评估需要划分前的标签,这里思考两种方法,
# 一是,不改变原来的训练函数,评估时使用训练数据对划分前的节点标签重新打标
# 二是,改进训练函数,在训练的同时为每个节点增加划分前的标签,这样可以保证评估时只使用测试数据,避免再次使用大量的训练数据
# 这里考虑第二种方法 写新的函数 createTreeWithLabel,当然也可以修改createTree来添加参数实现
if subTreeFlag == 0:
ratioPreDivision = equalNums(labelTest, featPreLabel) / labelTest.size
equalNum = 0
for val in labelTestSet.keys():
equalNum += equalNums(labelTestSet[val], featValueDict[val])
ratioAfterDivision = equalNum / labelTest.size
print("当前节点预划分标签的准确率:" + str(ratioPreDivision))
print("当前节点划分后的准确率:" + str(ratioAfterDivision))
# 如果划分后的测试数据准确率低于划分前的,则划分无效,进行剪枝,即使节点等于预划分标签
# 注意这里取的是小于,如果有需要 也可以取 小于等于
if ratioAfterDivision < ratioPreDivision:
newTree = featPreLabel
return newTree
xgTreeBeforePostPruning = {'Failclass': {"_vpdl": "是"
,'yes': 'no', 'no': {'Scholarship-num': {"_vpdl": "是"
,'0': 'yes', '1': {'Physically-fit': {"_vpdl": "是",'yes': {'Hostel-assessment': {"_vpdl": "是"
,'excllent': {'Grade-ranking': {"_vpdl": "是",'excllent': 'yes'}}}}, 'no': 'no'}}, '2': {'Hostel-assessment': {'good': 'no', 'excllent': 'yes'}}, '4': 'yes'}}}}
xgTreePostPruning = treePostPruning(xgTreeBeforePostPruning, xgDataTest, xgLabelTest, xgName)
createPlot(convertTree(xgTreeBeforePostPruning))
createPlot(xgTreeBeforePostPruning)
后剪枝评估时需要划分前的标签,这里思考两种方法:
一是,不改变原来的训练函数,评估时使用训练数据对划分前的节点标签重新打标
二是,改进训练函数,在训练的同时为每个节点增加划分前的标签,这样可以保证评估时只使用测试数据,避免再次使用大量的训练数据
结果如图:
代码参考及后剪枝划分标签参考:https://blog.csdn.net/ylhlly/articl****e/details/93213633
**上述决策树算法用的时ID3算法,接之前的【机器学习】决策树算法–1(算法介绍)中ID3算法已经以上述例子作了具体的讲述,具体算法核心过程:从根节点开始,计算所有特征里的信息熵,之后算出信息增益,并选择信息增益最大的特征作为下一个节点,继续建立这个特征的子节点,在对子节点进行递归调用上述方法,构建决策树,直到所有特征遍历完或者其信息增益均很小时停止;但是,对于编号的特征属性还是比其他特征属性要有所喜好。所以,信息增益对可取值数目较多的属性有所喜好。
还有就是离散型和连续型数据的区别,学会理解两种类型的区别以及决策树中处理方法。
对比未剪枝的决策树和经过预剪枝的决策树可以看出:预剪枝使得决策树的很多分支都没有“展开”,这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间开销。但是,另一方面,因为预剪枝是基于“贪心”的,所以,虽然当前划分不能提升泛华性能,但是基于该划分的后续划分却有可能导致性能提升,因此预剪枝决策树有可能带来欠拟合的风险。
**