一、介绍
决策树算法可以读取数据集合,构建决策树。决策树很多任务都是为了数据中所蕴涵的信息,因此决策树可以使一个不熟悉的数据集合,并从中提取出一系列规则。机器学习算法将使用这些规则。
例如这个例子,邮件分类的决策树。长方形表示判断,椭圆形代表终止。首先判断邮件的地址,如果是,则化为无聊时需要阅读的邮件,如果否则需要继续判断邮件中单词中是否包含曲棍球,同样如果是则化为需要及时处理的朋友邮件,如果否,则判为无需阅读的垃圾邮件。这是一个简单的决策树。接下来,我们将进行决策树的构造。
二、决策树的构造
在构造决策树时,我们需要有判断条件,那么将哪个特征在划分数据分类时起决定性作用。这是我们的首要问题,找到决定性特征,划分出最好的结果,就要对每个特征进行评估。原始数据集会被划分为几个数据子集,这些数据子集会分布在第一个决策点的所有分支上,如果某个分支下的数据属于同一类型,则不需要在进行划分,如果数据不属于同一类型,则需要进行重复划分数据集的过程。
基本的思想:检测数据集中每个子项是否属于同一分类。如果属于,则返回类标签。如果不属于,继续寻找划分数据集的最好特征,划分数据集,创建分支节点,然后对每个划分的子集,继续检测是否属于同一分类,循环上述过程。最后返回分支节点。
1.信息增益
划分数据集的大原则是:将无序的数据集变得更加有序,组织杂乱无章数据的一种方法是使用信息论度量信息。信息论是量化处理信息的分支科学,我们可以在划分数据之前使用信息论量化度量信息的内容。在划分数据之前之后信息发生的变化成为信息增益。获得信息增益最高的特征就是最好的选择。那么就要考虑如何计算信息增益,集合信息的度量方式称为香农熵或者简称为熵。熵为信息的期望值。如果待分类的事物可能划分在多个分类之中,则符号的信息定义为:
其中
是选择该分类的概率。熵:
from math import log
def calcShannonEnt(dataSet):
"""
计算香浓熵
1.统计每一个类别的个数
2.计算每一个类别的概率,每一个类别的个数除总个数
3.利用香浓熵公式求出香浓熵
"""
numTotal = len(dataSet) # 数据集的总数
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 # 累加相同类别的个数
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / numTotal # 计算概率
shannonEnt -= prob * log(prob, 2) # 计算香浓熵
return shannonEnt
得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集,接下来我们将学习如何划分数据集以及如何度量信息增益。在熵的计算中,混合的数据越多,也就是种类越多,熵就越大。
2.划分数据集
熵就是度量信息的无序程度,分类算法除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确划分了数据集。我们对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。
def splitData(dataSet, axis, value):
"""
按照给定特征划分数据集
:param dataSet: 待划分的数据集
:param axis:划分数据集的特征
:param value:特征的返回值
:return:抽取出来的数据集
"""
retDataSet = [] # 创建一个新的列表
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
""" 选择最好的划分数据集方式 """
numFeatures = len(dataSet[0]) - 1 # 获取特征的个数
baseEntropy = calcShannonEnt(dataSet) # 计算原始数据集的香浓熵
bestInfoGain = 0.0 # 最好的信息增益
bestFeature = -1 # 最好的特征
for i in range(numFeatures):
featList = [example[i] for example in dataSet] # 获取当前特征的值
uniqueVals = set(featList) # 去重
newEntropy = 0.0 # 新数据集的熵
for value in uniqueVals:
subdataSet = splitData(dataSet, i, value)
prob = len(subdataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subdataSet)
infoGain = baseEntropy - newEntropy
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i # 最好的特征
return bestFeature
注意:
- python不需考虑内存分配问题,在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生命周期,因此在划分数据集的操作中,需要在函数的开始声明一个新列表对象。为了保证不修改原数据。
- 信息增益是熵的减少,或者是数据无序度的减少。
经过选择之后,我们可以找出最好的特征用于划分数据。
3.递归构建决策树
我们已经完成了从数据集构造决策树算法所需要的子功能模块。工作原理:得到原始数据集,然后基于最好的属性值划分数据集,因为可能有多个特征值,因此可能存在大于两个分支的数据集划分。第一次划分后,数据将被向下传递到树分支的下一个节点,然后再这个节点,我们可以再次划分数据。因此采用递归的原则处理数据集。递归的结束条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都是有相同的分类。
def majorityCnt(classList):
""" 选择类别多的特征作为结果 """
classCount = {}
for vote in classList:
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):
"""
创建决策树
:param dataSet: 数据集
:param labels: 标签列表
:return: 决策树
"""
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(splitData(dataSet, bestFeat, value), subLabels) # 递归创建树
return myTree
三、使用matplotlib注解绘制树形图
1.matplotlb注解
maplotlib提供了一个注解工具annotations,可以在数据图形上添加文本注释。注解通常用于解释数据的内容。由于数据上面直接存在文本描述非常不好看,因此工具内嵌支持带箭头的划线工具,使得我们可以在合适的地方指向数据位置,并在此处添加描述信息。
首先,我们使用文本绘制树节点:
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)
2.构造注解树
绘制一颗完整的树,我们必须知道有多少个叶节点,以便可以正确确定x轴的长度,还需要知道树有多少层,以便可以确定y轴的高度。我们使用如下函数来获取叶子节点的数目和树的层数。下面的代码包含一个完整的构造画决策树的过程。
def getNumLeafs(myTree):
""" 获取叶子节点的数目,以此计算图的宽度 """
numLeafs = 0 # 叶子节点的数目
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
def getTreeDepth(myTree):
""" 计算叶子节点的深度,以此计算图的高度 """
maxDepth = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
thisDepth = 1 + getTreeDepth(secondDict[key]);
else:
thisDepth = 1
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
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]
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, nodeText):
# 计算宽和高
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = list(myTree.keys())[0]
cntrPt = (plotTree.xOff + (1.0 +float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff)
# 标记子节点属性值
plotMidText(cntrPt, parentPt, nodeText)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
# 减少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()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
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.keys())[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
现在我们创建了使用决策树的分类器,但是每次使用都必须重新构造分类器,因此,可以将分类器存储在硬盘上,使用构建好的决策树进行分类可以节省很多时间。我们使用pickle模块序列化对象,序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。
def storeTree(inputTree, filename):
""" 存储决策树 """
fw = open(filename, 'wb')
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
""" 加载决策树 """
fr = open(filename, 'rb')
return pickle.load(fr)
五、使用决策树预测隐形眼镜类型
下面我们使用隐形眼镜数据集测试决策树算法,
# 读取数据
fr = open('lenses.txt')
# 对数据做处理
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
# 标签数据
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
# 创建决策树
lensesTree = createTree(lenses, lensesLabels)
storeTree(lensesTree, 'lensesTree.txt')
createPlot(lensesTree)
到此,我们完成了决策树算法。然而这个很好的匹配了数据,但是匹配的选项有可能过多,造成过度匹配。我们可以裁剪决策树解决这个问题,我们使用的决策树算法为ID3.这个算法虽然很好,但是也存在缺陷。决策树还包括C4.5,CART等算法。
全部代码链接:https://github.com/guoyuantao/MachineLearning/tree/master/Decision_Tree
注意:此文章的内容参考《机器学习实战》。博客的主要目的为记录个人所学的知识。