我重生了,上一世因为没有好好学习,流浪街头,这一世,我要好好学习,逆天改命!
今外出试炼,突遇决策树妖,且看我给他剪枝。
目录
决策树剪枝
决策树的剪枝处理是为了防止过拟合(overfitting)而进行的一种策略。过拟合是指模型在训练数据上表现很好,但在未见过的测试数据上表现较差的情况。
剪枝处理有两种常见的方法:预剪枝(pre-pruning)和后剪枝(post-pruning)。
预剪枝的优点是可以减少决策树的复杂性,降低过拟合的风险。然而,它可能会导致欠拟合,因为有可能在提前停止分裂时丢失了一些重要的信息。
后剪枝的优点是可以更充分地利用训练数据,避免预剪枝可能导致的信息损失问题。同时,它也可以通过交叉验证选择最优的修剪版本。
一、预剪枝
预剪枝是在构建决策树的过程中,在每个节点处评估是否进行进一步的分裂。它考虑一些条件来决定是否停止分裂并将当前节点标记为叶子节点。常用的预剪枝条件包括:
- 最大深度限制:设定一个最大深度,当达到该深度时停止分裂。
- 叶子节点数量限制:设定一个最小样本数阈值,当节点中的样本数小于该阈值时停止分裂。
- 不纯度减少的阈值:计算分裂前后的不纯度减少量,如果不超过设定的阈值则停止分裂.
以选西瓜为例:
在剪枝之前
在所有判断郭好坏的条件中,脐部是首要条件,因此可以简化决策树:
使用预剪枝对决策树进行划分,需要评估每个节点分裂前后的泛化性能是否有提升。根节点已经被标记为叶子节点,并且其类别标记为训练集中样本数量最多的类别“好瓜”。对于使用脐部进行划分这个节点,需要评估其分裂前后的泛化性能。对该节点进行划分后得到的决策树可以正确分类出样本{4, 5, 8},但对其他样本进行分类时错误率较大,精度只有43.9%。可以得出结论:在使用预剪枝的情况下,不应该对该节点进行划分,保持该节点为叶子节点,其类别标记为训练集中样本数量最多的类别“好瓜”。
二、后剪枝
后剪枝是在构建完整的决策树之后,对决策树进行修剪。它采用交叉验证的方式评估每个内部节点的分裂是否有益于模型的泛化能力。常见的后剪枝算法包括:代价复杂度剪枝(Cost Complexity Pruning)和错误率预测剪枝(Error Estimation Pruning)。
- 代价复杂度剪枝:通过引入一个复杂度惩罚项来衡量修剪后模型的复杂性,从而选择最优的修剪版本。这个惩罚项可以由叶子节点的数量、树的深度等来表示。
- 错误率预测剪枝:基于子树的错误率预测来决定是否修剪该子树。如果修剪后的模型表现不比原模型差,则进行修剪,否则保留原模型。
应用后剪枝算法对决策树进行剪枝。首先考察节点(6),如果删除以此为根节点的子树,则替换后的叶子节点包括编号为{7,15}的训练样本。因此,将此叶子节点标记为“好瓜”。接着,在验证集上评估剪枝前后的泛化性能。在剪枝前的决策树中,验证集上的精度为42.9%。而在剪枝后的决策树中,验证集上的精度为57.1%。因此,后剪枝算法选择对该节点进行剪枝,剪枝后的决策树中节点(6)被替换为叶子节点,其类别为“好瓜”。以此进行剪枝,剪枝后的决策树如下图所示:
接下来,考察节点(5)。同样的操作,将以节点(5)为根节点的子树替换为叶子节点。替换后的叶子节点包含编号为{6, 7, 15}的训练样本。根据“多数原则”,将该叶子节点标记为“好瓜”。在验证集上,剪枝前后的决策树精度仍为57.1%。因此,对节点(5)不进行剪枝。
接着,考察节点(2)。按照相同的操作,将以节点(2)为根节点的子树替换为叶子节点。替换后的叶子节点包含编号为{1, 2, 3, 14}的训练样本。根据验证集上的精度,剪枝前的决策树为71.4%。因此,后剪枝算法选择对节点(2)进行剪枝。剪枝后的决策树如下所示。
创建决策树
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)
# 选择最优划分特征
bestFeatIndex = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeatIndex]
# 初始化决策树
myTree = {bestFeatLabel: {}}
del labels[bestFeatIndex] # 删除已经使用的特征名称
# 获取最优特征列中所有不同的取值
featValues = [example[bestFeatIndex] for example in dataSet]
uniqueValues = set(featValues)
# 对每个不同的取值递归构建子树
for value in uniqueValues:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(
splitDataSet(dataSet, bestFeatIndex, value), subLabels)
return myTree
预剪枝
import numpy as np
def createTreePrePruning(dataTrain, labelTrain, dataTest, labelTest, names, method='id3'):
"""
基于预剪枝策略生成决策树。
预剪枝:在决策树生成过程中,对每次划分进行评估,如果发现划分后性能下降或不变,则提前终止划分。
Args:
dataTrain: 二维列表,训练数据集。
labelTrain: 一维列表,训练数据集的分类标签。
dataTest: 二维列表,测试数据集。
labelTest: 一维列表,测试数据集的分类标签。
names: 一维列表,特征名称。
method: 字符串,特征选择方法。可选'id3'和'c4.5',默认使用'id3'。
Returns:
decisionTree: 字典类型,生成的决策树。
"""
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 voteLabel(labelTrain)
# 其他情况则选取最优特征
bestFeat, bestEnt = bestFeature(dataTrain, labelTrain, method=method)
# 取最优特征名称
bestFeatName = names[bestFeat]
# 从特征名称列表删除已选取的特征名称
names = np.delete(names, [bestFeat])
# 根据最优特征进行分割
dataTrainSet, labelTrainSet = splitFeatureData(dataTrain, labelTrain, bestFeat)
# 预剪枝评估
# 划分前的分类标签
labelTrainLabelPre = voteLabel(labelTrain)
labelTrainRatioPre = equalNums(labelTrain, labelTrainLabelPre) / labelTrain.size
# 如果没有测试数据,则继续划分
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, method)
elif dataTest is None:
return labelTrainLabelPre
# 如果有测试数据,则计算划分后的性能
else:
dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, bestFeat)
# 划分前的测试标签正确比例
labelTestRatioPre = equalNums(labelTest, labelTrainLabelPre) / labelTest.size
# 划分后每个特征值的分类标签正确的数量
labelTrainEqNumPost = 0
for val in labelTrainSet.keys():
labelTrainEqNumPost += equalNums(labelTestSet.get(val), voteLabel(labelTrainSet.get(val))) + 0.0
# 划分后的正确率
labelTestRatioPost = labelTrainEqNumPost / labelTest.size
# 如果划分后的性能下降或不变,则返回当前节点
if 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, method)
return decisionTree
# 在训练集上划分出训练数据和测试数据,生成预剪枝决策树,并可视化。
DataTrain, LabelTrain, DataTest, LabelTest = splitXgData20(Data, Label)
TreePrePruning = createTreePrePruning(DataTrain, LabelTrain, DataTest, LabelTest, Name, method='id3')
print("剪枝后的树")
createPlot(TreePrePruning)
与剪枝之后
后剪枝
import numpy as np
def createTreeWithLabel(data, labels, names, method='id3'):
data = np.asarray(data)
labels = np.asarray(labels)
names = np.asarray(names)
# 如果所有样本属于同一类别,返回该类别
if len(set(labels)) == 1:
return labels[0]
# 如果没有待分类特征或样本为空,返回多数类别
elif data.size == 0:
majorityLabel = voteLabel(labels)
return majorityLabel
# 选择最佳特征作为划分依据
bestFeat, bestEnt = bestFeature(data, labels, method=method)
bestFeatName = names[bestFeat]
names = np.delete(names, [bestFeat])
# 创建决策树节点
decisionTree = {bestFeatName: {}}
# 根据最佳特征进行划分
dataSet, labelSet = splitFeatureData(data, labels, bestFeat)
# 对每个特征值创建子树
for featValue in dataSet.keys():
decisionTree[bestFeatName][featValue] = createTreeWithLabel(dataSet.get(featValue), labelSet.get(featValue), names, method)
return decisionTree
# 将带预划分标签的树转化为常规的树
def convertTree(labeledTree):
labeledTreeNew = labeledTree.copy()
nodeName = list(labeledTree.keys())[0]
labeledTreeNew[nodeName] = labeledTree[nodeName].copy()
for val in list(labeledTree[nodeName].keys()):
if val == "_vpdl":
labeledTreeNew[nodeName].pop(val)
elif type(labeledTree[nodeName][val]) == dict:
labeledTreeNew[nodeName][val] = convertTree(labeledTree[nodeName][val])
return labeledTreeNew
# 后剪枝
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]
featCol = np.argwhere(names == featName)[0][0]
names = np.delete(names, [featCol])
newTree[featName] = labeledTree[featName].copy()
featValueDict = newTree[featName]
featPreLabel = featValueDict.pop("_vpdl")
subTreeFlag = 0
if sum(dataTest.shape) > 0:
dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, featCol)
dataFlag = 1
else:
dataFlag = 0
for featValue in featValueDict.keys():
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
newTree[featName][featValue] = convertTree(featValueDict[featValue])
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
if ratioAfterDivision < ratioPreDivision:
newTree = featPreLabel
return newTree
DataTrain, LabelTrain, DataTest, LabelTest = splitXgData20(Data, Label)
TreePrePruning = createTreeWithLabel(Data, Label, Name, method='id3')
print("剪枝后的树")
createPlot(TreePrePruning)
剪枝后
总结
-
预剪枝: 预剪枝是在构建决策树的过程中,在决策树达到最大深度或者无法继续划分时提前停止。预剪枝通过限制决策树的生长来防止过拟合现象。预剪枝在构建决策树的同时进行剪枝,因此训练时间开销相对较小。然而,预剪枝可能会导致丢失一些重要的特征或路径,从而导致决策树欠拟合。
-
后剪枝: 后剪枝是在决策树构建完成后,通过自下而上的方式剪枝决策树的节点。后剪枝通常保留更多的分支,因此有更好的泛化性能。由于后剪枝是在构建决策树之后进行剪枝,所以需要使用额外的测试数据集来评估决策树的性能,并决定哪些节点应该被剪掉。因此,后剪枝的训练时间开销相对较大。
预剪枝和后剪枝是两种常用的决策树剪枝策略。预剪枝可以在构建决策树时提前停止以防止过拟合,而后剪枝则在构建完成后通过剪枝来提高泛化性能。选择哪种剪枝策略取决于具体的数据集和问题需求。