一、决策树
1.决策树介绍
决策树是一个预测模型,它代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分叉路径则代表某个可能的属性值,而每个叶节点则对应从根节点到该叶节点所经历的路径所表示的对象的值。
决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树
2.基本流程
决策树是一种常见的机器学习算法,用于进行分类和回归任务。它的基本流程如下:
-
数据准备:首先,需要收集和准备用于构建决策树的数据。这些数据应包括输入特征和相应的标签或输出。
-
特征选择:在构建决策树之前,需要选择最佳的特征来作为节点。常用的特征选择算法有信息增益、增益率、基尼不纯度等。
-
构建树:开始构建决策树。选择一个特征作为根节点,将数据集分成不同的子集。对每个子集递归地重复这个过程,直到达到以下条件之一:
- 所有的子集都属于同一类别(纯净节点)。
- 没有更多的特征可供选择,或者数据集已经为空。
-
节点划分:在每个非纯净节点上,通过对比不同特征值的标签分布情况,选择一个最佳的划分规则。
-
剪枝:构建完整的决策树后,为了避免过拟合,可以进行剪枝操作。剪枝可以通过预剪枝或后剪枝实现,其中预剪枝在构建树的过程中进行剪枝,而后剪枝在构建完整树之后修剪无关的节点。
-
预测:使用构建好的决策树对新的未知样本进行分类或回归预测。从根节点开始,依次根据特征值选择相应的分支,直到到达叶子节点,得到预测结果。
-
评估:对构建的决策树进行评估,常用的指标包括准确率、精确度、召回率等。可以使用交叉验证等方法进行评估。
3.划分选择
决策树学习的关键在于如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“纯度”(purity)越来越高.
常用的决策树划分选择算法有以下几种:
3.1信息增益(ID3算法)
这里的信息是从信息论的信息,信息论里有一个非常重要的概念——信息熵,其中这个“熵”(entropy)是指对复杂系统的刻画,可以理解为系统由不稳定态到稳定态所需要丢失的部分,信息熵可以理解为信息由不干净到干净所需要丢失的部分。信息熵满足公式:
Ent(D)的值越小,则D的纯度越高
Ent(D)的最小值为0,最大值为log2|y|
3.2增益率(C4.5算法)
IV(a)被称为是的“固有值”,仔细观察该式子,可以发现改值的大小取决于属性 a 的纯度,如果 a 只含有少量的取值的话,那么 a 的纯度就比较高,否则的话,a 的取值越多,a 的纯度越低, IV(a)值也就越大,因此,最后得到的信息增益率就越低。
所以信息增益率是信息增益的值除以固有值,虽然以 ID 为根节点得到的信息增益非常大,但是此时固有值也非常大。除以一个非常大的值会使得比值变小,从而得到一个较小的信息增益率。如果将信息增益率作为划分标准会得到更好的划分效果。
虽然提高了分类效果,C4.5还是有一些缺点:
- C4.5生成的是多叉树,在计算机中二叉树模型会比多叉树运算效率高
- C4.5只能用于分类任务
3.3基尼系数(CART算法)
CART从统计建模的角度考虑问题,与信息论用信息熵衡量纯度不同,统计建模需要抽样,如果两次抽样的结果是一样的,则视为“纯”,下列公式反映了从数据D中随机抽两个样例,类别不一致的概率, Pk等于Pk 则两个样例概率一致,Gini(D)(不同概率)越小,数据集越纯:
结点很多时,每一个结点有不同的权重:
在候选属性集合A中,选择那个使得划分后基尼指数最小的属性作为最有划分属性。
4.剪枝处理
“剪枝”是决策树学习算法对付“过拟合”的主要手段。
可通过“剪枝”来一定程度避免因决策分支过多,以致于把训练集自身的一些特点当做所有数据都具有的一般性质而导致的过拟合。剪枝通常分为两种,分别是预剪枝和后剪枝。
4.1预剪枝
可以先将样本划分为训练集和验证集
4.1.1预剪枝 的优缺点
优点 :
降低过拟合风险 显著减少训练时间和测试时间开销。
缺点 :
欠拟合风险:有些分支的当前划分虽然不能提升泛化性能,但在其基础上进行的后续划分却有可能显著提高性能。预剪枝基于“贪心”本质禁止这些分支展开,带来了欠拟合风险。
4.2后剪枝
4.2.1后剪枝优缺点:
优点:
后剪枝比预剪枝保留了更多的分支,欠拟合风险小,泛化性能往往优于预剪枝决策树
缺点:
训练时间开销大:后剪枝过程是在生成完全决策树之后进行的,需要自底向上对所有非叶结点逐一计算
二、决策树实现
1.决策树具体实现
代码如下:
# 导入所需要的库
import math
import numpy as np
# 创建数据
def createDataSet():
# 数据
dataSet = [
[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no'],
]
# 列名
labels = ['F1-AGE', 'F2-WORK', 'F3-HOME', 'F4-LOAN']
return dataSet, labels
# 获取当前样本里最多的标签
def getMaxLabelByDataSet(curLabelList):
classCount = {}
maxKey, maxValue = None, None
for label in curLabelList:
if label in classCount.keys():
classCount[label] += 1
if maxValuex < classCount[label]:
maxKey, maxValue = label, classCount[label]
else:
classCount[label] = 1
if maxKey is None:
maxKey, maxValue = label, 1
return maxKey
# 计算熵值
def calcEntropy(dataSet):
# 1. 获取所有样本数
exampleNum = len(dataSet)
# 2. 计算每个标签值的出现数量
labelCount = {}
for featVec in dataSet:
curLabel = featVec[-1]
if curLabel in labelCount.keys():
labelCount[curLabel] += 1
else:
labelCount[curLabel] = 1
# 3. 计算熵值(对每个类别求熵值求和)
entropy = 0
for key, value in labelCount.items():
# 概率值
p = labelCount[key] / exampleNum
# 当前标签的熵值计算并追加
curEntropy = -p * math.log(p, 2)
entropy += curEntropy
# 4. 返回
return entropy
# 选择最好的特征进行分割,返回最好特征索引
def chooseBestFeatureToSplit(dataSet):
# 1. 计算特征个数 -1 是减去最后一列标签列
featureNum = len(dataSet[0]) - 1
# 2. 计算当前(未特征划分时)熵值
curEntropy = calcEntropy(dataSet)
# 3. 找最好特征划分
bestInfoGain = 0 # 最大信息增益
bestFeatureIndex = -1 # 最好特征索引
for i in range(featureNum):
# 拿到当前列特征
featList = [example[i] for example in dataSet]
# 获取唯一值
uniqueVals = set(featList)
# 新熵值
newEntropy = 0
# 计算分支(不同特征划分)的熵值
for val in uniqueVals:
# 根据当前特征划分dataSet
subDataSet = splitDataSet(dataSet, i, val)
# 加权概率值
weight = len(subDataSet) / len(dataSet)
# 计算熵值,追加到新熵值中
newEntropy += (calcEntropy(subDataSet) * weight)
# 计算信息增益
infoGain = curEntropy - newEntropy
# 更新最大信息增益
if bestInfoGain < infoGain:
bestInfoGain = infoGain
bestFeatureIndex = i
# 4. 返回
return bestFeatureIndex
# 根据当前选中的特征和唯一值去划分数据集
def splitDataSet(dataSet, featureIndex, value):
returnDataSet = []
for featVec in dataSet:
if featVec[featureIndex] == value:
# 将featureIndex那一列删除
deleteFeatVec = featVec[:featureIndex]
deleteFeatVec.extend(featVec[featureIndex + 1:])
# 将删除后的样本追加到新的dataset中
returnDataSet.append(deleteFeatVec)
return returnDataSet
# 递归生成决策树节点
def createTreeNode(dataSet, labels, featLabels):
# 取出当前节点的样本的标签 -1 表示在最后一位
curLabelList = [example[-1] for example in dataSet]
# -------------------- 停止条件 --------------------
# 1. 判断当前节点的样本的标签是不是已经全为1个值了,如果是则直接返回其唯一类别
if len(curLabelList) == curLabelList.count(curLabelList[0]):
return curLabelList[0]
# 2. 判断当前可划分的特征数是否为1,如果为1则直接返回当前样本里最多的标签
if len(labels) == 1:
return getMaxLabelByDataSet(curLabelList)
# -------------------- 下面是正常选择特征划分的步骤 --------------------
# 1. 选择最好的特征进行划分(返回值为索引)
bestFeatIndex = chooseBestFeatureToSplit(dataSet)
# 2. 利用索引获取真实值
bestFeatLabel = labels[bestFeatIndex]
# 3. 将特征划分加入当前决策树
featLabels.append(bestFeatLabel)
# 4. 构造当前节点
myTree = {bestFeatLabel: {}}
# 5. 删除被选择的特征
del labels[bestFeatIndex]
# 6. 获取当前最佳特征的那一列
featValues = [example[bestFeatIndex] for example in dataSet]
# 7. 去重(获取唯一值)
uniqueFeaValues = set(featValues)
# 8. 对每个唯一值进行分支
for value in uniqueFeaValues:
# 递归创建树
myTree[bestFeatLabel][value] = createTreeNode(
splitDataSet(dataSet, bestFeatIndex, value), labels.copy(),
featLabels.copy())
# 9. 返回
return myTree
# 测试一下!!!
# 1. 获取数据集
dataSet,labels = createDataSet()
# 2. 构建决策树
myDecisionTree = createTreeNode(dataSet,labels,[])
# 3. 输出
print(myDecisionTree)
2.添加剪枝功能
代码如下:
import math
def createDataSet():
# 创建数据集
dataSet = [
[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no'],
]
labels = ['F1-AGE', 'F2-WORK', 'F3-HOME', 'F4-LOAN']
return dataSet, labels
def getMaxLabelByDataSet(curLabelList):
# 获取样本中出现最多的标签
classCount = {}
maxKey, maxValue = None, None
for label in curLabelList:
if label in classCount.keys():
classCount[label] += 1
if maxValue < classCount[label]:
maxKey, maxValue = label, classCount[label]
else:
classCount[label] = 1
if maxKey is None:
maxKey, maxValue = label, 1
return maxKey
def calcEntropy(dataSet):
# 计算数据集的熵
exampleNum = len(dataSet)
labelCount = {}
for featVec in dataSet:
curLabel = featVec[-1]
if curLabel in labelCount.keys():
labelCount[curLabel] += 1
else:
labelCount[curLabel] = 1
entropy = 0
for key, value in labelCount.items():
p = labelCount[key] / exampleNum
curEntropy = -p * math.log(p, 2)
entropy += curEntropy
return entropy
def chooseBestFeatureToSplit(dataSet):
# 根据信息增益选择最佳划分特征
featureNum = len(dataSet[0]) - 1
curEntropy = calcEntropy(dataSet)
bestInfoGain = 0
bestFeatureIndex = -1
for i in range(featureNum):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0
for val in uniqueVals:
subDataSet = splitDataSet(dataSet, i, val)
weight = len(subDataSet) / len(dataSet)
newEntropy += (calcEntropy(subDataSet) * weight)
infoGain = curEntropy - newEntropy
if bestInfoGain < infoGain:
bestInfoGain = infoGain
bestFeatureIndex = i
return bestFeatureIndex
def splitDataSet(dataSet, featureIndex, value):
# 划分数据集
returnDataSet = []
for featVec in dataSet:
if featVec[featureIndex] == value:
deleteFeatVec = featVec[:featureIndex]
deleteFeatVec.extend(featVec[featureIndex + 1:])
returnDataSet.append(deleteFeatVec)
return returnDataSet
def createTreeNode(dataSet, labels, prePruning=False, testSet=None):
# 创建决策树节点
curLabelList = [example[-1] for example in dataSet]
# 若所有样本的标签相同,直接返回该标签作为叶子节点
if len(curLabelList) == curLabelList.count(curLabelList[0]):
return curLabelList[0]
# 若特征用完了,返回样本中最多的标签
if len(labels) == 1:
return getMaxLabelByDataSet(curLabelList)
# 预剪枝
if prePruning:
bestLabel = getMaxLabelByDataSet(curLabelList)
correctCount = 0
for test in testSet:
if test[-1] == bestLabel:
correctCount += 1
accuracy = correctCount / len(testSet)
maxAccuracy = 0
bestFeatIndex = -1
for i in range(len(labels)):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
curAccuracy = 0
for val in uniqueVals:
subDataSet = splitDataSet(dataSet, i, val)
if len(subDataSet) == 0:
continue
subLabelList = [example[-1] for example in subDataSet]
subBestLabel = getMaxLabelByDataSet(subLabelList)
correctCount = 0
for test in testSet:
if test[-1] == subBestLabel:
correctCount += 1
subAccuracy = correctCount / len(testSet)
curAccuracy += subAccuracy * len(subDataSet) / len(dataSet)
if curAccuracy > maxAccuracy:
maxAccuracy = curAccuracy
bestFeatIndex = i
if accuracy >= maxAccuracy:
return getMaxLabelByDataSet(curLabelList)
# 选择最佳划分特征
bestFeatIndex = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeatIndex]
decisionTree = {bestFeatLabel: {}}
del(labels[bestFeatIndex])
featValues = [example[bestFeatIndex] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
# 递归构建子节点
decisionTree[bestFeatLabel][value] = createTreeNode(splitDataSet(dataSet, bestFeatIndex, value), subLabels, prePruning, testSet)
return decisionTree
dataSet, labels = createDataSet()
decisionTree = createTreeNode(dataSet, labels, prePruning=True, testSet=dataSet)
print(decisionTree)
# 1. 获取数据集
dataSet,labels = createDataSet()
# 2. 构建决策树
myDecisionTree = createTreeNode(dataSet,labels,[])
# 3. 输出
print(myDecisionTree)
三、总结
决策树是一种常用的分类与回归算法,它具有以下优点:易于理解和解释、计算复杂度较低、对数据的预处理要求不高、能够同时处理多种数据类型以及具有较强的可解释性。此外,决策树还可以通过剪枝等手段来防止过拟合。
然而,决策树也有一些缺点:容易出现过拟合问题、对于连续型数据比较难处理、类别样本数量不均衡时容易偏向于数量较多的类别、对错误或者异常值比较敏感等。此外,由于决策树在构建过程中需要进行特征选择,因此可能会选择次优特征,导致最终的决策树不是全局最优的。这时,剪枝操作便能很好的避免决策树的过拟合现象。
总体来说,决策树是一种比较灵活、易于理解和解释的机器学习算法,尤其适用于处理存在交互影响的特征的数据集。但需要注意的是,在应用决策树时需要仔细考虑其适用范围以及如何避免过拟合等问题。