1.算法概述
决策树,就像它的名字一样,利用决策的方法形成的一棵树用来处理数据,它的工作原理与“二十个问题”的小游戏类似,都是通过层层分级缩小范围最后给出答案。
用判断邮件类别举个例子:判断一封邮件类别时,首先看它的发送邮件域名地址,若是’‘myEmployer.com’'则判断为无聊时阅读的邮件;否则进行下一层判断,根据是否包含"曲棍球"这个词来判断是需要及时处理的朋友邮件还是垃圾邮件
以上图像中,长方形的代表判断模块,椭圆形代表终止模块,左右箭头称作分支,这样就是一个决策树的基本框架
2.算法原理
信息增益
再深入研究该算法前,我们首先要对数据进行划分,这里采用ID3的算法来划分数据。同时,用信息增益来判断划分数据前后的变化好坏,使用信息增益最好的特征来对数据进行第一级的分层, 而信息增益的度量方式称作熵
有多个待分类的信息 x i x_i xi定义为 l ( x i ) = − log 2 p ( x i ) l(x_i)=-\log_2p(x_i) l(xi)=−log2p(xi)其中 p ( x i ) p(x_i) p(xi)是选择某分类的概率,而所有类别可能包含的信息期望值定义为 H = − ∑ i = 1 n p ( x i ) log 2 p ( x i ) H=-\sum_{i=1}^np(x_i)\log_2p(x_i) H=−i=1∑np(xi)log2p(xi)下面是计算给定数据集的熵的代码:
# 导入数学计算的库
from math import log
def calcShannonEnt(dataSet):
# 获取数据集的元素数量
numEntries = len(dataSet)
# 创建一个空字典,用于统计每个类别的出现次数
labelCounts = {}
# 遍历数据集,统计每个类别的出现次数
for featVec in dataSet:
# 获取当前特征的标签
currentLabel = featVec[-1]
# 如果当前标签没有在字典中,初始化为0
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
# 将当前标签的出现次数加1
labelCounts[currentLabel] += 1
# 计算香农熵
shannonEnt = 0.0
# 遍历字典中的每个标签
for key in labelCounts:
# 计算当前标签的概率(出现次数除以总元素数)
prob = float(labelCounts[key]) / numEntries
# 计算香农熵(取对数并转换为2为底的对数)
shannonEnt -= prob * log(prob, 2)
# 返回香农熵
return shannonEnt
我们先创建一个数据集函数createDataSet
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing', 'flippers']
return dataSet, labels
而后用calcShannonEnt
函数来测试以上数据集的熵
上述就是先对原始数据集计算熵,而后将第一个数据的标签’yes’改成’maybe’,可以看到再次计算的熵明显增大了,这也说明了数据变得更加“不稳定”了
划分数据集
决策树是种分类算法,而分类算法除了要测量信息熵,还要划分数据集和划分数据集的熵,以此判断是否正确划分了数据集,首先利用def splitDataSet
函数按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):
# 创建一个空列表,用于存储分割后的数据
retDataSet = []
# 遍历每一个数据点
for featVec in dataSet:
# 如果当前数据点的某一轴值等于给定的值
if featVec[axis] == value:
# 获取当前数据点在当前轴上的起始和结束位置,然后将该轴上的值删去
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
# 将分割后的数据添加到结果列表中
retDataSet.append(reducedFeatVec)
# 返回分割后的数据集
return retDataSet
这个函数会遍历输入的数据集,然后对于每一个数据点,如果其某一轴的值等于指定的值,就将这个轴上的值删去,并将剩余的部分加入到结果列表中。最终返回结果列表
例如,对于原始数据集,按照第0位是1的划分办法,能得到第八行的输出,第九行同理
之后我们能够用以下代码选择出最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
# 特征的数量就是数据集的长度减1,因为最后一个特征总是类别标签
numFeatures = len(dataSet[0]) - 1
# 计算数据集的香农熵
baseEntropy = calcShannonEnt(dataSet)
# 初始化最优特征和对应的信息增益
bestInfoGain = 0.0; bestFeature = -1
# 遍历每个特征,对于每个特征i:
for i in range(numFeatures):
# 根据特征i将数据集划分为若干子集
# example[i]是第i个特征对应的每个值,例如['A', 'A', 'B', 'B', 'B']
featList = [example[i] for example in dataSet]
# 生成一个只包含每个特征i的不同取值的集合
uniqueVals = set(featList)
# 计算每个子集的香农熵
newEntropy = 0.0
# 对于每个特征i的不同取值value:
for value in uniqueVals:
# 根据特征i和value将数据集划分为若干子集
subDataSet = splitDataSet(dataSet, i, value)
# 计算子集的概率
prob = len(subDataSet) / float(len(dataSet))
# 将子集的香农熵累加到新的香农熵中
newEntropy += prob * calcShannonEnt(subDataSet)
# 计算信息增益,信息增益越大,特征i的信息增益比越高
infoGain = baseEntropy - newEntropy
# 如果特征i的信息增益比当前最优的特征信息增益大
if (infoGain > bestInfoGain):
# 更新最优特征和对应的信息增益
bestInfoGain = infoGain
bestFeature = i
# 返回最优特征
return bestFeature
递归构建决策树
根据决策树的算法原理,采用递归的方法最合适,首先用以下代码计算给定列表中最多数的的类别
def majorityCnt(classList):
# 使用字典存储每个类别出现的次数
classCount = {}
# 遍历给定列表中的每个元素
for vote in classList:
# 如果当前元素未在字典中出现,则将其添加到字典中并设置为0
if vote not in classCount.keys(): classCount[vote] = 0
# 更新字典中当前元素对应的值
classCount[vote] += 1
# 对字典进行排序,并返回出现次数最多的类别
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
然后是创建决策树的代码
def createTree(dataSet, labels):
# 获取所有特征和对应的标签
classList = [example[-1] for example in dataSet]
# 检查数据集是否只有一类标签
if classList.count(classList[0]) == len(classList):
# 如果数据集只有一类标签,直接返回该类标签
return classList[0]
# 获取数据集的最小特征值
featLabels = labels[:]
# 选择具有最小特征值的数据集
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)
return myTree
createTree
函数用于创建决策树,该函数根据给定的特征值和标签列表(labels
)来划分数据集。它会递归地调用自身,直到数据集被完全划分。下面就是利用createTree
函数进行数据集的划分,首先是对no surfacing
特征划分,若为0则是no,否则再对值为1的数据进行flippers
特征的划分,若为0则是no,否则是yes
可视化决策树
首先确定一个树形图的框和箭头形式和一些注释,再基本形成一个树的框架
import matplotlib.pyplot as plt
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
def createPlot():
fig = plt.figure(1, facecolor='white')
fig.clf()
createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
plt.show()
通过以上代码能够得到下面这样的图,这样两个不同形态的树节点就绘制出来了
但是这样远远不够,我们需要知道有多少个叶节点以及树的深度,以便确定图像的x、y轴长度,因此创建了getNumLeafs
和getTreeDepth
两个函数来计算两者的值
def getNumLeafs(myTree):
# 定义函数 getNumLeafs,输入参数为二叉树
# 返回结果为二叉树的叶子节点数量
numLeafs = 0
# 初始化叶子节点数量
firstStr = list(myTree)[0]
# 获取二叉树的第一个节点
secondDict = myTree[firstStr]
# 获取二叉树的第一个节点的子节点
for key in secondDict.keys():
# 遍历第二个节点的所有键
if type(secondDict[key]).__name__ == 'dict':
# 如果第二个节点的子节点类型为字典,则递归调用 getNumLeafs 函数来计算该子节点的叶子节点数量,并将结果添加到 numLeafs 中
numLeafs += getNumLeafs(secondDict[key])
else:
# 如果第二个节点的子节点类型不是字典,则直接将该子节点添加到 numLeafs 中
numLeafs += 1
# 返回二叉树的叶子节点数量
return numLeafs
def getTreeDepth(myTree):
# 定义函数 getTreeDepth,输入参数为二叉树
# 返回结果为二叉树的深度,即树中最长路径的长度
maxDepth = 0
# 初始化树的深度
firstStr = list(myTree)[0]
# 获取二叉树的第一个节点
secondDict = myTree[firstStr]
# 获取二叉树的第一个节点的子节点
for key in secondDict.keys():
# 遍历第二个节点的所有键
if type(secondDict[key]).__name__ == 'dict':
# 如果第二个节点的子节点类型为字典,则递归调用 getTreeDepth 函数来计算该子节点的深度,并将结果添加到 maxDepth 中
maxDepth = max(maxDepth, getTreeDepth(secondDict[key]))
else:
# 如果第二个节点的子节点类型不是字典,则将该子节点的深度加1,并将其添加到 maxDepth 中
maxDepth = max(maxDepth, 1)
# 返回二叉树的深度
return maxDept
getNumLeafs
函数定义了一个递归方法来计算二叉树的叶子节点数量。它首先创建一个空的列表 numLeafs,并将其初始值设置为0。然后,它遍历二叉树的第一个节点,即列表的第一个元素。对于每个子节点,如果其类型为字典,则调用函数 getNumLeafs
来计算该字典的叶子节点数量,并将结果添加到 numLeafs 中。如果子节点的类型不是字典,则直接将其添加到 numLeafs 中。最后,返回 numLeafs 的值作为二叉树的叶子节点数量
而getTreeDepth
定义了一个递归方法来计算二叉树的深度。它首先创建一个空的列表 maxDepth,并将其初始值设置为0。然后,它遍历二叉树的所有节点,并计算从根节点到每个节点的最长路径的长度。对于每个节点,它计算该节点的子节点的最长路径长度,并将其加1,如果子节点的数量为1,则将其添加到 maxDepth 中。最后,返回 maxDepth 的值作为二叉树的深度
创造一个retrieveTree(i)
函数输出预先存储的树信息
def retrieveTree(i):
listOfTrees = [{'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 listOfTrees[I]
调用getNumLeafs
和getTreeDepth
函数能够得到该数据形成的树的叶数和深度
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, va="center", ha="center", rotation=30)
import matplotlib.pyplot as plt
def plotTree(myTree, parentPt, nodeTxt):
# 得到树的叶子数量和深度
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
# 计算父节点的坐标
cntrPt = (plotTree.xOff + (1.0 + float(getNumLeafs(myTree)))/2.0/plotTree.totalW, plotTree.yOff)
# 在父节点的中心位置添加文本
plotMidText(cntrPt, parentPt, nodeTxt)
# 在父节点的位置绘制子节点
plotNode(myTree[nodeTxt], cntrPt, parentPt, decisionNode)
# 递归地绘制子树
secondDict = myTree[nodeTxt]
# 计算下一个节点的y坐标
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
# 对于每个键值对,检查它们是否是字典。如果是,这是一个决策节点,递归地绘制它。否则,它是一个叶节点,显示它。
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict': #测试节点是否为,如果不是,则为叶节点
plotTree(secondDict[key], cntrPt, str(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()
# 创建一个子图,并设置其边框可见性为False
axprops = dict(xticks=[], yticks=[])
# 创建一个新的子图,并设置其边框可见性为False
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
#createPlot.ax1 = plt.subplot(111, frameon=False)
# 得到树的叶子数量和深度
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
# 归一化创建并显示一个树形图
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
最后得到如下所示的树形图
测试和存储分类器
使用决策树分类
# 定义函数,接受特征树、标签向量和测试向量作为输入
def classify(inputTree, featLabels, testVec):
# 获取输入特征树的第一个元素(键)
firstStr = list(inputTree)[0]
# 获取输入特征树的第一个元素对应的字典(键-值对)
secondDict = inputTree[firstStr]
# 获取标签向量中输入特征树第一个元素对应的索引(键)
featIndex = featLabels.index(firstStr)
# 获取测试向量中输入特征树第一个元素对应的值(键)
key = testVec[featIndex]
# 如果第二个字典(键-值对)是一个字典,则对其进行分类
if isinstance(secondDict, dict):
# 获取第二个字典中输入特征树第一个元素对应的值(键)
valueOfFeat = secondDict[key]
# 如果第二个字典中输入特征树第一个元素对应的值(键)也是一个字典,则继续对其进行分类
if isinstance(valueOfFeat, dict):
# 获取第二个字典中输入特征树第一个元素对应的值(键)中的测试向量中输入特征树第一个元素对应的值(键)
classLabel = classify(valueOfFeat, featLabels, testVec)
else:
classLabel = valueOfFeat
# 否则,直接返回输入特征树第一个元素对应的值(键)
else: classLabel = secondDict[key]
# 返回分类结果(标签向量中输入特征树第一个元素对应的值)
return classLabel
函数classify
用于对输入的特征树和标签向量进行分类,从下图可以看到在flippers判断节点中进行递归调用,从而形成一棵树
每次使用分类器时需要重新构造决策树,我们可以通过存储决策树减少构造决策树的时间,以下是存储和加载分类器模型的两个函数
# 定义函数,用于存储分类器模型
def storeTree(inputTree, filename):
# 导入pickle模块
import pickle
# 使用'wb'模式打开文件,以写入二进制数据
fw = open(filename, 'wb')
# 使用pickle.dump函数将输入特征树保存到文件中
pickle.dump(inputTree, fw)
# 关闭文件
fw.close()
# 定义函数,用于加载分类器模型
def grabTree(filename):
# 导入pickle模块
import pickle
# 使用'rb'模式打开文件,以读取二进制数据
fr = open(filename, 'rb')
# 使用pickle.load函数从文件中读取输入特征树
return pickle.load(fr)
通过上述代码可以将分类器存储到硬盘上,不用每次重新学习并构造一遍
3.算法示例
本次示例使用决策树来对患者需要佩戴的隐形眼镜进行预测
基本流程
- 收集数据:提供含有数据的文件,如txt、xlsx、csv等
- 准备数据:使用Python解析数据文件
- 分析数据:快速检查数据,确保正确解析数据内容,使用
createPlot
函数绘制最终的树形图 - 训练算法:利用
createTree
函数 - 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例
- 使用算法:存储树的结构,以便下次再使用
具体使用
能够看到以上先给出了不同的特征值,再使用createTree
函数创建决策树,并用createPlot
函数进行可视化
4.总结
本章学习了决策树的相关知识,决策树算法(Decision Tree Algorithm)是一种常用的分类和回归方法。它的基本原理是通过递归地划分数据集,从而构建一个类似于树状结构的模型。从算法的前期准备工作到实际操作都有了更详尽的了解。在对数据集进行划分时,主要是依靠熵来寻找最优方案划分数据集,直到数据集中的所有数据属于同一分类
同时由于文字并不能很好的直观体现出决策树的情况,因此使用matplotlib来对产生的结果进行可视化操作,在过程中也要注意叶子节点和树深度对于决策树影响,比如停止条件可以是达到预设的深度、节点中的样本数达到某个阈值等,调整好合适的参数才能使树的形状更好看
为了防止过拟合,对决策树进行剪枝是很有必要的,剪枝的主要目标是降低树的复杂度,提高泛化能力,决策树算法既可以用于分类问题,也可以用于回归问题。对于分类问题,树的叶子节点包含一个类标签;对于回归问题,叶子节点包含一个连续数值
决策树算法的优点是易于理解和解释,对于非线性和高维数据集具有较好的泛化能力。然而,它也存在一些缺点,如易受噪声影响、过拟合风险较高等