目录
一、为什么要剪枝?
二、剪枝策略
1.预剪枝
2.后剪枝
3.两种剪枝策略对比
三.连续值处理
离散化(二分法)
“剪枝”是决策树学习算法对付“过拟合”的主要手段,可一定程度避免因决策分支过多,以致于把训练集自身的一些特点当做所有数据都具有的一般性质而导致的“过拟合”。
预剪枝就是在决策树生成过程中,通过提前停止树的构建而对树剪枝。
主要方法有:
1.当决策树达到预设的高度时就停止决策树的生长。
2.达到某个节点的实例具有相同的特征向量。
3.当达到某个节点的实例个数小于某个阈值时停止决策树的生长。
4.当信息增益,增益率和基尼指数增益小于某个阈值的时候不在生长。
优点:降低过拟合风险 –显著减少训练时间和测试时间开销。
缺点:有些分支的当前划分虽然不能提升泛化性能,但在其基础上进行的后续划分却有可能显著提高性能。预剪枝基于 “贪心”本质禁止这些分支展开,带来了欠拟合风险。
代码实现:
def createTreePrePruning(dataTrain, labelTrain, dataValid, labelValid, feat_name, method='id3'):
dataTrain = np.asarray(dataTrain)
labelTrain = np.asarray(labelTrain)
dataValid = np.asarray(dataValid)
labelValid = np.asarray(labelValid)
feat_name = np.asarray(feat_name)
# 如果结果为单一结果
if len(set(labelTrain)) == 1:
return labelTrain[0]
# 如果没有待分类特征
elif dataTrain.size == 0:
return voteLabel(labelTrain)
# 其他情况则选取特征
bestFeat, bestEnt = bestFeature(dataTrain, labelTrain, method=method)
# 取特征名称
bestFeatName = feat_name[bestFeat]
# 从特征名称列表删除已取得特征名称
feat_name = np.delete(feat_name, [bestFeat])
# 根据最优特征进行分割
dataTrainSet, labelTrainSet = splitFeatureData(dataTrain, labelTrain, bestFeat)
# 预剪枝评估
# 划分前的分类标签
labelTrainLabelPre = voteLabel(labelTrain)
labelTrainRatioPre = equalNums(labelTrain, labelTrainLabelPre) / labelTrain.size
# 划分后的精度计算
if dataValid is not None:
dataValidSet, labelValidSet = splitFeatureData(dataValid, labelValid, bestFeat)
# 划分前的验证标签正确比例
labelValidRatioPre = equalNums(labelValid, labelTrainLabelPre) / labelValid.size
# 划分后 每个特征值的分类标签正确的数量
labelTrainEqNumPost = 0
for val in labelTrainSet.keys():
labelTrainEqNumPost += equalNums(labelValidSet.get(val), voteLabel(labelTrainSet.get(val))) + 0.0
# 划分后 正确的比例
labelValidRatioPost = labelTrainEqNumPost / labelValid.size
# 如果没有评估数据 但划分前的精度等于最小值0.5 则继续划分,这一步不是很理解
if dataValid 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, feat_name, method)
elif dataValid is None:
return labelTrainLabelPre
# 如果划分后的精度相比划分前的精度下降, 则直接作为叶子节点返回
elif labelValidRatioPost < labelValidRatioPre:
return labelTrainLabelPre
else:
# 根据选取的特征名称创建树节点
decisionTree = {bestFeatName: {}}
# 对最优特征的每个特征值所分的数据子集进行计算
for featValue in dataTrainSet.keys():
decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue),
labelTrainSet.get(featValue)
, dataValidSet.get(featValue),
labelValidSet.get(featValue)
, feat_name, method)
return decisionTree
后剪枝是先从训练集生成一颗完整的决策树,然后自底向上的对决策树进行剪枝,与预剪枝最大的不同就是:决策树是否生长完整。
优点:后剪枝比预剪枝保留了更多的分支,欠拟合风险小 ,泛化性能往往优于预剪枝决策树。 缺点:需要自底向上对所有非叶结点逐一计算,训练时间开销大。
代码实现:
def treePostPruning(labeledTree, dataValid, labelValid, feats):
labelValidSet = {}
newTree = labeledTree.copy()
dataValid = np.asarray(dataValid)
labelValid = np.asarray(labelValid)
feats = np.asarray(feats)
featName = list(labeledTree.keys())[0]
featCol = np.argwhere(feats == featName)[0][0]
feats = np.delete(feats, [featCol])
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(dataValid.shape) > 0 else 0
if dataFlag == 1:
# print("当前节点有划分数据!")
dataValidSet, labelValidSet = splitFeatureData(dataValid, labelValid, 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], dataValidSet.get(featValue),
labelValidSet.get(featValue), feats)
# 如果递归后为叶子 则后续进行评估
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(labelValid, featPreLabel) / labelValid.size
equalNum = 0
for val in labelValidSet.keys():
if val in featValueDict:
equalNum += equalNums(labelValidSet[val], featValueDict[val])
else:
equalNum += len(labelValidSet[val])/5 # 一共五类,随便选一类
ratioAfterDivision = equalNum / labelValid.size
# 如果划分后的测试数据准确率低于划分前的,则划分无效,进行剪枝,即使节点等于预划分标签
# 注意这里取的是小于,如果有需要 也可以取 小于等于
if ratioAfterDivision < ratioPreDivision:
newTree = featPreLabel
return newTree
连续属性取值数目非有限,无法直接进行划分。
第一步:将连续属性 a 在样本集 D 上出现 n 个不同的 取值从小到大排列,记为 a 1 , a 2 , ..., a n 。基于划分点t, 可将D分为子集Dt +和Dt-,其中Dt-包含那些在属性a上取 值不大于t的样本,Dt +包含那些在属性 a上取值大于t的 样本。考虑包含n-1个元素的候选划分点集合
即把区间 [a i , a i-1) 的中位点 (a i+a i-1)/2作为候选划分
第二步:采用离散属性值方法,计算这些划分点的增 益,选取最优的划分点进行样本集合的划分:
其中Gain(D, a, t)是样本集D基于划分点 t 二分后的信息 增益,于是, 就可选择使Gain(D, a, t)最大化的划分点。
参考文献:(12条消息) 决策树的生成与剪枝(原理与代码)_Muasci的博客-CSDN博客_决策树剪枝代码